diff --git a/.gitignore b/.gitignore
index 9904d9a..b1ce123 100644
--- a/.gitignore
+++ b/.gitignore
@@ -55,4 +55,5 @@ fastlane/test_output
fastlane/readme.md
# Java dump memory
-*.hprof
\ No newline at end of file
+*.hprof
+/.kotlin/
diff --git a/.run/publishToCentral.run.xml b/.run/publishToCentral.run.xml
index da54406..2a08658 100644
--- a/.run/publishToCentral.run.xml
+++ b/.run/publishToCentral.run.xml
@@ -1,17 +1,18 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/publishToLocal.run.xml b/.run/publishToLocal.run.xml
index 916ce8d..34df8c5 100644
--- a/.run/publishToLocal.run.xml
+++ b/.run/publishToLocal.run.xml
@@ -1,17 +1,17 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/FAQ.md b/FAQ.md
index 80587cd..7b30aa2 100644
--- a/FAQ.md
+++ b/FAQ.md
@@ -1,65 +1,99 @@
# Frequently Asked Questions
### The app is not responding when copy, move, and other IO tasks
+
Read the quick documentation, Javadoc or go to the source code.
All functions annotated by `@WorkerThread` must be called in the background thread,
otherwise `@UiThread` must be called in the main thread.
-If you ignore the annotation, your apps will lead to [ANR](https://developer.android.com/topic/performance/vitals/anr).
+If you ignore the annotation, your apps will lead
+to [ANR](https://developer.android.com/topic/performance/vitals/anr).
### How to open quick documentation?
+
Use keyboard shortcut Control + Q on Windows or Control + J on MacOS.
-More shortcuts can be found on [Android Studio keyboard shortcuts](https://developer.android.com/studio/intro/keyboard-shortcuts).
+More shortcuts can be found
+on [Android Studio keyboard shortcuts](https://developer.android.com/studio/intro/keyboard-shortcuts).
### Why permission dialog is not shown on API 29+?
+
No runtime permission is required to be prompted on scoped storage.
### How to upload the `DocumentFile` and `MediaFile` to server?
+
Read the input stream with extension function `openInputStream()` and upload it as Base64 text.
### File path returns empty string
-Getting file path (`getAbsolutePath()`, `getBasePath()`, etc.) may returns empty string if the `DocumentFile` is an instance of `androidx.documentfile.provider.SingleDocumentFile`. The following URIs are the example of `SingleDocumentFile`:
+
+Getting file path (`getAbsolutePath()`, `getBasePath()`, etc.) may returns empty string if
+the `DocumentFile` is an instance of `androidx.documentfile.provider.SingleDocumentFile`. The
+following URIs are the example of `SingleDocumentFile`:
+
```
content://com.android.providers.downloads.documents/document/9
content://com.android.providers.media.documents/document/document%3A34
```
+
Here're some notes:
+
* Empty file path is not this library's limitation, but Android OS itself.
-* To check if the file has guaranteed direct file path, extension function `DocumentFile.isTreeDocumentFile` will return `true`.
-* You can convert `SingleDocumentFile` to `MediaFile` and use `MediaFile.absolutePath`. If this still does not work, then there's no other way.
-* We don't recommend you to use direct file path for file management, such as reading, uploading it to the server, or importing it into your app.
-Because Android OS wants us to use URI, thus direct file path is useless. So you need to use extension function `Uri.openInputStream()` for `DocumentFile` and `MediaFile`.
+* To check if the file has guaranteed direct file path, extension
+ function `DocumentFile.isTreeDocumentFile` will return `true`.
+* You can convert `SingleDocumentFile` to `MediaFile` and use `MediaFile.absolutePath`. If this
+ still does not work, then there's no other way.
+* We don't recommend you to use direct file path for file management, such as reading, uploading it
+ to the server, or importing it into your app.
+ Because Android OS wants us to use URI, thus direct file path is useless. So you need to use
+ extension function `Uri.openInputStream()` for `DocumentFile` and `MediaFile`.
### How to check if a folder/file is writable?
+
Use `isWritable()` extension function, because `DocumentFile.canWrite()` sometimes buggy on API 30.
### Which paths are writable with `java.io.File` on scoped storage?
-Accessing files in scoped storage requires URI, but the following paths are exception and no storage permission needed:
+
+Accessing files in scoped storage requires URI, but the following paths are exception and no storage
+permission needed:
+
* `/storage/emulated/0/Android/data/`
* `/storage//Android/data/`
* `/data/user/0/` (API 24+)
* `/data/data/` (API 23-)
### What is the target branch for pull requests?
+
Use branch `release/*` if exists, or use `master` instead.
### I have Java projects, but this library is built in Kotlin. How can I use it?
+
Kotlin is compatible with Java. You can read Kotlin functions as Java methods.
Read: [Java Compatibility](https://github.com/anggrayudi/SimpleStorage/blob/master/JAVA_COMPATIBILITY.md)
### Why does SimpleStorage use Kotlin?
+
The main reasons why this library really needs Kotlin:
-* SimpleStorage requires thread suspension feature, but this feature is only provided by [Kotlin Coroutines](https://github.com/Kotlin/kotlinx.coroutines).
-* SimpleStorage contains many `String` & `Collection` manipulations, and Kotlin can overcome them in simple and easy ways.
+
+* SimpleStorage requires thread suspension feature, but this feature is only provided
+ by [Kotlin Coroutines](https://github.com/Kotlin/kotlinx.coroutines).
+* SimpleStorage contains many `String` & `Collection` manipulations, and Kotlin can overcome them in
+ simple and easy ways.
Other reasons are:
+
* Kotlin can shorten and simplify your code.
* Writing code in Kotlin is faster, thus it saves your time and improves your productivity.
-* [Google is Kotlin first](https://techcrunch.com/2019/05/07/kotlin-is-now-googles-preferred-language-for-android-app-development/) now.
+* [Google is Kotlin first](https://techcrunch.com/2019/05/07/kotlin-is-now-googles-preferred-language-for-android-app-development/)
+ now.
### What are SimpleStorage alternatives?
-You can't run from the fact that Google is Kotlin First now. Even Google has created [ModernStorage](https://github.com/google/modernstorage) (alternative for SimpleStorage) in Kotlin.
+
+You can't run from the fact that Google is Kotlin First now. Even Google has
+created [ModernStorage](https://github.com/google/modernstorage) (alternative for SimpleStorage) in
+Kotlin.
Learn Kotlin, or Google will leave you far behind.
-**We have no intention to create Java version of SimpleStorage.** It will double our works and requires a lot of effort.
-Keep in mind that we don't want to archive this library, even though Google has released the stable version of ModernStorage.
-This library has rich features that Google may not covers, e.g. moving, copying, compressing and scanning folders.
+**We have no intention to create Java version of SimpleStorage.** It will double our works and
+requires a lot of effort.
+Keep in mind that we don't want to archive this library, even though Google has released the stable
+version of ModernStorage.
+This library has rich features that Google may not covers, e.g. moving, copying, compressing and
+scanning folders.
diff --git a/JAVA_COMPATIBILITY.md b/JAVA_COMPATIBILITY.md
index 95ff40c..d8be6af 100644
--- a/JAVA_COMPATIBILITY.md
+++ b/JAVA_COMPATIBILITY.md
@@ -4,18 +4,25 @@ Kotlin is compatible with Java, meaning that Kotlin code is readable in Java.
## How to use?
-Simple Storage contains utility functions stored in `object` class, e.g. `DocumentFileCompat` and `MediaStoreCompat`.
+Simple Storage contains utility functions stored in `object` class, e.g. `DocumentFileCompat`
+and `MediaStoreCompat`.
These classes contain only static functions.
Additionally, this library also has extension functions, e.g. `DocumentFileExtKt` and `FileExtKt`.
-You can learn it [here](https://www.raywenderlich.com/10986797-extension-functions-and-properties-in-kotlin).
+You can learn
+it [here](https://www.raywenderlich.com/10986797-extension-functions-and-properties-in-kotlin).
### Extension Functions
-Common extension functions are stored in package `com.anggrayudi.storage.extension`. The others are in `com.anggrayudi.storage.file`.
-You'll find that the most useful extension functions come from `DocumentFileExtKt` and `FileExtKt`. They are:
-* `DocumentFile.getStorageId()` and `File.getStorageId()` → Get storage ID. Returns `primary` for external storage and something like `AAAA-BBBB` for SD card.
-* `DocumentFile.getAbsolutePath()` → Get file's absolute path. Returns something like `/storage/AAAA-BBBB/Music/My Love.mp3`.
+Common extension functions are stored in package `com.anggrayudi.storage.extension`. The others are
+in `com.anggrayudi.storage.file`.
+You'll find that the most useful extension functions come from `DocumentFileExtKt` and `FileExtKt`.
+They are:
+
+* `DocumentFile.getStorageId()` and `File.getStorageId()` → Get storage ID. Returns `primary` for
+ external storage and something like `AAAA-BBBB` for SD card.
+* `DocumentFile.getAbsolutePath()` → Get file's absolute path. Returns something
+ like `/storage/AAAA-BBBB/Music/My Love.mp3`.
* `DocumentFile.copyFileTo()` and `File.copyFileTo()`
* `DocumentFile.search()` and `File.search()`, etc.
@@ -43,7 +50,8 @@ their class names are renamed from using suffix `ExtKt` to `Utils`.
I will refer to utility functions stored in Kotlin `object` class so you can understand it easily.
You can find the most useful utility functions in `DocumentFileCompat` and `MediaStoreCompat`.
-Suppose that I want to get file from SD card with the following simple path: `AAAA-BBBB:Music/My Love.mp3`.
+Suppose that I want to get file from SD card with the following simple
+path: `AAAA-BBBB:Music/My Love.mp3`.
BTW, `AAAA-BBBB` is the SD card's storage ID for this example.
#### In Kotlin
@@ -65,5 +73,5 @@ Just go to the source code to check whether it has the annotation.
## Sample Code
* More sample code in Java can be found in
-[`JavaActivity`](https://github.com/anggrayudi/SimpleStorage/blob/master/sample/src/main/java/com/anggrayudi/storage/sample/activity/JavaActivity.java)
+ [`JavaActivity`](https://github.com/anggrayudi/SimpleStorage/blob/master/sample/src/main/java/com/anggrayudi/storage/sample/activity/JavaActivity.java)
* Learn Kotlin on [Udacity](https://classroom.udacity.com/courses/ud9011). It's easy and free!
\ No newline at end of file
diff --git a/README.md b/README.md
index 2accca3..531a2dd 100644
--- a/README.md
+++ b/README.md
@@ -1,20 +1,22 @@
# SimpleStorage
+
![Maven Central](https://img.shields.io/maven-central/v/com.anggrayudi/storage.svg)
[![Build Status](https://github.com/anggrayudi/SimpleStorage/workflows/Android%20CI/badge.svg)](https://github.com/anggrayudi/SimpleStorage/actions?query=workflow%3A%22Android+CI%22)
### Table of Contents
+
* [Overview](#overview)
- + [Java Compatibility](#java-compatibility)
+ + [Java Compatibility](#java-compatibility)
* [Terminology](#terminology)
* [Check Accessible Paths](#check-accessible-paths)
* [Read Files](#read-files)
- + [`DocumentFileCompat`](#documentfilecompat)
- - [Example](#example)
- + [`MediaStoreCompat`](#mediastorecompat)
- - [Example](#example-1)
+ + [`DocumentFileCompat`](#documentfilecompat)
+ - [Example](#example)
+ + [`MediaStoreCompat`](#mediastorecompat)
+ - [Example](#example-1)
* [Manage Files](#manage-files)
- + [`DocumentFile`](#documentfile)
- + [`MediaFile`](#mediafile)
+ + [`DocumentFile`](#documentfile)
+ + [`MediaFile`](#mediafile)
* [Request Storage Access, Pick Folder & Files, Request Create File, etc.](#request-storage-access-pick-folder--files-request-create-file-etc)
* [Move & Copy: Files & Folders](#move--copy-files--folders)
* [FAQ](#faq)
@@ -38,9 +40,11 @@ Adding Simple Storage into your project is pretty simple:
implementation "com.anggrayudi:storage:X.Y.Z"
```
-Where `X.Y.Z` is the library version: ![Maven Central](https://img.shields.io/maven-central/v/com.anggrayudi/storage.svg)
+Where `X.Y.Z` is the library
+version: ![Maven Central](https://img.shields.io/maven-central/v/com.anggrayudi/storage.svg)
-All versions can be found [here](https://oss.sonatype.org/#nexus-search;gav~com.anggrayudi~storage~~~~kw,versionexpand).
+All versions can be
+found [here](https://oss.sonatype.org/#nexus-search;gav~com.anggrayudi~storage~~~~kw,versionexpand).
To use `SNAPSHOT` version, you need to add this URL to the root Gradle:
```groovy
@@ -56,27 +60,38 @@ allprojects {
### Java Compatibility
-Simple Storage is built in Kotlin. Follow this [documentation](JAVA_COMPATIBILITY.md) to use it in your Java project.
+Simple Storage is built in Kotlin. Follow this [documentation](JAVA_COMPATIBILITY.md) to use it in
+your Java project.
## Terminology
![Alt text](art/terminology.png?raw=true "Simple Storage Terms")
### Other Terminology
-* Storage Permission – related to [runtime permissions](https://developer.android.com/training/permissions/requesting)
-* Storage Access – related to [URI permissions](https://developer.android.com/reference/android/content/ContentResolver#takePersistableUriPermission(android.net.Uri,%20int))
+
+* Storage Permission – related
+ to [runtime permissions](https://developer.android.com/training/permissions/requesting)
+* Storage Access – related
+ to [URI permissions](https://developer.android.com/reference/android/content/ContentResolver#takePersistableUriPermission(android.net.Uri,%20int))
## Check Accessible Paths
-To check whether you have access to particular paths, call `DocumentFileCompat.getAccessibleAbsolutePaths()`. The results will look like this in breakpoint:
+To check whether you have access to particular paths,
+call `DocumentFileCompat.getAccessibleAbsolutePaths()`. The results will look like this in
+breakpoint:
![Alt text](art/getAccessibleAbsolutePaths.png?raw=true "DocumentFileCompat.getAccessibleAbsolutePaths()")
-All paths in those locations are accessible via functions `DocumentFileCompat.from*()`, otherwise your action will be denied by the system if you want to
-access paths other than those. Functions `DocumentFileCompat.from*()` (next section) will return null as well. On API 28-, you can obtain it by requesting
-the runtime permission. For API 29+, it is obtained automatically by calling `SimpleStorageHelper#requestStorageAccess()` or
-`SimpleStorageHelper#openFolderPicker()`. The granted paths are persisted by this library via `ContentResolver#takePersistableUriPermission()`,
+All paths in those locations are accessible via functions `DocumentFileCompat.from*()`, otherwise
+your action will be denied by the system if you want to
+access paths other than those. Functions `DocumentFileCompat.from*()` (next section) will return
+null as well. On API 28-, you can obtain it by requesting
+the runtime permission. For API 29+, it is obtained automatically by
+calling `SimpleStorageHelper#requestStorageAccess()` or
+`SimpleStorageHelper#openFolderPicker()`. The granted paths are persisted by this library
+via `ContentResolver#takePersistableUriPermission()`,
so you don't need to remember them in preferences:
+
```kotlin
buttonSelectFolder.setOnClickListener {
storageHelper.openFolderPicker()
@@ -87,7 +102,9 @@ storageHelper.onFolderSelected = { requestCode, folder ->
}
```
-In the future, if you want to write files into the granted path, use `DocumentFileCompat.fromFullPath()`:
+In the future, if you want to write files into the granted path,
+use `DocumentFileCompat.fromFullPath()`:
+
```kotlin
val grantedPaths = DocumentFileCompat.getAccessibleAbsolutePaths(this)
val path = grantedPaths.values.firstOrNull()?.firstOrNull() ?: return
@@ -97,8 +114,10 @@ val file = folder?.makeFile(this, "notes", "text/plain")
## Read Files
-In Simple Storage, `DocumentFile` is used to access files when your app has been granted full storage access,
-included URI permissions for read and write. Whereas `MediaFile` is used to access media files from `MediaStore`
+In Simple Storage, `DocumentFile` is used to access files when your app has been granted full
+storage access,
+included URI permissions for read and write. Whereas `MediaFile` is used to access media files
+from `MediaStore`
without URI permissions to the storage.
You can read file with helper functions in `DocumentFileCompat` and `MediaStoreCompat`:
@@ -111,6 +130,7 @@ You can read file with helper functions in `DocumentFileCompat` and `MediaStoreC
* `DocumentFileCompat.fromPublicFolder()`
#### Example
+
```kotlin
val fileFromExternalStorage = DocumentFileCompat.fromSimplePath(context, basePath = "Download/MyMovie.mp4")
@@ -127,6 +147,7 @@ val fileFromSdCard = DocumentFileCompat.fromSimplePath(context, storageId = "901
* `MediaStoreCompat.fromMediaType()`
#### Example
+
```kotlin
val myVideo = MediaStoreCompat.fromFileName(context, MediaType.DOWNLOADS, "MyMovie.mp4")
@@ -137,9 +158,11 @@ val imageList = MediaStoreCompat.fromMediaType(context, MediaType.IMAGE)
### `DocumentFile`
-Since `java.io.File` has been deprecated in Android 10, thus you have to use `DocumentFile` for file management.
+Since `java.io.File` has been deprecated in Android 10, thus you have to use `DocumentFile` for file
+management.
Simple Storage adds Kotlin extension functions to `DocumentFile`, so you can manage files like this:
+
* `DocumentFile.getStorageId()`
* `DocumentFile.getStorageType()`
* `DocumentFile.getBasePath()`
@@ -153,6 +176,7 @@ Simple Storage adds Kotlin extension functions to `DocumentFile`, so you can man
### `MediaFile`
For media files, you can have similar capabilities to `DocumentFile`, i.e.:
+
* `MediaFile.absolutePath`
* `MediaFile.isPending`
* `MediaFile.delete()`
@@ -164,11 +188,14 @@ For media files, you can have similar capabilities to `DocumentFile`, i.e.:
## Request Storage Access, Pick Folder & Files, Request Create File, etc.
-Although user has granted read and write permissions during runtime, your app may still does not have full access to the storage,
-thus you cannot search, move and copy files. You can check whether you have the storage access via `SimpleStorage.hasStorageAccess()` or
+Although user has granted read and write permissions during runtime, your app may still does not
+have full access to the storage,
+thus you cannot search, move and copy files. You can check whether you have the storage access
+via `SimpleStorage.hasStorageAccess()` or
`DocumentFileCompat.getAccessibleAbsolutePaths()`.
-To enable full storage access, you need to open SAF and let user grant URI permissions for read and write access.
+To enable full storage access, you need to open SAF and let user grant URI permissions for read and
+write access.
This library provides you an helper class named `SimpleStorageHelper` to ease the request process:
```kotlin
@@ -235,6 +262,7 @@ If you want to use custom dialogs for `SimpleStorageHelper`, just copy the logic
## Move & Copy: Files & Folders
Simple Storage helps you in copying/moving files & folders via:
+
* `DocumentFile.copyFileTo()`
* `DocumentFile.moveFileTo()`
* `DocumentFile.copyFolderTo()`
@@ -286,8 +314,10 @@ folder.moveFolderTo(applicationContext, targetFolder, skipEmptyFiles = false, ca
})
```
-The coolest thing of this library is you can ask users to choose Merge, Replace, Create New, or Skip Duplicate folders & files
-whenever a conflict is found via `onConflict()`. Here're screenshots of the sample code when dealing with conflicts:
+The coolest thing of this library is you can ask users to choose Merge, Replace, Create New, or Skip
+Duplicate folders & files
+whenever a conflict is found via `onConflict()`. Here're screenshots of the sample code when dealing
+with conflicts:
![Alt text](art/parent-folder-conflict.png?raw=true "Parent Folder Conflict")
![Alt text](art/folder-content-conflict.png?raw=true "Folder Content Conflict")
@@ -303,6 +333,7 @@ Having trouble? Read the [Frequently Asked Questions](FAQ.md).
SimpleStorage is used in these open source projects.
Check how these repositories use it:
+
* [Snapdrop](https://github.com/anggrayudi/snapdrop-android)
* [MaterialPreference](https://github.com/anggrayudi/MaterialPreference)
* [Super Productivity](https://github.com/johannesjo/super-productivity-android)
diff --git a/build.gradle b/build.gradle
index 73850d9..1fc8712 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,16 +1,18 @@
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
apply from: 'versions.gradle'
addRepos(repositories)
- ext.kotlin_version = '1.8.22'
+ ext.kotlin_version = '2.0.0'
dependencies {
- classpath 'com.android.tools.build:gradle:8.0.2'
+ classpath 'com.android.tools.build:gradle:8.3.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- classpath 'com.vanniktech:gradle-maven-publish-plugin:0.22.0'
- classpath 'org.jetbrains.dokka:dokka-gradle-plugin:1.7.20'
+ classpath 'com.vanniktech:gradle-maven-publish-plugin:0.28.0'
+ classpath 'org.jetbrains.dokka:dokka-gradle-plugin:1.9.20'
}
}
@@ -18,7 +20,7 @@ allprojects {
addRepos(repositories)
//Support @JvmDefault
- tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
+ tasks.withType(KotlinCompile).configureEach {
kotlinOptions {
freeCompilerArgs = ['-Xjvm-default=all', '-opt-in=kotlin.RequiresOptIn']
jvmTarget = '1.8'
@@ -41,11 +43,10 @@ subprojects {
afterEvaluate {
android {
- compileSdkVersion 33
+ compileSdkVersion 34
defaultConfig {
minSdkVersion 19
- targetSdkVersion 33
versionCode 1
versionName "$VERSION_NAME"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -61,7 +62,7 @@ subprojects {
buildConfig true
}
}
- configurations.all {
+ configurations.configureEach {
resolutionStrategy {
// Force Kotlin to use current version
force "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
@@ -79,6 +80,6 @@ subprojects {
}
}
-task clean(type: Delete) {
- delete rootProject.buildDir
+tasks.register('clean', Delete) {
+ delete rootProject.layout.buildDirectory
}
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index f6b961f..d64cd49 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index f0d76de..a441313 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,7 @@
-#Tue Dec 01 18:57:30 WIB 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip
diff --git a/gradlew b/gradlew
index cccdd3d..1aa94a4 100755
--- a/gradlew
+++ b/gradlew
@@ -1,78 +1,127 @@
-#!/usr/bin/env sh
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
##############################################################################
-##
-## Gradle start up script for UN*X
-##
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
##############################################################################
# Attempt to set APP_HOME
+
# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
-
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
+MAX_FD=maximum
warn () {
echo "$*"
-}
+} >&2
die () {
echo
echo "$*"
echo
exit 1
-}
+} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MINGW* )
- msys=true
- ;;
- NONSTOP* )
- nonstop=true
- ;;
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACMD=$JAVA_HOME/jre/sh/java
else
- JAVACMD="$JAVA_HOME/bin/java"
+ JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -81,92 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
- JAVACMD="java"
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
+ fi
fi
# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
- MAX_FD_LIMIT=`ulimit -H -n`
- if [ $? -eq 0 ] ; then
- if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
- MAX_FD="$MAX_FD_LIMIT"
- fi
- ulimit -n $MAX_FD
- if [ $? -ne 0 ] ; then
- warn "Could not set maximum file descriptor limit: $MAX_FD"
- fi
- else
- warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
- fi
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
fi
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
- GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin ; then
- APP_HOME=`cygpath --path --mixed "$APP_HOME"`
- CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
- JAVACMD=`cygpath --unix "$JAVACMD"`
-
- # We build the pattern for arguments to be converted via cygpath
- ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
- SEP=""
- for dir in $ROOTDIRSRAW ; do
- ROOTDIRS="$ROOTDIRS$SEP$dir"
- SEP="|"
- done
- OURCYGPATTERN="(^($ROOTDIRS))"
- # Add a user-defined pattern to the cygpath arguments
- if [ "$GRADLE_CYGPATTERN" != "" ] ; then
- OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
- fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
- i=0
- for arg in "$@" ; do
- CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
- CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
-
- if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
- eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
- else
- eval `echo args$i`="\"$arg\""
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
fi
- i=$((i+1))
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
done
- case $i in
- (0) set -- ;;
- (1) set -- "$args0" ;;
- (2) set -- "$args0" "$args1" ;;
- (3) set -- "$args0" "$args1" "$args2" ;;
- (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
- esac
fi
-# Escape application args
-save () {
- for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
- echo " "
-}
-APP_ARGS=$(save "$@")
-
-# Collect all arguments for the java command, following the shell quoting and substitution rules
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
-# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
-if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
- cd "$(dirname "$0")"
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
fi
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
index e95643d..7101f8e 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -1,4 +1,20 @@
-@if "%DEBUG%" == "" @echo off
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -9,25 +25,29 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS=
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
+if %ERRORLEVEL% equ 0 goto execute
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
goto fail
@@ -35,48 +55,36 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-if exist "%JAVA_EXE%" goto init
+if exist "%JAVA_EXE%" goto execute
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
goto fail
-:init
-@rem Get command-line arguments, handling Windows variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
+if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
diff --git a/sample/build.gradle b/sample/build.gradle
index edf2f34..07b8a3e 100644
--- a/sample/build.gradle
+++ b/sample/build.gradle
@@ -78,9 +78,7 @@ dependencies {
implementation deps.timber
implementation deps.material_progressbar
+ implementation deps.material_dialogs_files
implementation 'androidx.preference:preference-ktx:1.2.0'
- //test
- testImplementation deps.junit
- testImplementation deps.mockk
}
\ No newline at end of file
diff --git a/sample/src/main/java/com/anggrayudi/storage/sample/activity/JavaActivity.java b/sample/src/main/java/com/anggrayudi/storage/sample/activity/JavaActivity.java
index f79a707..030e4e7 100644
--- a/sample/src/main/java/com/anggrayudi/storage/sample/activity/JavaActivity.java
+++ b/sample/src/main/java/com/anggrayudi/storage/sample/activity/JavaActivity.java
@@ -17,7 +17,6 @@
import com.anggrayudi.storage.SimpleStorageHelper;
import com.anggrayudi.storage.callback.FileCallback;
import com.anggrayudi.storage.file.DocumentFileUtils;
-import com.anggrayudi.storage.media.MediaFile;
import com.anggrayudi.storage.permission.ActivityPermissionRequest;
import com.anggrayudi.storage.permission.PermissionCallback;
import com.anggrayudi.storage.permission.PermissionReport;
@@ -109,15 +108,6 @@ public void onConflict(@NotNull DocumentFile destinationFile, @NotNull FileCallb
// do stuff
}
- @Override
- public void onCompleted(@NotNull Object result) {
- if (result instanceof DocumentFile) {
- // do stuff
- } else if (result instanceof MediaFile) {
- // do stuff
- }
- }
-
@Override
public void onReport(Report report) {
Timber.d("%s", report.getProgress());
diff --git a/sample/src/main/java/com/anggrayudi/storage/sample/fragment/SettingsFragment.java b/sample/src/main/java/com/anggrayudi/storage/sample/fragment/SettingsFragment.java
index b2bfbbd..b17fa62 100644
--- a/sample/src/main/java/com/anggrayudi/storage/sample/fragment/SettingsFragment.java
+++ b/sample/src/main/java/com/anggrayudi/storage/sample/fragment/SettingsFragment.java
@@ -21,9 +21,10 @@
import com.anggrayudi.storage.file.DocumentFileUtils;
import com.anggrayudi.storage.file.PublicDirectory;
import com.anggrayudi.storage.media.FileDescription;
-import com.anggrayudi.storage.media.MediaFile;
import com.anggrayudi.storage.sample.R;
+import java.util.Objects;
+
import timber.log.Timber;
/**
@@ -77,7 +78,7 @@ private void moveFileToSaveLocation(@NonNull DocumentFile sourceFile) {
// write any files into folder 'saveLocationFolder'
DocumentFileUtils.moveFileTo(sourceFile, requireContext(), saveLocationFolder, null, createCallback());
} else {
- FileDescription fileDescription = new FileDescription(sourceFile.getName(), "", sourceFile.getType());
+ FileDescription fileDescription = new FileDescription(Objects.requireNonNull(sourceFile.getName()), "", sourceFile.getType());
DocumentFileUtils.moveFileToDownloadMedia(sourceFile, requireContext(), fileDescription, createCallback());
}
}
@@ -95,17 +96,16 @@ public void onFailed(ErrorCode errorCode) {
}
@Override
- public void onCompleted(@NonNull Object file) {
+ public void onCompleted(FileCallback.Result result) {
final Uri uri;
final Context context = requireContext();
- if (file instanceof MediaFile) {
- final MediaFile mediaFile = (MediaFile) file;
- uri = mediaFile.getUri();
- } else if (file instanceof DocumentFile) {
- final DocumentFile documentFile = (DocumentFile) file;
+ if (result instanceof Result.MediaFile) {
+ uri = ((Result.MediaFile) result).getValue().getUri();
+ } else if (result instanceof FileCallback.Result.DocumentFile) {
+ final DocumentFile documentFile = ((Result.DocumentFile) result).getValue();
uri = DocumentFileUtils.isRawFile(documentFile)
- ? FileProvider.getUriForFile(context, context.getPackageName() + ".provider", DocumentFileUtils.toRawFile(documentFile, context))
+ ? FileProvider.getUriForFile(context, context.getPackageName() + ".provider", Objects.requireNonNull(DocumentFileUtils.toRawFile(documentFile, context)))
: documentFile.getUri();
} else {
return;
diff --git a/sample/src/main/java/com/anggrayudi/storage/sample/App.kt b/sample/src/main/kotlin/com/anggrayudi/storage/sample/App.kt
similarity index 100%
rename from sample/src/main/java/com/anggrayudi/storage/sample/App.kt
rename to sample/src/main/kotlin/com/anggrayudi/storage/sample/App.kt
diff --git a/sample/src/main/java/com/anggrayudi/storage/sample/StorageInfoAdapter.kt b/sample/src/main/kotlin/com/anggrayudi/storage/sample/StorageInfoAdapter.kt
similarity index 83%
rename from sample/src/main/java/com/anggrayudi/storage/sample/StorageInfoAdapter.kt
rename to sample/src/main/kotlin/com/anggrayudi/storage/sample/StorageInfoAdapter.kt
index 9b45777..cca5e9b 100644
--- a/sample/src/main/java/com/anggrayudi/storage/sample/StorageInfoAdapter.kt
+++ b/sample/src/main/kotlin/com/anggrayudi/storage/sample/StorageInfoAdapter.kt
@@ -30,7 +30,10 @@ class StorageInfoAdapter(
private val storageIds = DocumentFileCompat.getStorageIds(context)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
- return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.view_item_storage_info, parent, false))
+ return ViewHolder(
+ LayoutInflater.from(parent.context)
+ .inflate(R.layout.view_item_storage_info, parent, false)
+ )
}
@SuppressLint("SetTextI18n")
@@ -38,9 +41,18 @@ class StorageInfoAdapter(
ioScope.launch {
val storageId = storageIds[position]
val storageName = if (storageId == PRIMARY) "External Storage" else storageId
- val storageCapacity = Formatter.formatFileSize(context, DocumentFileCompat.getStorageCapacity(context, storageId))
- val storageUsedSpace = Formatter.formatFileSize(context, DocumentFileCompat.getUsedSpace(context, storageId))
- val storageFreeSpace = Formatter.formatFileSize(context, DocumentFileCompat.getFreeSpace(context, storageId))
+ val storageCapacity = Formatter.formatFileSize(
+ context,
+ DocumentFileCompat.getStorageCapacity(context, storageId)
+ )
+ val storageUsedSpace = Formatter.formatFileSize(
+ context,
+ DocumentFileCompat.getUsedSpace(context, storageId)
+ )
+ val storageFreeSpace = Formatter.formatFileSize(
+ context,
+ DocumentFileCompat.getFreeSpace(context, storageId)
+ )
uiScope.launch {
holder.run {
tvStorageName.text = storageName
@@ -61,7 +73,6 @@ class StorageInfoAdapter(
* A storageId may contains more than one granted URIs
*/
@SuppressLint("NewApi")
- @Suppress("DEPRECATION")
private fun showGrantedUris(context: Context, filterStorageId: String) {
val grantedPaths = DocumentFileCompat.getAccessibleAbsolutePaths(context)[filterStorageId]
if (grantedPaths == null) {
diff --git a/sample/src/main/java/com/anggrayudi/storage/sample/activity/BaseActivity.kt b/sample/src/main/kotlin/com/anggrayudi/storage/sample/activity/BaseActivity.kt
similarity index 100%
rename from sample/src/main/java/com/anggrayudi/storage/sample/activity/BaseActivity.kt
rename to sample/src/main/kotlin/com/anggrayudi/storage/sample/activity/BaseActivity.kt
diff --git a/sample/src/main/java/com/anggrayudi/storage/sample/activity/FileCompressionActivity.kt b/sample/src/main/kotlin/com/anggrayudi/storage/sample/activity/FileCompressionActivity.kt
similarity index 61%
rename from sample/src/main/java/com/anggrayudi/storage/sample/activity/FileCompressionActivity.kt
rename to sample/src/main/kotlin/com/anggrayudi/storage/sample/activity/FileCompressionActivity.kt
index 4524134..fbfea82 100644
--- a/sample/src/main/java/com/anggrayudi/storage/sample/activity/FileCompressionActivity.kt
+++ b/sample/src/main/kotlin/com/anggrayudi/storage/sample/activity/FileCompressionActivity.kt
@@ -40,8 +40,13 @@ class FileCompressionActivity : BaseActivity() {
storageHelper.onFileSelected = { requestCode, files ->
when (requestCode) {
- REQUEST_CODE_PICK_MEDIA_1 -> binding.layoutCompressFilesSrcMedia1.tvFilePath.updateFileSelectionView(files)
- REQUEST_CODE_PICK_MEDIA_2 -> binding.layoutCompressFilesSrcMedia2.tvFilePath.updateFileSelectionView(files)
+ REQUEST_CODE_PICK_MEDIA_1 -> binding.layoutCompressFilesSrcMedia1.tvFilePath.updateFileSelectionView(
+ files
+ )
+
+ REQUEST_CODE_PICK_MEDIA_2 -> binding.layoutCompressFilesSrcMedia2.tvFilePath.updateFileSelectionView(
+ files
+ )
}
}
binding.layoutCompressFilesSrcMedia1.btnBrowse.setOnClickListener {
@@ -53,8 +58,13 @@ class FileCompressionActivity : BaseActivity() {
storageHelper.onFolderSelected = { requestCode, folder ->
when (requestCode) {
- REQUEST_CODE_PICK_FOLDER_1 -> binding.layoutCompressFilesSrcFolder1.tvFilePath.updateFileSelectionView(folder)
- REQUEST_CODE_PICK_FOLDER_2 -> binding.layoutCompressFilesSrcFolder2.tvFilePath.updateFileSelectionView(folder)
+ REQUEST_CODE_PICK_FOLDER_1 -> binding.layoutCompressFilesSrcFolder1.tvFilePath.updateFileSelectionView(
+ folder
+ )
+
+ REQUEST_CODE_PICK_FOLDER_2 -> binding.layoutCompressFilesSrcFolder2.tvFilePath.updateFileSelectionView(
+ folder
+ )
}
}
binding.layoutCompressFilesSrcFolder1.btnBrowse.setOnClickListener {
@@ -84,33 +94,61 @@ class FileCompressionActivity : BaseActivity() {
}
val files = mutableListOf()
- (binding.layoutCompressFilesSrcMedia1.tvFilePath.tag as? List)?.let { files.addAll(it) }
- (binding.layoutCompressFilesSrcMedia2.tvFilePath.tag as? List)?.let { files.addAll(it) }
+ (binding.layoutCompressFilesSrcMedia1.tvFilePath.tag as? List)?.let {
+ files.addAll(
+ it
+ )
+ }
+ (binding.layoutCompressFilesSrcMedia2.tvFilePath.tag as? List)?.let {
+ files.addAll(
+ it
+ )
+ }
(binding.layoutCompressFilesSrcFolder1.tvFilePath.tag as? DocumentFile)?.let { files.add(it) }
(binding.layoutCompressFilesSrcFolder2.tvFilePath.tag as? DocumentFile)?.let { files.add(it) }
ioScope.launch {
- files.compressToZip(applicationContext, targetZip, callback = object : ZipCompressionCallback(uiScope) {
- override fun onCountingFiles() {
- // show a notification or dialog with indeterminate progress bar
- }
-
- override fun onStart(files: List, workerThread: Thread): Long = 500
-
- override fun onReport(report: Report) {
- Timber.d("onReport() -> ${report.progress.toInt()}% | Compressed ${report.fileCount} files")
- }
-
- override fun onCompleted(zipFile: DocumentFile, bytesCompressed: Long, totalFilesCompressed: Int, compressionRate: Float) {
- Timber.d("onCompleted() -> Compressed $totalFilesCompressed with compression rate %.2f", compressionRate)
- Toast.makeText(applicationContext, "Successfully compressed $totalFilesCompressed files", Toast.LENGTH_SHORT).show()
- }
-
- override fun onFailed(errorCode: ErrorCode, message: String?) {
- Timber.d("onFailed() -> $errorCode: $message")
- Toast.makeText(applicationContext, "Error compressing files: $errorCode", Toast.LENGTH_SHORT).show()
- }
- })
+ files.compressToZip(
+ applicationContext,
+ targetZip,
+ callback = object : ZipCompressionCallback(uiScope) {
+ override fun onCountingFiles() {
+ // show a notification or dialog with indeterminate progress bar
+ }
+
+ override fun onStart(files: List, workerThread: Thread): Long =
+ 500
+
+ override fun onReport(report: Report) {
+ Timber.d("onReport() -> ${report.progress.toInt()}% | Compressed ${report.fileCount} files")
+ }
+
+ override fun onCompleted(
+ zipFile: DocumentFile,
+ bytesCompressed: Long,
+ totalFilesCompressed: Int,
+ compressionRate: Float
+ ) {
+ Timber.d(
+ "onCompleted() -> Compressed $totalFilesCompressed with compression rate %.2f",
+ compressionRate
+ )
+ Toast.makeText(
+ applicationContext,
+ "Successfully compressed $totalFilesCompressed files",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+
+ override fun onFailed(errorCode: ErrorCode, message: String?) {
+ Timber.d("onFailed() -> $errorCode: $message")
+ Toast.makeText(
+ applicationContext,
+ "Error compressing files: $errorCode",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ })
}
}
diff --git a/sample/src/main/java/com/anggrayudi/storage/sample/activity/FileDecompressionActivity.kt b/sample/src/main/kotlin/com/anggrayudi/storage/sample/activity/FileDecompressionActivity.kt
similarity index 52%
rename from sample/src/main/java/com/anggrayudi/storage/sample/activity/FileDecompressionActivity.kt
rename to sample/src/main/kotlin/com/anggrayudi/storage/sample/activity/FileDecompressionActivity.kt
index eae3d63..c80943a 100644
--- a/sample/src/main/java/com/anggrayudi/storage/sample/activity/FileDecompressionActivity.kt
+++ b/sample/src/main/kotlin/com/anggrayudi/storage/sample/activity/FileDecompressionActivity.kt
@@ -67,47 +67,59 @@ class FileDecompressionActivity : BaseActivity() {
return
}
ioScope.launch {
- zipFile.decompressZip(applicationContext, targetFolder, object : ZipDecompressionCallback(uiScope) {
- var actionForAllConflicts: FileCallback.ConflictResolution? = null
+ zipFile.decompressZip(
+ applicationContext,
+ targetFolder,
+ object : ZipDecompressionCallback(uiScope) {
+ var actionForAllConflicts: FileCallback.ConflictResolution? = null
- override fun onFileConflict(destinationFile: DocumentFile, action: FileCallback.FileConflictAction) {
- actionForAllConflicts?.let {
- action.confirmResolution(it)
- return
- }
+ override fun onFileConflict(
+ destinationFile: DocumentFile,
+ action: FileCallback.FileConflictAction
+ ) {
+ actionForAllConflicts?.let {
+ action.confirmResolution(it)
+ return
+ }
- var doForAll = false
- MaterialDialog(this@FileDecompressionActivity)
- .cancelable(false)
- .title(text = "Conflict Found")
- .message(text = "File \"${destinationFile.name}\" already exists in destination. What's your action?")
- .checkBoxPrompt(text = "Apply to all") { doForAll = it }
- .listItems(items = mutableListOf("Replace", "Create New", "Skip Duplicate")) { _, index, _ ->
- val resolution = FileCallback.ConflictResolution.values()[index]
- if (doForAll) {
- actionForAllConflicts = resolution
+ var doForAll = false
+ MaterialDialog(this@FileDecompressionActivity)
+ .cancelable(false)
+ .title(text = "Conflict Found")
+ .message(text = "File \"${destinationFile.name}\" already exists in destination. What's your action?")
+ .checkBoxPrompt(text = "Apply to all") { doForAll = it }
+ .listItems(
+ items = mutableListOf(
+ "Replace",
+ "Create New",
+ "Skip Duplicate"
+ )
+ ) { _, index, _ ->
+ val resolution = FileCallback.ConflictResolution.values()[index]
+ if (doForAll) {
+ actionForAllConflicts = resolution
+ }
+ action.confirmResolution(resolution)
}
- action.confirmResolution(resolution)
- }
- .show()
- }
+ .show()
+ }
- override fun onCompleted(
- zipFile: DocumentFile,
- targetFolder: DocumentFile,
- decompressionInfo: DecompressionInfo
- ) {
- Toast.makeText(
- applicationContext,
- "Decompressed ${decompressionInfo.totalFilesDecompressed} files from ${zipFile.name}",
- Toast.LENGTH_SHORT
- ).show()
- }
+ override fun onCompleted(
+ zipFile: DocumentFile,
+ targetFolder: DocumentFile,
+ decompressionInfo: DecompressionInfo
+ ) {
+ Toast.makeText(
+ applicationContext,
+ "Decompressed ${decompressionInfo.totalFilesDecompressed} files from ${zipFile.name}",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
- override fun onFailed(errorCode: ErrorCode) {
- Toast.makeText(applicationContext, "$errorCode", Toast.LENGTH_SHORT).show()
- }
- })
+ override fun onFailed(errorCode: ErrorCode) {
+ Toast.makeText(applicationContext, "$errorCode", Toast.LENGTH_SHORT).show()
+ }
+ })
}
}
}
\ No newline at end of file
diff --git a/sample/src/main/java/com/anggrayudi/storage/sample/activity/MainActivity.kt b/sample/src/main/kotlin/com/anggrayudi/storage/sample/activity/MainActivity.kt
similarity index 75%
rename from sample/src/main/java/com/anggrayudi/storage/sample/activity/MainActivity.kt
rename to sample/src/main/kotlin/com/anggrayudi/storage/sample/activity/MainActivity.kt
index 0b84baf..a7a55db 100644
--- a/sample/src/main/java/com/anggrayudi/storage/sample/activity/MainActivity.kt
+++ b/sample/src/main/kotlin/com/anggrayudi/storage/sample/activity/MainActivity.kt
@@ -62,11 +62,18 @@ class MainActivity : AppCompatActivity() {
private val uiScope = CoroutineScope(Dispatchers.Main + job)
private val permissionRequest = ActivityPermissionRequest.Builder(this)
- .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
+ .withPermissions(
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ Manifest.permission.READ_EXTERNAL_STORAGE
+ )
.withCallback(object : PermissionCallback {
override fun onPermissionsChecked(result: PermissionResult, fromSystemDialog: Boolean) {
val grantStatus = if (result.areAllPermissionsGranted) "granted" else "denied"
- Toast.makeText(baseContext, "Storage permissions are $grantStatus", Toast.LENGTH_SHORT).show()
+ Toast.makeText(
+ baseContext,
+ "Storage permissions are $grantStatus",
+ Toast.LENGTH_SHORT
+ ).show()
}
override fun onShouldRedirectToSystemSettings(blockedPermissions: List) {
@@ -131,7 +138,11 @@ class MainActivity : AppCompatActivity() {
}
binding.layoutBaseOperation.btnCreateFile.setOnClickListener {
- storageHelper.createFile("text/plain", "Test create file", requestCode = REQUEST_CODE_CREATE_FILE)
+ storageHelper.createFile(
+ "text/plain",
+ "Test create file",
+ requestCode = REQUEST_CODE_CREATE_FILE
+ )
}
binding.btnCompressFiles.setOnClickListener {
@@ -160,17 +171,32 @@ class MainActivity : AppCompatActivity() {
storageHelper.onStorageAccessGranted = { _, root ->
Toast.makeText(
this,
- getString(com.anggrayudi.storage.R.string.ss_selecting_root_path_success_without_open_folder_picker, root.getAbsolutePath(this)),
+ getString(
+ com.anggrayudi.storage.R.string.ss_selecting_root_path_success_without_open_folder_picker,
+ root.getAbsolutePath(this)
+ ),
Toast.LENGTH_SHORT
).show()
}
storageHelper.onFileSelected = { requestCode, files ->
val file = files.first()
when (requestCode) {
- REQUEST_CODE_PICK_SOURCE_FILE_FOR_COPY -> binding.layoutCopySrcFile.tvFilePath.updateFileSelectionView(file)
- REQUEST_CODE_PICK_SOURCE_FILE_FOR_MOVE -> binding.layoutMoveSrcFile.tvFilePath.updateFileSelectionView(file)
- REQUEST_CODE_PICK_SOURCE_FILE_FOR_MULTIPLE_COPY -> binding.layoutCopyMultipleFilesSourceFile.tvFilePath.updateFileSelectionView(file)
- REQUEST_CODE_PICK_SOURCE_FILE_FOR_MULTIPLE_MOVE -> binding.layoutMoveMultipleFilesSourceFile.tvFilePath.updateFileSelectionView(file)
+ REQUEST_CODE_PICK_SOURCE_FILE_FOR_COPY -> binding.layoutCopySrcFile.tvFilePath.updateFileSelectionView(
+ file
+ )
+
+ REQUEST_CODE_PICK_SOURCE_FILE_FOR_MOVE -> binding.layoutMoveSrcFile.tvFilePath.updateFileSelectionView(
+ file
+ )
+
+ REQUEST_CODE_PICK_SOURCE_FILE_FOR_MULTIPLE_COPY -> binding.layoutCopyMultipleFilesSourceFile.tvFilePath.updateFileSelectionView(
+ file
+ )
+
+ REQUEST_CODE_PICK_SOURCE_FILE_FOR_MULTIPLE_MOVE -> binding.layoutMoveMultipleFilesSourceFile.tvFilePath.updateFileSelectionView(
+ file
+ )
+
REQUEST_CODE_PICK_FILE_FOR_RENAME -> renameFile(file)
REQUEST_CODE_PICK_FILE_FOR_DELETE -> deleteFiles(files)
else -> {
@@ -181,24 +207,51 @@ class MainActivity : AppCompatActivity() {
}
storageHelper.onFolderSelected = { requestCode, folder ->
when (requestCode) {
- REQUEST_CODE_PICK_TARGET_FOLDER_FOR_FILE_COPY -> binding.layoutCopyFileTargetFolder.tvFilePath.updateFolderSelectionView(folder)
- REQUEST_CODE_PICK_TARGET_FOLDER_FOR_FILE_MOVE -> binding.layoutMoveFileTargetFolder.tvFilePath.updateFolderSelectionView(folder)
- REQUEST_CODE_PICK_SOURCE_FOLDER_FOR_COPY -> binding.layoutCopyFolderSrcFolder.tvFilePath.updateFolderSelectionView(folder)
- REQUEST_CODE_PICK_TARGET_FOLDER_FOR_FOLDER_COPY -> binding.layoutCopyFolderTargetFolder.tvFilePath.updateFolderSelectionView(folder)
- REQUEST_CODE_PICK_SOURCE_FOLDER_FOR_MOVE -> binding.layoutMoveFolderSrcFolder.tvFilePath.updateFolderSelectionView(folder)
- REQUEST_CODE_PICK_TARGET_FOLDER_FOR_FOLDER_MOVE -> binding.layoutMoveFolderTargetFolder.tvFilePath.updateFolderSelectionView(folder)
-
- REQUEST_CODE_PICK_SOURCE_FOLDER_FOR_MULTIPLE_COPY -> binding.layoutCopyMultipleFilesSourceFolder.tvFilePath.updateFolderSelectionView(folder)
+ REQUEST_CODE_PICK_TARGET_FOLDER_FOR_FILE_COPY -> binding.layoutCopyFileTargetFolder.tvFilePath.updateFolderSelectionView(
+ folder
+ )
+
+ REQUEST_CODE_PICK_TARGET_FOLDER_FOR_FILE_MOVE -> binding.layoutMoveFileTargetFolder.tvFilePath.updateFolderSelectionView(
+ folder
+ )
+
+ REQUEST_CODE_PICK_SOURCE_FOLDER_FOR_COPY -> binding.layoutCopyFolderSrcFolder.tvFilePath.updateFolderSelectionView(
+ folder
+ )
+
+ REQUEST_CODE_PICK_TARGET_FOLDER_FOR_FOLDER_COPY -> binding.layoutCopyFolderTargetFolder.tvFilePath.updateFolderSelectionView(
+ folder
+ )
+
+ REQUEST_CODE_PICK_SOURCE_FOLDER_FOR_MOVE -> binding.layoutMoveFolderSrcFolder.tvFilePath.updateFolderSelectionView(
+ folder
+ )
+
+ REQUEST_CODE_PICK_TARGET_FOLDER_FOR_FOLDER_MOVE -> binding.layoutMoveFolderTargetFolder.tvFilePath.updateFolderSelectionView(
+ folder
+ )
+
+ REQUEST_CODE_PICK_SOURCE_FOLDER_FOR_MULTIPLE_COPY -> binding.layoutCopyMultipleFilesSourceFolder.tvFilePath.updateFolderSelectionView(
+ folder
+ )
+
REQUEST_CODE_PICK_TARGET_FOLDER_FOR_MULTIPLE_FILE_COPY -> binding.layoutCopyMultipleFilesTargetFolder.tvFilePath.updateFolderSelectionView(
folder
)
- REQUEST_CODE_PICK_SOURCE_FOLDER_FOR_MULTIPLE_MOVE -> binding.layoutMoveMultipleFilesSourceFolder.tvFilePath.updateFolderSelectionView(folder)
+ REQUEST_CODE_PICK_SOURCE_FOLDER_FOR_MULTIPLE_MOVE -> binding.layoutMoveMultipleFilesSourceFolder.tvFilePath.updateFolderSelectionView(
+ folder
+ )
+
REQUEST_CODE_PICK_TARGET_FOLDER_FOR_MULTIPLE_FILE_MOVE -> binding.layoutMoveMultipleFilesTargetFolder.tvFilePath.updateFolderSelectionView(
folder
)
- else -> Toast.makeText(baseContext, folder.getAbsolutePath(this), Toast.LENGTH_SHORT).show()
+ else -> Toast.makeText(
+ baseContext,
+ folder.getAbsolutePath(this),
+ Toast.LENGTH_SHORT
+ ).show()
}
}
storageHelper.onFileCreated = { requestCode, file ->
@@ -226,7 +279,8 @@ class MainActivity : AppCompatActivity() {
ioScope.launch {
val newName = file.changeName(baseContext, text.toString())?.name
uiScope.launch {
- val message = if (newName != null) "File renamed to $newName" else "Failed to rename ${file.fullName}"
+ val message =
+ if (newName != null) "File renamed to $newName" else "Failed to rename ${file.fullName}"
Toast.makeText(baseContext, message, Toast.LENGTH_SHORT).show()
}
}
@@ -239,7 +293,11 @@ class MainActivity : AppCompatActivity() {
ioScope.launch {
val deleted = files.count { it.delete() }
uiScope.launch {
- Toast.makeText(baseContext, "Deleted $deleted of ${files.size} files", Toast.LENGTH_SHORT).show()
+ Toast.makeText(
+ baseContext,
+ "Deleted $deleted of ${files.size} files",
+ Toast.LENGTH_SHORT
+ ).show()
}
}
}
@@ -265,22 +323,33 @@ class MainActivity : AppCompatActivity() {
storageHelper.openFolderPicker(REQUEST_CODE_PICK_TARGET_FOLDER_FOR_MULTIPLE_FILE_COPY)
}
binding.btnStartCopyMultipleFiles.setOnClickListener {
- val targetFolder = binding.layoutCopyMultipleFilesTargetFolder.tvFilePath.tag as? DocumentFile
+ val targetFolder =
+ binding.layoutCopyMultipleFilesTargetFolder.tvFilePath.tag as? DocumentFile
if (targetFolder == null) {
Toast.makeText(this, "Please select target folder", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
- val sourceFolder = binding.layoutCopyMultipleFilesSourceFolder.tvFilePath.tag as? DocumentFile
- val sourceFile = binding.layoutCopyMultipleFilesSourceFile.tvFilePath.tag as? DocumentFile
+ val sourceFolder =
+ binding.layoutCopyMultipleFilesSourceFolder.tvFilePath.tag as? DocumentFile
+ val sourceFile =
+ binding.layoutCopyMultipleFilesSourceFile.tvFilePath.tag as? DocumentFile
val sources = listOfNotNull(sourceFolder, sourceFile)
if (sources.isEmpty()) {
- Toast.makeText(this, "Please select the source file and/or folder", Toast.LENGTH_SHORT).show()
+ Toast.makeText(
+ this,
+ "Please select the source file and/or folder",
+ Toast.LENGTH_SHORT
+ ).show()
return@setOnClickListener
}
Toast.makeText(this, "Copying...", Toast.LENGTH_SHORT).show()
ioScope.launch {
- sources.copyTo(applicationContext, targetFolder, callback = createMultipleFileCallback(false))
+ sources.copyTo(
+ applicationContext,
+ targetFolder,
+ callback = createMultipleFileCallback(false)
+ )
}
}
}
@@ -296,62 +365,83 @@ class MainActivity : AppCompatActivity() {
storageHelper.openFolderPicker(REQUEST_CODE_PICK_TARGET_FOLDER_FOR_MULTIPLE_FILE_MOVE)
}
binding.btnStartMoveMultipleFiles.setOnClickListener {
- val targetFolder = binding.layoutMoveMultipleFilesTargetFolder.tvFilePath.tag as? DocumentFile
+ val targetFolder =
+ binding.layoutMoveMultipleFilesTargetFolder.tvFilePath.tag as? DocumentFile
if (targetFolder == null) {
Toast.makeText(this, "Please select target folder", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
- val sourceFolder = binding.layoutMoveMultipleFilesSourceFolder.tvFilePath.tag as? DocumentFile
- val sourceFile = binding.layoutMoveMultipleFilesSourceFile.tvFilePath.tag as? DocumentFile
+ val sourceFolder =
+ binding.layoutMoveMultipleFilesSourceFolder.tvFilePath.tag as? DocumentFile
+ val sourceFile =
+ binding.layoutMoveMultipleFilesSourceFile.tvFilePath.tag as? DocumentFile
val sources = listOfNotNull(sourceFolder, sourceFile)
if (sources.isEmpty()) {
- Toast.makeText(this, "Please select the source file and/or folder", Toast.LENGTH_SHORT).show()
+ Toast.makeText(
+ this,
+ "Please select the source file and/or folder",
+ Toast.LENGTH_SHORT
+ ).show()
return@setOnClickListener
}
Toast.makeText(this, "Moving...", Toast.LENGTH_SHORT).show()
ioScope.launch {
- sources.moveTo(applicationContext, targetFolder, callback = createMultipleFileCallback(true))
+ sources.moveTo(
+ applicationContext,
+ targetFolder,
+ callback = createMultipleFileCallback(true)
+ )
}
}
}
- private fun createMultipleFileCallback(isMoveFileMode: Boolean) = object : MultipleFileCallback(uiScope) {
- val mode = if (isMoveFileMode) "Moved" else "Copied"
+ private fun createMultipleFileCallback(isMoveFileMode: Boolean) =
+ object : MultipleFileCallback(uiScope) {
+ val mode = if (isMoveFileMode) "Moved" else "Copied"
- override fun onStart(files: List, totalFilesToCopy: Int, workerThread: Thread): Long {
- return 1000 // update progress every 1 second
- }
+ override fun onStart(
+ files: List,
+ totalFilesToCopy: Int,
+ workerThread: Thread
+ ): Long {
+ return 1000 // update progress every 1 second
+ }
- override fun onParentConflict(
- destinationParentFolder: DocumentFile,
- conflictedFolders: MutableList,
- conflictedFiles: MutableList,
- action: ParentFolderConflictAction
- ) {
- handleParentFolderConflict(conflictedFolders, conflictedFiles, action)
- }
+ override fun onParentConflict(
+ destinationParentFolder: DocumentFile,
+ conflictedFolders: MutableList,
+ conflictedFiles: MutableList,
+ action: ParentFolderConflictAction
+ ) {
+ handleParentFolderConflict(conflictedFolders, conflictedFiles, action)
+ }
- override fun onContentConflict(
- destinationParentFolder: DocumentFile,
- conflictedFiles: MutableList,
- action: FolderCallback.FolderContentConflictAction
- ) {
- handleFolderContentConflict(action, conflictedFiles)
- }
+ override fun onContentConflict(
+ destinationParentFolder: DocumentFile,
+ conflictedFiles: MutableList,
+ action: FolderCallback.FolderContentConflictAction
+ ) {
+ handleFolderContentConflict(action, conflictedFiles)
+ }
- override fun onReport(report: Report) {
- Timber.d("onReport() -> ${report.progress.toInt()}% | $mode ${report.fileCount} files")
- }
+ override fun onReport(report: Report) {
+ Timber.d("onReport() -> ${report.progress.toInt()}% | $mode ${report.fileCount} files")
+ }
- override fun onCompleted(result: Result) {
- Toast.makeText(baseContext, "$mode ${result.totalCopiedFiles} of ${result.totalFilesToCopy} files", Toast.LENGTH_SHORT).show()
- }
+ override fun onCompleted(result: Result) {
+ Toast.makeText(
+ baseContext,
+ "$mode ${result.totalCopiedFiles} of ${result.totalFilesToCopy} files",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
- override fun onFailed(errorCode: ErrorCode) {
- Toast.makeText(baseContext, "An error has occurred: $errorCode", Toast.LENGTH_SHORT).show()
+ override fun onFailed(errorCode: ErrorCode) {
+ Toast.makeText(baseContext, "An error has occurred: $errorCode", Toast.LENGTH_SHORT)
+ .show()
+ }
}
- }
private fun setupFolderCopy() {
binding.layoutCopyFolderSrcFolder.btnBrowse.setOnClickListener {
@@ -373,7 +463,12 @@ class MainActivity : AppCompatActivity() {
}
Toast.makeText(this, "Copying...", Toast.LENGTH_SHORT).show()
ioScope.launch {
- folder.copyFolderTo(applicationContext, targetFolder, false, callback = createFolderCallback(false))
+ folder.copyFolderTo(
+ applicationContext,
+ targetFolder,
+ false,
+ callback = createFolderCallback(false)
+ )
}
}
}
@@ -398,7 +493,12 @@ class MainActivity : AppCompatActivity() {
}
Toast.makeText(this, "Moving...", Toast.LENGTH_SHORT).show()
ioScope.launch {
- folder.moveFolderTo(applicationContext, targetFolder, false, callback = createFolderCallback(true))
+ folder.moveFolderTo(
+ applicationContext,
+ targetFolder,
+ false,
+ callback = createFolderCallback(true)
+ )
}
}
}
@@ -414,11 +514,19 @@ class MainActivity : AppCompatActivity() {
// Inform user that the app is counting & calculating files
}
- override fun onStart(folder: DocumentFile, totalFilesToCopy: Int, workerThread: Thread): Long {
+ override fun onStart(
+ folder: DocumentFile,
+ totalFilesToCopy: Int,
+ workerThread: Thread
+ ): Long {
return 1000 // update progress every 1 second
}
- override fun onParentConflict(destinationFolder: DocumentFile, action: ParentFolderConflictAction, canMerge: Boolean) {
+ override fun onParentConflict(
+ destinationFolder: DocumentFile,
+ action: ParentFolderConflictAction,
+ canMerge: Boolean
+ ) {
handleParentFolderConflict(destinationFolder, action, canMerge)
}
@@ -435,11 +543,16 @@ class MainActivity : AppCompatActivity() {
}
override fun onCompleted(result: Result) {
- Toast.makeText(baseContext, "$mode ${result.totalCopiedFiles} of ${result.totalFilesToCopy} files", Toast.LENGTH_SHORT).show()
+ Toast.makeText(
+ baseContext,
+ "$mode ${result.totalCopiedFiles} of ${result.totalFilesToCopy} files",
+ Toast.LENGTH_SHORT
+ ).show()
}
override fun onFailed(errorCode: ErrorCode) {
- Toast.makeText(baseContext, "An error has occurred: $errorCode", Toast.LENGTH_SHORT).show()
+ Toast.makeText(baseContext, "An error has occurred: $errorCode", Toast.LENGTH_SHORT)
+ .show()
}
}
@@ -510,13 +623,15 @@ class MainActivity : AppCompatActivity() {
.cancelable(false)
.positiveButton(android.R.string.cancel) { workerThread.interrupt() }
.customView(R.layout.dialog_copy_progress).apply {
- tvStatus = getCustomView().findViewById(R.id.tvProgressStatus).apply {
- text = "Copying file: 0%"
- }
-
- progressBar = getCustomView().findViewById(R.id.progressCopy).apply {
- isIndeterminate = true
- }
+ tvStatus =
+ getCustomView().findViewById(R.id.tvProgressStatus).apply {
+ text = context.getString(R.string.copying_file, 0)
+ }
+
+ progressBar =
+ getCustomView().findViewById(R.id.progressCopy).apply {
+ isIndeterminate = true
+ }
show()
}
}
@@ -524,17 +639,18 @@ class MainActivity : AppCompatActivity() {
}
override fun onReport(report: Report) {
- tvStatus?.text = "Copying file: ${report.progress.toInt()}%"
+ tvStatus?.text = getString(R.string.copying_file, report.progress.toInt())
progressBar?.isIndeterminate = false
progressBar?.progress = report.progress.toInt()
}
override fun onFailed(errorCode: ErrorCode) {
dialog?.dismiss()
- Toast.makeText(baseContext, "Failed copying file: $errorCode", Toast.LENGTH_SHORT).show()
+ Toast.makeText(baseContext, "Failed copying file: $errorCode", Toast.LENGTH_SHORT)
+ .show()
}
- override fun onCompleted(result: Any) {
+ override fun onCompleted(result: FileCallback.Result) {
dialog?.dismiss()
Toast.makeText(baseContext, "File copied successfully", Toast.LENGTH_SHORT).show()
}
@@ -582,8 +698,15 @@ class MainActivity : AppCompatActivity() {
.title(text = "Conflict Found")
.message(text = "Folder \"${currentSolution.target.name}\" already exists in destination. What's your action?")
.checkBoxPrompt(text = "Apply to all") { doForAll = it }
- .listItems(items = mutableListOf("Replace", "Merge", "Create New", "Skip Duplicate").apply { if (!canMerge) remove("Merge") }) { _, index, _ ->
- currentSolution.solution = FolderCallback.ConflictResolution.values()[if (!canMerge && index > 0) index + 1 else index]
+ .listItems(
+ items = mutableListOf(
+ "Replace",
+ "Merge",
+ "Create New",
+ "Skip Duplicate"
+ ).apply { if (!canMerge) remove("Merge") }) { _, index, _ ->
+ currentSolution.solution =
+ FolderCallback.ConflictResolution.values()[if (!canMerge && index > 0) index + 1 else index]
newSolution.add(currentSolution)
if (doForAll) {
conflictedFolders.forEach { it.solution = currentSolution.solution }
@@ -613,8 +736,15 @@ class MainActivity : AppCompatActivity() {
.title(text = "Conflict Found")
.message(text = "File \"${currentSolution.target.name}\" already exists in destination. What's your action?")
.checkBoxPrompt(text = "Apply to all") { doForAll = it }
- .listItems(items = mutableListOf("Replace", "Create New", "Skip Duplicate")) { _, index, _ ->
- currentSolution.solution = FolderCallback.ConflictResolution.values()[if (index > 0) index + 1 else index]
+ .listItems(
+ items = mutableListOf(
+ "Replace",
+ "Create New",
+ "Skip Duplicate"
+ )
+ ) { _, index, _ ->
+ currentSolution.solution =
+ FolderCallback.ConflictResolution.values()[if (index > 0) index + 1 else index]
newSolution.add(currentSolution)
if (doForAll) {
conflictedFiles.forEach { it.solution = currentSolution.solution }
@@ -627,22 +757,37 @@ class MainActivity : AppCompatActivity() {
.show()
}
- private fun handleParentFolderConflict(destinationFolder: DocumentFile, action: FolderCallback.ParentFolderConflictAction, canMerge: Boolean) {
+ private fun handleParentFolderConflict(
+ destinationFolder: DocumentFile,
+ action: FolderCallback.ParentFolderConflictAction,
+ canMerge: Boolean
+ ) {
MaterialDialog(this)
.cancelable(false)
.title(text = "Conflict Found")
.message(text = "Folder \"${destinationFolder.name}\" already exists in destination. What's your action?")
- .listItems(items = mutableListOf("Replace", "Merge", "Create New", "Skip Duplicate").apply { if (!canMerge) remove("Merge") }) { _, index, _ ->
- val resolution = FolderCallback.ConflictResolution.values()[if (!canMerge && index > 0) index + 1 else index]
+ .listItems(
+ items = mutableListOf(
+ "Replace",
+ "Merge",
+ "Create New",
+ "Skip Duplicate"
+ ).apply { if (!canMerge) remove("Merge") }) { _, index, _ ->
+ val resolution =
+ FolderCallback.ConflictResolution.values()[if (!canMerge && index > 0) index + 1 else index]
action.confirmResolution(resolution)
if (resolution == FolderCallback.ConflictResolution.SKIP) {
- Toast.makeText(this, "Skipped duplicate folders & files", Toast.LENGTH_SHORT).show()
+ Toast.makeText(this, "Skipped duplicate folders & files", Toast.LENGTH_SHORT)
+ .show()
}
}
.show()
}
- private fun handleFolderContentConflict(action: FolderCallback.FolderContentConflictAction, conflictedFiles: MutableList) {
+ private fun handleFolderContentConflict(
+ action: FolderCallback.FolderContentConflictAction,
+ conflictedFiles: MutableList
+ ) {
val newSolution = ArrayList(conflictedFiles.size)
askSolution(action, conflictedFiles, newSolution)
}
@@ -694,9 +839,12 @@ class MainActivity : AppCompatActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.main, menu)
- menu.findItem(R.id.action_open_fragment).intent = Intent(this, SampleFragmentActivity::class.java)
- menu.findItem(R.id.action_pref_save_location).intent = Intent(this, SettingsActivity::class.java)
- menu.findItem(R.id.action_settings).intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:$packageName"))
+ menu.findItem(R.id.action_open_fragment).intent =
+ Intent(this, SampleFragmentActivity::class.java)
+ menu.findItem(R.id.action_pref_save_location).intent =
+ Intent(this, SettingsActivity::class.java)
+ menu.findItem(R.id.action_settings).intent =
+ Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:$packageName"))
menu.findItem(R.id.action_about).intent = Intent(
Intent.ACTION_VIEW,
Uri.parse("https://github.com/anggrayudi/SimpleStorage")
@@ -714,7 +862,12 @@ class MainActivity : AppCompatActivity() {
0 -> "https://www.paypal.com/paypalme/hardiannicko"
else -> "https://saweria.co/hardiannicko"
}
- startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
+ startActivity(
+ Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse(url)
+ ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ )
}
.show()
}
@@ -759,9 +912,14 @@ class MainActivity : AppCompatActivity() {
thread {
file.openOutputStream(context)?.use {
try {
- @Suppress("BlockingMethodInNonBlockingContext")
it.write("Welcome to SimpleStorage!\nRequest code: $requestCode\nTime: ${System.currentTimeMillis()}".toByteArray())
- launchOnUiThread { Toast.makeText(context, "Successfully created file \"${file.name}\"", Toast.LENGTH_SHORT).show() }
+ launchOnUiThread {
+ Toast.makeText(
+ context,
+ "Successfully created file \"${file.name}\"",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
} catch (e: IOException) {
e.printStackTrace()
}
diff --git a/sample/src/main/java/com/anggrayudi/storage/sample/activity/SampleFragmentActivity.kt b/sample/src/main/kotlin/com/anggrayudi/storage/sample/activity/SampleFragmentActivity.kt
similarity index 100%
rename from sample/src/main/java/com/anggrayudi/storage/sample/activity/SampleFragmentActivity.kt
rename to sample/src/main/kotlin/com/anggrayudi/storage/sample/activity/SampleFragmentActivity.kt
diff --git a/sample/src/main/java/com/anggrayudi/storage/sample/activity/SettingsActivity.kt b/sample/src/main/kotlin/com/anggrayudi/storage/sample/activity/SettingsActivity.kt
similarity index 100%
rename from sample/src/main/java/com/anggrayudi/storage/sample/activity/SettingsActivity.kt
rename to sample/src/main/kotlin/com/anggrayudi/storage/sample/activity/SettingsActivity.kt
diff --git a/sample/src/main/java/com/anggrayudi/storage/sample/fragment/SampleFragment.kt b/sample/src/main/kotlin/com/anggrayudi/storage/sample/fragment/SampleFragment.kt
similarity index 73%
rename from sample/src/main/java/com/anggrayudi/storage/sample/fragment/SampleFragment.kt
rename to sample/src/main/kotlin/com/anggrayudi/storage/sample/fragment/SampleFragment.kt
index bdcb32f..0bc8559 100644
--- a/sample/src/main/java/com/anggrayudi/storage/sample/fragment/SampleFragment.kt
+++ b/sample/src/main/kotlin/com/anggrayudi/storage/sample/fragment/SampleFragment.kt
@@ -11,7 +11,11 @@ import com.afollestad.materialdialogs.MaterialDialog
import com.anggrayudi.storage.SimpleStorageHelper
import com.anggrayudi.storage.file.fullName
import com.anggrayudi.storage.file.getAbsolutePath
-import com.anggrayudi.storage.permission.*
+import com.anggrayudi.storage.permission.FragmentPermissionRequest
+import com.anggrayudi.storage.permission.PermissionCallback
+import com.anggrayudi.storage.permission.PermissionReport
+import com.anggrayudi.storage.permission.PermissionRequest
+import com.anggrayudi.storage.permission.PermissionResult
import com.anggrayudi.storage.sample.R
import com.anggrayudi.storage.sample.activity.MainActivity
import com.anggrayudi.storage.sample.databinding.InclBaseOperationBinding
@@ -24,11 +28,18 @@ class SampleFragment : Fragment(R.layout.incl_base_operation) {
// In Fragment, build permissionRequest before onCreate() is called
private val permissionRequest = FragmentPermissionRequest.Builder(this)
- .withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
+ .withPermissions(
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ Manifest.permission.READ_EXTERNAL_STORAGE
+ )
.withCallback(object : PermissionCallback {
override fun onPermissionsChecked(result: PermissionResult, fromSystemDialog: Boolean) {
val grantStatus = if (result.areAllPermissionsGranted) "granted" else "denied"
- Toast.makeText(requireContext(), "Storage permissions are $grantStatus", Toast.LENGTH_SHORT).show()
+ Toast.makeText(
+ requireContext(),
+ "Storage permissions are $grantStatus",
+ Toast.LENGTH_SHORT
+ ).show()
}
override fun onDisplayConsentDialog(request: PermissionRequest) {
@@ -86,17 +97,29 @@ class SampleFragment : Fragment(R.layout.incl_base_operation) {
}
binding.btnCreateFile.setOnClickListener {
- storageHelper.createFile("text/plain", "Test create file", requestCode = MainActivity.REQUEST_CODE_CREATE_FILE)
+ storageHelper.createFile(
+ "text/plain",
+ "Test create file",
+ requestCode = MainActivity.REQUEST_CODE_CREATE_FILE
+ )
}
}
private fun setupSimpleStorage(savedInstanceState: Bundle?) {
storageHelper = SimpleStorageHelper(this, savedInstanceState)
- storageHelper.onFileSelected = { requestCode, files ->
- Toast.makeText(requireContext(), "File selected: ${files.first().fullName}", Toast.LENGTH_SHORT).show()
+ storageHelper.onFileSelected = { _, files ->
+ Toast.makeText(
+ requireContext(),
+ "File selected: ${files.first().fullName}",
+ Toast.LENGTH_SHORT
+ ).show()
}
- storageHelper.onFolderSelected = { requestCode, folder ->
- Toast.makeText(requireContext(), folder.getAbsolutePath(requireContext()), Toast.LENGTH_SHORT).show()
+ storageHelper.onFolderSelected = { _, folder ->
+ Toast.makeText(
+ requireContext(),
+ folder.getAbsolutePath(requireContext()),
+ Toast.LENGTH_SHORT
+ ).show()
}
storageHelper.onFileCreated = { requestCode, file ->
MainActivity.writeTestFile(requireContext().applicationContext, requestCode, file)
diff --git a/sample/src/main/res/layout/dialog_copy_progress.xml b/sample/src/main/res/layout/dialog_copy_progress.xml
index ea2012a..bd40b2f 100644
--- a/sample/src/main/res/layout/dialog_copy_progress.xml
+++ b/sample/src/main/res/layout/dialog_copy_progress.xml
@@ -4,8 +4,8 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:paddingHorizontal="24dp"
- android:orientation="vertical">
+ android:orientation="vertical"
+ android:paddingHorizontal="24dp">
Simple Storage
+ Copying file: %1$s%
\ No newline at end of file
diff --git a/sample/src/test/java/com/anggrayudi/storage/sample/ExampleUnitTest.kt b/sample/src/test/java/com/anggrayudi/storage/sample/ExampleUnitTest.kt
deleted file mode 100644
index cfee345..0000000
--- a/sample/src/test/java/com/anggrayudi/storage/sample/ExampleUnitTest.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.anggrayudi.storage.sample
-
-import org.junit.Assert.assertEquals
-import org.junit.Test
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-class ExampleUnitTest {
-
- @Test
- fun addition_isCorrect() {
- assertEquals(4, 2 + 2)
- }
-}
\ No newline at end of file
diff --git a/storage/build.gradle b/storage/build.gradle
index 1e17cfa..5a45e1b 100644
--- a/storage/build.gradle
+++ b/storage/build.gradle
@@ -12,7 +12,6 @@ android {
buildTypes {
release {
minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
@@ -26,15 +25,14 @@ android {
}
dependencies {
- api deps.appcompat
- api deps.activity
- api deps.core_ktx
- api deps.fragment
+ implementation deps.appcompat
+ implementation deps.activity
+ implementation deps.core_ktx
+ implementation deps.fragment
api deps.documentfile
- api deps.coroutines.core
- api deps.coroutines.android
- api 'com.afollestad.material-dialogs:files:3.3.0'
- implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1"
+ implementation deps.coroutines.android
+ implementation deps.material_dialogs_files
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3"
testImplementation deps.junit
testImplementation deps.mockk
diff --git a/storage/proguard-rules.pro b/storage/proguard-rules.pro
deleted file mode 100644
index 481bb43..0000000
--- a/storage/proguard-rules.pro
+++ /dev/null
@@ -1,21 +0,0 @@
-# Add project specific ProGuard rules here.
-# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
-#
-# For more details, see
-# http://developer.android.com/guide/developing/tools/proguard.html
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-# public *;
-#}
-
-# Uncomment this to preserve the line number information for
-# debugging stack traces.
-#-keepattributes SourceFile,LineNumberTable
-
-# If you keep the line number information, uncomment this to
-# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/storage/src/main/AndroidManifest.xml b/storage/src/main/AndroidManifest.xml
index b278484..e04e3a7 100644
--- a/storage/src/main/AndroidManifest.xml
+++ b/storage/src/main/AndroidManifest.xml
@@ -1,7 +1,8 @@
-
- get() = when (this) {
- IMAGE -> ImageMediaDirectory.values().map { Environment.getExternalStoragePublicDirectory(it.folderName) }
- AUDIO -> AudioMediaDirectory.values().map { Environment.getExternalStoragePublicDirectory(it.folderName) }
- VIDEO -> VideoMediaDirectory.values().map { Environment.getExternalStoragePublicDirectory(it.folderName) }
- DOWNLOADS -> listOf(PublicDirectory.DOWNLOADS.file)
- }
-
- val mimeType: String
- get() = when (this) {
- IMAGE -> MimeType.IMAGE
- AUDIO -> MimeType.AUDIO
- VIDEO -> MimeType.VIDEO
- else -> MimeType.UNKNOWN
- }
-}
\ No newline at end of file
diff --git a/storage/src/main/java/com/anggrayudi/storage/media/VideoMediaDirectory.kt b/storage/src/main/java/com/anggrayudi/storage/media/VideoMediaDirectory.kt
deleted file mode 100644
index daaebb8..0000000
--- a/storage/src/main/java/com/anggrayudi/storage/media/VideoMediaDirectory.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.anggrayudi.storage.media
-
-import android.os.Environment
-
-/**
- * Created on 06/09/20
- * @author Anggrayudi H
- */
-enum class VideoMediaDirectory(val folderName: String) {
- MOVIES(Environment.DIRECTORY_MOVIES),
- DCIM(Environment.DIRECTORY_DCIM)
-}
\ No newline at end of file
diff --git a/storage/src/main/java/com/anggrayudi/storage/ActivityWrapper.kt b/storage/src/main/kotlin/com/anggrayudi/storage/ActivityWrapper.kt
similarity index 100%
rename from storage/src/main/java/com/anggrayudi/storage/ActivityWrapper.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/ActivityWrapper.kt
diff --git a/storage/src/main/java/com/anggrayudi/storage/ComponentActivityWrapper.kt b/storage/src/main/kotlin/com/anggrayudi/storage/ComponentActivityWrapper.kt
similarity index 76%
rename from storage/src/main/java/com/anggrayudi/storage/ComponentActivityWrapper.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/ComponentActivityWrapper.kt
index f8e65bb..a454708 100644
--- a/storage/src/main/java/com/anggrayudi/storage/ComponentActivityWrapper.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/ComponentActivityWrapper.kt
@@ -10,14 +10,16 @@ import androidx.activity.result.contract.ActivityResultContracts
* Created on 18/08/20
* @author Anggrayudi H
*/
-internal class ComponentActivityWrapper(private val _activity: ComponentActivity) : ComponentWrapper {
+internal class ComponentActivityWrapper(private val _activity: ComponentActivity) :
+ ComponentWrapper {
lateinit var storage: SimpleStorage
var requestCode = 0
- private val activityResultLauncher = _activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- storage.onActivityResult(requestCode, it.resultCode, it.data)
- }
+ private val activityResultLauncher =
+ _activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ storage.onActivityResult(requestCode, it.resultCode, it.data)
+ }
override val context: Context
get() = _activity
diff --git a/storage/src/main/java/com/anggrayudi/storage/ComponentWrapper.kt b/storage/src/main/kotlin/com/anggrayudi/storage/ComponentWrapper.kt
similarity index 100%
rename from storage/src/main/java/com/anggrayudi/storage/ComponentWrapper.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/ComponentWrapper.kt
diff --git a/storage/src/main/java/com/anggrayudi/storage/FileWrapper.kt b/storage/src/main/kotlin/com/anggrayudi/storage/FileWrapper.kt
similarity index 81%
rename from storage/src/main/java/com/anggrayudi/storage/FileWrapper.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/FileWrapper.kt
index db607c7..6cac22d 100644
--- a/storage/src/main/java/com/anggrayudi/storage/FileWrapper.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/FileWrapper.kt
@@ -4,7 +4,15 @@ import android.content.Context
import android.net.Uri
import androidx.annotation.WorkerThread
import androidx.documentfile.provider.DocumentFile
-import com.anggrayudi.storage.file.*
+import com.anggrayudi.storage.file.baseName
+import com.anggrayudi.storage.file.extension
+import com.anggrayudi.storage.file.getAbsolutePath
+import com.anggrayudi.storage.file.getBasePath
+import com.anggrayudi.storage.file.getRelativePath
+import com.anggrayudi.storage.file.isEmpty
+import com.anggrayudi.storage.file.mimeTypeByFileName
+import com.anggrayudi.storage.file.openInputStream
+import com.anggrayudi.storage.file.openOutputStream
import com.anggrayudi.storage.media.MediaFile
import java.io.InputStream
import java.io.OutputStream
@@ -71,7 +79,8 @@ interface FileWrapper {
override fun getRelativePath(context: Context): String = mediaFile.relativePath
- override fun openOutputStream(context: Context, append: Boolean): OutputStream? = mediaFile.openOutputStream(append)
+ override fun openOutputStream(context: Context, append: Boolean): OutputStream? =
+ mediaFile.openOutputStream(append)
override fun openInputStream(context: Context): InputStream? = mediaFile.openInputStream()
@@ -97,15 +106,19 @@ interface FileWrapper {
override fun isEmpty(context: Context): Boolean = documentFile.isEmpty(context)
- override fun getAbsolutePath(context: Context): String = documentFile.getAbsolutePath(context)
+ override fun getAbsolutePath(context: Context): String =
+ documentFile.getAbsolutePath(context)
override fun getBasePath(context: Context): String = documentFile.getBasePath(context)
- override fun getRelativePath(context: Context): String = documentFile.getRelativePath(context)
+ override fun getRelativePath(context: Context): String =
+ documentFile.getRelativePath(context)
- override fun openOutputStream(context: Context, append: Boolean): OutputStream? = documentFile.openOutputStream(context, append)
+ override fun openOutputStream(context: Context, append: Boolean): OutputStream? =
+ documentFile.openOutputStream(context, append)
- override fun openInputStream(context: Context): InputStream? = documentFile.openInputStream(context)
+ override fun openInputStream(context: Context): InputStream? =
+ documentFile.openInputStream(context)
override fun delete(): Boolean = documentFile.delete()
}
diff --git a/storage/src/main/java/com/anggrayudi/storage/FragmentWrapper.kt b/storage/src/main/kotlin/com/anggrayudi/storage/FragmentWrapper.kt
similarity index 81%
rename from storage/src/main/java/com/anggrayudi/storage/FragmentWrapper.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/FragmentWrapper.kt
index 5cdea8e..12fc841 100644
--- a/storage/src/main/java/com/anggrayudi/storage/FragmentWrapper.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/FragmentWrapper.kt
@@ -16,9 +16,10 @@ internal class FragmentWrapper(private val fragment: Fragment) : ComponentWrappe
lateinit var storage: SimpleStorage
var requestCode = 0
- private val activityResultLauncher = fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- storage.onActivityResult(requestCode, it.resultCode, it.data)
- }
+ private val activityResultLauncher =
+ fragment.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ storage.onActivityResult(requestCode, it.resultCode, it.data)
+ }
override val context: Context
get() = fragment.requireContext()
diff --git a/storage/src/main/java/com/anggrayudi/storage/SimpleStorage.kt b/storage/src/main/kotlin/com/anggrayudi/storage/SimpleStorage.kt
similarity index 77%
rename from storage/src/main/java/com/anggrayudi/storage/SimpleStorage.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/SimpleStorage.kt
index d20242d..24d37fc 100644
--- a/storage/src/main/java/com/anggrayudi/storage/SimpleStorage.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/SimpleStorage.kt
@@ -23,10 +23,27 @@ import androidx.fragment.app.Fragment
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.callbacks.onCancel
import com.afollestad.materialdialogs.files.folderChooser
-import com.anggrayudi.storage.callback.*
-import com.anggrayudi.storage.extension.*
-import com.anggrayudi.storage.file.*
+import com.anggrayudi.storage.callback.CreateFileCallback
+import com.anggrayudi.storage.callback.FilePickerCallback
+import com.anggrayudi.storage.callback.FileReceiverCallback
+import com.anggrayudi.storage.callback.FolderPickerCallback
+import com.anggrayudi.storage.callback.StorageAccessCallback
+import com.anggrayudi.storage.extension.fromSingleUri
+import com.anggrayudi.storage.extension.fromTreeUri
+import com.anggrayudi.storage.extension.getStorageId
+import com.anggrayudi.storage.extension.isDocumentsDocument
+import com.anggrayudi.storage.extension.isDownloadsDocument
+import com.anggrayudi.storage.extension.isExternalStorageDocument
+import com.anggrayudi.storage.file.DocumentFileCompat
+import com.anggrayudi.storage.file.FileFullPath
+import com.anggrayudi.storage.file.MimeType
+import com.anggrayudi.storage.file.PublicDirectory
import com.anggrayudi.storage.file.StorageId.PRIMARY
+import com.anggrayudi.storage.file.StorageType
+import com.anggrayudi.storage.file.canModify
+import com.anggrayudi.storage.file.getAbsolutePath
+import com.anggrayudi.storage.file.getBasePath
+import com.anggrayudi.storage.file.getStorageId
import java.io.File
import kotlin.concurrent.thread
@@ -41,7 +58,9 @@ class SimpleStorage private constructor(private val wrapper: ComponentWrapper) {
savedState?.let { onRestoreInstanceState(it) }
}
- constructor(activity: ComponentActivity, savedState: Bundle? = null) : this(ComponentActivityWrapper(activity)) {
+ constructor(activity: ComponentActivity, savedState: Bundle? = null) : this(
+ ComponentActivityWrapper(activity)
+ ) {
savedState?.let { onRestoreInstanceState(it) }
(wrapper as ComponentActivityWrapper).storage = this
}
@@ -131,7 +150,8 @@ class SimpleStorage private constructor(private val wrapper: ComponentWrapper) {
* @return `true` if storage permissions and URI permissions are granted for read and write access.
* @see [DocumentFileCompat.getStorageIds]
*/
- fun isStorageAccessGranted(storageId: String) = DocumentFileCompat.isAccessGranted(context, storageId)
+ fun isStorageAccessGranted(storageId: String) =
+ DocumentFileCompat.isAccessGranted(context, storageId)
private var expectedStorageTypeForAccessRequest = StorageType.UNKNOWN
@@ -161,7 +181,8 @@ class SimpleStorage private constructor(private val wrapper: ComponentWrapper) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (hasStoragePermission(context)) {
if (expectedStorageType == StorageType.EXTERNAL && !isSdCardPresent) {
- val root = DocumentFileCompat.getRootDocumentFile(context, PRIMARY, true) ?: return
+ val root =
+ DocumentFileCompat.getRootDocumentFile(context, PRIMARY, true) ?: return
saveUriPermission(root.uri)
storageAccessCallback?.onRootPathPermissionGranted(requestCode, root)
return
@@ -225,7 +246,10 @@ class SimpleStorage private constructor(private val wrapper: ComponentWrapper) {
*/
@SuppressLint("InlinedApi")
@JvmOverloads
- fun openFolderPicker(requestCode: Int = requestCodeFolderPicker, initialPath: FileFullPath? = null) {
+ fun openFolderPicker(
+ requestCode: Int = requestCodeFolderPicker,
+ initialPath: FileFullPath? = null
+ ) {
initialPath?.checkIfStorageIdIsAccessibleInSafSelector()
requestCodeFolderPicker = requestCode
@@ -258,7 +282,6 @@ class SimpleStorage private constructor(private val wrapper: ComponentWrapper) {
}
}
- @Suppress("DEPRECATION")
private var lastVisitedFolder: File = Environment.getExternalStorageDirectory()
/**
@@ -293,7 +316,8 @@ class SimpleStorage private constructor(private val wrapper: ComponentWrapper) {
private fun addInitialPathToIntent(intent: Intent, initialPath: FileFullPath?) {
if (Build.VERSION.SDK_INT >= 26) {
- initialPath?.toDocumentUri(context)?.let { intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it) }
+ initialPath?.toDocumentUri(context)
+ ?.let { intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, it) }
}
}
@@ -304,7 +328,9 @@ class SimpleStorage private constructor(private val wrapper: ComponentWrapper) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
val selectedFolder = context.fromTreeUri(uri) ?: return
if (!expectedStorageTypeForAccessRequest.isExpected(storageType) ||
- !expectedBasePathForAccessRequest.isNullOrEmpty() && selectedFolder.getBasePath(context) != expectedBasePathForAccessRequest
+ !expectedBasePathForAccessRequest.isNullOrEmpty() && selectedFolder.getBasePath(
+ context
+ ) != expectedBasePathForAccessRequest
) {
storageAccessCallback?.onExpectedStorageNotSelected(
requestCode,
@@ -317,14 +343,23 @@ class SimpleStorage private constructor(private val wrapper: ComponentWrapper) {
}
} else if (!expectedStorageTypeForAccessRequest.isExpected(storageType)) {
val rootPath = context.fromTreeUri(uri)?.getAbsolutePath(context).orEmpty()
- storageAccessCallback?.onRootPathNotSelected(requestCode, rootPath, uri, storageType, expectedStorageTypeForAccessRequest)
+ storageAccessCallback?.onRootPathNotSelected(
+ requestCode,
+ rootPath,
+ uri,
+ storageType,
+ expectedStorageTypeForAccessRequest
+ )
return
}
if (uri.isDownloadsDocument) {
if (uri.toString() == DocumentFileCompat.DOWNLOADS_TREE_URI) {
saveUriPermission(uri)
- storageAccessCallback?.onRootPathPermissionGranted(requestCode, context.fromTreeUri(uri) ?: return)
+ storageAccessCallback?.onRootPathPermissionGranted(
+ requestCode,
+ context.fromTreeUri(uri) ?: return
+ )
} else {
storageAccessCallback?.onRootPathNotSelected(
requestCode,
@@ -340,7 +375,10 @@ class SimpleStorage private constructor(private val wrapper: ComponentWrapper) {
if (uri.isDocumentsDocument) {
if (uri.toString() == DocumentFileCompat.DOCUMENTS_TREE_URI) {
saveUriPermission(uri)
- storageAccessCallback?.onRootPathPermissionGranted(requestCode, context.fromTreeUri(uri) ?: return)
+ storageAccessCallback?.onRootPathPermissionGranted(
+ requestCode,
+ context.fromTreeUri(uri) ?: return
+ )
} else {
storageAccessCallback?.onRootPathNotSelected(
requestCode,
@@ -354,23 +392,41 @@ class SimpleStorage private constructor(private val wrapper: ComponentWrapper) {
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R && !uri.isExternalStorageDocument) {
- storageAccessCallback?.onRootPathNotSelected(requestCode, externalStoragePath, uri, StorageType.EXTERNAL, expectedStorageTypeForAccessRequest)
+ storageAccessCallback?.onRootPathNotSelected(
+ requestCode,
+ externalStoragePath,
+ uri,
+ StorageType.EXTERNAL,
+ expectedStorageTypeForAccessRequest
+ )
return
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && storageId == PRIMARY) {
saveUriPermission(uri)
- storageAccessCallback?.onRootPathPermissionGranted(requestCode, context.fromTreeUri(uri) ?: return)
+ storageAccessCallback?.onRootPathPermissionGranted(
+ requestCode,
+ context.fromTreeUri(uri) ?: return
+ )
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || DocumentFileCompat.isRootUri(uri)) {
if (saveUriPermission(uri)) {
- storageAccessCallback?.onRootPathPermissionGranted(requestCode, context.fromTreeUri(uri) ?: return)
+ storageAccessCallback?.onRootPathPermissionGranted(
+ requestCode,
+ context.fromTreeUri(uri) ?: return
+ )
} else {
storageAccessCallback?.onStoragePermissionDenied(requestCode)
}
} else {
if (storageId == PRIMARY) {
- storageAccessCallback?.onRootPathNotSelected(requestCode, externalStoragePath, uri, StorageType.EXTERNAL, expectedStorageTypeForAccessRequest)
+ storageAccessCallback?.onRootPathNotSelected(
+ requestCode,
+ externalStoragePath,
+ uri,
+ StorageType.EXTERNAL,
+ expectedStorageTypeForAccessRequest
+ )
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
val sm = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
@@ -382,7 +438,13 @@ class SimpleStorage private constructor(private val wrapper: ComponentWrapper) {
return
}
}
- storageAccessCallback?.onRootPathNotSelected(requestCode, "/storage/$storageId", uri, StorageType.SD_CARD, expectedStorageTypeForAccessRequest)
+ storageAccessCallback?.onRootPathNotSelected(
+ requestCode,
+ "/storage/$storageId",
+ uri,
+ StorageType.SD_CARD,
+ expectedStorageTypeForAccessRequest
+ )
}
}
}
@@ -396,7 +458,8 @@ class SimpleStorage private constructor(private val wrapper: ComponentWrapper) {
folderPickerCallback?.onStorageAccessDenied(requestCode, folder, storageType, storageId)
return
}
- if (uri.toString().let { it == DocumentFileCompat.DOWNLOADS_TREE_URI || it == DocumentFileCompat.DOCUMENTS_TREE_URI }
+ if (uri.toString()
+ .let { it == DocumentFileCompat.DOWNLOADS_TREE_URI || it == DocumentFileCompat.DOCUMENTS_TREE_URI }
|| DocumentFileCompat.isRootUri(uri)
&& (Build.VERSION.SDK_INT < Build.VERSION_CODES.N && storageType == StorageType.SD_CARD || Build.VERSION.SDK_INT == Build.VERSION_CODES.Q)
&& !DocumentFileCompat.isStorageUriPermissionGranted(context, storageId)
@@ -429,8 +492,16 @@ class SimpleStorage private constructor(private val wrapper: ComponentWrapper) {
DocumentFile.fromFile(File(fullPath))
} else context.fromSingleUri(uri)?.let { file ->
// content://com.android.externalstorage.documents/document/15FA-160C%3Aabc.txt
- if (Build.VERSION.SDK_INT < 21 && file.getStorageId(context).matches(DocumentFileCompat.SD_CARD_STORAGE_ID_REGEX)) {
- DocumentFile.fromFile(DocumentFileCompat.getKitkatSdCardRootFile(file.getBasePath(context)))
+ if (Build.VERSION.SDK_INT < 21 && file.getStorageId(context)
+ .matches(DocumentFileCompat.SD_CARD_STORAGE_ID_REGEX)
+ ) {
+ DocumentFile.fromFile(
+ DocumentFileCompat.getKitkatSdCardRootFile(
+ file.getBasePath(
+ context
+ )
+ )
+ )
} else {
file
}
@@ -508,8 +579,14 @@ class SimpleStorage private constructor(private val wrapper: ComponentWrapper) {
fun onSaveInstanceState(outState: Bundle) {
outState.putString(KEY_LAST_VISITED_FOLDER, lastVisitedFolder.path)
- outState.putString(KEY_EXPECTED_BASE_PATH_FOR_ACCESS_REQUEST, expectedBasePathForAccessRequest)
- outState.putInt(KEY_EXPECTED_STORAGE_TYPE_FOR_ACCESS_REQUEST, expectedStorageTypeForAccessRequest.ordinal)
+ outState.putString(
+ KEY_EXPECTED_BASE_PATH_FOR_ACCESS_REQUEST,
+ expectedBasePathForAccessRequest
+ )
+ outState.putInt(
+ KEY_EXPECTED_STORAGE_TYPE_FOR_ACCESS_REQUEST,
+ expectedStorageTypeForAccessRequest.ordinal
+ )
outState.putInt(KEY_REQUEST_CODE_STORAGE_ACCESS, requestCodeStorageAccess)
outState.putInt(KEY_REQUEST_CODE_FOLDER_PICKER, requestCodeFolderPicker)
outState.putInt(KEY_REQUEST_CODE_FILE_PICKER, requestCodeFilePicker)
@@ -521,13 +598,31 @@ class SimpleStorage private constructor(private val wrapper: ComponentWrapper) {
fun onRestoreInstanceState(savedInstanceState: Bundle) {
savedInstanceState.getString(KEY_LAST_VISITED_FOLDER)?.let { lastVisitedFolder = File(it) }
- expectedBasePathForAccessRequest = savedInstanceState.getString(KEY_EXPECTED_BASE_PATH_FOR_ACCESS_REQUEST)
- expectedStorageTypeForAccessRequest = StorageType.values()[savedInstanceState.getInt(KEY_EXPECTED_STORAGE_TYPE_FOR_ACCESS_REQUEST)]
- requestCodeStorageAccess = savedInstanceState.getInt(KEY_REQUEST_CODE_STORAGE_ACCESS, DEFAULT_REQUEST_CODE_STORAGE_ACCESS)
- requestCodeFolderPicker = savedInstanceState.getInt(KEY_REQUEST_CODE_FOLDER_PICKER, DEFAULT_REQUEST_CODE_FOLDER_PICKER)
- requestCodeFilePicker = savedInstanceState.getInt(KEY_REQUEST_CODE_FILE_PICKER, DEFAULT_REQUEST_CODE_FILE_PICKER)
- requestCodeCreateFile = savedInstanceState.getInt(KEY_REQUEST_CODE_CREATE_FILE, DEFAULT_REQUEST_CODE_CREATE_FILE)
- if (wrapper is FragmentWrapper && savedInstanceState.containsKey(KEY_REQUEST_CODE_FRAGMENT_PICKER)) {
+ expectedBasePathForAccessRequest =
+ savedInstanceState.getString(KEY_EXPECTED_BASE_PATH_FOR_ACCESS_REQUEST)
+ expectedStorageTypeForAccessRequest = StorageType.values()[savedInstanceState.getInt(
+ KEY_EXPECTED_STORAGE_TYPE_FOR_ACCESS_REQUEST
+ )]
+ requestCodeStorageAccess = savedInstanceState.getInt(
+ KEY_REQUEST_CODE_STORAGE_ACCESS,
+ DEFAULT_REQUEST_CODE_STORAGE_ACCESS
+ )
+ requestCodeFolderPicker = savedInstanceState.getInt(
+ KEY_REQUEST_CODE_FOLDER_PICKER,
+ DEFAULT_REQUEST_CODE_FOLDER_PICKER
+ )
+ requestCodeFilePicker = savedInstanceState.getInt(
+ KEY_REQUEST_CODE_FILE_PICKER,
+ DEFAULT_REQUEST_CODE_FILE_PICKER
+ )
+ requestCodeCreateFile = savedInstanceState.getInt(
+ KEY_REQUEST_CODE_CREATE_FILE,
+ DEFAULT_REQUEST_CODE_CREATE_FILE
+ )
+ if (wrapper is FragmentWrapper && savedInstanceState.containsKey(
+ KEY_REQUEST_CODE_FRAGMENT_PICKER
+ )
+ ) {
wrapper.requestCode = savedInstanceState.getInt(KEY_REQUEST_CODE_FRAGMENT_PICKER)
}
}
@@ -551,7 +646,8 @@ class SimpleStorage private constructor(private val wrapper: ComponentWrapper) {
}
private fun saveUriPermission(root: Uri) = try {
- val writeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ val writeFlags =
+ Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
context.contentResolver.takePersistableUriPermission(root, writeFlags)
cleanupRedundantUriPermissions(context.applicationContext)
true
@@ -561,14 +657,22 @@ class SimpleStorage private constructor(private val wrapper: ComponentWrapper) {
companion object {
- private const val KEY_REQUEST_CODE_STORAGE_ACCESS = BuildConfig.LIBRARY_PACKAGE_NAME + ".requestCodeStorageAccess"
- private const val KEY_REQUEST_CODE_FOLDER_PICKER = BuildConfig.LIBRARY_PACKAGE_NAME + ".requestCodeFolderPicker"
- private const val KEY_REQUEST_CODE_FILE_PICKER = BuildConfig.LIBRARY_PACKAGE_NAME + ".requestCodeFilePicker"
- private const val KEY_REQUEST_CODE_CREATE_FILE = BuildConfig.LIBRARY_PACKAGE_NAME + ".requestCodeCreateFile"
- private const val KEY_REQUEST_CODE_FRAGMENT_PICKER = BuildConfig.LIBRARY_PACKAGE_NAME + ".requestCodeFragmentPicker"
- private const val KEY_EXPECTED_STORAGE_TYPE_FOR_ACCESS_REQUEST = BuildConfig.LIBRARY_PACKAGE_NAME + ".expectedStorageTypeForAccessRequest"
- private const val KEY_EXPECTED_BASE_PATH_FOR_ACCESS_REQUEST = BuildConfig.LIBRARY_PACKAGE_NAME + ".expectedBasePathForAccessRequest"
- private const val KEY_LAST_VISITED_FOLDER = BuildConfig.LIBRARY_PACKAGE_NAME + ".lastVisitedFolder"
+ private const val KEY_REQUEST_CODE_STORAGE_ACCESS =
+ BuildConfig.LIBRARY_PACKAGE_NAME + ".requestCodeStorageAccess"
+ private const val KEY_REQUEST_CODE_FOLDER_PICKER =
+ BuildConfig.LIBRARY_PACKAGE_NAME + ".requestCodeFolderPicker"
+ private const val KEY_REQUEST_CODE_FILE_PICKER =
+ BuildConfig.LIBRARY_PACKAGE_NAME + ".requestCodeFilePicker"
+ private const val KEY_REQUEST_CODE_CREATE_FILE =
+ BuildConfig.LIBRARY_PACKAGE_NAME + ".requestCodeCreateFile"
+ private const val KEY_REQUEST_CODE_FRAGMENT_PICKER =
+ BuildConfig.LIBRARY_PACKAGE_NAME + ".requestCodeFragmentPicker"
+ private const val KEY_EXPECTED_STORAGE_TYPE_FOR_ACCESS_REQUEST =
+ BuildConfig.LIBRARY_PACKAGE_NAME + ".expectedStorageTypeForAccessRequest"
+ private const val KEY_EXPECTED_BASE_PATH_FOR_ACCESS_REQUEST =
+ BuildConfig.LIBRARY_PACKAGE_NAME + ".expectedBasePathForAccessRequest"
+ private const val KEY_LAST_VISITED_FOLDER =
+ BuildConfig.LIBRARY_PACKAGE_NAME + ".lastVisitedFolder"
private const val TAG = "SimpleStorage"
private const val DEFAULT_REQUEST_CODE_STORAGE_ACCESS: Int = 1
@@ -580,7 +684,6 @@ class SimpleStorage private constructor(private val wrapper: ComponentWrapper) {
const val KITKAT_SD_CARD_PATH = "/storage/$KITKAT_SD_CARD_ID"
@JvmStatic
- @Suppress("DEPRECATION")
val externalStoragePath: String
get() = Environment.getExternalStorageDirectory().absolutePath
@@ -593,7 +696,10 @@ class SimpleStorage private constructor(private val wrapper: ComponentWrapper) {
fun getDefaultExternalStorageIntent(context: Context): Intent {
return Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
if (Build.VERSION.SDK_INT >= 26) {
- putExtra(DocumentsContract.EXTRA_INITIAL_URI, context.fromTreeUri(DocumentFileCompat.createDocumentUri(PRIMARY))?.uri)
+ putExtra(
+ DocumentsContract.EXTRA_INITIAL_URI,
+ context.fromTreeUri(DocumentFileCompat.createDocumentUri(PRIMARY))?.uri
+ )
}
}
}
@@ -603,7 +709,10 @@ class SimpleStorage private constructor(private val wrapper: ComponentWrapper) {
*/
@JvmStatic
fun hasStoragePermission(context: Context): Boolean {
- return checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
+ return checkSelfPermission(
+ context,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE
+ ) == PackageManager.PERMISSION_GRANTED
&& hasStorageReadPermission(context)
}
@@ -612,12 +721,18 @@ class SimpleStorage private constructor(private val wrapper: ComponentWrapper) {
*/
@JvmStatic
fun hasStorageReadPermission(context: Context): Boolean {
- return checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
+ return checkSelfPermission(
+ context,
+ Manifest.permission.READ_EXTERNAL_STORAGE
+ ) == PackageManager.PERMISSION_GRANTED
}
@JvmStatic
fun hasFullDiskAccess(context: Context, storageId: String): Boolean {
- return hasStorageAccess(context, DocumentFileCompat.buildAbsolutePath(context, storageId, ""))
+ return hasStorageAccess(
+ context,
+ DocumentFileCompat.buildAbsolutePath(context, storageId, "")
+ )
}
/**
@@ -631,10 +746,20 @@ class SimpleStorage private constructor(private val wrapper: ComponentWrapper) {
*/
@JvmStatic
@JvmOverloads
- fun hasStorageAccess(context: Context, fullPath: String, requiresWriteAccess: Boolean = true): Boolean {
- return DocumentFileCompat.getAccessibleRootDocumentFile(context, fullPath, requiresWriteAccess) != null
+ fun hasStorageAccess(
+ context: Context,
+ fullPath: String,
+ requiresWriteAccess: Boolean = true
+ ): Boolean {
+ return DocumentFileCompat.getAccessibleRootDocumentFile(
+ context,
+ fullPath,
+ requiresWriteAccess
+ ) != null
&& (Build.VERSION.SDK_INT > Build.VERSION_CODES.P
- || requiresWriteAccess && hasStoragePermission(context) || !requiresWriteAccess && hasStorageReadPermission(context))
+ || requiresWriteAccess && hasStoragePermission(context) || !requiresWriteAccess && hasStorageReadPermission(
+ context
+ ))
}
/**
@@ -653,10 +778,17 @@ class SimpleStorage private constructor(private val wrapper: ComponentWrapper) {
val persistedUris = resolver.persistedUriPermissions
.filter { it.isReadPermission && it.isWritePermission && it.uri.isExternalStorageDocument }
.map { it.uri }
- val writeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
- val uniqueUriParents = DocumentFileCompat.findUniqueParents(context, persistedUris.mapNotNull { it.path?.substringAfter("/tree/") })
+ val writeFlags =
+ Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ val uniqueUriParents = DocumentFileCompat.findUniqueParents(
+ context,
+ persistedUris.mapNotNull { it.path?.substringAfter("/tree/") })
persistedUris.forEach {
- if (DocumentFileCompat.buildAbsolutePath(context, it.path.orEmpty().substringAfter("/tree/")) !in uniqueUriParents) {
+ if (DocumentFileCompat.buildAbsolutePath(
+ context,
+ it.path.orEmpty().substringAfter("/tree/")
+ ) !in uniqueUriParents
+ ) {
resolver.releasePersistableUriPermission(it, writeFlags)
Log.d(TAG, "Removed redundant URI permission => $it")
}
diff --git a/storage/src/main/java/com/anggrayudi/storage/SimpleStorageHelper.kt b/storage/src/main/kotlin/com/anggrayudi/storage/SimpleStorageHelper.kt
similarity index 79%
rename from storage/src/main/java/com/anggrayudi/storage/SimpleStorageHelper.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/SimpleStorageHelper.kt
index 5200e1c..a8717e8 100644
--- a/storage/src/main/java/com/anggrayudi/storage/SimpleStorageHelper.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/SimpleStorageHelper.kt
@@ -38,13 +38,18 @@ class SimpleStorageHelper {
// For unknown Activity type
@JvmOverloads
- constructor(activity: Activity, requestCodeForPermissionDialog: Int, savedState: Bundle? = null) {
+ constructor(
+ activity: Activity,
+ requestCodeForPermissionDialog: Int,
+ savedState: Bundle? = null
+ ) {
storage = SimpleStorage(activity)
init(savedState)
- permissionRequest = ActivityPermissionRequest.Builder(activity, requestCodeForPermissionDialog)
- .withPermissions(*rwPermission)
- .withCallback(permissionCallback)
- .build()
+ permissionRequest =
+ ActivityPermissionRequest.Builder(activity, requestCodeForPermissionDialog)
+ .withPermissions(*rwPermission)
+ .withCallback(permissionCallback)
+ .build()
}
@JvmOverloads
@@ -79,7 +84,12 @@ class SimpleStorageHelper {
}
@SuppressLint("NewApi")
- override fun onStorageAccessDenied(requestCode: Int, folder: DocumentFile?, storageType: StorageType, storageId: String) {
+ override fun onStorageAccessDenied(
+ requestCode: Int,
+ folder: DocumentFile?,
+ storageType: StorageType,
+ storageId: String
+ ) {
if (storageType == StorageType.UNKNOWN) {
onStoragePermissionDenied(requestCode)
return
@@ -89,7 +99,13 @@ class SimpleStorageHelper {
.setMessage(R.string.ss_storage_access_denied_confirm)
.setNegativeButton(android.R.string.cancel) { _, _ -> reset() }
.setPositiveButton(android.R.string.ok) { _, _ ->
- storage.requestStorageAccess(initialPath = FileFullPath(storage.context, storageId, ""))
+ storage.requestStorageAccess(
+ initialPath = FileFullPath(
+ storage.context,
+ storageId,
+ ""
+ )
+ )
}.show()
}
@@ -107,11 +123,15 @@ class SimpleStorageHelper {
}
}
- var onFileSelected: ((requestCode: Int, /* non-empty list */ files: List) -> Unit)? = null
+ var onFileSelected: ((requestCode: Int, /* non-empty list */ files: List) -> Unit)? =
+ null
set(callback) {
field = callback
storage.filePickerCallback = object : FilePickerCallback {
- override fun onStoragePermissionDenied(requestCode: Int, files: List?) {
+ override fun onStoragePermissionDenied(
+ requestCode: Int,
+ files: List?
+ ) {
requestStoragePermission { if (it) storage.openFilePicker() else reset() }
}
@@ -174,7 +194,8 @@ class SimpleStorageHelper {
selectedStorageType: StorageType,
expectedStorageType: StorageType
) {
- val storageType = if (expectedStorageType.isExpected(selectedStorageType)) selectedStorageType else expectedStorageType
+ val storageType =
+ if (expectedStorageType.isExpected(selectedStorageType)) selectedStorageType else expectedStorageType
val messageRes = if (rootPath.isEmpty()) {
storage.context.getString(if (storageType == StorageType.SD_CARD) R.string.ss_please_select_root_storage_sdcard else R.string.ss_please_select_root_storage_primary)
} else {
@@ -188,7 +209,11 @@ class SimpleStorageHelper {
.setNegativeButton(android.R.string.cancel) { _, _ -> reset() }
.setPositiveButton(android.R.string.ok) { _, _ ->
storage.requestStorageAccess(
- initialPath = FileFullPath(storage.context, uri.getStorageId(storage.context), ""),
+ initialPath = FileFullPath(
+ storage.context,
+ uri.getStorageId(storage.context),
+ ""
+ ),
expectedStorageType = expectedStorageType
)
}.show()
@@ -206,24 +231,34 @@ class SimpleStorageHelper {
val toastFilePicker: () -> Unit = {
Toast.makeText(
context,
- context.getString(R.string.ss_selecting_root_path_success_with_open_folder_picker, root.getAbsolutePath(context)),
+ context.getString(
+ R.string.ss_selecting_root_path_success_with_open_folder_picker,
+ root.getAbsolutePath(context)
+ ),
Toast.LENGTH_LONG
).show()
}
when (pickerToOpenOnceGranted) {
TYPE_FILE_PICKER -> {
- storage.openFilePicker(filterMimeTypes = filterMimeTypes.orEmpty().toTypedArray())
+ storage.openFilePicker(
+ filterMimeTypes = filterMimeTypes.orEmpty().toTypedArray()
+ )
toastFilePicker()
}
+
TYPE_FOLDER_PICKER -> {
storage.openFolderPicker()
toastFilePicker()
}
+
else -> {
Toast.makeText(
context,
- context.getString(R.string.ss_selecting_root_path_success_without_open_folder_picker, root.getAbsolutePath(context)),
+ context.getString(
+ R.string.ss_selecting_root_path_success_without_open_folder_picker,
+ root.getAbsolutePath(context)
+ ),
Toast.LENGTH_SHORT
).show()
}
@@ -251,7 +286,11 @@ class SimpleStorageHelper {
.setNegativeButton(android.R.string.cancel) { _, _ -> reset() }
.setPositiveButton(android.R.string.ok) { _, _ ->
storage.requestStorageAccess(
- initialPath = FileFullPath(storage.context, expectedStorageType, expectedBasePath),
+ initialPath = FileFullPath(
+ storage.context,
+ expectedStorageType,
+ expectedBasePath
+ ),
expectedStorageType = expectedStorageType,
expectedBasePath = expectedBasePath
)
@@ -295,7 +334,11 @@ class SimpleStorageHelper {
override fun onPermissionsChecked(result: PermissionResult, fromSystemDialog: Boolean) {
val granted = result.areAllPermissionsGranted
if (!granted) {
- Toast.makeText(storage.context, R.string.ss_please_grant_storage_permission, Toast.LENGTH_SHORT).show()
+ Toast.makeText(
+ storage.context,
+ R.string.ss_please_grant_storage_permission,
+ Toast.LENGTH_SHORT
+ ).show()
}
onPermissionsResult?.invoke(granted)
onPermissionsResult = null
@@ -309,7 +352,10 @@ class SimpleStorageHelper {
}
private val rwPermission: Array
- get() = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
+ get() = arrayOf(
+ Manifest.permission.WRITE_EXTERNAL_STORAGE,
+ Manifest.permission.READ_EXTERNAL_STORAGE
+ )
private fun reset() {
pickerToOpenOnceGranted = 0
@@ -319,11 +365,18 @@ class SimpleStorageHelper {
private fun handleMissingActivityHandler() {
reset()
- Toast.makeText(storage.context, R.string.ss_missing_saf_activity_handler, Toast.LENGTH_SHORT).show()
+ Toast.makeText(
+ storage.context,
+ R.string.ss_missing_saf_activity_handler,
+ Toast.LENGTH_SHORT
+ ).show()
}
@JvmOverloads
- fun openFolderPicker(requestCode: Int = storage.requestCodeFolderPicker, initialPath: FileFullPath? = null) {
+ fun openFolderPicker(
+ requestCode: Int = storage.requestCodeFolderPicker,
+ initialPath: FileFullPath? = null
+ ) {
pickerToOpenOnceGranted = TYPE_FOLDER_PICKER
originalRequestCode = requestCode
storage.openFolderPicker(requestCode, initialPath)
@@ -354,7 +407,12 @@ class SimpleStorageHelper {
) {
pickerToOpenOnceGranted = 0
originalRequestCode = requestCode
- storage.requestStorageAccess(requestCode, initialPath, expectedStorageType, expectedBasePath)
+ storage.requestStorageAccess(
+ requestCode,
+ initialPath,
+ expectedStorageType,
+ expectedBasePath
+ )
}
@JvmOverloads
@@ -394,9 +452,12 @@ class SimpleStorageHelper {
const val TYPE_FILE_PICKER = 1
const val TYPE_FOLDER_PICKER = 2
- private const val KEY_OPEN_FOLDER_PICKER_ONCE_GRANTED = BuildConfig.LIBRARY_PACKAGE_NAME + ".pickerToOpenOnceGranted"
- private const val KEY_ORIGINAL_REQUEST_CODE = BuildConfig.LIBRARY_PACKAGE_NAME + ".originalRequestCode"
- private const val KEY_FILTER_MIME_TYPES = BuildConfig.LIBRARY_PACKAGE_NAME + ".filterMimeTypes"
+ private const val KEY_OPEN_FOLDER_PICKER_ONCE_GRANTED =
+ BuildConfig.LIBRARY_PACKAGE_NAME + ".pickerToOpenOnceGranted"
+ private const val KEY_ORIGINAL_REQUEST_CODE =
+ BuildConfig.LIBRARY_PACKAGE_NAME + ".originalRequestCode"
+ private const val KEY_FILTER_MIME_TYPES =
+ BuildConfig.LIBRARY_PACKAGE_NAME + ".filterMimeTypes"
@JvmStatic
fun redirectToSystemSettings(context: Context) {
@@ -404,7 +465,10 @@ class SimpleStorageHelper {
.setMessage(R.string.ss_storage_permission_permanently_disabled)
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.setPositiveButton(android.R.string.ok) { _, _ ->
- val intentSetting = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:${context.packageName}"))
+ val intentSetting = Intent(
+ Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
+ Uri.parse("package:${context.packageName}")
+ )
.addCategory(Intent.CATEGORY_DEFAULT)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intentSetting)
diff --git a/storage/src/main/java/com/anggrayudi/storage/callback/BaseFileCallback.kt b/storage/src/main/kotlin/com/anggrayudi/storage/callback/BaseFileCallback.kt
similarity index 88%
rename from storage/src/main/java/com/anggrayudi/storage/callback/BaseFileCallback.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/callback/BaseFileCallback.kt
index 4cf9888..238732b 100644
--- a/storage/src/main/java/com/anggrayudi/storage/callback/BaseFileCallback.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/callback/BaseFileCallback.kt
@@ -1,6 +1,5 @@
package com.anggrayudi.storage.callback
-import androidx.annotation.RestrictTo
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import com.anggrayudi.storage.file.FileSize
@@ -10,8 +9,8 @@ import kotlinx.coroutines.CoroutineScope
* Created on 02/06/21
* @author Anggrayudi H
*/
-abstract class BaseFileCallback
-@RestrictTo(RestrictTo.Scope.LIBRARY) constructor(var uiScope: CoroutineScope) {
+abstract class BaseFileCallback(override val uiScope: CoroutineScope) :
+ ScopeHoldingCallback {
@UiThread
open fun onValidate() {
diff --git a/storage/src/main/java/com/anggrayudi/storage/callback/CreateFileCallback.kt b/storage/src/main/kotlin/com/anggrayudi/storage/callback/CreateFileCallback.kt
similarity index 72%
rename from storage/src/main/java/com/anggrayudi/storage/callback/CreateFileCallback.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/callback/CreateFileCallback.kt
index 9dc3f14..5a2fde9 100644
--- a/storage/src/main/java/com/anggrayudi/storage/callback/CreateFileCallback.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/callback/CreateFileCallback.kt
@@ -9,13 +9,9 @@ import androidx.documentfile.provider.DocumentFile
*/
interface CreateFileCallback {
- fun onCanceledByUser(requestCode: Int) {
- // default implementation
- }
+ fun onCanceledByUser(requestCode: Int)
- fun onActivityHandlerNotFound(requestCode: Int, intent: Intent) {
- // default implementation
- }
+ fun onActivityHandlerNotFound(requestCode: Int, intent: Intent)
fun onFileCreated(requestCode: Int, file: DocumentFile)
}
\ No newline at end of file
diff --git a/storage/src/main/java/com/anggrayudi/storage/callback/FileCallback.kt b/storage/src/main/kotlin/com/anggrayudi/storage/callback/FileCallback.kt
similarity index 74%
rename from storage/src/main/java/com/anggrayudi/storage/callback/FileCallback.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/callback/FileCallback.kt
index b336d78..4d65cb2 100644
--- a/storage/src/main/java/com/anggrayudi/storage/callback/FileCallback.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/callback/FileCallback.kt
@@ -14,9 +14,7 @@ import kotlinx.coroutines.GlobalScope
* Created on 17/08/20
* @author Anggrayudi H
*/
-abstract class FileCallback @OptIn(DelicateCoroutinesApi::class) @JvmOverloads constructor(
- uiScope: CoroutineScope = GlobalScope
-) : BaseFileCallback(uiScope) {
+abstract class FileCallback @OptIn(DelicateCoroutinesApi::class) @JvmOverloads constructor(uiScope: CoroutineScope = GlobalScope) : BaseFileCallback(uiScope) {
/**
* @param file can be [DocumentFile] or [MediaFile]
@@ -39,18 +37,10 @@ abstract class FileCallback @OptIn(DelicateCoroutinesApi::class) @JvmOverloads c
action.confirmResolution(ConflictResolution.CREATE_NEW)
}
- /**
- * @param result can be [DocumentFile] or [MediaFile]
- */
- @UiThread
- override fun onCompleted(result: Any) {
- // default implementation
- }
-
class FileConflictAction(private val continuation: CancellableContinuation) {
fun confirmResolution(resolution: ConflictResolution) {
- continuation.resumeWith(Result.success(resolution))
+ continuation.resumeWith(kotlin.Result.success(resolution))
}
}
@@ -96,5 +86,22 @@ abstract class FileCallback @OptIn(DelicateCoroutinesApi::class) @JvmOverloads c
* @param progress in percent
* @param writeSpeed in bytes
*/
- class Report(val progress: Float, val bytesMoved: Long, val writeSpeed: Int)
+ data class Report(val progress: Float, val bytesMoved: Long, val writeSpeed: Int)
+
+ sealed interface Result {
+ @JvmInline
+ value class MediaFile(val value: com.anggrayudi.storage.media.MediaFile) : Result
+
+ @JvmInline
+ value class DocumentFile(val value: androidx.documentfile.provider.DocumentFile) : Result
+
+ companion object {
+ internal fun get(value: Any): Result =
+ when (value) {
+ is com.anggrayudi.storage.media.MediaFile -> MediaFile(value)
+ is androidx.documentfile.provider.DocumentFile -> DocumentFile(value)
+ else -> throw IllegalArgumentException("Result must be either of type ${com.anggrayudi.storage.media.MediaFile::class.java.name} or ${androidx.documentfile.provider.DocumentFile::class.java.name}")
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/storage/src/main/java/com/anggrayudi/storage/callback/FileConflictCallback.kt b/storage/src/main/kotlin/com/anggrayudi/storage/callback/FileConflictCallback.kt
similarity index 93%
rename from storage/src/main/java/com/anggrayudi/storage/callback/FileConflictCallback.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/callback/FileConflictCallback.kt
index d9002e9..aa874ff 100644
--- a/storage/src/main/java/com/anggrayudi/storage/callback/FileConflictCallback.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/callback/FileConflictCallback.kt
@@ -11,8 +11,8 @@ import kotlinx.coroutines.GlobalScope
* @author Anggrayudi Hardiannico A.
*/
abstract class FileConflictCallback @OptIn(DelicateCoroutinesApi::class) @JvmOverloads constructor(
- var uiScope: CoroutineScope = GlobalScope
-) {
+ override val uiScope: CoroutineScope = GlobalScope
+): ScopeHoldingCallback {
/**
* Do not call `super` when you override this function.
diff --git a/storage/src/main/java/com/anggrayudi/storage/callback/FilePickerCallback.kt b/storage/src/main/kotlin/com/anggrayudi/storage/callback/FilePickerCallback.kt
similarity index 100%
rename from storage/src/main/java/com/anggrayudi/storage/callback/FilePickerCallback.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/callback/FilePickerCallback.kt
diff --git a/storage/src/main/java/com/anggrayudi/storage/callback/FileReceiverCallback.kt b/storage/src/main/kotlin/com/anggrayudi/storage/callback/FileReceiverCallback.kt
similarity index 100%
rename from storage/src/main/java/com/anggrayudi/storage/callback/FileReceiverCallback.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/callback/FileReceiverCallback.kt
diff --git a/storage/src/main/java/com/anggrayudi/storage/callback/FolderCallback.kt b/storage/src/main/kotlin/com/anggrayudi/storage/callback/FolderCallback.kt
similarity index 89%
rename from storage/src/main/java/com/anggrayudi/storage/callback/FolderCallback.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/callback/FolderCallback.kt
index e630ccc..88d7a75 100644
--- a/storage/src/main/java/com/anggrayudi/storage/callback/FolderCallback.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/callback/FolderCallback.kt
@@ -44,12 +44,20 @@ abstract class FolderCallback @OptIn(DelicateCoroutinesApi::class) @JvmOverloads
* This happens if the destination is a file.
*/
@UiThread
- open fun onParentConflict(destinationFolder: DocumentFile, action: ParentFolderConflictAction, canMerge: Boolean) {
+ open fun onParentConflict(
+ destinationFolder: DocumentFile,
+ action: ParentFolderConflictAction,
+ canMerge: Boolean
+ ) {
action.confirmResolution(ConflictResolution.CREATE_NEW)
}
@UiThread
- open fun onContentConflict(destinationFolder: DocumentFile, conflictedFiles: MutableList, action: FolderContentConflictAction) {
+ open fun onContentConflict(
+ destinationFolder: DocumentFile,
+ conflictedFiles: MutableList,
+ action: FolderContentConflictAction
+ ) {
action.confirmResolution(conflictedFiles)
}
@@ -104,7 +112,7 @@ abstract class FolderCallback @OptIn(DelicateCoroutinesApi::class) @JvmOverloads
}
}
- class FileConflict(
+ data class FileConflict(
val source: DocumentFile,
val target: DocumentFile,
var solution: FileCallback.ConflictResolution = FileCallback.ConflictResolution.CREATE_NEW
@@ -129,7 +137,7 @@ abstract class FolderCallback @OptIn(DelicateCoroutinesApi::class) @JvmOverloads
* @param writeSpeed in bytes
* @param fileCount total files/folders that are successfully copied/moved
*/
- class Report(val progress: Float, val bytesMoved: Long, val writeSpeed: Int, val fileCount: Int)
+ data class Report(val progress: Float, val bytesMoved: Long, val writeSpeed: Int, val fileCount: Int)
/**
* If `totalCopiedFiles` are less than `totalFilesToCopy`, then some files cannot be copied/moved or the files are skipped due to [ConflictResolution.MERGE]
@@ -139,5 +147,10 @@ abstract class FolderCallback @OptIn(DelicateCoroutinesApi::class) @JvmOverloads
* @param totalFilesToCopy total files, not folders
* @param totalCopiedFiles total files, not folders
*/
- class Result(val folder: DocumentFile, val totalFilesToCopy: Int, val totalCopiedFiles: Int, val success: Boolean)
+ data class Result(
+ val folder: DocumentFile,
+ val totalFilesToCopy: Int,
+ val totalCopiedFiles: Int,
+ val success: Boolean
+ )
}
\ No newline at end of file
diff --git a/storage/src/main/java/com/anggrayudi/storage/callback/FolderPickerCallback.kt b/storage/src/main/kotlin/com/anggrayudi/storage/callback/FolderPickerCallback.kt
similarity index 86%
rename from storage/src/main/java/com/anggrayudi/storage/callback/FolderPickerCallback.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/callback/FolderPickerCallback.kt
index 4dabc76..59ceb89 100644
--- a/storage/src/main/java/com/anggrayudi/storage/callback/FolderPickerCallback.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/callback/FolderPickerCallback.kt
@@ -27,7 +27,12 @@ interface FolderPickerCallback {
* @param folder selected folder that has no read and write permission
* @param storageType `null` if `folder`'s authority is not [DocumentFileCompat.EXTERNAL_STORAGE_AUTHORITY]
*/
- fun onStorageAccessDenied(requestCode: Int, folder: DocumentFile?, storageType: StorageType, storageId: String)
+ fun onStorageAccessDenied(
+ requestCode: Int,
+ folder: DocumentFile?,
+ storageType: StorageType,
+ storageId: String
+ )
fun onFolderSelected(requestCode: Int, folder: DocumentFile)
}
\ No newline at end of file
diff --git a/storage/src/main/java/com/anggrayudi/storage/callback/MultipleFileCallback.kt b/storage/src/main/kotlin/com/anggrayudi/storage/callback/MultipleFileCallback.kt
similarity index 88%
rename from storage/src/main/java/com/anggrayudi/storage/callback/MultipleFileCallback.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/callback/MultipleFileCallback.kt
index 33ac178..8369ba7 100644
--- a/storage/src/main/java/com/anggrayudi/storage/callback/MultipleFileCallback.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/callback/MultipleFileCallback.kt
@@ -15,7 +15,9 @@ import kotlinx.coroutines.GlobalScope
*/
abstract class MultipleFileCallback @OptIn(DelicateCoroutinesApi::class) @JvmOverloads constructor(
uiScope: CoroutineScope = GlobalScope
-) : BaseFileCallback(uiScope) {
+) : BaseFileCallback(
+ uiScope
+) {
/**
* The reason can be one of:
@@ -24,7 +26,10 @@ abstract class MultipleFileCallback @OptIn(DelicateCoroutinesApi::class) @JvmOve
* * [FolderCallback.ErrorCode.TARGET_FOLDER_CANNOT_HAVE_SAME_PATH_WITH_SOURCE_FOLDER]
*/
@UiThread
- open fun onInvalidSourceFilesFound(invalidSourceFiles: Map, action: InvalidSourceFilesAction) {
+ open fun onInvalidSourceFilesFound(
+ invalidSourceFiles: Map,
+ action: InvalidSourceFilesAction
+ ) {
action.confirmResolution(false)
}
@@ -39,7 +44,8 @@ abstract class MultipleFileCallback @OptIn(DelicateCoroutinesApi::class) @JvmOve
* Setting negative value will cancel the operation.
*/
@UiThread
- open fun onStart(files: List, totalFilesToCopy: Int, workerThread: Thread): Long = 0
+ open fun onStart(files: List, totalFilesToCopy: Int, workerThread: Thread): Long =
+ 0
/**
* Do not call `super` when you override this function.
@@ -85,7 +91,7 @@ abstract class MultipleFileCallback @OptIn(DelicateCoroutinesApi::class) @JvmOve
}
}
- class ParentConflict(
+ data class ParentConflict(
val source: DocumentFile,
val target: DocumentFile,
val canMerge: Boolean,
@@ -109,7 +115,7 @@ abstract class MultipleFileCallback @OptIn(DelicateCoroutinesApi::class) @JvmOve
* @param writeSpeed in bytes
* @param fileCount total files/folders that are successfully copied/moved
*/
- class Report(val progress: Float, val bytesMoved: Long, val writeSpeed: Int, val fileCount: Int)
+ data class Report(val progress: Float, val bytesMoved: Long, val writeSpeed: Int, val fileCount: Int)
/**
* If `totalCopiedFiles` are less than `totalFilesToCopy`, then some files cannot be copied/moved or the files are skipped due to [ConflictResolution.MERGE]
@@ -119,5 +125,10 @@ abstract class MultipleFileCallback @OptIn(DelicateCoroutinesApi::class) @JvmOve
* @param totalFilesToCopy total files, not folders
* @param totalCopiedFiles total files, not folders
*/
- class Result(val files: List, val totalFilesToCopy: Int, val totalCopiedFiles: Int, val success: Boolean)
+ data class Result(
+ val files: List,
+ val totalFilesToCopy: Int,
+ val totalCopiedFiles: Int,
+ val success: Boolean
+ )
}
\ No newline at end of file
diff --git a/storage/src/main/kotlin/com/anggrayudi/storage/callback/ScopeHoldingCallback.kt b/storage/src/main/kotlin/com/anggrayudi/storage/callback/ScopeHoldingCallback.kt
new file mode 100644
index 0000000..399844b
--- /dev/null
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/callback/ScopeHoldingCallback.kt
@@ -0,0 +1,12 @@
+package com.anggrayudi.storage.callback
+
+import com.anggrayudi.storage.extension.postToUi
+import kotlinx.coroutines.CoroutineScope
+
+interface ScopeHoldingCallback {
+ val uiScope: CoroutineScope
+
+ fun postToUiScope(action: () -> Unit) {
+ uiScope.postToUi(action)
+ }
+}
\ No newline at end of file
diff --git a/storage/src/main/java/com/anggrayudi/storage/callback/StorageAccessCallback.kt b/storage/src/main/kotlin/com/anggrayudi/storage/callback/StorageAccessCallback.kt
similarity index 85%
rename from storage/src/main/java/com/anggrayudi/storage/callback/StorageAccessCallback.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/callback/StorageAccessCallback.kt
index 8c6dcdd..596acfa 100644
--- a/storage/src/main/java/com/anggrayudi/storage/callback/StorageAccessCallback.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/callback/StorageAccessCallback.kt
@@ -22,7 +22,13 @@ interface StorageAccessCallback {
/**
* Triggered on Android 10 and lower.
*/
- fun onRootPathNotSelected(requestCode: Int, rootPath: String, uri: Uri, selectedStorageType: StorageType, expectedStorageType: StorageType)
+ fun onRootPathNotSelected(
+ requestCode: Int,
+ rootPath: String,
+ uri: Uri,
+ selectedStorageType: StorageType,
+ expectedStorageType: StorageType
+ )
/**
* Triggered on Android 11 and higher.
diff --git a/storage/src/main/java/com/anggrayudi/storage/callback/ZipCompressionCallback.kt b/storage/src/main/kotlin/com/anggrayudi/storage/callback/ZipCompressionCallback.kt
similarity index 84%
rename from storage/src/main/java/com/anggrayudi/storage/callback/ZipCompressionCallback.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/callback/ZipCompressionCallback.kt
index f4f7d21..63dd4b7 100644
--- a/storage/src/main/java/com/anggrayudi/storage/callback/ZipCompressionCallback.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/callback/ZipCompressionCallback.kt
@@ -14,7 +14,7 @@ import kotlinx.coroutines.GlobalScope
* @author Anggrayudi H
*/
abstract class ZipCompressionCallback @OptIn(DelicateCoroutinesApi::class)
-@JvmOverloads constructor(var uiScope: CoroutineScope = GlobalScope) {
+@JvmOverloads constructor(override val uiScope: CoroutineScope = GlobalScope): ScopeHoldingCallback {
@UiThread
open fun onCountingFiles() {
@@ -57,7 +57,12 @@ abstract class ZipCompressionCallback @OptIn(DelicateCoroutinesApi::class)
* @param compressionRate size reduction in percent, e.g. 23.5
*/
@UiThread
- open fun onCompleted(zipFile: DocumentFile, bytesCompressed: Long, totalFilesCompressed: Int, compressionRate: Float) {
+ open fun onCompleted(
+ zipFile: DocumentFile,
+ bytesCompressed: Long,
+ totalFilesCompressed: Int,
+ compressionRate: Float
+ ) {
// default implementation
}
@@ -69,7 +74,12 @@ abstract class ZipCompressionCallback @OptIn(DelicateCoroutinesApi::class)
/**
* @param progress always `0` when compressing [MediaFile]
*/
- class Report(val progress: Float, val bytesCompressed: Long, val writeSpeed: Int, val fileCount: Int)
+ data class Report(
+ val progress: Float,
+ val bytesCompressed: Long,
+ val writeSpeed: Int,
+ val fileCount: Int
+ )
enum class ErrorCode {
STORAGE_PERMISSION_DENIED,
diff --git a/storage/src/main/java/com/anggrayudi/storage/callback/ZipDecompressionCallback.kt b/storage/src/main/kotlin/com/anggrayudi/storage/callback/ZipDecompressionCallback.kt
similarity index 87%
rename from storage/src/main/java/com/anggrayudi/storage/callback/ZipDecompressionCallback.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/callback/ZipDecompressionCallback.kt
index 96cd853..90b17cb 100644
--- a/storage/src/main/java/com/anggrayudi/storage/callback/ZipDecompressionCallback.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/callback/ZipDecompressionCallback.kt
@@ -56,7 +56,11 @@ abstract class ZipDecompressionCallback @OptIn(DelicateCoroutinesApi::class)
* But for decompressing [MediaFile], it is always `0` because we can't get the actual zip file size from SAF database.
*/
@UiThread
- open fun onCompleted(zipFile: T, targetFolder: DocumentFile, decompressionInfo: DecompressionInfo) {
+ open fun onCompleted(
+ zipFile: T,
+ targetFolder: DocumentFile,
+ decompressionInfo: DecompressionInfo
+ ) {
// default implementation
}
@@ -71,14 +75,19 @@ abstract class ZipDecompressionCallback @OptIn(DelicateCoroutinesApi::class)
* so only `bytesDecompressed` and `fileCount` that can be provided.
* @param fileCount decompressed files in total
*/
- class Report(val bytesDecompressed: Long, val writeSpeed: Int, val fileCount: Int)
+ data class Report(val bytesDecompressed: Long, val writeSpeed: Int, val fileCount: Int)
/**
* @param decompressionRate size expansion in percent, e.g. 23.5.
* @param skippedDecompressedBytes total skipped bytes because the file already exists and the user has selected [FileCallback.ConflictResolution.SKIP]
* @param bytesDecompressed total decompressed bytes, excluded skipped files
*/
- class DecompressionInfo(val bytesDecompressed: Long, val skippedDecompressedBytes: Long, val totalFilesDecompressed: Int, val decompressionRate: Float)
+ data class DecompressionInfo(
+ val bytesDecompressed: Long,
+ val skippedDecompressedBytes: Long,
+ val totalFilesDecompressed: Int,
+ val decompressionRate: Float
+ )
enum class ErrorCode {
STORAGE_PERMISSION_DENIED,
diff --git a/storage/src/main/java/com/anggrayudi/storage/extension/ContextExt.kt b/storage/src/main/kotlin/com/anggrayudi/storage/extension/ContextExt.kt
similarity index 100%
rename from storage/src/main/java/com/anggrayudi/storage/extension/ContextExt.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/extension/ContextExt.kt
diff --git a/storage/src/main/java/com/anggrayudi/storage/extension/CoroutineExt.kt b/storage/src/main/kotlin/com/anggrayudi/storage/extension/CoroutineExt.kt
similarity index 73%
rename from storage/src/main/java/com/anggrayudi/storage/extension/CoroutineExt.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/extension/CoroutineExt.kt
index 06c7abd..ba70c03 100644
--- a/storage/src/main/java/com/anggrayudi/storage/extension/CoroutineExt.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/extension/CoroutineExt.kt
@@ -2,7 +2,14 @@
package com.anggrayudi.storage.extension
-import kotlinx.coroutines.*
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.suspendCancellableCoroutine
/**
* @author Anggrayudi Hardiannico A. (anggrayudi.hardiannico@dana.id)
@@ -36,9 +43,13 @@ fun startCoroutineTimer(
}
@Suppress("OPT_IN_USAGE")
-fun launchOnUiThread(action: suspend CoroutineScope.() -> Unit) = GlobalScope.launch(Dispatchers.Main, block = action)
+fun launchOnUiThread(action: suspend CoroutineScope.() -> Unit) =
+ GlobalScope.launch(Dispatchers.Main, block = action)
-inline fun awaitUiResultWithPending(uiScope: CoroutineScope, crossinline action: (CancellableContinuation) -> Unit): R {
+inline fun awaitUiResultWithPending(
+ uiScope: CoroutineScope,
+ crossinline action: (CancellableContinuation) -> Unit
+): R {
return runBlocking {
suspendCancellableCoroutine {
uiScope.launch(Dispatchers.Main) { action(it) }
diff --git a/storage/src/main/java/com/anggrayudi/storage/extension/IOExt.kt b/storage/src/main/kotlin/com/anggrayudi/storage/extension/IOExt.kt
similarity index 100%
rename from storage/src/main/java/com/anggrayudi/storage/extension/IOExt.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/extension/IOExt.kt
diff --git a/storage/src/main/java/com/anggrayudi/storage/extension/PrimitivesExt.kt b/storage/src/main/kotlin/com/anggrayudi/storage/extension/PrimitivesExt.kt
similarity index 100%
rename from storage/src/main/java/com/anggrayudi/storage/extension/PrimitivesExt.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/extension/PrimitivesExt.kt
diff --git a/storage/src/main/java/com/anggrayudi/storage/extension/TextExt.kt b/storage/src/main/kotlin/com/anggrayudi/storage/extension/TextExt.kt
similarity index 97%
rename from storage/src/main/java/com/anggrayudi/storage/extension/TextExt.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/extension/TextExt.kt
index c74c06c..9b515b1 100644
--- a/storage/src/main/java/com/anggrayudi/storage/extension/TextExt.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/extension/TextExt.kt
@@ -48,7 +48,9 @@ fun String.replaceCompletely(match: String, replaceWith: String) = let {
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun String.isKitkatSdCardStorageId() =
- Build.VERSION.SDK_INT < 21 && (this == StorageId.KITKAT_SDCARD || this.matches(DocumentFileCompat.SD_CARD_STORAGE_ID_REGEX))
+ Build.VERSION.SDK_INT < 21 && (this == StorageId.KITKAT_SDCARD || this.matches(
+ DocumentFileCompat.SD_CARD_STORAGE_ID_REGEX
+ ))
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun String.hasParent(parentPath: String): Boolean {
diff --git a/storage/src/main/java/com/anggrayudi/storage/extension/UriExt.kt b/storage/src/main/kotlin/com/anggrayudi/storage/extension/UriExt.kt
similarity index 88%
rename from storage/src/main/java/com/anggrayudi/storage/extension/UriExt.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/extension/UriExt.kt
index faaed3f..dcab145 100644
--- a/storage/src/main/java/com/anggrayudi/storage/extension/UriExt.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/extension/UriExt.kt
@@ -11,7 +11,12 @@ import com.anggrayudi.storage.file.DocumentFileCompat
import com.anggrayudi.storage.file.StorageId.PRIMARY
import com.anggrayudi.storage.file.getStorageId
import com.anggrayudi.storage.media.MediaFile
-import java.io.*
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
/**
* Created on 12/15/20
@@ -67,7 +72,10 @@ fun Uri.openOutputStream(context: Context, append: Boolean = true): OutputStream
if (isRawFile) {
FileOutputStream(File(path ?: return null), append)
} else {
- context.contentResolver.openOutputStream(this, if (append && isTreeDocumentFile) "wa" else "w")
+ context.contentResolver.openOutputStream(
+ this,
+ if (append && isTreeDocumentFile) "wa" else "w"
+ )
}
} catch (e: IOException) {
null
diff --git a/storage/src/main/java/com/anggrayudi/storage/file/CreateMode.kt b/storage/src/main/kotlin/com/anggrayudi/storage/file/CreateMode.kt
similarity index 100%
rename from storage/src/main/java/com/anggrayudi/storage/file/CreateMode.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/file/CreateMode.kt
diff --git a/storage/src/main/java/com/anggrayudi/storage/file/DocumentFileCompat.kt b/storage/src/main/kotlin/com/anggrayudi/storage/file/DocumentFileCompat.kt
similarity index 81%
rename from storage/src/main/java/com/anggrayudi/storage/file/DocumentFileCompat.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/file/DocumentFileCompat.kt
index 6abf04f..dd32084 100644
--- a/storage/src/main/java/com/anggrayudi/storage/file/DocumentFileCompat.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/file/DocumentFileCompat.kt
@@ -16,7 +16,18 @@ import com.anggrayudi.storage.FileWrapper
import com.anggrayudi.storage.SimpleStorage
import com.anggrayudi.storage.SimpleStorage.Companion.KITKAT_SD_CARD_ID
import com.anggrayudi.storage.SimpleStorage.Companion.KITKAT_SD_CARD_PATH
-import com.anggrayudi.storage.extension.*
+import com.anggrayudi.storage.extension.fromSingleUri
+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.isRawFile
+import com.anggrayudi.storage.extension.isTreeDocumentFile
+import com.anggrayudi.storage.extension.replaceCompletely
+import com.anggrayudi.storage.extension.trimFileSeparator
import com.anggrayudi.storage.file.StorageId.DATA
import com.anggrayudi.storage.file.StorageId.HOME
import com.anggrayudi.storage.file.StorageId.KITKAT_SDCARD
@@ -68,7 +79,8 @@ object DocumentFileCompat {
val SD_CARD_STORAGE_PATH_REGEX = Regex("/storage/$SD_CARD_STORAGE_ID_REGEX(.*?)")
@RestrictTo(RestrictTo.Scope.LIBRARY)
- fun getKitkatSdCardRootFile(basePath: String = "") = File(KITKAT_SD_CARD_PATH + "/$basePath".trimEnd('/'))
+ fun getKitkatSdCardRootFile(basePath: String = "") =
+ File(KITKAT_SD_CARD_PATH + "/$basePath".trimEnd('/'))
@JvmStatic
fun isRootUri(uri: Uri): Boolean {
@@ -109,9 +121,15 @@ object DocumentFileCompat {
val dataDir = context.dataDirectory.path
val externalStoragePath = SimpleStorage.externalStoragePath
when {
- fullPath.startsWith(externalStoragePath) -> fullPath.substringAfter(externalStoragePath)
+ fullPath.startsWith(externalStoragePath) -> fullPath.substringAfter(
+ externalStoragePath
+ )
+
fullPath.startsWith(dataDir) -> fullPath.substringAfter(dataDir)
- fullPath.startsWith(KITKAT_SD_CARD_PATH) -> fullPath.substringAfter(KITKAT_SD_CARD_PATH)
+ fullPath.startsWith(KITKAT_SD_CARD_PATH) -> fullPath.substringAfter(
+ KITKAT_SD_CARD_PATH
+ )
+
else -> if (fullPath.matches(SD_CARD_STORAGE_PATH_REGEX)) {
fullPath.substringAfter("/storage/", "").substringAfter('/', "")
} else ""
@@ -125,10 +143,17 @@ object DocumentFileCompat {
@JvmStatic
fun fromUri(context: Context, uri: Uri): DocumentFile? {
return when {
- uri.isRawFile -> File(uri.path ?: return null).run { if (canRead()) DocumentFile.fromFile(this) else null }
- uri.isTreeDocumentFile -> context.fromTreeUri(uri)?.run { if (isDownloadsDocument) toWritableDownloadsDocumentFile(context) else this }
+ uri.isRawFile -> File(
+ uri.path ?: return null
+ ).run { if (canRead()) DocumentFile.fromFile(this) else null }
+
+ uri.isTreeDocumentFile -> context.fromTreeUri(uri)
+ ?.run { if (isDownloadsDocument) toWritableDownloadsDocumentFile(context) else this }
+
else -> context.fromSingleUri(uri)?.let {
- if (Build.VERSION.SDK_INT < 21 && it.getStorageId(context).matches(SD_CARD_STORAGE_ID_REGEX)) {
+ if (Build.VERSION.SDK_INT < 21 && it.getStorageId(context)
+ .matches(SD_CARD_STORAGE_ID_REGEX)
+ ) {
DocumentFile.fromFile(getKitkatSdCardRootFile(it.getBasePath(context)))
} else {
it
@@ -158,9 +183,18 @@ object DocumentFileCompat {
return if (basePath.isEmpty() && storageId != HOME) {
getRootDocumentFile(context, storageId, requiresWriteAccess, considerRawFile)
} else {
- val file = exploreFile(context, storageId, basePath, documentType, requiresWriteAccess, considerRawFile)
+ val file = exploreFile(
+ context,
+ storageId,
+ basePath,
+ documentType,
+ requiresWriteAccess,
+ considerRawFile
+ )
if (file == null && storageId == PRIMARY && basePath.hasParent(Environment.DIRECTORY_DOWNLOADS)) {
- val downloads = context.fromTreeUri(Uri.parse(DOWNLOADS_TREE_URI))?.takeIf { it.canRead() } ?: return null
+ val downloads =
+ context.fromTreeUri(Uri.parse(DOWNLOADS_TREE_URI))?.takeIf { it.canRead() }
+ ?: return null
downloads.child(context, basePath.substringAfter('/', ""))?.takeIf {
documentType == DocumentFileType.ANY
|| documentType == DocumentFileType.FILE && it.isFile
@@ -193,7 +227,14 @@ object DocumentFileCompat {
fromFile(context, File(fullPath), documentType, requiresWriteAccess, considerRawFile)
} else {
// simple path
- fromSimplePath(context, fullPath.substringBefore(':'), fullPath.substringAfter(':'), documentType, requiresWriteAccess, considerRawFile)
+ fromSimplePath(
+ context,
+ fullPath.substringBefore(':'),
+ fullPath.substringAfter(':'),
+ documentType,
+ requiresWriteAccess,
+ considerRawFile
+ )
}
}
@@ -216,16 +257,36 @@ object DocumentFileCompat {
requiresWriteAccess: Boolean = false,
considerRawFile: Boolean = true
): DocumentFile? {
- return if (file.checkRequirements(context, requiresWriteAccess, considerRawFile || Build.VERSION.SDK_INT < 21)) {
+ return if (file.checkRequirements(
+ context,
+ requiresWriteAccess,
+ considerRawFile || Build.VERSION.SDK_INT < 21
+ )
+ ) {
if (documentType == DocumentFileType.FILE && !file.isFile || documentType == DocumentFileType.FOLDER && !file.isDirectory) {
null
} else {
DocumentFile.fromFile(file)
}
} else {
- val basePath = file.getBasePath(context).removeForbiddenCharsFromFilename().trimFileSeparator()
- exploreFile(context, file.getStorageId(context), basePath, documentType, requiresWriteAccess, considerRawFile)
- ?: fromSimplePath(context, file.getStorageId(context), basePath, documentType, requiresWriteAccess, considerRawFile)
+ val basePath =
+ file.getBasePath(context).removeForbiddenCharsFromFilename().trimFileSeparator()
+ exploreFile(
+ context,
+ file.getStorageId(context),
+ basePath,
+ documentType,
+ requiresWriteAccess,
+ considerRawFile
+ )
+ ?: fromSimplePath(
+ context,
+ file.getStorageId(context),
+ basePath,
+ documentType,
+ requiresWriteAccess,
+ considerRawFile
+ )
}
}
@@ -236,7 +297,6 @@ object DocumentFileCompat {
*/
@JvmOverloads
@JvmStatic
- @Suppress("DEPRECATION")
fun fromPublicFolder(
context: Context,
type: PublicDirectory,
@@ -313,7 +373,11 @@ object DocumentFileCompat {
DocumentFile.fromFile(Environment.getExternalStorageDirectory())
}
} else if (considerRawFile) {
- getRootRawFile(context, storageId, requiresWriteAccess)?.let { DocumentFile.fromFile(it) }
+ getRootRawFile(
+ context,
+ storageId,
+ requiresWriteAccess
+ )?.let { DocumentFile.fromFile(it) }
?: context.fromTreeUri(createDocumentUri(storageId))
} else {
context.fromTreeUri(createDocumentUri(storageId))
@@ -330,7 +394,6 @@ object DocumentFileCompat {
* @param fullPath construct it using [buildAbsolutePath] or [buildSimplePath]
* @return `null` if accessible root path is not found in [ContentResolver.getPersistedUriPermissions], or the folder does not exist.
*/
- @Suppress("DEPRECATION")
@JvmOverloads
@JvmStatic
fun getAccessibleRootDocumentFile(
@@ -369,7 +432,10 @@ object DocumentFileCompat {
if (uriPath != null && it.uri.isExternalStorageDocument) {
val currentStorageId = uriPath.substringBefore(':').substringAfterLast('/')
val currentRootFolder = uriPath.substringAfter(':', "")
- if (currentStorageId == storageId && (currentRootFolder.isEmpty() || cleanBasePath.hasParent(currentRootFolder))) {
+ if (currentStorageId == storageId && (currentRootFolder.isEmpty() || cleanBasePath.hasParent(
+ currentRootFolder
+ ))
+ ) {
return context.fromTreeUri(it.uri)
}
}
@@ -388,15 +454,22 @@ object DocumentFileCompat {
*/
@JvmOverloads
@JvmStatic
- @Suppress("DEPRECATION")
- fun getRootRawFile(context: Context, storageId: String, requiresWriteAccess: Boolean = false): File? {
+ fun getRootRawFile(
+ context: Context,
+ storageId: String,
+ requiresWriteAccess: Boolean = false
+ ): File? {
val rootFile = when {
storageId == PRIMARY || storageId == HOME -> Environment.getExternalStorageDirectory()
storageId == DATA -> context.dataDirectory
storageId.isKitkatSdCardStorageId() -> getKitkatSdCardRootFile()
else -> File("/storage/$storageId")
}
- return rootFile.takeIf { rootFile.canRead() && (requiresWriteAccess && rootFile.isWritable(context) || !requiresWriteAccess) }
+ return rootFile.takeIf {
+ rootFile.canRead() && (requiresWriteAccess && rootFile.isWritable(
+ context
+ ) || !requiresWriteAccess)
+ }
}
@JvmStatic
@@ -429,7 +502,10 @@ object DocumentFileCompat {
@JvmStatic
fun buildSimplePath(context: Context, absolutePath: String): String {
- return buildSimplePath(getStorageId(context, absolutePath), getBasePath(context, absolutePath))
+ return buildSimplePath(
+ getStorageId(context, absolutePath),
+ getBasePath(context, absolutePath)
+ )
}
@JvmOverloads
@@ -444,10 +520,12 @@ object DocumentFileCompat {
}
@JvmStatic
- fun doesExist(context: Context, fullPath: String) = fromFullPath(context, fullPath)?.exists() == true
+ fun doesExist(context: Context, fullPath: String) =
+ fromFullPath(context, fullPath)?.exists() == true
@JvmStatic
- fun delete(context: Context, fullPath: String) = fromFullPath(context, fullPath)?.delete() == true
+ fun delete(context: Context, fullPath: String) =
+ fromFullPath(context, fullPath)?.delete() == true
/**
* Check if storage has URI permission for read and write access.
@@ -462,14 +540,17 @@ object DocumentFileCompat {
isUriPermissionGranted(context, createDocumentUri(storageId, basePath))
@JvmStatic
- fun isDownloadsUriPermissionGranted(context: Context) = isUriPermissionGranted(context, Uri.parse(DOWNLOADS_TREE_URI))
+ fun isDownloadsUriPermissionGranted(context: Context) =
+ isUriPermissionGranted(context, Uri.parse(DOWNLOADS_TREE_URI))
@JvmStatic
- fun isDocumentsUriPermissionGranted(context: Context) = isUriPermissionGranted(context, Uri.parse(DOCUMENTS_TREE_URI))
+ fun isDocumentsUriPermissionGranted(context: Context) =
+ isUriPermissionGranted(context, Uri.parse(DOCUMENTS_TREE_URI))
- private fun isUriPermissionGranted(context: Context, uri: Uri) = context.contentResolver.persistedUriPermissions.any {
- it.isReadPermission && it.isWritePermission && it.uri == uri
- }
+ private fun isUriPermissionGranted(context: Context, uri: Uri) =
+ context.contentResolver.persistedUriPermissions.any {
+ it.isReadPermission && it.isWritePermission && it.uri == uri
+ }
/**
* Get all storage IDs on this device. The first index is primary storage.
@@ -525,7 +606,11 @@ object DocumentFileCompat {
val storageId = uriPath.substringBefore(':').substringAfterLast('/')
val rootFolder = uriPath.substringAfter(':', "")
if (storageId == PRIMARY) {
- storages[PRIMARY]?.add("${Environment.getExternalStorageDirectory()}/$rootFolder".trimEnd('/'))
+ storages[PRIMARY]?.add(
+ "${Environment.getExternalStorageDirectory()}/$rootFolder".trimEnd(
+ '/'
+ )
+ )
} else if (storageId.matches(SD_CARD_STORAGE_ID_REGEX)) {
val paths = storages[storageId] ?: HashSet()
paths.add("/storage/$storageId/$rootFolder".trimEnd('/'))
@@ -574,7 +659,10 @@ object DocumentFileCompat {
): DocumentFile? {
val tryCreateWithRawFile: () -> DocumentFile? = {
val folder = File(fullPath.removeForbiddenCharsFromFilename()).apply { mkdirs() }
- if (folder.isDirectory && folder.canRead() && (requiresWriteAccess && folder.isWritable(context) || !requiresWriteAccess)) {
+ if (folder.isDirectory && folder.canRead() && (requiresWriteAccess && folder.isWritable(
+ context
+ ) || !requiresWriteAccess)
+ ) {
// Consider java.io.File for faster performance
DocumentFile.fromFile(folder)
} else null
@@ -582,7 +670,9 @@ object DocumentFileCompat {
if (considerRawFile && fullPath.startsWith('/') || fullPath.startsWith(context.dataDirectory.path)) {
tryCreateWithRawFile()?.let { return it }
}
- var currentDirectory = getAccessibleRootDocumentFile(context, fullPath, requiresWriteAccess, considerRawFile) ?: return null
+ var currentDirectory =
+ getAccessibleRootDocumentFile(context, fullPath, requiresWriteAccess, considerRawFile)
+ ?: return null
if (currentDirectory.isRawFile) {
return tryCreateWithRawFile()
}
@@ -626,21 +716,42 @@ object DocumentFileCompat {
for (path in findUniqueDeepestSubFolders(context, cleanedFullPaths)) {
// use java.io.File for faster performance
val folder = File(path).apply { mkdirs() }
- if (shouldUseRawFile && folder.isDirectory && folder.canRead() || path.startsWith(dataDir)) {
+ if (shouldUseRawFile && folder.isDirectory && folder.canRead() || path.startsWith(
+ dataDir
+ )
+ ) {
cleanedFullPaths.forEachIndexed { index, s ->
if (path.hasParent(s)) {
- results[index] = DocumentFile.fromFile(File(getDirectorySequence(s).joinToString(prefix = "/", separator = "/")))
+ results[index] = DocumentFile.fromFile(
+ File(
+ getDirectorySequence(s).joinToString(
+ prefix = "/",
+ separator = "/"
+ )
+ )
+ )
}
}
} else {
- var currentDirectory = getAccessibleRootDocumentFile(context, path, requiresWriteAccess, shouldUseRawFile) ?: continue
+ var currentDirectory = getAccessibleRootDocumentFile(
+ context,
+ path,
+ requiresWriteAccess,
+ shouldUseRawFile
+ ) ?: continue
val isRawFile = currentDirectory.isRawFile
val resolver = context.contentResolver
getDirectorySequence(getBasePath(context, path)).forEach {
try {
- val directory = if (isRawFile) currentDirectory.quickFindRawFile(it) else currentDirectory.quickFindTreeFile(context, resolver, it)
+ val directory =
+ if (isRawFile) currentDirectory.quickFindRawFile(it) else currentDirectory.quickFindTreeFile(
+ context,
+ resolver,
+ it
+ )
if (directory == null) {
- currentDirectory = currentDirectory.createDirectory(it) ?: return@forEach
+ currentDirectory =
+ currentDirectory.createDirectory(it) ?: return@forEach
val fullPath = currentDirectory.getAbsolutePath(context)
cleanedFullPaths.forEachIndexed { index, s ->
if (fullPath == s) {
@@ -669,22 +780,29 @@ object DocumentFileCompat {
}
@JvmStatic
- fun createDownloadWithMediaStoreFallback(context: Context, file: FileDescription): FileWrapper? {
- val publicFolder = fromPublicFolder(context, PublicDirectory.DOWNLOADS, requiresWriteAccess = true)
+ fun createDownloadWithMediaStoreFallback(
+ context: Context,
+ file: FileDescription
+ ): FileWrapper? {
+ val publicFolder =
+ fromPublicFolder(context, PublicDirectory.DOWNLOADS, requiresWriteAccess = true)
return if (publicFolder == null && Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
MediaStoreCompat.createDownload(context, file)?.let { FileWrapper.Media(it) }
} else {
- publicFolder?.makeFile(context, file.name, file.mimeType)?.let { FileWrapper.Document(it) }
+ publicFolder?.makeFile(context, file.name, file.mimeType)
+ ?.let { FileWrapper.Document(it) }
}
}
@JvmStatic
fun createPictureWithMediaStoreFallback(context: Context, file: FileDescription): FileWrapper? {
- val publicFolder = fromPublicFolder(context, PublicDirectory.PICTURES, requiresWriteAccess = true)
+ val publicFolder =
+ fromPublicFolder(context, PublicDirectory.PICTURES, requiresWriteAccess = true)
return if (publicFolder == null && Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
MediaStoreCompat.createImage(context, file)?.let { FileWrapper.Media(it) }
} else {
- publicFolder?.makeFile(context, file.name, file.mimeType)?.let { FileWrapper.Document(it) }
+ publicFolder?.makeFile(context, file.name, file.mimeType)
+ ?.let { FileWrapper.Document(it) }
}
}
@@ -718,9 +836,15 @@ object DocumentFileCompat {
}
}
- private fun getParentPath(path: String): String? = getDirectorySequence(path).let { it.getOrNull(it.size - 2) }
+ private fun getParentPath(path: String): String? =
+ getDirectorySequence(path).let { it.getOrNull(it.size - 2) }
- private fun mkdirsParentDirectory(context: Context, storageId: String, basePath: String, considerRawFile: Boolean): DocumentFile? {
+ private fun mkdirsParentDirectory(
+ context: Context,
+ storageId: String,
+ basePath: String,
+ considerRawFile: Boolean
+ ): DocumentFile? {
val parentPath = getParentPath(basePath)
return if (parentPath != null) {
mkdirs(context, buildAbsolutePath(context, storageId, parentPath), considerRawFile)
@@ -788,7 +912,11 @@ object DocumentFileCompat {
}
}
val rawFile = File(buildAbsolutePath(context, storageId, basePath))
- if ((considerRawFile || storageId == DATA) && rawFile.canRead() && rawFile.shouldWritable(context, requiresWriteAccess)) {
+ if ((considerRawFile || storageId == DATA) && rawFile.canRead() && rawFile.shouldWritable(
+ context,
+ requiresWriteAccess
+ )
+ ) {
return if (documentType == DocumentFileType.ANY || documentType == DocumentFileType.FILE && rawFile.isFile
|| documentType == DocumentFileType.FOLDER && rawFile.isDirectory
) {
@@ -797,35 +925,50 @@ object DocumentFileCompat {
null
}
}
- val file = if (Build.VERSION.SDK_INT == 29 && (storageId == HOME || storageId == PRIMARY && basePath.hasParent(Environment.DIRECTORY_DOCUMENTS))) {
- getRootDocumentFile(context, storageId, requiresWriteAccess, considerRawFile)?.child(context, basePath)
- ?: context.fromTreeUri(Uri.parse(DOCUMENTS_TREE_URI))?.child(context, basePath.substringAfter(Environment.DIRECTORY_DOCUMENTS))
- ?: return null
- } else if (Build.VERSION.SDK_INT < 30) {
- getRootDocumentFile(context, storageId, requiresWriteAccess, considerRawFile)?.child(context, basePath) ?: return null
- } else {
- val directorySequence = getDirectorySequence(basePath).toMutableList()
- val parentTree = ArrayList(directorySequence.size)
- var grantedFile: DocumentFile? = null
- // Find granted file tree.
- // For example, /storage/emulated/0/Music may not granted, but /storage/emulated/0/Music/Pop is granted by user.
- while (directorySequence.isNotEmpty()) {
- parentTree.add(directorySequence.removeFirst())
- val folderTree = parentTree.joinToString(separator = "/")
- try {
- grantedFile = context.fromTreeUri(createDocumentUri(storageId, folderTree))
- if (grantedFile?.canRead() == true) break
- } catch (e: SecurityException) {
- // ignore
- }
- }
- if (grantedFile == null || directorySequence.isEmpty()) {
- grantedFile
+ val file =
+ if (Build.VERSION.SDK_INT == 29 && (storageId == HOME || storageId == PRIMARY && basePath.hasParent(
+ Environment.DIRECTORY_DOCUMENTS
+ ))
+ ) {
+ getRootDocumentFile(
+ context,
+ storageId,
+ requiresWriteAccess,
+ considerRawFile
+ )?.child(context, basePath)
+ ?: context.fromTreeUri(Uri.parse(DOCUMENTS_TREE_URI))
+ ?.child(context, basePath.substringAfter(Environment.DIRECTORY_DOCUMENTS))
+ ?: return null
+ } else if (Build.VERSION.SDK_INT < 30) {
+ getRootDocumentFile(
+ context,
+ storageId,
+ requiresWriteAccess,
+ considerRawFile
+ )?.child(context, basePath) ?: return null
} else {
- val fileTree = directorySequence.joinToString(prefix = "/", separator = "/")
- context.fromTreeUri(Uri.parse(grantedFile.uri.toString() + Uri.encode(fileTree)))
+ val directorySequence = getDirectorySequence(basePath).toMutableList()
+ val parentTree = ArrayList(directorySequence.size)
+ var grantedFile: DocumentFile? = null
+ // Find granted file tree.
+ // For example, /storage/emulated/0/Music may not granted, but /storage/emulated/0/Music/Pop is granted by user.
+ while (directorySequence.isNotEmpty()) {
+ parentTree.add(directorySequence.removeFirst())
+ val folderTree = parentTree.joinToString(separator = "/")
+ try {
+ grantedFile = context.fromTreeUri(createDocumentUri(storageId, folderTree))
+ if (grantedFile?.canRead() == true) break
+ } catch (e: SecurityException) {
+ // ignore
+ }
+ }
+ if (grantedFile == null || directorySequence.isEmpty()) {
+ grantedFile
+ } else {
+ val fileTree = directorySequence.joinToString(prefix = "/", separator = "/")
+ context.fromTreeUri(Uri.parse(grantedFile.uri.toString() + Uri.encode(fileTree)))
+ }
}
- }
return file?.takeIf {
it.canRead() && (documentType == DocumentFileType.ANY
|| documentType == DocumentFileType.FILE && it.isFile || documentType == DocumentFileType.FOLDER && it.isDirectory)
@@ -857,7 +1000,10 @@ object DocumentFileCompat {
* * `/storage/emulated/0/Alarm/Morning`
*/
@JvmStatic
- fun findUniqueDeepestSubFolders(context: Context, folderFullPaths: Collection): List {
+ fun findUniqueDeepestSubFolders(
+ context: Context,
+ folderFullPaths: Collection
+ ): List {
val paths = folderFullPaths.map { buildAbsolutePath(context, it) }.distinct()
val results = ArrayList(paths)
paths.forEach { path ->
@@ -926,6 +1072,7 @@ object DocumentFileCompat {
stats.f_bavail * stats.f_frsize
} ?: 0
}
+
else -> 0
}
} catch (e: Throwable) {
@@ -945,6 +1092,7 @@ object DocumentFileCompat {
stats.f_blocks * stats.f_frsize - stats.f_bavail * stats.f_frsize
} ?: 0
}
+
else -> 0
}
} catch (e: Throwable) {
@@ -964,6 +1112,7 @@ object DocumentFileCompat {
stats.f_blocks * stats.f_frsize
} ?: 0
}
+
else -> 0
}
} catch (e: Throwable) {
@@ -991,7 +1140,11 @@ object DocumentFileCompat {
if (folder.canRead()) {
DocumentFile.fromFile(folder)
} else {
- getAccessibleRootDocumentFile(context, folder.absolutePath, considerRawFile = false)
+ getAccessibleRootDocumentFile(
+ context,
+ folder.absolutePath,
+ considerRawFile = false
+ )
}
}
}
diff --git a/storage/src/main/java/com/anggrayudi/storage/file/DocumentFileExt.kt b/storage/src/main/kotlin/com/anggrayudi/storage/file/DocumentFileExt.kt
similarity index 70%
rename from storage/src/main/java/com/anggrayudi/storage/file/DocumentFileExt.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/file/DocumentFileExt.kt
index 36c3cd5..51e2984 100644
--- a/storage/src/main/java/com/anggrayudi/storage/file/DocumentFileExt.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/file/DocumentFileExt.kt
@@ -100,7 +100,8 @@ val DocumentFile.id: String
val DocumentFile.rootId: String
get() = DocumentsContract.getRootId(uri)
-fun DocumentFile.isExternalStorageManager(context: Context) = isRawFile && File(uri.path!!).isExternalStorageManager(context)
+fun DocumentFile.isExternalStorageManager(context: Context) =
+ isRawFile && File(uri.path!!).isExternalStorageManager(context)
@RestrictTo(RestrictTo.Scope.LIBRARY)
fun DocumentFile.inKitkatSdCard() = Build.VERSION.SDK_INT < 21 && uri.path?.let {
@@ -130,7 +131,13 @@ fun DocumentFile.isEmpty(context: Context): Boolean {
toRawFile(context)?.list().isNullOrEmpty()
} else try {
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, id)
- context.contentResolver.query(childrenUri, arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID), null, null, null)?.use { it.count == 0 }
+ context.contentResolver.query(
+ childrenUri,
+ arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID),
+ null,
+ null,
+ null
+ )?.use { it.count == 0 }
?: true
} catch (e: Exception) {
true
@@ -145,7 +152,7 @@ fun DocumentFile.isEmpty(context: Context): Boolean {
@WorkerThread
fun DocumentFile.getProperties(context: Context, callback: FileProperties.CalculationCallback) {
when {
- !canRead() -> callback.uiScope.postToUi { callback.onError() }
+ !canRead() -> callback.postToUiScope { callback.onError() }
isDirectory -> {
val properties = FileProperties(
@@ -156,17 +163,18 @@ fun DocumentFile.getProperties(context: Context, callback: FileProperties.Calcul
lastModified = lastModified().let { if (it > 0) Date(it) else null }
)
if (isEmpty(context)) {
- callback.uiScope.postToUi { callback.onComplete(properties) }
+ callback.postToUiScope { callback.onComplete(properties) }
} else {
- val timer = if (callback.updateInterval < 1) null else startCoroutineTimer(repeatMillis = callback.updateInterval) {
- callback.uiScope.postToUi { callback.onUpdate(properties) }
- }
+ val timer =
+ if (callback.updateInterval < 1) null else startCoroutineTimer(repeatMillis = callback.updateInterval) {
+ callback.postToUiScope { callback.onUpdate(properties) }
+ }
val thread = Thread.currentThread()
walkFileTreeForInfo(properties, thread)
timer?.cancel()
// need to store isInterrupted in a variable, because calling it from UI thread always returns false
val interrupted = thread.isInterrupted
- callback.uiScope.postToUi {
+ callback.postToUiScope {
if (interrupted) {
callback.onCanceled(properties)
} else {
@@ -184,7 +192,7 @@ fun DocumentFile.getProperties(context: Context, callback: FileProperties.Calcul
isVirtual = isVirtual,
lastModified = lastModified().let { if (it > 0) Date(it) else null }
)
- callback.uiScope.postToUi { callback.onComplete(properties) }
+ callback.postToUiScope { callback.onComplete(properties) }
}
}
}
@@ -232,8 +240,9 @@ fun DocumentFile.inInternalStorage(context: Context) = inInternalStorage(getStor
* `true` if this file located in primary storage, i.e. external storage.
* All files created by [DocumentFile.fromFile] are always treated from external storage.
*/
-fun DocumentFile.inPrimaryStorage(context: Context) = isTreeDocumentFile && getStorageId(context) == PRIMARY
- || isRawFile && uri.path.orEmpty().startsWith(SimpleStorage.externalStoragePath)
+fun DocumentFile.inPrimaryStorage(context: Context) =
+ isTreeDocumentFile && getStorageId(context) == PRIMARY
+ || isRawFile && uri.path.orEmpty().startsWith(SimpleStorage.externalStoragePath)
/**
* `true` if this file located in SD Card
@@ -241,7 +250,8 @@ fun DocumentFile.inPrimaryStorage(context: Context) = isTreeDocumentFile && getS
fun DocumentFile.inSdCardStorage(context: Context) =
getStorageId(context).let { it.matches(DocumentFileCompat.SD_CARD_STORAGE_ID_REGEX) || Build.VERSION.SDK_INT < 21 && it == StorageId.KITKAT_SDCARD }
-fun DocumentFile.inDataStorage(context: Context) = isRawFile && File(uri.path!!).inDataStorage(context)
+fun DocumentFile.inDataStorage(context: Context) =
+ isRawFile && File(uri.path!!).inDataStorage(context)
/**
* `true` if this file was created with [File]
@@ -286,7 +296,14 @@ val DocumentFile.mimeTypeByFileName: String?
fun DocumentFile.toRawFile(context: Context): File? {
return when {
isRawFile -> File(uri.path ?: return null)
- inPrimaryStorage(context) -> File("${SimpleStorage.externalStoragePath}/${getBasePath(context)}")
+ inPrimaryStorage(context) -> File(
+ "${SimpleStorage.externalStoragePath}/${
+ getBasePath(
+ context
+ )
+ }"
+ )
+
else -> getStorageId(context).let { storageId ->
if (storageId.isKitkatSdCardStorageId()) {
DocumentFileCompat.getKitkatSdCardRootFile(getBasePath(context))
@@ -305,7 +322,11 @@ fun DocumentFile.toRawDocumentFile(context: Context): DocumentFile? {
fun DocumentFile.toTreeDocumentFile(context: Context): DocumentFile? {
return if (isRawFile) {
- DocumentFileCompat.fromFile(context, toRawFile(context) ?: return null, considerRawFile = false)
+ DocumentFileCompat.fromFile(
+ context,
+ toRawFile(context) ?: return null,
+ considerRawFile = false
+ )
} else if (isTreeDocumentFile) {
this
} else {
@@ -316,7 +337,8 @@ fun DocumentFile.toTreeDocumentFile(context: Context): DocumentFile? {
}
}
-fun DocumentFile.toMediaFile(context: Context) = if (isTreeDocumentFile) null else MediaFile(context, uri)
+fun DocumentFile.toMediaFile(context: Context) =
+ if (isTreeDocumentFile) null else MediaFile(context, uri)
/**
* It will try converting [androidx.documentfile.provider.SingleDocumentFile]
@@ -326,7 +348,11 @@ fun DocumentFile.toMediaFile(context: Context) = if (isTreeDocumentFile) null el
* @see toTreeDocumentFile
*/
@JvmOverloads
-fun DocumentFile.changeName(context: Context, newBaseName: String, newExtension: String? = null): DocumentFile? {
+fun DocumentFile.changeName(
+ context: Context,
+ newBaseName: String,
+ newExtension: String? = null
+): DocumentFile? {
val newFileExtension = newExtension ?: extension
val newName = "$newBaseName.$newFileExtension".trimEnd('.')
if (newName.isEmpty()) {
@@ -358,7 +384,11 @@ fun DocumentFile.changeName(context: Context, newBaseName: String, newExtension:
* @param path single file name or file path. Empty string returns to itself.
*/
@JvmOverloads
-fun DocumentFile.child(context: Context, path: String, requiresWriteAccess: Boolean = false): DocumentFile? {
+fun DocumentFile.child(
+ context: Context,
+ path: String,
+ requiresWriteAccess: Boolean = false
+): DocumentFile? {
return when {
path.isEmpty() -> this
isDirectory -> {
@@ -368,7 +398,8 @@ fun DocumentFile.child(context: Context, path: String, requiresWriteAccess: Bool
var currentDirectory = this
val resolver = context.contentResolver
DocumentFileCompat.getDirectorySequence(path).forEach {
- val directory = currentDirectory.quickFindTreeFile(context, resolver, it) ?: return null
+ val directory =
+ currentDirectory.quickFindTreeFile(context, resolver, it) ?: return null
if (directory.canRead()) {
currentDirectory = directory
} else {
@@ -396,15 +427,26 @@ fun DocumentFile.quickFindRawFile(name: String): DocumentFile? {
*/
@SuppressLint("NewApi")
@RestrictTo(RestrictTo.Scope.LIBRARY)
-fun DocumentFile.quickFindTreeFile(context: Context, resolver: ContentResolver, name: String): DocumentFile? {
+fun DocumentFile.quickFindTreeFile(
+ context: Context,
+ resolver: ContentResolver,
+ name: String
+): DocumentFile? {
try {
// Optimized algorithm. Do not change unless you really know algorithm complexity.
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, id)
- resolver.query(childrenUri, arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID), null, null, null)?.use {
+ resolver.query(
+ childrenUri,
+ arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID),
+ null,
+ null,
+ null
+ )?.use {
val columnName = arrayOf(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
while (it.moveToNext()) {
try {
- val documentUri = DocumentsContract.buildDocumentUriUsingTree(uri, it.getString(0))
+ val documentUri =
+ DocumentsContract.buildDocumentUriUsingTree(uri, it.getString(0))
resolver.query(documentUri, columnName, null, null, null)?.use { childCursor ->
if (childCursor.moveToFirst() && name == childCursor.getString(0))
return context.fromTreeUri(documentUri)
@@ -421,14 +463,23 @@ fun DocumentFile.quickFindTreeFile(context: Context, resolver: ContentResolver,
}
@RestrictTo(RestrictTo.Scope.LIBRARY)
-fun DocumentFile.shouldWritable(context: Context, requiresWriteAccess: Boolean) = requiresWriteAccess && isWritable(context) || !requiresWriteAccess
+fun DocumentFile.shouldWritable(context: Context, requiresWriteAccess: Boolean) =
+ requiresWriteAccess && isWritable(context) || !requiresWriteAccess
@RestrictTo(RestrictTo.Scope.LIBRARY)
-fun DocumentFile.takeIfWritable(context: Context, requiresWriteAccess: Boolean) = takeIf { it.shouldWritable(context, requiresWriteAccess) }
+fun DocumentFile.takeIfWritable(context: Context, requiresWriteAccess: Boolean) =
+ takeIf { it.shouldWritable(context, requiresWriteAccess) }
@RestrictTo(RestrictTo.Scope.LIBRARY)
-fun DocumentFile.checkRequirements(context: Context, requiresWriteAccess: Boolean, considerRawFile: Boolean) = canRead() &&
- (considerRawFile || isExternalStorageManager(context)) && shouldWritable(context, requiresWriteAccess)
+fun DocumentFile.checkRequirements(
+ context: Context,
+ requiresWriteAccess: Boolean,
+ considerRawFile: Boolean
+) = canRead() &&
+ (considerRawFile || isExternalStorageManager(context)) && shouldWritable(
+ context,
+ requiresWriteAccess
+)
/**
* @return File path without storage ID. Returns empty `String` if:
@@ -436,7 +487,6 @@ fun DocumentFile.checkRequirements(context: Context, requiresWriteAccess: Boolea
* * It is not a raw file and the authority is neither [DocumentFileCompat.EXTERNAL_STORAGE_AUTHORITY] nor [DocumentFileCompat.DOWNLOADS_FOLDER_AUTHORITY]
* * The authority is [DocumentFileCompat.DOWNLOADS_FOLDER_AUTHORITY], but [isTreeDocumentFile] returns `false`
*/
-@Suppress("DEPRECATION")
fun DocumentFile.getBasePath(context: Context): String {
val path = uri.path.orEmpty()
val storageID = getStorageId(context)
@@ -444,7 +494,9 @@ fun DocumentFile.getBasePath(context: Context): String {
isRawFile -> File(path).getBasePath(context)
isDocumentsDocument -> {
- "${Environment.DIRECTORY_DOCUMENTS}/${path.substringAfterLast("/home:", "")}".trimEnd('/')
+ "${Environment.DIRECTORY_DOCUMENTS}/${path.substringAfterLast("/home:", "")}".trimEnd(
+ '/'
+ )
}
isExternalStorageDocument && path.contains("/document/$storageID:") -> {
@@ -475,7 +527,8 @@ fun DocumentFile.getBasePath(context: Context): String {
}
}
- else -> path.substringAfterLast(SimpleStorage.externalStoragePath, "").trimFileSeparator()
+ else -> path.substringAfterLast(SimpleStorage.externalStoragePath, "")
+ .trimFileSeparator()
}
}
@@ -532,7 +585,8 @@ fun DocumentFile.getRootPath(context: Context) = when {
else -> SimpleStorage.externalStoragePath
}
-fun DocumentFile.getRelativePath(context: Context) = getBasePath(context).substringBeforeLast('/', "")
+fun DocumentFile.getRelativePath(context: Context) =
+ getBasePath(context).substringBeforeLast('/', "")
/**
* * For file in SD Card => `/storage/6881-2249/Music/song.mp3`
@@ -548,7 +602,6 @@ fun DocumentFile.getRelativePath(context: Context) = getBasePath(context).substr
* @see File.getAbsolutePath
* @see getSimplePath
*/
-@Suppress("DEPRECATION")
fun DocumentFile.getAbsolutePath(context: Context): String {
val path = uri.path.orEmpty()
val storageID = getStorageId(context)
@@ -569,7 +622,8 @@ fun DocumentFile.getAbsolutePath(context: Context): String {
}
}
- uri.toString().let { it == DocumentFileCompat.DOWNLOADS_TREE_URI || it == "${DocumentFileCompat.DOWNLOADS_TREE_URI}/document/downloads" } ->
+ uri.toString()
+ .let { it == DocumentFileCompat.DOWNLOADS_TREE_URI || it == "${DocumentFileCompat.DOWNLOADS_TREE_URI}/document/downloads" } ->
PublicDirectory.DOWNLOADS.absolutePath
isDownloadsDocument -> {
@@ -587,7 +641,9 @@ fun DocumentFile.getAbsolutePath(context: Context): String {
while (parent.parentFile?.also { parent = it } != null) {
parentTree.add(parent.name.orEmpty())
}
- "${SimpleStorage.externalStoragePath}/${parentTree.reversed().joinToString("/")}".trimEnd('/')
+ "${SimpleStorage.externalStoragePath}/${
+ parentTree.reversed().joinToString("/")
+ }".trimEnd('/')
} else {
// we can't use msf/msd ID as MediaFile ID to fetch relative path, so just return empty String
""
@@ -599,7 +655,10 @@ fun DocumentFile.getAbsolutePath(context: Context): String {
}
!isTreeDocumentFile -> ""
- inPrimaryStorage(context) -> "${SimpleStorage.externalStoragePath}/${getBasePath(context)}".trimEnd('/')
+ inPrimaryStorage(context) -> "${SimpleStorage.externalStoragePath}/${getBasePath(context)}".trimEnd(
+ '/'
+ )
+
else -> "/storage/$storageID/${getBasePath(context)}".trimEnd('/')
}
}
@@ -607,7 +666,8 @@ fun DocumentFile.getAbsolutePath(context: Context): String {
/**
* @see getAbsolutePath
*/
-fun DocumentFile.getSimplePath(context: Context) = "${getStorageId(context)}:${getBasePath(context)}".removePrefix(":")
+fun DocumentFile.getSimplePath(context: Context) =
+ "${getStorageId(context)}:${getBasePath(context)}".removePrefix(":")
@JvmOverloads
fun DocumentFile.findParent(context: Context, requiresWriteAccess: Boolean = true): DocumentFile? {
@@ -616,15 +676,21 @@ fun DocumentFile.findParent(context: Context, requiresWriteAccess: Boolean = tru
if (parentPath.isEmpty()) {
null
} else {
- DocumentFileCompat.fromFullPath(context, parentPath, requiresWriteAccess = requiresWriteAccess)?.also {
+ DocumentFileCompat.fromFullPath(
+ context,
+ parentPath,
+ requiresWriteAccess = requiresWriteAccess
+ )?.also {
try {
val field = DocumentFile::class.java.getDeclaredField("mParent")
field.isAccessible = true
field.set(this, it)
} catch (e: Exception) {
Log.w(
- "DocumentFileUtils", "Cannot modify field mParent in androidx.documentfile.provider.DocumentFile. " +
- "Please exclude DocumentFile from obfuscation.", e
+ "DocumentFileUtils",
+ "Cannot modify field mParent in androidx.documentfile.provider.DocumentFile. " +
+ "Please exclude DocumentFile from obfuscation.",
+ e
)
}
}
@@ -649,11 +715,21 @@ fun DocumentFile.recreateFile(context: Context): DocumentFile? {
}
@JvmOverloads
-fun DocumentFile.getRootDocumentFile(context: Context, requiresWriteAccess: Boolean = false) = when {
- isTreeDocumentFile -> DocumentFileCompat.getRootDocumentFile(context, getStorageId(context), requiresWriteAccess)
- isRawFile -> uri.path?.run { File(this).getRootRawFile(context, requiresWriteAccess)?.let { DocumentFile.fromFile(it) } }
- else -> null
-}
+fun DocumentFile.getRootDocumentFile(context: Context, requiresWriteAccess: Boolean = false) =
+ when {
+ isTreeDocumentFile -> DocumentFileCompat.getRootDocumentFile(
+ context,
+ getStorageId(context),
+ requiresWriteAccess
+ )
+
+ isRawFile -> uri.path?.run {
+ File(this).getRootRawFile(context, requiresWriteAccess)
+ ?.let { DocumentFile.fromFile(it) }
+ }
+
+ else -> null
+ }
/**
* @return `true` if this file exists and writeable. [DocumentFile.canWrite] may return false if you have no URI permission for read & write access.
@@ -664,13 +740,18 @@ fun DocumentFile.canModify(context: Context) = canRead() && isWritable(context)
* Use it, because [DocumentFile.canWrite] is unreliable on Android 10.
* Read [this issue](https://github.com/anggrayudi/SimpleStorage/issues/24#issuecomment-830000378)
*/
-fun DocumentFile.isWritable(context: Context) = if (isRawFile) File(uri.path!!).isWritable(context) else canWrite()
+fun DocumentFile.isWritable(context: Context) =
+ if (isRawFile) File(uri.path!!).isWritable(context) else canWrite()
fun DocumentFile.isRootUriPermissionGranted(context: Context): Boolean {
- return isExternalStorageDocument && DocumentFileCompat.isStorageUriPermissionGranted(context, getStorageId(context))
+ return isExternalStorageDocument && DocumentFileCompat.isStorageUriPermissionGranted(
+ context,
+ getStorageId(context)
+ )
}
-fun DocumentFile.getFormattedSize(context: Context): String = Formatter.formatFileSize(context, length())
+fun DocumentFile.getFormattedSize(context: Context): String =
+ Formatter.formatFileSize(context, length())
/**
* Avoid duplicate file name.
@@ -688,7 +769,9 @@ fun DocumentFile.autoIncrementFileName(context: Context, filename: String): Stri
val prefix = "$baseName ("
var lastFileCount = files.filter {
val name = it.name.orEmpty()
- name.startsWith(prefix) && (DocumentFileCompat.FILE_NAME_DUPLICATION_REGEX_WITH_EXTENSION.matches(name)
+ name.startsWith(prefix) && (DocumentFileCompat.FILE_NAME_DUPLICATION_REGEX_WITH_EXTENSION.matches(
+ name
+ )
|| DocumentFileCompat.FILE_NAME_DUPLICATION_REGEX_WITHOUT_EXTENSION.matches(name))
}.maxOfOrNull {
it.name.orEmpty().substringAfterLast('(', "")
@@ -706,7 +789,11 @@ fun DocumentFile.autoIncrementFileName(context: Context, filename: String): Stri
*/
@WorkerThread
@JvmOverloads
-fun DocumentFile.createBinaryFile(context: Context, name: String, mode: CreateMode = CreateMode.CREATE_NEW) =
+fun DocumentFile.createBinaryFile(
+ context: Context,
+ name: String,
+ mode: CreateMode = CreateMode.CREATE_NEW
+) =
makeFile(context, name, MimeType.BINARY_FILE, mode)
/**
@@ -739,11 +826,12 @@ fun DocumentFile.makeFile(
val filename = cleanName.substringAfterLast('/')
val extensionByName = MimeType.getExtensionFromFileName(cleanName)
- val extension = if (extensionByName.isNotEmpty() && (mimeType == null || mimeType == MimeType.UNKNOWN || mimeType == MimeType.BINARY_FILE)) {
- extensionByName
- } else {
- MimeType.getExtensionFromMimeTypeOrFileName(mimeType, cleanName)
- }
+ val extension =
+ if (extensionByName.isNotEmpty() && (mimeType == null || mimeType == MimeType.UNKNOWN || mimeType == MimeType.BINARY_FILE)) {
+ extensionByName
+ } else {
+ MimeType.getExtensionFromMimeTypeOrFileName(mimeType, cleanName)
+ }
val baseFileName = filename.removeSuffix(".$extension")
val fullFileName = "$baseFileName.$extension".trimEnd('.')
@@ -770,7 +858,14 @@ fun DocumentFile.makeFile(
if (isRawFile) {
// RawDocumentFile does not avoid duplicate file name, but TreeDocumentFile does.
- return DocumentFile.fromFile(toRawFile(context)?.makeFile(context, cleanName, mimeType, createMode) ?: return null)
+ return DocumentFile.fromFile(
+ toRawFile(context)?.makeFile(
+ context,
+ cleanName,
+ mimeType,
+ createMode
+ ) ?: return null
+ )
}
val correctMimeType = MimeType.getMimeTypeFromExtension(extension).let {
@@ -802,20 +897,28 @@ fun DocumentFile.makeFolder(
}
if (isRawFile) {
- return DocumentFile.fromFile(toRawFile(context)?.makeFolder(context, name, mode) ?: return null)
+ return DocumentFile.fromFile(
+ toRawFile(context)?.makeFolder(context, name, mode) ?: return null
+ )
}
// if name is "Aduhhh/Now/Dee", system will convert it to Aduhhh_Now_Dee, so create a sequence
- val directorySequence = DocumentFileCompat.getDirectorySequence(name.removeForbiddenCharsFromFilename()).toMutableList()
+ val directorySequence =
+ DocumentFileCompat.getDirectorySequence(name.removeForbiddenCharsFromFilename())
+ .toMutableList()
val folderNameLevel1 = directorySequence.removeFirstOrNull() ?: return null
- var currentDirectory = if (isDownloadsDocument && isTreeDocumentFile) (toWritableDownloadsDocumentFile(context) ?: return null) else this
+ var currentDirectory =
+ if (isDownloadsDocument && isTreeDocumentFile) (toWritableDownloadsDocumentFile(context)
+ ?: return null) else this
val folderLevel1 = currentDirectory.child(context, folderNameLevel1)
currentDirectory = if (folderLevel1 == null || mode == CreateMode.CREATE_NEW) {
currentDirectory.createDirectory(folderNameLevel1) ?: return null
} else if (mode == CreateMode.REPLACE) {
folderLevel1.forceDelete(context, true)
- if (folderLevel1.isDirectory) folderLevel1 else currentDirectory.createDirectory(folderNameLevel1) ?: return null
+ if (folderLevel1.isDirectory) folderLevel1 else currentDirectory.createDirectory(
+ folderNameLevel1
+ ) ?: return null
} else if (mode != CreateMode.SKIP_IF_EXISTS && folderLevel1.isDirectory && folderLevel1.canRead()) {
folderLevel1
} else {
@@ -848,13 +951,23 @@ fun DocumentFile.toWritableDownloadsDocumentFile(context: Context): DocumentFile
return if (isDownloadsDocument) {
val path = uri.path.orEmpty()
when {
- uri.toString() == "${DocumentFileCompat.DOWNLOADS_TREE_URI}/document/downloads" -> takeIf { it.isWritable(context) }
+ uri.toString() == "${DocumentFileCompat.DOWNLOADS_TREE_URI}/document/downloads" -> takeIf {
+ it.isWritable(
+ context
+ )
+ }
// content://com.android.providers.downloads.documents/document/raw%3A%2Fstorage%2Femulated%2F0%2FDownload%2Fscreenshot.jpeg
// content://com.android.providers.downloads.documents/tree/downloads/document/raw%3A%2Fstorage%2Femulated%2F0%2FDownload%2FIKO5
// raw:/storage/emulated/0/Download/IKO5
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && (path.startsWith("/tree/downloads/document/raw:") || path.startsWith("/document/raw:")) -> {
- val downloads = DocumentFileCompat.fromPublicFolder(context, PublicDirectory.DOWNLOADS, considerRawFile = false) ?: return null
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && (path.startsWith("/tree/downloads/document/raw:") || path.startsWith(
+ "/document/raw:"
+ )) -> {
+ val downloads = DocumentFileCompat.fromPublicFolder(
+ context,
+ PublicDirectory.DOWNLOADS,
+ considerRawFile = false
+ ) ?: return null
val fullPath = path.substringAfterLast("/document/raw:")
val subFile = fullPath.substringAfter("/${Environment.DIRECTORY_DOWNLOADS}", "")
downloads.child(context, subFile, true)
@@ -892,7 +1005,10 @@ fun DocumentFile.toWritableDownloadsDocumentFile(context: Context): DocumentFile
/**
* @param names full file names, with their extension
*/
-fun DocumentFile.findFiles(names: Array, documentType: DocumentFileType = DocumentFileType.ANY): List {
+fun DocumentFile.findFiles(
+ names: Array,
+ documentType: DocumentFileType = DocumentFileType.ANY
+): List {
val files = listFiles().filter { it.name in names }
return when (documentType) {
DocumentFileType.FILE -> files.filter { it.isFile }
@@ -901,12 +1017,14 @@ fun DocumentFile.findFiles(names: Array, documentType: DocumentFileType
}
}
-fun DocumentFile.findFolder(name: String): DocumentFile? = listFiles().find { it.name == name && it.isDirectory }
+fun DocumentFile.findFolder(name: String): DocumentFile? =
+ listFiles().find { it.name == name && it.isDirectory }
/**
* Expect the file is a file literally, not a folder.
*/
-fun DocumentFile.findFileLiterally(name: String): DocumentFile? = listFiles().find { it.name == name && it.isFile }
+fun DocumentFile.findFileLiterally(name: String): DocumentFile? =
+ listFiles().find { it.name == name && it.isFile }
/**
* @param recursive walk into sub folders
@@ -938,22 +1056,28 @@ fun DocumentFile.search(
if (regex != null) {
sequence = sequence.filter { regex.matches(it.name.orEmpty()) }
}
- val hasMimeTypeFilter = !mimeTypes.isNullOrEmpty() && !mimeTypes.any { it == MimeType.UNKNOWN }
+ val hasMimeTypeFilter =
+ !mimeTypes.isNullOrEmpty() && !mimeTypes.any { it == MimeType.UNKNOWN }
when {
- hasMimeTypeFilter || documentType == DocumentFileType.FILE -> sequence = sequence.filter { it.isFile }
- documentType == DocumentFileType.FOLDER -> sequence = sequence.filter { it.isDirectory }
+ hasMimeTypeFilter || documentType == DocumentFileType.FILE -> sequence =
+ sequence.filter { it.isFile }
+
+ documentType == DocumentFileType.FOLDER -> sequence =
+ sequence.filter { it.isDirectory }
}
if (hasMimeTypeFilter) {
sequence = sequence.filter { it.matchesMimeTypes(mimeTypes!!) }
}
val result = sequence.toList()
- if (name.isEmpty()) result else result.firstOrNull { it.name == name }?.let { listOf(it) } ?: emptyList()
+ if (name.isEmpty()) result else result.firstOrNull { it.name == name }
+ ?.let { listOf(it) } ?: emptyList()
}
}
}
private fun DocumentFile.matchesMimeTypes(filterMimeTypes: Array): Boolean {
- return filterMimeTypes.isEmpty() || !MimeTypeFilter.matches(mimeTypeByFileName, filterMimeTypes).isNullOrEmpty()
+ return filterMimeTypes.isEmpty() || !MimeTypeFilter.matches(mimeTypeByFileName, filterMimeTypes)
+ .isNullOrEmpty()
}
private fun DocumentFile.walkFileTreeForSearch(
@@ -982,11 +1106,22 @@ private fun DocumentFile.walkFileTreeForSearch(
} else {
if (documentType != DocumentFileType.FILE) {
val folderName = file.name.orEmpty()
- if ((nameFilter.isEmpty() || folderName == nameFilter) && (regex == null || regex.matches(folderName))) {
+ if ((nameFilter.isEmpty() || folderName == nameFilter) && (regex == null || regex.matches(
+ folderName
+ ))
+ ) {
fileTree.add(file)
}
}
- fileTree.addAll(file.walkFileTreeForSearch(documentType, mimeTypes, nameFilter, regex, thread))
+ fileTree.addAll(
+ file.walkFileTreeForSearch(
+ documentType,
+ mimeTypes,
+ nameFilter,
+ regex,
+ thread
+ )
+ )
}
}
return fileTree
@@ -1070,20 +1205,29 @@ private fun DocumentFile.walkFileTreeAndDeleteEmptyFolders(): List
*/
@JvmOverloads
@WorkerThread
-fun DocumentFile.openOutputStream(context: Context, append: Boolean = true) = uri.openOutputStream(context, append)
+fun DocumentFile.openOutputStream(context: Context, append: Boolean = true) =
+ uri.openOutputStream(context, append)
@WorkerThread
fun DocumentFile.openInputStream(context: Context) = uri.openInputStream(context)
@UiThread
fun DocumentFile.openFileIntent(context: Context, authority: String) = Intent(Intent.ACTION_VIEW)
- .setData(if (isRawFile) FileProvider.getUriForFile(context, authority, File(uri.path!!)) else uri)
+ .setData(
+ if (isRawFile) FileProvider.getUriForFile(
+ context,
+ authority,
+ File(uri.path!!)
+ ) else uri
+ )
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
-fun DocumentFile.hasParent(context: Context, parent: DocumentFile) = getAbsolutePath(context).hasParent(parent.getAbsolutePath(context))
+fun DocumentFile.hasParent(context: Context, parent: DocumentFile) =
+ getAbsolutePath(context).hasParent(parent.getAbsolutePath(context))
-fun DocumentFile.childOf(context: Context, parent: DocumentFile) = getAbsolutePath(context).childOf(parent.getAbsolutePath(context))
+fun DocumentFile.childOf(context: Context, parent: DocumentFile) =
+ getAbsolutePath(context).childOf(parent.getAbsolutePath(context))
private fun DocumentFile.walkFileTree(context: Context): List {
val fileTree = mutableListOf()
@@ -1136,7 +1280,7 @@ fun List.compressToZip(
deleteSourceWhenComplete: Boolean = false,
callback: ZipCompressionCallback
) {
- callback.uiScope.postToUi { callback.onCountingFiles() }
+ callback.postToUiScope { callback.onCountingFiles() }
val treeFiles = ArrayList(size)
val mediaFiles = ArrayList(size)
var foldersBasePath = mutableListOf()
@@ -1144,8 +1288,11 @@ fun List.compressToZip(
for (srcFile in distinctBy { it.uri }) {
if (srcFile.exists()) {
if (!srcFile.canRead()) {
- callback.uiScope.postToUi {
- callback.onFailed(ZipCompressionCallback.ErrorCode.STORAGE_PERMISSION_DENIED, "Can't read file: ${srcFile.uri}")
+ callback.postToUiScope {
+ callback.onFailed(
+ ZipCompressionCallback.ErrorCode.STORAGE_PERMISSION_DENIED,
+ "Can't read file: ${srcFile.uri}"
+ )
}
return
} else if (srcFile.isFile) {
@@ -1158,7 +1305,12 @@ fun List.compressToZip(
directories.add(srcFile)
}
} else {
- callback.uiScope.postToUi { callback.onFailed(ZipCompressionCallback.ErrorCode.MISSING_ENTRY_FILE, "File not found: ${srcFile.uri}") }
+ callback.postToUiScope {
+ callback.onFailed(
+ ZipCompressionCallback.ErrorCode.MISSING_ENTRY_FILE,
+ "File not found: ${srcFile.uri}"
+ )
+ }
return
}
}
@@ -1171,11 +1323,15 @@ fun List.compressToZip(
*/
class EntryFile(val file: DocumentFile, var path: String) {
- override fun equals(other: Any?) = this === other || other is EntryFile && path == other.path
+ override fun equals(other: Any?) =
+ this === other || other is EntryFile && path == other.path
+
override fun hashCode() = path.hashCode()
}
- val srcFolders = directories.map { EntryFile(it, it.getAbsolutePath(context)) }.distinctBy { it.path }.toMutableList()
+ val srcFolders =
+ directories.map { EntryFile(it, it.getAbsolutePath(context)) }.distinctBy { it.path }
+ .toMutableList()
DocumentFileCompat.findUniqueParents(context, srcFolders.map { it.path }).forEach { parent ->
srcFolders.removeAll { it.path.childOf(parent) }
}
@@ -1192,7 +1348,12 @@ fun List.compressToZip(
val totalFiles = treeFiles.size + mediaFiles.size
if (totalFiles == 0) {
- callback.uiScope.postToUi { callback.onFailed(ZipCompressionCallback.ErrorCode.MISSING_ENTRY_FILE, "No entry files found") }
+ callback.postToUiScope {
+ callback.onFailed(
+ ZipCompressionCallback.ErrorCode.MISSING_ENTRY_FILE,
+ "No entry files found"
+ )
+ }
return
}
@@ -1200,15 +1361,24 @@ fun List.compressToZip(
treeFiles.forEach { actualFilesSize += it.length() }
mediaFiles.forEach { actualFilesSize += it.length() }
- if (!callback.onCheckFreeSpace(DocumentFileCompat.getFreeSpace(context, targetZipFile.getStorageId(context)), actualFilesSize)) {
- callback.uiScope.postToUi { callback.onFailed(ZipCompressionCallback.ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH) }
+ if (!callback.onCheckFreeSpace(
+ DocumentFileCompat.getFreeSpace(
+ context,
+ targetZipFile.getStorageId(context)
+ ), actualFilesSize
+ )
+ ) {
+ callback.postToUiScope { callback.onFailed(ZipCompressionCallback.ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH) }
return
}
val entryFiles = ArrayList(totalFiles)
treeFiles.forEach { entryFiles.add(EntryFile(it, it.getBasePath(context))) }
- val parentPaths = DocumentFileCompat.findUniqueParents(context, entryFiles.map { "/" + it.path.substringBeforeLast('/') }).map { it.trim('/') }
- foldersBasePath = DocumentFileCompat.findUniqueParents(context, foldersBasePath.map { "/$it" }).map { it.trim('/') }.toMutableList()
+ val parentPaths = DocumentFileCompat.findUniqueParents(
+ context,
+ entryFiles.map { "/" + it.path.substringBeforeLast('/') }).map { it.trim('/') }
+ foldersBasePath = DocumentFileCompat.findUniqueParents(context, foldersBasePath.map { "/$it" })
+ .map { it.trim('/') }.toMutableList()
entryFiles.forEach { entry ->
for (parentPath in parentPaths) {
if (entry.path.startsWith(parentPath)) {
@@ -1225,27 +1395,37 @@ fun List.compressToZip(
mediaFiles.forEach { entryFiles.add(EntryFile(it, it.fullName)) }
val duplicateFiles = entryFiles.groupingBy { it }.eachCount().filterValues { it > 1 }
if (duplicateFiles.isNotEmpty()) {
- callback.uiScope.postToUi {
- callback.onFailed(ZipCompressionCallback.ErrorCode.DUPLICATE_ENTRY_FILE, "Found duplicate entry files: ${duplicateFiles.keys.map { it.file.uri }}")
+ callback.postToUiScope {
+ callback.onFailed(
+ ZipCompressionCallback.ErrorCode.DUPLICATE_ENTRY_FILE,
+ "Found duplicate entry files: ${duplicateFiles.keys.map { it.file.uri }}"
+ )
}
return
}
var zipFile: DocumentFile? = targetZipFile
if (!targetZipFile.exists() || targetZipFile.isDirectory) {
- zipFile = targetZipFile.findParent(context)?.makeFile(context, targetZipFile.fullName, MimeType.ZIP)
+ zipFile = targetZipFile.findParent(context)
+ ?.makeFile(context, targetZipFile.fullName, MimeType.ZIP)
}
if (zipFile == null) {
- callback.uiScope.postToUi { callback.onFailed(ZipCompressionCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
+ callback.postToUiScope { callback.onFailed(ZipCompressionCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
return
}
if (!zipFile.isWritable(context)) {
- callback.uiScope.postToUi { callback.onFailed(ZipCompressionCallback.ErrorCode.STORAGE_PERMISSION_DENIED, "Destination ZIP file is not writable") }
+ callback.postToUiScope {
+ callback.onFailed(
+ ZipCompressionCallback.ErrorCode.STORAGE_PERMISSION_DENIED,
+ "Destination ZIP file is not writable"
+ )
+ }
return
}
val thread = Thread.currentThread()
- val reportInterval = awaitUiResult(callback.uiScope) { callback.onStart(entryFiles.map { it.file }, thread) }
+ val reportInterval =
+ awaitUiResult(callback.uiScope) { callback.onStart(entryFiles.map { it.file }, thread) }
if (reportInterval < 0) return
var success = false
@@ -1259,8 +1439,13 @@ fun List.compressToZip(
// using timer on small file is useless. We set minimum 10MB.
if (reportInterval > 0 && actualFilesSize > 10 * FileSize.MB) {
timer = startCoroutineTimer(repeatMillis = reportInterval) {
- val report = ZipCompressionCallback.Report(bytesCompressed * 100f / actualFilesSize, bytesCompressed, writeSpeed, fileCompressedCount)
- callback.uiScope.postToUi { callback.onReport(report) }
+ val report = ZipCompressionCallback.Report(
+ bytesCompressed * 100f / actualFilesSize,
+ bytesCompressed,
+ writeSpeed,
+ fileCompressedCount
+ )
+ callback.postToUiScope { callback.onReport(report) }
writeSpeed = 0
}
}
@@ -1280,13 +1465,28 @@ fun List.compressToZip(
}
success = true
} catch (e: InterruptedIOException) {
- callback.uiScope.postToUi { callback.onFailed(ZipCompressionCallback.ErrorCode.CANCELED) }
+ callback.postToUiScope { callback.onFailed(ZipCompressionCallback.ErrorCode.CANCELED) }
} catch (e: FileNotFoundException) {
- callback.uiScope.postToUi { callback.onFailed(ZipCompressionCallback.ErrorCode.MISSING_ENTRY_FILE, e.message) }
+ callback.postToUiScope {
+ callback.onFailed(
+ ZipCompressionCallback.ErrorCode.MISSING_ENTRY_FILE,
+ e.message
+ )
+ }
} catch (e: IOException) {
- callback.uiScope.postToUi { callback.onFailed(ZipCompressionCallback.ErrorCode.UNKNOWN_IO_ERROR, e.message) }
+ callback.postToUiScope {
+ callback.onFailed(
+ ZipCompressionCallback.ErrorCode.UNKNOWN_IO_ERROR,
+ e.message
+ )
+ }
} catch (e: SecurityException) {
- callback.uiScope.postToUi { callback.onFailed(ZipCompressionCallback.ErrorCode.STORAGE_PERMISSION_DENIED, e.message) }
+ callback.postToUiScope {
+ callback.onFailed(
+ ZipCompressionCallback.ErrorCode.STORAGE_PERMISSION_DENIED,
+ e.message
+ )
+ }
} finally {
timer?.cancel()
zos.closeEntryQuietly()
@@ -1294,11 +1494,18 @@ fun List.compressToZip(
}
if (success) {
if (deleteSourceWhenComplete) {
- callback.uiScope.postToUi { callback.onDeleteEntryFiles() }
+ callback.postToUiScope { callback.onDeleteEntryFiles() }
forEach { it.forceDelete(context) }
}
val sizeReduction = (actualFilesSize - zipFile.length()).toFloat() / actualFilesSize * 100
- callback.uiScope.postToUi { callback.onCompleted(zipFile, actualFilesSize, totalFiles, sizeReduction) }
+ callback.postToUiScope {
+ callback.onCompleted(
+ zipFile,
+ actualFilesSize,
+ totalFiles,
+ sizeReduction
+ )
+ }
} else {
zipFile.delete()
}
@@ -1314,22 +1521,22 @@ fun DocumentFile.decompressZip(
targetFolder: DocumentFile,
callback: ZipDecompressionCallback
) {
- callback.uiScope.postToUi { callback.onValidate() }
+ callback.postToUiScope { callback.onValidate() }
if (exists()) {
if (!canRead()) {
- callback.uiScope.postToUi { callback.onFailed(ZipDecompressionCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
+ callback.postToUiScope { callback.onFailed(ZipDecompressionCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
return
} else if (isFile) {
if (type != MimeType.ZIP && name?.endsWith(".zip", ignoreCase = true) != false) {
- callback.uiScope.postToUi { callback.onFailed(ZipDecompressionCallback.ErrorCode.NOT_A_ZIP_FILE) }
+ callback.postToUiScope { callback.onFailed(ZipDecompressionCallback.ErrorCode.NOT_A_ZIP_FILE) }
return
}
} else {
- callback.uiScope.postToUi { callback.onFailed(ZipDecompressionCallback.ErrorCode.NOT_A_ZIP_FILE) }
+ callback.postToUiScope { callback.onFailed(ZipDecompressionCallback.ErrorCode.NOT_A_ZIP_FILE) }
return
}
} else {
- callback.uiScope.postToUi { callback.onFailed(ZipDecompressionCallback.ErrorCode.MISSING_ZIP_FILE) }
+ callback.postToUiScope { callback.onFailed(ZipDecompressionCallback.ErrorCode.MISSING_ZIP_FILE) }
return
}
@@ -1338,13 +1545,19 @@ fun DocumentFile.decompressZip(
destFolder = targetFolder.findParent(context)?.makeFolder(context, targetFolder.fullName)
}
if (destFolder == null || !destFolder.isWritable(context)) {
- callback.uiScope.postToUi { callback.onFailed(ZipDecompressionCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
+ callback.postToUiScope { callback.onFailed(ZipDecompressionCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
return
}
val zipSize = length()
- if (!callback.onCheckFreeSpace(DocumentFileCompat.getFreeSpace(context, targetFolder.getStorageId(context)), zipSize)) {
- callback.uiScope.postToUi { callback.onFailed(ZipDecompressionCallback.ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH) }
+ if (!callback.onCheckFreeSpace(
+ DocumentFileCompat.getFreeSpace(
+ context,
+ targetFolder.getStorageId(context)
+ ), zipSize
+ )
+ ) {
+ callback.postToUiScope { callback.onFailed(ZipDecompressionCallback.ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH) }
return
}
@@ -1365,8 +1578,12 @@ fun DocumentFile.decompressZip(
// using timer on small file is useless. We set minimum 10MB.
if (reportInterval > 0 && zipSize > 10 * FileSize.MB) {
timer = startCoroutineTimer(repeatMillis = reportInterval) {
- val report = ZipDecompressionCallback.Report(bytesDecompressed, writeSpeed, fileDecompressedCount)
- callback.uiScope.postToUi { callback.onReport(report) }
+ val report = ZipDecompressionCallback.Report(
+ bytesDecompressed,
+ writeSpeed,
+ fileDecompressedCount
+ )
+ callback.postToUiScope { callback.onReport(report) }
writeSpeed = 0
}
}
@@ -1378,12 +1595,16 @@ fun DocumentFile.decompressZip(
destFolder.makeFolder(context, entry.name, CreateMode.REUSE)
} else {
val folder = entry.name.substringBeforeLast('/', "").let {
- if (it.isEmpty()) destFolder else destFolder.makeFolder(context, it, CreateMode.REUSE)
+ if (it.isEmpty()) destFolder else destFolder.makeFolder(
+ context,
+ it,
+ CreateMode.REUSE
+ )
} ?: throw IOException()
val fileName = entry.name.substringAfterLast('/')
targetFile = folder.makeFile(context, fileName, onConflict = callback)
if (targetFile == null) {
- callback.uiScope.postToUi { callback.onFailed(ZipDecompressionCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
+ callback.postToUiScope { callback.onFailed(ZipDecompressionCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
canSuccess = false
break
}
@@ -1408,17 +1629,17 @@ fun DocumentFile.decompressZip(
}
success = canSuccess
} catch (e: InterruptedIOException) {
- callback.uiScope.postToUi { callback.onFailed(ZipDecompressionCallback.ErrorCode.CANCELED) }
+ callback.postToUiScope { callback.onFailed(ZipDecompressionCallback.ErrorCode.CANCELED) }
} catch (e: FileNotFoundException) {
- callback.uiScope.postToUi { callback.onFailed(ZipDecompressionCallback.ErrorCode.MISSING_ZIP_FILE) }
+ callback.postToUiScope { callback.onFailed(ZipDecompressionCallback.ErrorCode.MISSING_ZIP_FILE) }
} catch (e: IOException) {
if (e.message?.contains("no space", true) == true) {
- callback.uiScope.postToUi { callback.onFailed(ZipDecompressionCallback.ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH) }
+ callback.postToUiScope { callback.onFailed(ZipDecompressionCallback.ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH) }
} else {
- callback.uiScope.postToUi { callback.onFailed(ZipDecompressionCallback.ErrorCode.UNKNOWN_IO_ERROR) }
+ callback.postToUiScope { callback.onFailed(ZipDecompressionCallback.ErrorCode.UNKNOWN_IO_ERROR) }
}
} catch (e: SecurityException) {
- callback.uiScope.postToUi { callback.onFailed(ZipDecompressionCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
+ callback.postToUiScope { callback.onFailed(ZipDecompressionCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
} finally {
timer?.cancel()
zis.closeEntryQuietly()
@@ -1427,8 +1648,13 @@ fun DocumentFile.decompressZip(
if (success) {
// Sometimes, the decompressed size is smaller than the compressed size, and you may get negative values. You should worry about this.
val sizeExpansion = (bytesDecompressed - zipSize).toFloat() / zipSize * 100
- val info = ZipDecompressionCallback.DecompressionInfo(bytesDecompressed, skippedDecompressedBytes, fileDecompressedCount, sizeExpansion)
- callback.uiScope.postToUi { callback.onCompleted(this, destFolder, info) }
+ val info = ZipDecompressionCallback.DecompressionInfo(
+ bytesDecompressed,
+ skippedDecompressedBytes,
+ fileDecompressedCount,
+ sizeExpansion
+ )
+ callback.postToUiScope { callback.onCompleted(this, destFolder, info) }
} else {
targetFile?.delete()
}
@@ -1463,26 +1689,38 @@ private fun List.copyTo(
) {
val pair = doesMeetCopyRequirements(context, targetParentFolder, callback) ?: return
- callback.uiScope.postToUi { callback.onPrepare() }
+ callback.postToUiScope { callback.onPrepare() }
val validSources = pair.second
val writableTargetParentFolder = pair.first
- val conflictResolutions = validSources.handleParentFolderConflict(context, writableTargetParentFolder, callback) ?: return
- validSources.removeAll(conflictResolutions.filter { it.solution == FolderCallback.ConflictResolution.SKIP }.map { it.source })
+ val conflictResolutions =
+ validSources.handleParentFolderConflict(context, writableTargetParentFolder, callback)
+ ?: return
+ validSources.removeAll(conflictResolutions.filter { it.solution == FolderCallback.ConflictResolution.SKIP }
+ .map { it.source })
if (validSources.isEmpty()) {
return
}
- callback.uiScope.postToUi { callback.onCountingFiles() }
+ callback.postToUiScope { 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 resolution = conflictResolutions.find { it.source == src }?.solution ?: FolderCallback.ConflictResolution.CREATE_NEW
+ 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)
+ val children =
+ if (skipEmptyFiles) src.walkFileTreeAndSkipEmptyFiles() else src.walkFileTree(
+ context
+ )
var totalFilesToCopy = 0
var totalSizeToCopy = 0L
children.forEach {
@@ -1494,11 +1732,12 @@ private fun List.copyTo(
SourceInfo(children, totalSizeToCopy, totalFilesToCopy, resolution)
}
// allow empty folders, but empty files need check
- }.filterValues { it.children != null || (skipEmptyFiles && it.size > 0 || !skipEmptyFiles) }.toMutableMap()
+ }.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) }
+ callback.postToUiScope { callback.onCompleted(result) }
return
}
@@ -1525,7 +1764,7 @@ private fun List.copyTo(
FolderCallback.ErrorCode.STORAGE_PERMISSION_DENIED -> MultipleFileCallback.ErrorCode.STORAGE_PERMISSION_DENIED
else -> return
}
- callback.uiScope.postToUi { callback.onFailed(errorCode) }
+ callback.postToUiScope { callback.onFailed(errorCode) }
return
}
}
@@ -1539,22 +1778,39 @@ private fun List.copyTo(
}
if (sourceInfos.isEmpty()) {
- val result = MultipleFileCallback.Result(results.map { it.value }, copiedFiles, copiedFiles, true)
- callback.uiScope.postToUi { callback.onCompleted(result) }
+ val result = MultipleFileCallback.Result(
+ results.map { it.value },
+ copiedFiles,
+ copiedFiles,
+ true
+ )
+ callback.postToUiScope { callback.onCompleted(result) }
return
}
}
val totalSizeToCopy = sourceInfos.values.sumOf { it.size }
- if (!callback.onCheckFreeSpace(DocumentFileCompat.getFreeSpace(context, writableTargetParentFolder.getStorageId(context)), totalSizeToCopy)) {
- callback.uiScope.postToUi { callback.onFailed(MultipleFileCallback.ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH) }
+ if (!callback.onCheckFreeSpace(
+ DocumentFileCompat.getFreeSpace(
+ context,
+ writableTargetParentFolder.getStorageId(context)
+ ), totalSizeToCopy
+ )
+ ) {
+ callback.postToUiScope { callback.onFailed(MultipleFileCallback.ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH) }
return
}
val thread = Thread.currentThread()
val totalFilesToCopy = sourceInfos.values.sumOf { it.totalFiles }
- val reportInterval = awaitUiResult(callback.uiScope) { callback.onStart(sourceInfos.map { it.key }, totalFilesToCopy, thread) }
+ val reportInterval = awaitUiResult(callback.uiScope) {
+ callback.onStart(
+ sourceInfos.map { it.key },
+ totalFilesToCopy,
+ thread
+ )
+ }
if (reportInterval < 0) return
var totalCopiedFiles = 0
@@ -1564,8 +1820,13 @@ private fun List.copyTo(
val startTimer: (Boolean) -> Unit = { start ->
if (start && reportInterval > 0) {
timer = startCoroutineTimer(repeatMillis = reportInterval) {
- val report = MultipleFileCallback.Report(bytesMoved * 100f / totalSizeToCopy, bytesMoved, writeSpeed, totalCopiedFiles)
- callback.uiScope.postToUi { callback.onReport(report) }
+ val report = MultipleFileCallback.Report(
+ bytesMoved * 100f / totalSizeToCopy,
+ bytesMoved,
+ writeSpeed,
+ totalCopiedFiles
+ )
+ callback.postToUiScope { callback.onReport(report) }
writeSpeed = 0
}
}
@@ -1573,14 +1834,20 @@ private fun List.copyTo(
startTimer(totalSizeToCopy > 10 * FileSize.MB)
var targetFile: DocumentFile? = null
- var canceled = false // is required to prevent the callback from called again on next FOR iteration after the thread was interrupted
+ var canceled =
+ false // is required to prevent the callback from called again on next FOR iteration after the thread was interrupted
val notifyCanceled: (MultipleFileCallback.ErrorCode) -> Unit = { errorCode ->
if (!canceled) {
canceled = true
timer?.cancel()
targetFile?.delete()
- val result = MultipleFileCallback.Result(results.map { it.value }, totalFilesToCopy, totalCopiedFiles, false)
- callback.uiScope.postToUi {
+ val result = MultipleFileCallback.Result(
+ results.map { it.value },
+ totalFilesToCopy,
+ totalCopiedFiles,
+ false
+ )
+ callback.postToUiScope {
callback.onFailed(errorCode)
callback.onCompleted(result)
}
@@ -1617,7 +1884,7 @@ private fun List.copyTo(
true
} else {
timer?.cancel()
- callback.uiScope.postToUi { callback.onFailed(errorCode) }
+ callback.postToUiScope { callback.onFailed(errorCode) }
false
}
}
@@ -1631,11 +1898,15 @@ private fun List.copyTo(
}
val mode = info.conflictResolution.toCreateMode()
val targetRootFile = writableTargetParentFolder.let {
- if (src.isDirectory) it.makeFolder(context, src.fullName, mode) else it.makeFile(context, src.fullName, src.mimeType, mode)
+ if (src.isDirectory) it.makeFolder(
+ context,
+ src.fullName,
+ mode
+ ) else it.makeFile(context, src.fullName, src.mimeType, mode)
}
if (targetRootFile == null) {
timer?.cancel()
- callback.uiScope.postToUi { callback.onFailed(MultipleFileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
+ callback.postToUiScope { callback.onFailed(MultipleFileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
return
}
@@ -1657,7 +1928,8 @@ private fun List.copyTo(
continue
}
- val filename = sourceFile.getSubPath(context, srcParentAbsolutePath) ?: sourceFile.fullName
+ val filename =
+ sourceFile.getSubPath(context, srcParentAbsolutePath) ?: sourceFile.fullName
if (filename.isEmpty()) continue
if (sourceFile.isDirectory) {
@@ -1669,7 +1941,8 @@ private fun List.copyTo(
continue
}
- targetFile = targetRootFile.makeFile(context, filename, sourceFile.type, CreateMode.REUSE)
+ targetFile =
+ targetRootFile.makeFile(context, filename, sourceFile.type, CreateMode.REUSE)
if (targetFile != null && targetFile.length() > 0) {
conflictedFiles.add(FolderCallback.FileConflict(sourceFile, targetFile))
continue
@@ -1696,15 +1969,24 @@ private fun List.copyTo(
if (deleteSourceWhenComplete && success) {
sourceInfos.forEach { (src, _) -> src.forceDelete(context) }
}
- val result = MultipleFileCallback.Result(results.map { it.value }, totalFilesToCopy, totalCopiedFiles, success)
- callback.uiScope.postToUi { callback.onCompleted(result) }
+ val result = MultipleFileCallback.Result(
+ results.map { it.value },
+ totalFilesToCopy,
+ totalCopiedFiles,
+ success
+ )
+ callback.postToUiScope { callback.onCompleted(result) }
true
} else false
}
if (finalize()) return
val solutions = awaitUiResultWithPending>(callback.uiScope) {
- callback.onContentConflict(writableTargetParentFolder, conflictedFiles, FolderCallback.FolderContentConflictAction(it))
+ callback.onContentConflict(
+ writableTargetParentFolder,
+ conflictedFiles,
+ FolderCallback.FolderContentConflictAction(it)
+ )
}.filter {
// free up space first, by deleting some files
if (it.solution == FileCallback.ConflictResolution.SKIP) {
@@ -1753,14 +2035,14 @@ private fun List.doesMeetCopyRequirements(
targetParentFolder: DocumentFile,
callback: MultipleFileCallback
): Pair>? {
- callback.uiScope.postToUi { callback.onValidate() }
+ callback.postToUiScope { callback.onValidate() }
if (!targetParentFolder.isDirectory) {
- callback.uiScope.postToUi { callback.onFailed(MultipleFileCallback.ErrorCode.INVALID_TARGET_FOLDER) }
+ callback.postToUiScope { callback.onFailed(MultipleFileCallback.ErrorCode.INVALID_TARGET_FOLDER) }
return null
}
if (!targetParentFolder.isWritable(context)) {
- callback.uiScope.postToUi { callback.onFailed(MultipleFileCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
+ callback.postToUiScope { callback.onFailed(MultipleFileCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
return null
}
@@ -1771,7 +2053,10 @@ private fun List.doesMeetCopyRequirements(
!it.exists() -> Pair(it, FolderCallback.ErrorCode.SOURCE_FILE_NOT_FOUND)
!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)
+ Pair(
+ it,
+ FolderCallback.ErrorCode.TARGET_FOLDER_CANNOT_HAVE_SAME_PATH_WITH_SOURCE_FOLDER
+ )
else -> null
}
@@ -1779,25 +2064,41 @@ private fun List.doesMeetCopyRequirements(
if (invalidSourceFiles.isNotEmpty()) {
val abort = awaitUiResultWithPending(callback.uiScope) {
- callback.onInvalidSourceFilesFound(invalidSourceFiles, MultipleFileCallback.InvalidSourceFilesAction(it))
+ callback.onInvalidSourceFilesFound(
+ invalidSourceFiles,
+ MultipleFileCallback.InvalidSourceFilesAction(it)
+ )
}
if (abort) {
- callback.uiScope.postToUi { callback.onFailed(MultipleFileCallback.ErrorCode.CANCELED) }
+ callback.postToUiScope { callback.onFailed(MultipleFileCallback.ErrorCode.CANCELED) }
return null
}
if (invalidSourceFiles.size == size) {
- callback.uiScope.postToUi { callback.onCompleted(MultipleFileCallback.Result(emptyList(), 0, 0, true)) }
+ callback.postToUiScope {
+ callback.onCompleted(
+ MultipleFileCallback.Result(
+ emptyList(),
+ 0,
+ 0,
+ true
+ )
+ )
+ }
return null
}
}
- val writableFolder = targetParentFolder.let { if (it.isDownloadsDocument) it.toWritableDownloadsDocumentFile(context) else it }
+ val writableFolder = targetParentFolder.let {
+ if (it.isDownloadsDocument) it.toWritableDownloadsDocumentFile(context) else it
+ }
if (writableFolder == null) {
- callback.uiScope.postToUi { callback.onFailed(MultipleFileCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
+ callback.postToUiScope { callback.onFailed(MultipleFileCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
return null
}
- return Pair(writableFolder, sourceFiles.toMutableList().apply { removeAll(invalidSourceFiles.map { it.key }) })
+ return Pair(
+ writableFolder,
+ sourceFiles.toMutableList().apply { removeAll(invalidSourceFiles.map { it.key }) })
}
private fun DocumentFile.tryMoveFolderByRenamingPath(
@@ -1822,9 +2123,15 @@ private fun DocumentFile.tryMoveFolderByRenamingPath(
}
if (isExternalStorageManager(context)) {
- val sourceFile = toRawFile(context) ?: return FolderCallback.ErrorCode.STORAGE_PERMISSION_DENIED
+ val sourceFile =
+ toRawFile(context) ?: return FolderCallback.ErrorCode.STORAGE_PERMISSION_DENIED
writableTargetParentFolder.toRawFile(context)?.let { destinationFolder ->
- sourceFile.moveTo(context, destinationFolder, targetFolderParentName, conflictResolution.toFileConflictResolution())?.let {
+ sourceFile.moveTo(
+ context,
+ destinationFolder,
+ targetFolderParentName,
+ conflictResolution.toFileConflictResolution()
+ )?.let {
if (skipEmptyFiles) it.deleteEmptyFolders(context)
return DocumentFile.fromFile(it)
}
@@ -1833,11 +2140,20 @@ private fun DocumentFile.tryMoveFolderByRenamingPath(
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && !isRawFile && writableTargetParentFolder.isTreeDocumentFile) {
- val movedFileUri = parentFile?.uri?.let { DocumentsContract.moveDocument(context.contentResolver, uri, it, writableTargetParentFolder.uri) }
+ val movedFileUri = parentFile?.uri?.let {
+ DocumentsContract.moveDocument(
+ context.contentResolver,
+ uri,
+ it,
+ writableTargetParentFolder.uri
+ )
+ }
if (movedFileUri != null) {
val newFile = context.fromTreeUri(movedFileUri)
return if (newFile != null && newFile.isDirectory) {
- if (newFolderNameInTargetPath != null) newFile.renameTo(targetFolderParentName)
+ if (newFolderNameInTargetPath != null) newFile.renameTo(
+ targetFolderParentName
+ )
if (skipEmptyFiles) newFile.deleteEmptyFolders(context)
newFile
} else {
@@ -1860,7 +2176,14 @@ fun DocumentFile.moveFolderTo(
newFolderNameInTargetPath: String? = null,
callback: FolderCallback
) {
- copyFolderTo(context, targetParentFolder, skipEmptyFiles, newFolderNameInTargetPath, true, callback)
+ copyFolderTo(
+ context,
+ targetParentFolder,
+ skipEmptyFiles,
+ newFolderNameInTargetPath,
+ true,
+ callback
+ )
}
@WorkerThread
@@ -1871,7 +2194,14 @@ fun DocumentFile.copyFolderTo(
newFolderNameInTargetPath: String? = null,
callback: FolderCallback
) {
- copyFolderTo(context, targetParentFolder, skipEmptyFiles, newFolderNameInTargetPath, false, callback)
+ copyFolderTo(
+ context,
+ targetParentFolder,
+ skipEmptyFiles,
+ newFolderNameInTargetPath,
+ false,
+ callback
+ )
}
/**
@@ -1885,26 +2215,44 @@ private fun DocumentFile.copyFolderTo(
deleteSourceWhenComplete: Boolean,
callback: FolderCallback
) {
- val writableTargetParentFolder = doesMeetCopyRequirements(context, targetParentFolder, newFolderNameInTargetPath, callback) ?: return
+ val writableTargetParentFolder =
+ doesMeetCopyRequirements(context, targetParentFolder, newFolderNameInTargetPath, callback)
+ ?: return
- callback.uiScope.postToUi { callback.onPrepare() }
+ callback.postToUiScope { callback.onPrepare() }
- val targetFolderParentName = (newFolderNameInTargetPath ?: name.orEmpty()).removeForbiddenCharsFromFilename().trimFileSeparator()
- val conflictResolution = handleParentFolderConflict(context, targetParentFolder, targetFolderParentName, callback)
+ val targetFolderParentName =
+ (newFolderNameInTargetPath ?: name.orEmpty()).removeForbiddenCharsFromFilename()
+ .trimFileSeparator()
+ val conflictResolution =
+ handleParentFolderConflict(context, targetParentFolder, targetFolderParentName, callback)
if (conflictResolution == FolderCallback.ConflictResolution.SKIP) {
return
}
- callback.uiScope.postToUi { callback.onCountingFiles() }
+ callback.postToUiScope { callback.onCountingFiles() }
val filesToCopy = if (skipEmptyFiles) walkFileTreeAndSkipEmptyFiles() else walkFileTree(context)
if (filesToCopy.isEmpty()) {
- val targetFolder = writableTargetParentFolder.makeFolder(context, targetFolderParentName, conflictResolution.toCreateMode())
+ val targetFolder = writableTargetParentFolder.makeFolder(
+ context,
+ targetFolderParentName,
+ conflictResolution.toCreateMode()
+ )
if (targetFolder == null) {
- callback.uiScope.postToUi { callback.onFailed(FolderCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
+ callback.postToUiScope { callback.onFailed(FolderCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
} else {
if (deleteSourceWhenComplete) delete()
- callback.uiScope.postToUi { callback.onCompleted(FolderCallback.Result(targetFolder, 0, 0, true)) }
+ callback.postToUiScope {
+ callback.onCompleted(
+ FolderCallback.Result(
+ targetFolder,
+ 0,
+ 0,
+ true
+ )
+ )
+ }
}
return
}
@@ -1920,7 +2268,7 @@ private fun DocumentFile.copyFolderTo(
val thread = Thread.currentThread()
if (thread.isInterrupted) {
- callback.uiScope.postToUi { callback.onFailed(FolderCallback.ErrorCode.CANCELED) }
+ callback.postToUiScope { callback.onFailed(FolderCallback.ErrorCode.CANCELED) }
return
}
@@ -1934,28 +2282,48 @@ private fun DocumentFile.copyFolderTo(
conflictResolution
)) {
is DocumentFile -> {
- callback.uiScope.postToUi { callback.onCompleted(FolderCallback.Result(result, totalFilesToCopy, totalFilesToCopy, true)) }
+ callback.postToUiScope {
+ callback.onCompleted(
+ FolderCallback.Result(
+ result,
+ totalFilesToCopy,
+ totalFilesToCopy,
+ true
+ )
+ )
+ }
return
}
is FolderCallback.ErrorCode -> {
- callback.uiScope.postToUi { callback.onFailed(result) }
+ callback.postToUiScope { callback.onFailed(result) }
return
}
}
}
- if (!callback.onCheckFreeSpace(DocumentFileCompat.getFreeSpace(context, writableTargetParentFolder.getStorageId(context)), totalSizeToCopy)) {
- callback.uiScope.postToUi { callback.onFailed(FolderCallback.ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH) }
+ if (!callback.onCheckFreeSpace(
+ DocumentFileCompat.getFreeSpace(
+ context,
+ writableTargetParentFolder.getStorageId(context)
+ ), totalSizeToCopy
+ )
+ ) {
+ callback.postToUiScope { callback.onFailed(FolderCallback.ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH) }
return
}
- val reportInterval = awaitUiResult(callback.uiScope) { callback.onStart(this, totalFilesToCopy, thread) }
+ val reportInterval =
+ awaitUiResult(callback.uiScope) { callback.onStart(this, totalFilesToCopy, thread) }
if (reportInterval < 0) return
- val targetFolder = writableTargetParentFolder.makeFolder(context, targetFolderParentName, conflictResolution.toCreateMode())
+ val targetFolder = writableTargetParentFolder.makeFolder(
+ context,
+ targetFolderParentName,
+ conflictResolution.toCreateMode()
+ )
if (targetFolder == null) {
- callback.uiScope.postToUi { callback.onFailed(FolderCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
+ callback.postToUiScope { callback.onFailed(FolderCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
return
}
@@ -1966,8 +2334,13 @@ private fun DocumentFile.copyFolderTo(
val startTimer: (Boolean) -> Unit = { start ->
if (start && reportInterval > 0) {
timer = startCoroutineTimer(repeatMillis = reportInterval) {
- val report = FolderCallback.Report(bytesMoved * 100f / totalSizeToCopy, bytesMoved, writeSpeed, totalCopiedFiles)
- callback.uiScope.postToUi { callback.onReport(report) }
+ val report = FolderCallback.Report(
+ bytesMoved * 100f / totalSizeToCopy,
+ bytesMoved,
+ writeSpeed,
+ totalCopiedFiles
+ )
+ callback.postToUiScope { callback.onReport(report) }
writeSpeed = 0
}
}
@@ -1975,15 +2348,23 @@ private fun DocumentFile.copyFolderTo(
startTimer(totalSizeToCopy > 10 * FileSize.MB)
var targetFile: DocumentFile? = null
- var canceled = false // is required to prevent the callback from called again on next FOR iteration after the thread was interrupted
+ var canceled =
+ false // is required to prevent the callback from called again on next FOR iteration after the thread was interrupted
val notifyCanceled: (FolderCallback.ErrorCode) -> Unit = { errorCode ->
if (!canceled) {
canceled = true
timer?.cancel()
targetFile?.delete()
- callback.uiScope.postToUi {
+ callback.postToUiScope {
callback.onFailed(errorCode)
- callback.onCompleted(FolderCallback.Result(targetFolder, totalFilesToCopy, totalCopiedFiles, false))
+ callback.onCompleted(
+ FolderCallback.Result(
+ targetFolder,
+ totalFilesToCopy,
+ totalCopiedFiles,
+ false
+ )
+ )
}
}
}
@@ -2018,7 +2399,7 @@ private fun DocumentFile.copyFolderTo(
true
} else {
timer?.cancel()
- callback.uiScope.postToUi { callback.onFailed(errorCode) }
+ callback.postToUiScope { callback.onFailed(errorCode) }
false
}
}
@@ -2069,14 +2450,27 @@ private fun DocumentFile.copyFolderTo(
timer?.cancel()
if (!success || conflictedFiles.isEmpty()) {
if (deleteSourceWhenComplete && success) forceDelete(context)
- callback.uiScope.postToUi { callback.onCompleted(FolderCallback.Result(targetFolder, totalFilesToCopy, totalCopiedFiles, success)) }
+ callback.postToUiScope {
+ callback.onCompleted(
+ FolderCallback.Result(
+ targetFolder,
+ totalFilesToCopy,
+ totalCopiedFiles,
+ success
+ )
+ )
+ }
true
} else false
}
if (finalize()) return
val solutions = awaitUiResultWithPending>(callback.uiScope) {
- callback.onContentConflict(targetFolder, conflictedFiles, FolderCallback.FolderContentConflictAction(it))
+ callback.onContentConflict(
+ targetFolder,
+ conflictedFiles,
+ FolderCallback.FolderContentConflictAction(it)
+ )
}.filter {
// free up space first, by deleting some files
if (it.solution == FileCallback.ConflictResolution.SKIP) {
@@ -2098,7 +2492,8 @@ private fun DocumentFile.copyFolderTo(
continue
}
val filename = conflict.target.name.orEmpty()
- targetFile = conflict.target.findParent(context)?.makeFile(context, filename, mode = conflict.solution.toCreateMode())
+ targetFile = conflict.target.findParent(context)
+ ?.makeFile(context, filename, mode = conflict.solution.toCreateMode())
if (targetFile == null) {
notifyCanceled(FolderCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET)
return
@@ -2138,31 +2533,33 @@ private fun DocumentFile.doesMeetCopyRequirements(
newFolderNameInTargetPath: String?,
callback: FolderCallback
): DocumentFile? {
- callback.uiScope.postToUi { callback.onValidate() }
+ callback.postToUiScope { callback.onValidate() }
if (!isDirectory) {
- callback.uiScope.postToUi { callback.onFailed(FolderCallback.ErrorCode.SOURCE_FOLDER_NOT_FOUND) }
+ callback.postToUiScope { callback.onFailed(FolderCallback.ErrorCode.SOURCE_FOLDER_NOT_FOUND) }
return null
}
if (!targetParentFolder.isDirectory) {
- callback.uiScope.postToUi { callback.onFailed(FolderCallback.ErrorCode.INVALID_TARGET_FOLDER) }
+ callback.postToUiScope { callback.onFailed(FolderCallback.ErrorCode.INVALID_TARGET_FOLDER) }
return null
}
if (!canRead() || !targetParentFolder.isWritable(context)) {
- callback.uiScope.postToUi { callback.onFailed(FolderCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
+ callback.postToUiScope { callback.onFailed(FolderCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
return null
}
if (targetParentFolder.getAbsolutePath(context) == parentFile?.getAbsolutePath(context) && (newFolderNameInTargetPath.isNullOrEmpty() || name == newFolderNameInTargetPath)) {
- callback.uiScope.postToUi { callback.onFailed(FolderCallback.ErrorCode.TARGET_FOLDER_CANNOT_HAVE_SAME_PATH_WITH_SOURCE_FOLDER) }
+ callback.postToUiScope { callback.onFailed(FolderCallback.ErrorCode.TARGET_FOLDER_CANNOT_HAVE_SAME_PATH_WITH_SOURCE_FOLDER) }
return null
}
- val writableFolder = targetParentFolder.let { if (it.isDownloadsDocument) it.toWritableDownloadsDocumentFile(context) else it }
+ val writableFolder = targetParentFolder.let {
+ if (it.isDownloadsDocument) it.toWritableDownloadsDocumentFile(context) else it
+ }
if (writableFolder == null) {
- callback.uiScope.postToUi { callback.onFailed(FolderCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
+ callback.postToUiScope { callback.onFailed(FolderCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
}
return writableFolder
}
@@ -2193,7 +2590,7 @@ fun DocumentFile.copyFileTo(
) {
val targetFolder = DocumentFileCompat.mkdirs(context, targetFolderAbsolutePath, true)
if (targetFolder == null) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
} else {
copyFileTo(context, targetFolder, fileDescription, callback)
}
@@ -2210,13 +2607,26 @@ fun DocumentFile.copyFileTo(
callback: FileCallback
) {
if (fileDescription?.subFolder.isNullOrEmpty()) {
- copyFileTo(context, targetFolder, fileDescription?.name, fileDescription?.mimeType, callback)
+ copyFileTo(
+ context,
+ targetFolder,
+ fileDescription?.name,
+ fileDescription?.mimeType,
+ callback
+ )
} else {
- val targetDirectory = targetFolder.makeFolder(context, fileDescription?.subFolder.orEmpty(), CreateMode.REUSE)
+ val targetDirectory =
+ targetFolder.makeFolder(context, fileDescription?.subFolder.orEmpty(), CreateMode.REUSE)
if (targetDirectory == null) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
} else {
- copyFileTo(context, targetDirectory, fileDescription?.name, fileDescription?.mimeType, callback)
+ copyFileTo(
+ context,
+ targetDirectory,
+ fileDescription?.name,
+ fileDescription?.mimeType,
+ callback
+ )
}
}
}
@@ -2228,18 +2638,29 @@ private fun DocumentFile.copyFileTo(
newMimeTypeInTargetPath: String?,
callback: FileCallback
) {
- val writableTargetFolder = doesMeetCopyRequirements(context, targetFolder, newFilenameInTargetPath, callback) ?: return
+ val writableTargetFolder =
+ doesMeetCopyRequirements(context, targetFolder, newFilenameInTargetPath, callback) ?: return
- callback.uiScope.postToUi { callback.onPrepare() }
+ callback.postToUiScope { callback.onPrepare() }
- if (!callback.onCheckFreeSpace(DocumentFileCompat.getFreeSpace(context, writableTargetFolder.getStorageId(context)), length())) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH) }
+ if (!callback.onCheckFreeSpace(
+ DocumentFileCompat.getFreeSpace(
+ context,
+ writableTargetFolder.getStorageId(context)
+ ), length()
+ )
+ ) {
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH) }
return
}
- val cleanFileName = MimeType.getFullFileName(newFilenameInTargetPath ?: name.orEmpty(), newMimeTypeInTargetPath ?: mimeTypeByFileName)
+ val cleanFileName = MimeType.getFullFileName(
+ newFilenameInTargetPath ?: name.orEmpty(),
+ newMimeTypeInTargetPath ?: mimeTypeByFileName
+ )
.removeForbiddenCharsFromFilename().trimFileSeparator()
- val fileConflictResolution = handleFileConflict(context, writableTargetFolder, cleanFileName, callback)
+ val fileConflictResolution =
+ handleFileConflict(context, writableTargetFolder, cleanFileName, callback)
if (fileConflictResolution == FileCallback.ConflictResolution.SKIP) {
return
}
@@ -2251,14 +2672,26 @@ private fun DocumentFile.copyFileTo(
try {
val targetFile = createTargetFile(
- context, writableTargetFolder, cleanFileName, newMimeTypeInTargetPath ?: mimeTypeByFileName,
- fileConflictResolution.toCreateMode(), callback
+ context,
+ writableTargetFolder,
+ cleanFileName,
+ newMimeTypeInTargetPath ?: mimeTypeByFileName,
+ fileConflictResolution.toCreateMode(),
+ callback
) ?: return
createFileStreams(context, this, targetFile, callback) { inputStream, outputStream ->
- copyFileStream(inputStream, outputStream, targetFile, watchProgress, reportInterval, false, callback)
+ copyFileStream(
+ inputStream,
+ outputStream,
+ targetFile,
+ watchProgress,
+ reportInterval,
+ false,
+ callback
+ )
}
} catch (e: Exception) {
- callback.uiScope.postToUi { callback.onFailed(e.toFileCallbackErrorCode()) }
+ callback.postToUiScope { callback.onFailed(e.toFileCallbackErrorCode()) }
}
}
@@ -2271,31 +2704,32 @@ private fun DocumentFile.doesMeetCopyRequirements(
newFilenameInTargetPath: String?,
callback: FileCallback
): DocumentFile? {
- callback.uiScope.postToUi { callback.onValidate() }
+ callback.postToUiScope { callback.onValidate() }
if (!isFile) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.SOURCE_FILE_NOT_FOUND) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.SOURCE_FILE_NOT_FOUND) }
return null
}
if (!targetFolder.isDirectory) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.TARGET_FOLDER_NOT_FOUND) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.TARGET_FOLDER_NOT_FOUND) }
return null
}
if (!canRead() || !targetFolder.isWritable(context)) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
return null
}
if (parentFile?.getAbsolutePath(context) == targetFolder.getAbsolutePath(context) && (newFilenameInTargetPath.isNullOrEmpty() || name == newFilenameInTargetPath)) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.TARGET_FOLDER_CANNOT_HAVE_SAME_PATH_WITH_SOURCE_FOLDER) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.TARGET_FOLDER_CANNOT_HAVE_SAME_PATH_WITH_SOURCE_FOLDER) }
return null
}
- val writableFolder = targetFolder.let { if (it.isDownloadsDocument) it.toWritableDownloadsDocumentFile(context) else it }
+ val writableFolder =
+ targetFolder.let { if (it.isDownloadsDocument) it.toWritableDownloadsDocumentFile(context) else it }
if (writableFolder == null) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
}
return writableFolder
}
@@ -2315,7 +2749,7 @@ private fun createFileStreams(
is FolderCallback -> FolderCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET
else -> FileCallback.ErrorCode.TARGET_FILE_NOT_FOUND
}
- callback.uiScope.postToUi { callback.onFailed(errorCode as Enum) }
+ callback.postToUiScope { callback.onFailed(errorCode as Enum) }
return
}
@@ -2327,7 +2761,7 @@ private fun createFileStreams(
is FolderCallback -> FolderCallback.ErrorCode.SOURCE_FILE_NOT_FOUND
else -> FileCallback.ErrorCode.SOURCE_FILE_NOT_FOUND
}
- callback.uiScope.postToUi { callback.onFailed(errorCode as Enum) }
+ callback.postToUiScope { callback.onFailed(errorCode as Enum) }
return
}
@@ -2343,14 +2777,14 @@ private inline fun createFileStreams(
) {
val outputStream = targetFile.openOutputStream()
if (outputStream == null) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.TARGET_FILE_NOT_FOUND) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.TARGET_FILE_NOT_FOUND) }
return
}
val inputStream = sourceFile.openInputStream(context)
if (inputStream == null) {
outputStream.closeStreamQuietly()
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.SOURCE_FILE_NOT_FOUND) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.SOURCE_FILE_NOT_FOUND) }
return
}
@@ -2367,7 +2801,7 @@ private fun createTargetFile(
): DocumentFile? {
val targetFile = targetFolder.makeFile(context, newFilenameInTargetPath, mimeType, mode)
if (targetFile == null) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
}
return targetFile
}
@@ -2392,8 +2826,9 @@ private fun DocumentFile.copyFileStream(
// using timer on small file is useless. We set minimum 10MB.
if (watchProgress && srcSize > 10 * FileSize.MB) {
timer = startCoroutineTimer(repeatMillis = reportInterval) {
- val report = FileCallback.Report(bytesMoved * 100f / srcSize, bytesMoved, writeSpeed)
- callback.uiScope.postToUi { callback.onReport(report) }
+ val report =
+ FileCallback.Report(bytesMoved * 100f / srcSize, bytesMoved, writeSpeed)
+ callback.postToUiScope { callback.onReport(report) }
writeSpeed = 0
}
}
@@ -2412,7 +2847,7 @@ private fun DocumentFile.copyFileStream(
if (targetFile is MediaFile) {
targetFile.length = srcSize
}
- callback.uiScope.postToUi { callback.onCompleted(targetFile) }
+ callback.postToUiScope { callback.onCompleted(FileCallback.Result.get(targetFile)) }
} finally {
timer?.cancel()
inputStream.closeStreamQuietly()
@@ -2446,7 +2881,7 @@ fun DocumentFile.moveFileTo(
) {
val targetFolder = DocumentFileCompat.mkdirs(context, targetFolderAbsolutePath, true)
if (targetFolder == null) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
} else {
moveFileTo(context, targetFolder, fileDescription, callback)
}
@@ -2463,13 +2898,26 @@ fun DocumentFile.moveFileTo(
callback: FileCallback
) {
if (fileDescription?.subFolder.isNullOrEmpty()) {
- moveFileTo(context, targetFolder, fileDescription?.name, fileDescription?.mimeType, callback)
+ moveFileTo(
+ context,
+ targetFolder,
+ fileDescription?.name,
+ fileDescription?.mimeType,
+ callback
+ )
} else {
- val targetDirectory = targetFolder.makeFolder(context, fileDescription?.subFolder.orEmpty(), CreateMode.REUSE)
+ val targetDirectory =
+ targetFolder.makeFolder(context, fileDescription?.subFolder.orEmpty(), CreateMode.REUSE)
if (targetDirectory == null) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
} else {
- moveFileTo(context, targetDirectory, fileDescription?.name, fileDescription?.mimeType, callback)
+ moveFileTo(
+ context,
+ targetDirectory,
+ fileDescription?.name,
+ fileDescription?.mimeType,
+ callback
+ )
}
}
}
@@ -2481,20 +2929,36 @@ private fun DocumentFile.moveFileTo(
newMimeTypeInTargetPath: String?,
callback: FileCallback
) {
- val writableTargetFolder = doesMeetCopyRequirements(context, targetFolder, newFilenameInTargetPath, callback) ?: return
+ val writableTargetFolder =
+ doesMeetCopyRequirements(context, targetFolder, newFilenameInTargetPath, callback) ?: return
- callback.uiScope.postToUi { callback.onPrepare() }
+ callback.postToUiScope { callback.onPrepare() }
- val cleanFileName = MimeType.getFullFileName(newFilenameInTargetPath ?: name.orEmpty(), newMimeTypeInTargetPath ?: mimeTypeByFileName)
+ val cleanFileName = MimeType.getFullFileName(
+ newFilenameInTargetPath ?: name.orEmpty(),
+ newMimeTypeInTargetPath ?: mimeTypeByFileName
+ )
.removeForbiddenCharsFromFilename().trimFileSeparator()
- val fileConflictResolution = handleFileConflict(context, writableTargetFolder, cleanFileName, callback)
+ val fileConflictResolution =
+ handleFileConflict(context, writableTargetFolder, cleanFileName, callback)
if (fileConflictResolution == FileCallback.ConflictResolution.SKIP) {
return
}
if (inInternalStorage(context)) {
- toRawFile(context)?.moveTo(context, writableTargetFolder.getAbsolutePath(context), cleanFileName, fileConflictResolution)?.let {
- callback.uiScope.postToUi { callback.onCompleted(DocumentFile.fromFile(it)) }
+ toRawFile(context)?.moveTo(
+ context,
+ writableTargetFolder.getAbsolutePath(context),
+ cleanFileName,
+ fileConflictResolution
+ )?.let {
+ callback.postToUiScope {
+ callback.onCompleted(
+ FileCallback.Result.DocumentFile(
+ DocumentFile.fromFile(it)
+ )
+ )
+ }
return
}
}
@@ -2503,38 +2967,59 @@ private fun DocumentFile.moveFileTo(
if (isExternalStorageManager(context) && getStorageId(context) == targetStorageId) {
val sourceFile = toRawFile(context)
if (sourceFile == null) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
return
}
writableTargetFolder.toRawFile(context)?.let { destinationFolder ->
- sourceFile.moveTo(context, destinationFolder, cleanFileName, fileConflictResolution)?.let {
- callback.uiScope.postToUi { callback.onCompleted(DocumentFile.fromFile(it)) }
- return
- }
+ sourceFile.moveTo(context, destinationFolder, cleanFileName, fileConflictResolution)
+ ?.let {
+ callback.postToUiScope { callback.onCompleted(FileCallback.Result.DocumentFile(DocumentFile.fromFile(it))) }
+ return
+ }
}
}
try {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && !isRawFile && writableTargetFolder.isTreeDocumentFile && getStorageId(context) == targetStorageId) {
- val movedFileUri = parentFile?.uri?.let { DocumentsContract.moveDocument(context.contentResolver, uri, it, writableTargetFolder.uri) }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && !isRawFile && writableTargetFolder.isTreeDocumentFile && getStorageId(
+ context
+ ) == targetStorageId
+ ) {
+ val movedFileUri = parentFile?.uri?.let {
+ DocumentsContract.moveDocument(
+ context.contentResolver,
+ uri,
+ it,
+ writableTargetFolder.uri
+ )
+ }
if (movedFileUri != null) {
val newFile = context.fromTreeUri(movedFileUri)
if (newFile != null && newFile.isFile) {
if (newFilenameInTargetPath != null) newFile.renameTo(cleanFileName)
- callback.uiScope.postToUi { callback.onCompleted(newFile) }
+ callback.postToUiScope {
+ callback.onCompleted(
+ FileCallback.Result.DocumentFile(
+ newFile
+ )
+ )
+ }
} else {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.TARGET_FILE_NOT_FOUND) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.TARGET_FILE_NOT_FOUND) }
}
return
}
}
- if (!callback.onCheckFreeSpace(DocumentFileCompat.getFreeSpace(context, targetStorageId), length())) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH) }
+ if (!callback.onCheckFreeSpace(
+ DocumentFileCompat.getFreeSpace(context, targetStorageId),
+ length()
+ )
+ ) {
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH) }
return
}
} catch (e: Throwable) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
return
}
@@ -2545,14 +3030,26 @@ private fun DocumentFile.moveFileTo(
try {
val targetFile = createTargetFile(
- context, writableTargetFolder, cleanFileName, newMimeTypeInTargetPath ?: mimeTypeByFileName,
- fileConflictResolution.toCreateMode(), callback
+ context,
+ writableTargetFolder,
+ cleanFileName,
+ newMimeTypeInTargetPath ?: mimeTypeByFileName,
+ fileConflictResolution.toCreateMode(),
+ callback
) ?: return
createFileStreams(context, this, targetFile, callback) { inputStream, outputStream ->
- copyFileStream(inputStream, outputStream, targetFile, watchProgress, reportInterval, true, callback)
+ copyFileStream(
+ inputStream,
+ outputStream,
+ targetFile,
+ watchProgress,
+ reportInterval,
+ true,
+ callback
+ )
}
} catch (e: Exception) {
- callback.uiScope.postToUi { callback.onFailed(e.toFileCallbackErrorCode()) }
+ callback.postToUiScope { callback.onFailed(e.toFileCallbackErrorCode()) }
}
}
@@ -2561,11 +3058,11 @@ private fun DocumentFile.moveFileTo(
*/
private fun DocumentFile.simpleCheckSourceFile(callback: FileCallback): Boolean {
if (!isFile) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.SOURCE_FILE_NOT_FOUND) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.SOURCE_FILE_NOT_FOUND) }
return true
}
if (!canRead()) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
return true
}
return false
@@ -2581,20 +3078,26 @@ private fun DocumentFile.copyFileToMedia(
) {
if (simpleCheckSourceFile(callback)) return
- val publicFolder = DocumentFileCompat.fromPublicFolder(context, publicDirectory, fileDescription.subFolder, true)
+ val publicFolder = DocumentFileCompat.fromPublicFolder(
+ context,
+ publicDirectory,
+ fileDescription.subFolder,
+ true
+ )
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || deleteSourceFileWhenComplete && !isRawFile && publicFolder?.isTreeDocumentFile == true) {
if (publicFolder == null) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
return
}
publicFolder.child(context, fileDescription.fullName)?.let {
if (mode == CreateMode.REPLACE) {
if (!it.forceDelete(context)) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
return
}
} else {
- fileDescription.name = publicFolder.autoIncrementFileName(context, it.name.orEmpty())
+ fileDescription.name =
+ publicFolder.autoIncrementFileName(context, it.name.orEmpty())
}
}
fileDescription.subFolder = ""
@@ -2611,7 +3114,7 @@ private fun DocumentFile.copyFileToMedia(
MediaStoreCompat.createImage(context, fileDescription, mode = validMode)
}
if (mediaFile == null) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
} else {
copyFileTo(context, mediaFile, deleteSourceFileWhenComplete, callback)
}
@@ -2620,25 +3123,45 @@ private fun DocumentFile.copyFileToMedia(
@WorkerThread
@JvmOverloads
-fun DocumentFile.copyFileToDownloadMedia(context: Context, fileDescription: FileDescription, callback: FileCallback, mode: CreateMode = CreateMode.CREATE_NEW) {
+fun DocumentFile.copyFileToDownloadMedia(
+ context: Context,
+ fileDescription: FileDescription,
+ callback: FileCallback,
+ mode: CreateMode = CreateMode.CREATE_NEW
+) {
copyFileToMedia(context, fileDescription, callback, PublicDirectory.DOWNLOADS, false, mode)
}
@WorkerThread
@JvmOverloads
-fun DocumentFile.copyFileToPictureMedia(context: Context, fileDescription: FileDescription, callback: FileCallback, mode: CreateMode = CreateMode.CREATE_NEW) {
+fun DocumentFile.copyFileToPictureMedia(
+ context: Context,
+ fileDescription: FileDescription,
+ callback: FileCallback,
+ mode: CreateMode = CreateMode.CREATE_NEW
+) {
copyFileToMedia(context, fileDescription, callback, PublicDirectory.PICTURES, false, mode)
}
@WorkerThread
@JvmOverloads
-fun DocumentFile.moveFileToDownloadMedia(context: Context, fileDescription: FileDescription, callback: FileCallback, mode: CreateMode = CreateMode.CREATE_NEW) {
+fun DocumentFile.moveFileToDownloadMedia(
+ context: Context,
+ fileDescription: FileDescription,
+ callback: FileCallback,
+ mode: CreateMode = CreateMode.CREATE_NEW
+) {
copyFileToMedia(context, fileDescription, callback, PublicDirectory.DOWNLOADS, true, mode)
}
@WorkerThread
@JvmOverloads
-fun DocumentFile.moveFileToPictureMedia(context: Context, fileDescription: FileDescription, callback: FileCallback, mode: CreateMode = CreateMode.CREATE_NEW) {
+fun DocumentFile.moveFileToPictureMedia(
+ context: Context,
+ fileDescription: FileDescription,
+ callback: FileCallback,
+ mode: CreateMode = CreateMode.CREATE_NEW
+) {
copyFileToMedia(context, fileDescription, callback, PublicDirectory.PICTURES, true, mode)
}
@@ -2667,7 +3190,7 @@ private fun DocumentFile.copyFileTo(
if (simpleCheckSourceFile(callback)) return
if (!callback.onCheckFreeSpace(DocumentFileCompat.getFreeSpace(context, PRIMARY), length())) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH) }
return
}
@@ -2678,10 +3201,18 @@ private fun DocumentFile.copyFileTo(
try {
createFileStreams(context, this, targetFile, callback) { inputStream, outputStream ->
- copyFileStream(inputStream, outputStream, targetFile, watchProgress, reportInterval, deleteSourceFileWhenComplete, callback)
+ copyFileStream(
+ inputStream,
+ outputStream,
+ targetFile,
+ watchProgress,
+ reportInterval,
+ deleteSourceFileWhenComplete,
+ callback
+ )
}
} catch (e: Exception) {
- callback.uiScope.postToUi { callback.onFailed(e.toFileCallbackErrorCode()) }
+ callback.postToUiScope { callback.onFailed(e.toFileCallbackErrorCode()) }
}
}
@@ -2704,9 +3235,9 @@ private fun handleFileConflict(
callback.onConflict(targetFile, FileCallback.FileConflictAction(it))
}
if (resolution == FileCallback.ConflictResolution.REPLACE) {
- callback.uiScope.postToUi { callback.onDeleteConflictedFiles() }
+ callback.postToUiScope { callback.onDeleteConflictedFiles() }
if (!targetFile.forceDelete(context)) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
return FileCallback.ConflictResolution.SKIP
}
}
@@ -2727,24 +3258,30 @@ private fun handleParentFolderConflict(
return FolderCallback.ConflictResolution.MERGE
}
- val resolution = awaitUiResultWithPending(callback.uiScope) {
- callback.onParentConflict(targetFolder, FolderCallback.ParentFolderConflictAction(it), canMerge)
- }
+ val resolution =
+ awaitUiResultWithPending(callback.uiScope) {
+ callback.onParentConflict(
+ targetFolder,
+ FolderCallback.ParentFolderConflictAction(it),
+ canMerge
+ )
+ }
when (resolution) {
FolderCallback.ConflictResolution.REPLACE -> {
- callback.uiScope.postToUi { callback.onDeleteConflictedFiles() }
+ callback.postToUiScope { callback.onDeleteConflictedFiles() }
val isFolder = targetFolder.isDirectory
if (targetFolder.forceDelete(context, true)) {
if (!isFolder) {
- val newFolder = targetFolder.parentFile?.createDirectory(targetFolderParentName)
+ val newFolder =
+ targetFolder.parentFile?.createDirectory(targetFolderParentName)
if (newFolder == null) {
- callback.uiScope.postToUi { callback.onFailed(FolderCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
+ callback.postToUiScope { callback.onFailed(FolderCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
return FolderCallback.ConflictResolution.SKIP
}
}
} else {
- callback.uiScope.postToUi { callback.onFailed(FolderCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
+ callback.postToUiScope { callback.onFailed(FolderCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
return FolderCallback.ConflictResolution.SKIP
}
}
@@ -2752,13 +3289,14 @@ private fun handleParentFolderConflict(
FolderCallback.ConflictResolution.MERGE -> {
if (targetFolder.isFile) {
if (targetFolder.delete()) {
- val newFolder = targetFolder.parentFile?.createDirectory(targetFolderParentName)
+ val newFolder =
+ targetFolder.parentFile?.createDirectory(targetFolderParentName)
if (newFolder == null) {
- callback.uiScope.postToUi { callback.onFailed(FolderCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
+ callback.postToUiScope { callback.onFailed(FolderCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
return FolderCallback.ConflictResolution.SKIP
}
} else {
- callback.uiScope.postToUi { callback.onFailed(FolderCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
+ callback.postToUiScope { callback.onFailed(FolderCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
return FolderCallback.ConflictResolution.SKIP
}
}
@@ -2783,31 +3321,39 @@ private fun List.handleParentFolderConflict(
val conflicts = conflictedFiles.map {
val sourceFile = first { src -> src.name == it.name }
val canMerge = sourceFile.isDirectory && it.isDirectory
- val solution = if (canMerge && it.isEmpty(context)) FolderCallback.ConflictResolution.MERGE else FolderCallback.ConflictResolution.CREATE_NEW
+ val solution =
+ if (canMerge && it.isEmpty(context)) FolderCallback.ConflictResolution.MERGE else FolderCallback.ConflictResolution.CREATE_NEW
MultipleFileCallback.ParentConflict(sourceFile, it, canMerge, solution)
}
- val unresolvedConflicts = conflicts.filter { it.solution != FolderCallback.ConflictResolution.MERGE }.toMutableList()
+ val unresolvedConflicts =
+ conflicts.filter { it.solution != FolderCallback.ConflictResolution.MERGE }.toMutableList()
if (unresolvedConflicts.isNotEmpty()) {
val unresolvedFiles = unresolvedConflicts.filter { it.source.isFile }.toMutableList()
val unresolvedFolders = unresolvedConflicts.filter { it.source.isDirectory }.toMutableList()
- val resolution = awaitUiResultWithPending>(callback.uiScope) {
- callback.onParentConflict(targetParentFolder, unresolvedFolders, unresolvedFiles, MultipleFileCallback.ParentFolderConflictAction(it))
- }
+ val resolution =
+ awaitUiResultWithPending>(callback.uiScope) {
+ callback.onParentConflict(
+ targetParentFolder,
+ unresolvedFolders,
+ unresolvedFiles,
+ MultipleFileCallback.ParentFolderConflictAction(it)
+ )
+ }
if (resolution.any { it.solution == FolderCallback.ConflictResolution.REPLACE }) {
- callback.uiScope.postToUi { callback.onDeleteConflictedFiles() }
+ callback.postToUiScope { callback.onDeleteConflictedFiles() }
}
resolution.forEach { conflict ->
when (conflict.solution) {
FolderCallback.ConflictResolution.REPLACE -> {
if (!conflict.target.let { it.forceDelete(context, true) || !it.exists() }) {
- callback.uiScope.postToUi { callback.onFailed(MultipleFileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
+ callback.postToUiScope { callback.onFailed(MultipleFileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
return null
}
}
FolderCallback.ConflictResolution.MERGE -> {
if (conflict.target.isFile && !conflict.target.delete()) {
- callback.uiScope.postToUi { callback.onFailed(MultipleFileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
+ callback.postToUiScope { callback.onFailed(MultipleFileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
return null
}
}
@@ -2817,7 +3363,8 @@ private fun List.handleParentFolderConflict(
}
}
}
- return resolution.toMutableList().apply { addAll(conflicts.filter { it.solution == FolderCallback.ConflictResolution.MERGE }) }
+ return resolution.toMutableList()
+ .apply { addAll(conflicts.filter { it.solution == FolderCallback.ConflictResolution.MERGE }) }
}
return emptyList()
}
\ No newline at end of file
diff --git a/storage/src/main/java/com/anggrayudi/storage/file/DocumentFileType.kt b/storage/src/main/kotlin/com/anggrayudi/storage/file/DocumentFileType.kt
similarity index 100%
rename from storage/src/main/java/com/anggrayudi/storage/file/DocumentFileType.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/file/DocumentFileType.kt
diff --git a/storage/src/main/java/com/anggrayudi/storage/file/FileExt.kt b/storage/src/main/kotlin/com/anggrayudi/storage/file/FileExt.kt
similarity index 99%
rename from storage/src/main/java/com/anggrayudi/storage/file/FileExt.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/file/FileExt.kt
index 82ea05a..62422cd 100644
--- a/storage/src/main/java/com/anggrayudi/storage/file/FileExt.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/file/FileExt.kt
@@ -151,7 +151,6 @@ fun File.inKitkatSdCard() =
* @return `true` if you have full disk access
* @see Environment.isExternalStorageManager
*/
-@Suppress("DEPRECATION")
fun File.isExternalStorageManager(context: Context) = Build.VERSION.SDK_INT > Build.VERSION_CODES.Q && Environment.isExternalStorageManager(this)
|| Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
(path.startsWith(SimpleStorage.externalStoragePath) || Build.VERSION.SDK_INT < 21 && path.startsWith(StorageId.KITKAT_SDCARD))
diff --git a/storage/src/main/java/com/anggrayudi/storage/file/FileFullPath.kt b/storage/src/main/kotlin/com/anggrayudi/storage/file/FileFullPath.kt
similarity index 90%
rename from storage/src/main/java/com/anggrayudi/storage/file/FileFullPath.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/file/FileFullPath.kt
index 9d58460..3f105dc 100644
--- a/storage/src/main/java/com/anggrayudi/storage/file/FileFullPath.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/file/FileFullPath.kt
@@ -55,6 +55,7 @@ class FileFullPath {
simplePath = "$storageId:$basePath"
absolutePath = "$rootPath/$basePath".trimEnd('/')
}
+
fullPath.startsWith(context.dataDirectory.path) -> {
storageId = StorageId.DATA
val rootPath = context.dataDirectory.path
@@ -62,15 +63,18 @@ class FileFullPath {
simplePath = "$storageId:$basePath"
absolutePath = "$rootPath/$basePath".trimEnd('/')
}
+
fullPath.startsWith(KITKAT_SD_CARD_PATH) -> {
storageId = KITKAT_SDCARD
basePath = fullPath.substringAfter(KITKAT_SD_CARD_PATH, "").trimFileSeparator()
simplePath = "$storageId:$basePath"
absolutePath = "$KITKAT_SD_CARD_PATH/$basePath".trimEnd('/')
}
+
else -> if (fullPath.matches(DocumentFileCompat.SD_CARD_STORAGE_PATH_REGEX)) {
storageId = fullPath.substringAfter("/storage/", "").substringBefore('/')
- basePath = fullPath.substringAfter("/storage/$storageId", "").trimFileSeparator()
+ basePath =
+ fullPath.substringAfter("/storage/$storageId", "").trimFileSeparator()
simplePath = "$storageId:$basePath"
absolutePath = "/storage/$storageId/$basePath".trimEnd('/')
} else {
@@ -109,11 +113,12 @@ class FileFullPath {
constructor(context: Context, file: File) : this(context, file.path.orEmpty())
- private fun buildAbsolutePath(context: Context, storageId: String, basePath: String) = if (storageId.isEmpty()) "" else when (storageId) {
- StorageId.PRIMARY -> "${SimpleStorage.externalStoragePath}/$basePath".trimEnd('/')
- StorageId.DATA -> "${context.dataDirectory.path}/$basePath".trimEnd('/')
- else -> "/storage/$storageId/$basePath".trimEnd('/')
- }
+ private fun buildAbsolutePath(context: Context, storageId: String, basePath: String) =
+ if (storageId.isEmpty()) "" else when (storageId) {
+ StorageId.PRIMARY -> "${SimpleStorage.externalStoragePath}/$basePath".trimEnd('/')
+ StorageId.DATA -> "${context.dataDirectory.path}/$basePath".trimEnd('/')
+ else -> "/storage/$storageId/$basePath".trimEnd('/')
+ }
private fun buildBaseAndAbsolutePaths(context: Context) {
absolutePath = buildAbsolutePath(context, storageId, basePath)
@@ -121,7 +126,10 @@ class FileFullPath {
}
val uri: Uri?
- get() = if (storageId.isEmpty()) null else DocumentFileCompat.createDocumentUri(storageId, basePath)
+ get() = if (storageId.isEmpty()) null else DocumentFileCompat.createDocumentUri(
+ storageId,
+ basePath
+ )
fun toDocumentUri(context: Context): Uri? {
return context.fromTreeUri(uri ?: return null)?.uri
diff --git a/storage/src/main/java/com/anggrayudi/storage/file/FileProperties.kt b/storage/src/main/kotlin/com/anggrayudi/storage/file/FileProperties.kt
similarity index 88%
rename from storage/src/main/java/com/anggrayudi/storage/file/FileProperties.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/file/FileProperties.kt
index 41363a4..4ed79f0 100644
--- a/storage/src/main/java/com/anggrayudi/storage/file/FileProperties.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/file/FileProperties.kt
@@ -3,10 +3,11 @@ package com.anggrayudi.storage.file
import android.content.Context
import android.text.format.Formatter
import androidx.annotation.UiThread
+import com.anggrayudi.storage.callback.ScopeHoldingCallback
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
-import java.util.*
+import java.util.Date
/**
* Created on 03/06/21
@@ -29,8 +30,8 @@ data class FileProperties(
abstract class CalculationCallback(
val updateInterval: Long = 500, // 500ms
@OptIn(DelicateCoroutinesApi::class)
- var uiScope: CoroutineScope = GlobalScope
- ) {
+ override val uiScope: CoroutineScope = GlobalScope
+ ) : ScopeHoldingCallback {
@UiThread
open fun onUpdate(properties: FileProperties) {
diff --git a/storage/src/main/java/com/anggrayudi/storage/file/FileSize.kt b/storage/src/main/kotlin/com/anggrayudi/storage/file/FileSize.kt
similarity index 100%
rename from storage/src/main/java/com/anggrayudi/storage/file/FileSize.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/file/FileSize.kt
diff --git a/storage/src/main/java/com/anggrayudi/storage/file/MimeType.kt b/storage/src/main/kotlin/com/anggrayudi/storage/file/MimeType.kt
similarity index 78%
rename from storage/src/main/java/com/anggrayudi/storage/file/MimeType.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/file/MimeType.kt
index b587617..fe7ce0e 100644
--- a/storage/src/main/java/com/anggrayudi/storage/file/MimeType.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/file/MimeType.kt
@@ -38,7 +38,11 @@ object MimeType {
return cleanName
}
}
- return getExtensionFromMimeType(mimeType).let { if (it.isEmpty() || cleanName.endsWith(".$it")) cleanName else "$cleanName.$it".trimEnd('.') }
+ return getExtensionFromMimeType(mimeType).let {
+ if (it.isEmpty() || cleanName.endsWith(".$it")) cleanName else "$cleanName.$it".trimEnd(
+ '.'
+ )
+ }
}
/**
@@ -48,12 +52,16 @@ object MimeType {
*/
@JvmStatic
fun getExtensionFromMimeType(mimeType: String?): String {
- return mimeType?.let { if (it == BINARY_FILE) "bin" else MimeTypeMap.getSingleton().getExtensionFromMimeType(it) }.orEmpty()
+ return mimeType?.let {
+ if (it == BINARY_FILE) "bin" else MimeTypeMap.getSingleton()
+ .getExtensionFromMimeType(it)
+ }.orEmpty()
}
@JvmStatic
fun getBaseFileName(filename: String?): String {
- return if (hasExtension(filename)) filename.orEmpty().substringBeforeLast('.') else filename.orEmpty()
+ return if (hasExtension(filename)) filename.orEmpty()
+ .substringBeforeLast('.') else filename.orEmpty()
}
@JvmStatic
@@ -74,7 +82,9 @@ object MimeType {
*/
@JvmStatic
fun getExtensionFromMimeTypeOrFileName(mimeType: String?, filename: String): String {
- return if (mimeType == null || mimeType == UNKNOWN) getExtensionFromFileName(filename) else getExtensionFromMimeType(mimeType)
+ return if (mimeType == null || mimeType == UNKNOWN) getExtensionFromFileName(filename) else getExtensionFromMimeType(
+ mimeType
+ )
}
/**
@@ -82,7 +92,11 @@ object MimeType {
*/
@JvmStatic
fun getMimeTypeFromExtension(fileExtension: String): String {
- return if (fileExtension.equals("bin", ignoreCase = true)) BINARY_FILE else MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension)
+ return if (fileExtension.equals(
+ "bin",
+ ignoreCase = true
+ )
+ ) BINARY_FILE else MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension)
?: UNKNOWN
}
diff --git a/storage/src/main/java/com/anggrayudi/storage/file/PublicDirectory.kt b/storage/src/main/kotlin/com/anggrayudi/storage/file/PublicDirectory.kt
similarity index 98%
rename from storage/src/main/java/com/anggrayudi/storage/file/PublicDirectory.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/file/PublicDirectory.kt
index 524089f..38268d7 100644
--- a/storage/src/main/java/com/anggrayudi/storage/file/PublicDirectory.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/file/PublicDirectory.kt
@@ -68,7 +68,6 @@ enum class PublicDirectory(val folderName: String) {
*/
DOCUMENTS(Environment.DIRECTORY_DOCUMENTS);
- @Suppress("DEPRECATION")
val file: File
get() = Environment.getExternalStoragePublicDirectory(folderName)
diff --git a/storage/src/main/java/com/anggrayudi/storage/file/StorageId.kt b/storage/src/main/kotlin/com/anggrayudi/storage/file/StorageId.kt
similarity index 95%
rename from storage/src/main/java/com/anggrayudi/storage/file/StorageId.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/file/StorageId.kt
index 2fb429f..53477f6 100644
--- a/storage/src/main/java/com/anggrayudi/storage/file/StorageId.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/file/StorageId.kt
@@ -28,7 +28,7 @@ object StorageId {
/**
* For `/storage/emulated/0/Documents`
- * It is only exists on API 29-
+ * Only exists on API 29-
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
const val HOME = "home"
diff --git a/storage/src/main/java/com/anggrayudi/storage/file/StorageType.kt b/storage/src/main/kotlin/com/anggrayudi/storage/file/StorageType.kt
similarity index 100%
rename from storage/src/main/java/com/anggrayudi/storage/file/StorageType.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/file/StorageType.kt
diff --git a/storage/src/main/java/com/anggrayudi/storage/media/FileDescription.kt b/storage/src/main/kotlin/com/anggrayudi/storage/media/FileDescription.kt
similarity index 100%
rename from storage/src/main/java/com/anggrayudi/storage/media/FileDescription.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/media/FileDescription.kt
diff --git a/storage/src/main/kotlin/com/anggrayudi/storage/media/MediaDirectory.kt b/storage/src/main/kotlin/com/anggrayudi/storage/media/MediaDirectory.kt
new file mode 100644
index 0000000..7def07b
--- /dev/null
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/media/MediaDirectory.kt
@@ -0,0 +1,37 @@
+package com.anggrayudi.storage.media
+
+import android.os.Environment
+
+sealed interface MediaDirectory {
+ val folderName: String
+
+ /**
+ * Created on 06/09/20
+ * @author Anggrayudi H
+ */
+ enum class Image(override val folderName: String) : MediaDirectory {
+ PICTURES(Environment.DIRECTORY_PICTURES),
+ DCIM(Environment.DIRECTORY_DCIM)
+ }
+
+ /**
+ * Created on 06/09/20
+ * @author Anggrayudi H
+ */
+ enum class Video(override val folderName: String) : MediaDirectory {
+ MOVIES(Environment.DIRECTORY_MOVIES),
+ DCIM(Environment.DIRECTORY_DCIM)
+ }
+
+ /**
+ * Created on 06/09/20
+ * @author Anggrayudi H
+ */
+ enum class Audio(override val folderName: String) : MediaDirectory {
+ MUSIC(Environment.DIRECTORY_MUSIC),
+ PODCASTS(Environment.DIRECTORY_PODCASTS),
+ RINGTONES(Environment.DIRECTORY_RINGTONES),
+ ALARMS(Environment.DIRECTORY_ALARMS),
+ NOTIFICATIONS(Environment.DIRECTORY_NOTIFICATIONS)
+ }
+}
\ No newline at end of file
diff --git a/storage/src/main/java/com/anggrayudi/storage/media/MediaFile.kt b/storage/src/main/kotlin/com/anggrayudi/storage/media/MediaFile.kt
similarity index 63%
rename from storage/src/main/java/com/anggrayudi/storage/media/MediaFile.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/media/MediaFile.kt
index 0f75c23..3621b02 100644
--- a/storage/src/main/java/com/anggrayudi/storage/media/MediaFile.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/media/MediaFile.kt
@@ -18,11 +18,43 @@ import androidx.core.content.FileProvider
import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.SimpleStorage
import com.anggrayudi.storage.callback.FileCallback
-import com.anggrayudi.storage.extension.*
-import com.anggrayudi.storage.file.*
+import com.anggrayudi.storage.extension.awaitUiResult
+import com.anggrayudi.storage.extension.awaitUiResultWithPending
+import com.anggrayudi.storage.extension.closeStreamQuietly
+import com.anggrayudi.storage.extension.getString
+import com.anggrayudi.storage.extension.isRawFile
+import com.anggrayudi.storage.extension.openInputStream
+import com.anggrayudi.storage.extension.replaceCompletely
+import com.anggrayudi.storage.extension.startCoroutineTimer
+import com.anggrayudi.storage.extension.toDocumentFile
+import com.anggrayudi.storage.extension.toInt
+import com.anggrayudi.storage.extension.trimFileSeparator
+import com.anggrayudi.storage.file.CreateMode
+import com.anggrayudi.storage.file.DocumentFileCompat
import com.anggrayudi.storage.file.DocumentFileCompat.removeForbiddenCharsFromFilename
+import com.anggrayudi.storage.file.FileSize
+import com.anggrayudi.storage.file.MimeType
+import com.anggrayudi.storage.file.child
+import com.anggrayudi.storage.file.copyFileTo
+import com.anggrayudi.storage.file.forceDelete
+import com.anggrayudi.storage.file.fullName
+import com.anggrayudi.storage.file.getBasePath
+import com.anggrayudi.storage.file.getStorageId
+import com.anggrayudi.storage.file.isEmpty
+import com.anggrayudi.storage.file.makeFile
+import com.anggrayudi.storage.file.makeFolder
+import com.anggrayudi.storage.file.mimeType
+import com.anggrayudi.storage.file.moveFileTo
+import com.anggrayudi.storage.file.openOutputStream
+import com.anggrayudi.storage.file.toDocumentFile
+import com.anggrayudi.storage.file.toFileCallbackErrorCode
import kotlinx.coroutines.Job
-import java.io.*
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
/**
* Created on 06/09/20
@@ -35,22 +67,6 @@ class MediaFile(context: Context, val uri: Uri) {
private val context = context.applicationContext
- interface AccessCallback {
-
- /**
- * When this function called, you can ask user's consent to modify other app's files.
- * @see RecoverableSecurityException
- * @see [android.app.Activity.startIntentSenderForResult]
- */
- fun onWriteAccessDenied(mediaFile: MediaFile, sender: IntentSender)
- }
-
- /**
- * Only useful for Android 10 and higher.
- * @see RecoverableSecurityException
- */
- var accessCallback: AccessCallback? = null
-
/**
* Some media files do not return file extension. This function helps you to fix this kind of issue.
*/
@@ -79,7 +95,13 @@ class MediaFile(context: Context, val uri: Uri) {
* @see [mimeType]
*/
val type: String?
- get() = toRawFile()?.name?.let { MimeType.getMimeTypeFromExtension(MimeType.getExtensionFromFileName(it)) }
+ get() = toRawFile()?.name?.let {
+ MimeType.getMimeTypeFromExtension(
+ MimeType.getExtensionFromFileName(
+ it
+ )
+ )
+ }
?: getColumnInfoString(MediaStore.MediaColumns.MIME_TYPE)
/**
@@ -93,7 +115,8 @@ class MediaFile(context: Context, val uri: Uri) {
get() = toRawFile()?.length() ?: getColumnInfoLong(MediaStore.MediaColumns.SIZE)
set(value) {
try {
- val contentValues = ContentValues(1).apply { put(MediaStore.MediaColumns.SIZE, value) }
+ val contentValues =
+ ContentValues(1).apply { put(MediaStore.MediaColumns.SIZE, value) }
context.contentResolver.update(uri, contentValues, null, null)
} catch (e: SecurityException) {
handleSecurityException(e)
@@ -145,10 +168,15 @@ class MediaFile(context: Context, val uri: Uri) {
* from [Intent.ACTION_OPEN_DOCUMENT] or [Intent.ACTION_CREATE_DOCUMENT].
* @see toDocumentFile
*/
- @Deprecated("Accessing files with java.io.File only works on app private directory since Android 10.")
+ @Deprecated("Accessing files with java.io.File only works on app-private directories since Android 10.")
fun toRawFile() = if (isRawFile) uri.path?.let { File(it) } else null
- fun toDocumentFile() = absolutePath.let { if (it.isEmpty()) null else DocumentFileCompat.fromFullPath(context, it) }
+ fun toDocumentFile() = absolutePath.let {
+ if (it.isEmpty()) null else DocumentFileCompat.fromFullPath(
+ context,
+ it
+ )
+ }
val absolutePath: String
@SuppressLint("InlinedApi")
@@ -158,7 +186,13 @@ class MediaFile(context: Context, val uri: Uri) {
file != null -> file.path
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q -> {
try {
- context.contentResolver.query(uri, arrayOf(MediaStore.MediaColumns.DATA), null, null, null)?.use { cursor ->
+ context.contentResolver.query(
+ uri,
+ arrayOf(MediaStore.MediaColumns.DATA),
+ null,
+ null,
+ null
+ )?.use { cursor ->
if (cursor.moveToFirst()) {
cursor.getString(MediaStore.MediaColumns.DATA)
} else ""
@@ -167,15 +201,24 @@ class MediaFile(context: Context, val uri: Uri) {
""
}
}
+
else -> {
- val projection = arrayOf(MediaStore.MediaColumns.RELATIVE_PATH, MediaStore.MediaColumns.DISPLAY_NAME)
- context.contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
- if (cursor.moveToFirst()) {
- val relativePath = cursor.getString(MediaStore.MediaColumns.RELATIVE_PATH) ?: return ""
- val name = cursor.getString(MediaStore.MediaColumns.DISPLAY_NAME)
- "${SimpleStorage.externalStoragePath}/$relativePath/$name".trimEnd('/').replaceCompletely("//", "/")
- } else ""
- }.orEmpty()
+ val projection = arrayOf(
+ MediaStore.MediaColumns.RELATIVE_PATH,
+ MediaStore.MediaColumns.DISPLAY_NAME
+ )
+ context.contentResolver.query(uri, projection, null, null, null)
+ ?.use { cursor ->
+ if (cursor.moveToFirst()) {
+ val relativePath =
+ cursor.getString(MediaStore.MediaColumns.RELATIVE_PATH)
+ ?: return ""
+ val name = cursor.getString(MediaStore.MediaColumns.DISPLAY_NAME)
+ "${SimpleStorage.externalStoragePath}/$relativePath/$name".trimEnd(
+ '/'
+ ).replaceCompletely("//", "/")
+ } else ""
+ }.orEmpty()
}
}
}
@@ -192,28 +235,43 @@ class MediaFile(context: Context, val uri: Uri) {
val file = toRawFile()
return when {
file != null -> {
- file.path.substringBeforeLast('/').replaceFirst(SimpleStorage.externalStoragePath, "").trimFileSeparator() + "/"
+ file.path.substringBeforeLast('/')
+ .replaceFirst(SimpleStorage.externalStoragePath, "")
+ .trimFileSeparator() + "/"
}
+
Build.VERSION.SDK_INT < Build.VERSION_CODES.Q -> {
try {
- context.contentResolver.query(uri, arrayOf(MediaStore.MediaColumns.DATA), null, null, null)?.use { cursor ->
+ context.contentResolver.query(
+ uri,
+ arrayOf(MediaStore.MediaColumns.DATA),
+ null,
+ null,
+ null
+ )?.use { cursor ->
if (cursor.moveToFirst()) {
val realFolderAbsolutePath =
- cursor.getString(MediaStore.MediaColumns.DATA).orEmpty().substringBeforeLast('/')
- realFolderAbsolutePath.replaceFirst(SimpleStorage.externalStoragePath, "").trimFileSeparator() + "/"
+ cursor.getString(MediaStore.MediaColumns.DATA).orEmpty()
+ .substringBeforeLast('/')
+ realFolderAbsolutePath.replaceFirst(
+ SimpleStorage.externalStoragePath,
+ ""
+ ).trimFileSeparator() + "/"
} else ""
}.orEmpty()
} catch (e: Exception) {
""
}
}
+
else -> {
val projection = arrayOf(MediaStore.MediaColumns.RELATIVE_PATH)
- context.contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
- if (cursor.moveToFirst()) {
- cursor.getString(MediaStore.MediaColumns.RELATIVE_PATH)
- } else ""
- }.orEmpty()
+ context.contentResolver.query(uri, projection, null, null, null)
+ ?.use { cursor ->
+ if (cursor.moveToFirst()) {
+ cursor.getString(MediaStore.MediaColumns.RELATIVE_PATH)
+ } else ""
+ }.orEmpty()
}
}
}
@@ -236,7 +294,8 @@ class MediaFile(context: Context, val uri: Uri) {
*/
fun renameTo(newName: String): Boolean {
val file = toRawFile()
- val contentValues = ContentValues(1).apply { put(MediaStore.MediaColumns.DISPLAY_NAME, newName) }
+ val contentValues =
+ ContentValues(1).apply { put(MediaStore.MediaColumns.DISPLAY_NAME, newName) }
return if (file != null) {
context.contentResolver.update(uri, contentValues, null, null)
file.renameTo(File(file.parent, newName))
@@ -254,7 +313,8 @@ class MediaFile(context: Context, val uri: Uri) {
get() = getColumnInfoInt(MediaStore.MediaColumns.IS_PENDING) == 1
@RequiresApi(Build.VERSION_CODES.Q)
set(value) {
- val contentValues = ContentValues(1).apply { put(MediaStore.MediaColumns.IS_PENDING, value.toInt()) }
+ val contentValues =
+ ContentValues(1).apply { put(MediaStore.MediaColumns.IS_PENDING, value.toInt()) }
try {
context.contentResolver.update(uri, contentValues, null, null)
} catch (e: SecurityException) {
@@ -262,17 +322,32 @@ class MediaFile(context: Context, val uri: Uri) {
}
}
- private fun handleSecurityException(e: SecurityException, callback: FileCallback? = null) {
+ /**
+ * @param onWriteAccessDenied To ask for the user's consent to modify other app's files. Only called starting from Android 10.
+ * @see RecoverableSecurityException
+ * @see [android.app.Activity.startIntentSenderForResult]
+ */
+ private fun handleSecurityException(
+ e: SecurityException,
+ callback: FileCallback? = null,
+ onWriteAccessDenied: ((MediaFile, IntentSender) -> Unit)? = null
+ ) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && e is RecoverableSecurityException) {
- accessCallback?.onWriteAccessDenied(this, e.userAction.actionIntent.intentSender)
+ onWriteAccessDenied?.invoke(this, e.userAction.actionIntent.intentSender)
} else {
- callback?.uiScope?.postToUi { callback.onFailed(FileCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
+ callback?.postToUiScope { callback.onFailed(FileCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
}
}
@UiThread
fun openFileIntent(authority: String) = Intent(Intent.ACTION_VIEW)
- .setData(if (isRawFile) FileProvider.getUriForFile(context, authority, File(uri.path!!)) else uri)
+ .setData(
+ if (isRawFile) FileProvider.getUriForFile(
+ context,
+ authority,
+ File(uri.path!!)
+ ) else uri
+ )
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
@@ -310,7 +385,8 @@ class MediaFile(context: Context, val uri: Uri) {
@TargetApi(Build.VERSION_CODES.Q)
fun moveTo(relativePath: String): Boolean {
- val contentValues = ContentValues(1).apply { put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath) }
+ val contentValues =
+ ContentValues(1).apply { put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath) }
return try {
context.contentResolver.update(uri, contentValues, null, null) > 0
} catch (e: SecurityException) {
@@ -320,31 +396,47 @@ class MediaFile(context: Context, val uri: Uri) {
}
@WorkerThread
- fun moveTo(targetFolder: DocumentFile, fileDescription: FileDescription? = null, callback: FileCallback) {
- val sourceFile = toDocumentFile()
- if (sourceFile != null) {
- sourceFile.moveFileTo(context, targetFolder, fileDescription, callback)
+ fun moveTo(
+ targetFolder: DocumentFile,
+ fileDescription: FileDescription? = null,
+ callback: FileCallback
+ ) {
+ toDocumentFile()?.let {
+ it.moveFileTo(context, targetFolder, fileDescription, callback)
return
}
- if (!callback.onCheckFreeSpace(DocumentFileCompat.getFreeSpace(context, targetFolder.getStorageId(context)), length)) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH) }
+ if (!callback.onCheckFreeSpace(
+ DocumentFileCompat.getFreeSpace(
+ context,
+ targetFolder.getStorageId(context)
+ ), length
+ )
+ ) {
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH) }
return
}
val targetDirectory = if (fileDescription?.subFolder.isNullOrEmpty()) {
targetFolder
} else {
- val directory = targetFolder.makeFolder(context, fileDescription?.subFolder.orEmpty(), CreateMode.REUSE)
+ val directory = targetFolder.makeFolder(
+ context,
+ fileDescription?.subFolder.orEmpty(),
+ CreateMode.REUSE
+ )
if (directory == null) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
return
} else {
directory
}
}
- val cleanFileName = MimeType.getFullFileName(fileDescription?.name ?: name.orEmpty(), fileDescription?.mimeType ?: type)
+ val cleanFileName = MimeType.getFullFileName(
+ fileDescription?.name ?: name.orEmpty(),
+ fileDescription?.mimeType ?: type
+ )
.removeForbiddenCharsFromFilename().trimFileSeparator()
val conflictResolution = handleFileConflict(targetDirectory, cleanFileName, callback)
if (conflictResolution == FileCallback.ConflictResolution.SKIP) {
@@ -361,41 +453,66 @@ class MediaFile(context: Context, val uri: Uri) {
conflictResolution.toCreateMode(), callback
) ?: return
createFileStreams(targetFile, callback) { inputStream, outputStream ->
- copyFileStream(inputStream, outputStream, targetFile, watchProgress, reportInterval, true, callback)
+ copyFileStream(
+ inputStream,
+ outputStream,
+ targetFile,
+ watchProgress,
+ reportInterval,
+ true,
+ callback
+ )
}
} catch (e: SecurityException) {
handleSecurityException(e, callback)
} catch (e: Exception) {
- callback.uiScope.postToUi { callback.onFailed(e.toFileCallbackErrorCode()) }
+ callback.postToUiScope { callback.onFailed(e.toFileCallbackErrorCode()) }
}
}
@WorkerThread
- fun copyTo(targetFolder: DocumentFile, fileDescription: FileDescription? = null, callback: FileCallback) {
+ fun copyTo(
+ targetFolder: DocumentFile,
+ fileDescription: FileDescription? = null,
+ callback: FileCallback
+ ) {
val sourceFile = toDocumentFile()
if (sourceFile != null) {
sourceFile.copyFileTo(context, targetFolder, fileDescription, callback)
return
}
- if (!callback.onCheckFreeSpace(DocumentFileCompat.getFreeSpace(context, targetFolder.getStorageId(context)), length)) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH) }
+ if (!callback.onCheckFreeSpace(
+ DocumentFileCompat.getFreeSpace(
+ context,
+ targetFolder.getStorageId(context)
+ ), length
+ )
+ ) {
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH) }
return
}
val targetDirectory = if (fileDescription?.subFolder.isNullOrEmpty()) {
targetFolder
} else {
- val directory = targetFolder.makeFolder(context, fileDescription?.subFolder.orEmpty(), CreateMode.REUSE)
+ val directory = targetFolder.makeFolder(
+ context,
+ fileDescription?.subFolder.orEmpty(),
+ CreateMode.REUSE
+ )
if (directory == null) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
return
} else {
directory
}
}
- val cleanFileName = MimeType.getFullFileName(fileDescription?.name ?: name.orEmpty(), fileDescription?.mimeType ?: type)
+ val cleanFileName = MimeType.getFullFileName(
+ fileDescription?.name ?: name.orEmpty(),
+ fileDescription?.mimeType ?: type
+ )
.removeForbiddenCharsFromFilename().trimFileSeparator()
val conflictResolution = handleFileConflict(targetDirectory, cleanFileName, callback)
if (conflictResolution == FileCallback.ConflictResolution.SKIP) {
@@ -411,12 +528,20 @@ class MediaFile(context: Context, val uri: Uri) {
conflictResolution.toCreateMode(), callback
) ?: return
createFileStreams(targetFile, callback) { inputStream, outputStream ->
- copyFileStream(inputStream, outputStream, targetFile, watchProgress, reportInterval, false, callback)
+ copyFileStream(
+ inputStream,
+ outputStream,
+ targetFile,
+ watchProgress,
+ reportInterval,
+ false,
+ callback
+ )
}
} catch (e: SecurityException) {
handleSecurityException(e, callback)
} catch (e: Exception) {
- callback.uiScope.postToUi { callback.onFailed(e.toFileCallbackErrorCode()) }
+ callback.postToUiScope { callback.onFailed(e.toFileCallbackErrorCode()) }
}
}
@@ -428,23 +553,27 @@ class MediaFile(context: Context, val uri: Uri) {
callback: FileCallback
): DocumentFile? {
try {
- val absolutePath = DocumentFileCompat.buildAbsolutePath(context, targetDirectory.getStorageId(context), targetDirectory.getBasePath(context))
+ val absolutePath = DocumentFileCompat.buildAbsolutePath(
+ context,
+ targetDirectory.getStorageId(context),
+ targetDirectory.getBasePath(context)
+ )
val targetFolder = DocumentFileCompat.mkdirs(context, absolutePath)
if (targetFolder == null) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
return null
}
val targetFile = targetFolder.makeFile(context, fileName, mimeType, mode)
if (targetFile == null) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
} else {
return targetFile
}
} catch (e: SecurityException) {
handleSecurityException(e, callback)
} catch (e: Exception) {
- callback.uiScope.postToUi { callback.onFailed(e.toFileCallbackErrorCode()) }
+ callback.postToUiScope { callback.onFailed(e.toFileCallbackErrorCode()) }
}
return null
}
@@ -456,13 +585,13 @@ class MediaFile(context: Context, val uri: Uri) {
) {
val outputStream = targetFile.openOutputStream(context)
if (outputStream == null) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.TARGET_FILE_NOT_FOUND) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.TARGET_FILE_NOT_FOUND) }
return
}
val inputStream = openInputStream()
if (inputStream == null) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.SOURCE_FILE_NOT_FOUND) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.SOURCE_FILE_NOT_FOUND) }
outputStream.closeStreamQuietly()
return
}
@@ -487,8 +616,9 @@ class MediaFile(context: Context, val uri: Uri) {
// using timer on small file is useless. We set minimum 10MB.
if (watchProgress && srcSize > 10 * FileSize.MB) {
timer = startCoroutineTimer(repeatMillis = reportInterval) {
- val report = FileCallback.Report(bytesMoved * 100f / srcSize, bytesMoved, writeSpeed)
- callback.uiScope.postToUi { callback.onReport(report) }
+ val report =
+ FileCallback.Report(bytesMoved * 100f / srcSize, bytesMoved, writeSpeed)
+ callback.postToUiScope { callback.onReport(report) }
writeSpeed = 0
}
}
@@ -504,7 +634,13 @@ class MediaFile(context: Context, val uri: Uri) {
if (deleteSourceFileWhenComplete) {
delete()
}
- callback.uiScope.postToUi { callback.onCompleted(targetFile) }
+ callback.postToUiScope {
+ callback.onCompleted(
+ FileCallback.Result.DocumentFile(
+ targetFile
+ )
+ )
+ }
} finally {
timer?.cancel()
inputStream.closeStreamQuietly()
@@ -523,7 +659,7 @@ class MediaFile(context: Context, val uri: Uri) {
}
if (resolution == FileCallback.ConflictResolution.REPLACE) {
if (!targetFile.forceDelete(context)) {
- callback.uiScope.postToUi { callback.onFailed(FileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
+ callback.postToUiScope { callback.onFailed(FileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
return FileCallback.ConflictResolution.SKIP
}
}
diff --git a/storage/src/main/java/com/anggrayudi/storage/media/MediaFileExt.kt b/storage/src/main/kotlin/com/anggrayudi/storage/media/MediaFileExt.kt
similarity index 58%
rename from storage/src/main/java/com/anggrayudi/storage/media/MediaFileExt.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/media/MediaFileExt.kt
index 5ab23a3..e5021ac 100644
--- a/storage/src/main/java/com/anggrayudi/storage/media/MediaFileExt.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/media/MediaFileExt.kt
@@ -7,8 +7,19 @@ import androidx.annotation.WorkerThread
import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.callback.ZipCompressionCallback
import com.anggrayudi.storage.callback.ZipDecompressionCallback
-import com.anggrayudi.storage.extension.*
-import com.anggrayudi.storage.file.*
+import com.anggrayudi.storage.extension.awaitUiResult
+import com.anggrayudi.storage.extension.closeEntryQuietly
+import com.anggrayudi.storage.extension.closeStreamQuietly
+import com.anggrayudi.storage.extension.postToUi
+import com.anggrayudi.storage.extension.startCoroutineTimer
+import com.anggrayudi.storage.file.CreateMode
+import com.anggrayudi.storage.file.MimeType
+import com.anggrayudi.storage.file.findParent
+import com.anggrayudi.storage.file.fullName
+import com.anggrayudi.storage.file.isWritable
+import com.anggrayudi.storage.file.makeFile
+import com.anggrayudi.storage.file.makeFolder
+import com.anggrayudi.storage.file.openOutputStream
import kotlinx.coroutines.Job
import java.io.FileNotFoundException
import java.io.IOException
@@ -21,7 +32,6 @@ import java.util.zip.ZipOutputStream
* Created on 21/01/22
* @author Anggrayudi H
*/
-
@WorkerThread
fun List.compressToZip(
context: Context,
@@ -29,23 +39,35 @@ fun List.compressToZip(
deleteSourceWhenComplete: Boolean = false,
callback: ZipCompressionCallback
) {
- callback.uiScope.postToUi { callback.onCountingFiles() }
+ callback.uiScope.postToUi { }
+ callback.postToUiScope { callback.onCountingFiles() }
val entryFiles = distinctBy { it.uri }.filter { !it.isEmpty }
if (entryFiles.isEmpty()) {
- callback.uiScope.postToUi { callback.onFailed(ZipCompressionCallback.ErrorCode.MISSING_ENTRY_FILE, "No entry files found") }
+ callback.postToUiScope {
+ callback.onFailed(
+ ZipCompressionCallback.ErrorCode.MISSING_ENTRY_FILE,
+ "No entry files found"
+ )
+ }
return
}
var zipFile: DocumentFile? = targetZipFile
if (!targetZipFile.exists() || targetZipFile.isDirectory) {
- zipFile = targetZipFile.findParent(context)?.makeFile(context, targetZipFile.fullName, MimeType.ZIP)
+ zipFile = targetZipFile.findParent(context)
+ ?.makeFile(context, targetZipFile.fullName, MimeType.ZIP)
}
if (zipFile == null) {
- callback.uiScope.postToUi { callback.onFailed(ZipCompressionCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
+ callback.postToUiScope { callback.onFailed(ZipCompressionCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
return
}
if (!zipFile.isWritable(context)) {
- callback.uiScope.postToUi { callback.onFailed(ZipCompressionCallback.ErrorCode.STORAGE_PERMISSION_DENIED, "Destination ZIP file is not writable") }
+ callback.postToUiScope {
+ callback.onFailed(
+ ZipCompressionCallback.ErrorCode.STORAGE_PERMISSION_DENIED,
+ "Destination ZIP file is not writable"
+ )
+ }
return
}
@@ -63,8 +85,13 @@ fun List.compressToZip(
var fileCompressedCount = 0
if (reportInterval > 0) {
timer = startCoroutineTimer(repeatMillis = reportInterval) {
- val report = ZipCompressionCallback.Report(0f, bytesCompressed, writeSpeed, fileCompressedCount)
- callback.uiScope.postToUi { callback.onReport(report) }
+ val report = ZipCompressionCallback.Report(
+ 0f,
+ bytesCompressed,
+ writeSpeed,
+ fileCompressedCount
+ )
+ callback.postToUiScope { callback.onReport(report) }
writeSpeed = 0
}
}
@@ -84,17 +111,27 @@ fun List.compressToZip(
}
success = true
} catch (e: InterruptedIOException) {
- callback.uiScope.postToUi { callback.onFailed(ZipCompressionCallback.ErrorCode.CANCELED) }
+ callback.postToUiScope { callback.onFailed(ZipCompressionCallback.ErrorCode.CANCELED) }
} catch (e: FileNotFoundException) {
- callback.uiScope.postToUi { callback.onFailed(ZipCompressionCallback.ErrorCode.MISSING_ENTRY_FILE, e.message) }
+ callback.postToUiScope {
+ callback.onFailed(
+ ZipCompressionCallback.ErrorCode.MISSING_ENTRY_FILE,
+ e.message
+ )
+ }
} catch (e: IOException) {
if (e.message?.contains("no space", true) == true) {
- callback.uiScope.postToUi { callback.onFailed(ZipCompressionCallback.ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH) }
+ callback.postToUiScope { callback.onFailed(ZipCompressionCallback.ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH) }
} else {
- callback.uiScope.postToUi { callback.onFailed(ZipCompressionCallback.ErrorCode.UNKNOWN_IO_ERROR) }
+ callback.postToUiScope { callback.onFailed(ZipCompressionCallback.ErrorCode.UNKNOWN_IO_ERROR) }
}
} catch (e: SecurityException) {
- callback.uiScope.postToUi { callback.onFailed(ZipCompressionCallback.ErrorCode.STORAGE_PERMISSION_DENIED, e.message) }
+ callback.postToUiScope {
+ callback.onFailed(
+ ZipCompressionCallback.ErrorCode.STORAGE_PERMISSION_DENIED,
+ e.message
+ )
+ }
} finally {
timer?.cancel()
zos.closeEntryQuietly()
@@ -102,11 +139,18 @@ fun List.compressToZip(
}
if (success) {
if (deleteSourceWhenComplete) {
- callback.uiScope.postToUi { callback.onDeleteEntryFiles() }
+ callback.postToUiScope { callback.onDeleteEntryFiles() }
forEach { it.delete() }
}
val sizeReduction = (bytesCompressed - zipFile.length()).toFloat() / bytesCompressed * 100
- callback.uiScope.postToUi { callback.onCompleted(zipFile, bytesCompressed, entryFiles.size, sizeReduction) }
+ callback.postToUiScope {
+ callback.onCompleted(
+ zipFile,
+ bytesCompressed,
+ entryFiles.size,
+ sizeReduction
+ )
+ }
} else {
zipFile.delete()
}
@@ -118,13 +162,13 @@ fun MediaFile.decompressZip(
targetFolder: DocumentFile,
callback: ZipDecompressionCallback
) {
- callback.uiScope.postToUi { callback.onValidate() }
+ callback.postToUiScope { callback.onValidate() }
if (isEmpty) {
- callback.uiScope.postToUi { callback.onFailed(ZipDecompressionCallback.ErrorCode.MISSING_ZIP_FILE) }
+ callback.postToUiScope { callback.onFailed(ZipDecompressionCallback.ErrorCode.MISSING_ZIP_FILE) }
return
}
if (mimeType != MimeType.ZIP) {
- callback.uiScope.postToUi { callback.onFailed(ZipDecompressionCallback.ErrorCode.NOT_A_ZIP_FILE) }
+ callback.postToUiScope { callback.onFailed(ZipDecompressionCallback.ErrorCode.NOT_A_ZIP_FILE) }
return
}
@@ -133,7 +177,7 @@ fun MediaFile.decompressZip(
destFolder = targetFolder.findParent(context)?.makeFolder(context, targetFolder.fullName)
}
if (destFolder == null || !destFolder.isWritable(context)) {
- callback.uiScope.postToUi { callback.onFailed(ZipDecompressionCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
+ callback.postToUiScope { callback.onFailed(ZipDecompressionCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
return
}
@@ -153,8 +197,12 @@ fun MediaFile.decompressZip(
var writeSpeed = 0
if (reportInterval > 0) {
timer = startCoroutineTimer(repeatMillis = reportInterval) {
- val report = ZipDecompressionCallback.Report(bytesDecompressed, writeSpeed, fileDecompressedCount)
- callback.uiScope.postToUi { callback.onReport(report) }
+ val report = ZipDecompressionCallback.Report(
+ bytesDecompressed,
+ writeSpeed,
+ fileDecompressedCount
+ )
+ callback.postToUiScope { callback.onReport(report) }
writeSpeed = 0
}
}
@@ -166,12 +214,16 @@ fun MediaFile.decompressZip(
destFolder.makeFolder(context, entry.name, CreateMode.REUSE)
} else {
val folder = entry.name.substringBeforeLast('/', "").let {
- if (it.isEmpty()) destFolder else destFolder.makeFolder(context, it, CreateMode.REUSE)
+ if (it.isEmpty()) destFolder else destFolder.makeFolder(
+ context,
+ it,
+ CreateMode.REUSE
+ )
} ?: throw IOException()
val fileName = entry.name.substringAfterLast('/')
targetFile = folder.makeFile(context, fileName)
if (targetFile == null) {
- callback.uiScope.postToUi { callback.onFailed(ZipDecompressionCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
+ callback.postToUiScope { callback.onFailed(ZipDecompressionCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) }
canSuccess = false
break
}
@@ -196,25 +248,30 @@ fun MediaFile.decompressZip(
}
success = canSuccess
} catch (e: InterruptedIOException) {
- callback.uiScope.postToUi { callback.onFailed(ZipDecompressionCallback.ErrorCode.CANCELED) }
+ callback.postToUiScope { callback.onFailed(ZipDecompressionCallback.ErrorCode.CANCELED) }
} catch (e: FileNotFoundException) {
- callback.uiScope.postToUi { callback.onFailed(ZipDecompressionCallback.ErrorCode.MISSING_ZIP_FILE) }
+ callback.postToUiScope { callback.onFailed(ZipDecompressionCallback.ErrorCode.MISSING_ZIP_FILE) }
} catch (e: IOException) {
if (e.message?.contains("no space", true) == true) {
- callback.uiScope.postToUi { callback.onFailed(ZipDecompressionCallback.ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH) }
+ callback.postToUiScope { callback.onFailed(ZipDecompressionCallback.ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH) }
} else {
- callback.uiScope.postToUi { callback.onFailed(ZipDecompressionCallback.ErrorCode.UNKNOWN_IO_ERROR) }
+ callback.postToUiScope { callback.onFailed(ZipDecompressionCallback.ErrorCode.UNKNOWN_IO_ERROR) }
}
} catch (e: SecurityException) {
- callback.uiScope.postToUi { callback.onFailed(ZipDecompressionCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
+ callback.postToUiScope { callback.onFailed(ZipDecompressionCallback.ErrorCode.STORAGE_PERMISSION_DENIED) }
} finally {
timer?.cancel()
zis.closeEntryQuietly()
zis.closeStreamQuietly()
}
if (success) {
- val info = ZipDecompressionCallback.DecompressionInfo(bytesDecompressed, skippedDecompressedBytes, fileDecompressedCount, 0f)
- callback.uiScope.postToUi { callback.onCompleted(this, destFolder, info) }
+ val info = ZipDecompressionCallback.DecompressionInfo(
+ bytesDecompressed,
+ skippedDecompressedBytes,
+ fileDecompressedCount,
+ 0f
+ )
+ callback.postToUiScope { callback.onCompleted(this, destFolder, info) }
} else {
targetFile?.delete()
}
diff --git a/storage/src/main/java/com/anggrayudi/storage/media/MediaStoreCompat.kt b/storage/src/main/kotlin/com/anggrayudi/storage/media/MediaStoreCompat.kt
similarity index 68%
rename from storage/src/main/java/com/anggrayudi/storage/media/MediaStoreCompat.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/media/MediaStoreCompat.kt
index 03b9944..055c955 100644
--- a/storage/src/main/java/com/anggrayudi/storage/media/MediaStoreCompat.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/media/MediaStoreCompat.kt
@@ -13,8 +13,18 @@ import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.extension.getString
import com.anggrayudi.storage.extension.trimFileName
import com.anggrayudi.storage.extension.trimFileSeparator
-import com.anggrayudi.storage.file.*
+import com.anggrayudi.storage.file.CreateMode
+import com.anggrayudi.storage.file.DocumentFileCompat
import com.anggrayudi.storage.file.DocumentFileCompat.removeForbiddenCharsFromFilename
+import com.anggrayudi.storage.file.DocumentFileType
+import com.anggrayudi.storage.file.MimeType
+import com.anggrayudi.storage.file.PublicDirectory
+import com.anggrayudi.storage.file.autoIncrementFileName
+import com.anggrayudi.storage.file.canModify
+import com.anggrayudi.storage.file.child
+import com.anggrayudi.storage.file.createNewFileIfPossible
+import com.anggrayudi.storage.file.recreateFile
+import com.anggrayudi.storage.file.search
import java.io.File
/**
@@ -30,8 +40,18 @@ object MediaStoreCompat {
@JvmStatic
@JvmOverloads
- fun createDownload(context: Context, file: FileDescription, mode: CreateMode = CreateMode.CREATE_NEW): MediaFile? {
- return createMedia(context, MediaType.DOWNLOADS, Environment.DIRECTORY_DOWNLOADS, file, mode)
+ fun createDownload(
+ context: Context,
+ file: FileDescription,
+ mode: CreateMode = CreateMode.CREATE_NEW
+ ): MediaFile? {
+ return createMedia(
+ context,
+ MediaType.DOWNLOADS,
+ Environment.DIRECTORY_DOWNLOADS,
+ file,
+ mode
+ )
}
@JvmOverloads
@@ -39,7 +59,7 @@ object MediaStoreCompat {
fun createImage(
context: Context,
file: FileDescription,
- relativeParentDirectory: ImageMediaDirectory = ImageMediaDirectory.PICTURES,
+ relativeParentDirectory: MediaDirectory.Image = MediaDirectory.Image.PICTURES,
mode: CreateMode = CreateMode.CREATE_NEW
): MediaFile? {
return createMedia(context, MediaType.IMAGE, relativeParentDirectory.folderName, file, mode)
@@ -50,7 +70,7 @@ object MediaStoreCompat {
fun createAudio(
context: Context,
file: FileDescription,
- relativeParentDirectory: AudioMediaDirectory = AudioMediaDirectory.MUSIC,
+ relativeParentDirectory: MediaDirectory.Audio = MediaDirectory.Audio.MUSIC,
mode: CreateMode = CreateMode.CREATE_NEW
): MediaFile? {
return createMedia(context, MediaType.AUDIO, relativeParentDirectory.folderName, file, mode)
@@ -61,7 +81,7 @@ object MediaStoreCompat {
fun createVideo(
context: Context,
file: FileDescription,
- relativeParentDirectory: VideoMediaDirectory = VideoMediaDirectory.MOVIES,
+ relativeParentDirectory: MediaDirectory.Video = MediaDirectory.Video.MOVIES,
mode: CreateMode = CreateMode.CREATE_NEW
): MediaFile? {
return createMedia(context, MediaType.VIDEO, relativeParentDirectory.folderName, file, mode)
@@ -69,7 +89,12 @@ object MediaStoreCompat {
@JvmStatic
@JvmOverloads
- fun createMedia(context: Context, fullPath: String, file: FileDescription, mode: CreateMode = CreateMode.CREATE_NEW): MediaFile? {
+ fun createMedia(
+ context: Context,
+ fullPath: String,
+ file: FileDescription,
+ mode: CreateMode = CreateMode.CREATE_NEW
+ ): MediaFile? {
val basePath = DocumentFileCompat.getBasePath(context, fullPath).trimFileSeparator()
if (basePath.isEmpty()) {
return null
@@ -77,9 +102,9 @@ object MediaStoreCompat {
val mediaFolder = basePath.substringBefore('/')
val mediaType = when (mediaFolder) {
Environment.DIRECTORY_DOWNLOADS -> MediaType.DOWNLOADS
- in ImageMediaDirectory.values().map { it.folderName } -> MediaType.IMAGE
- in AudioMediaDirectory.values().map { it.folderName } -> MediaType.AUDIO
- in VideoMediaDirectory.values().map { it.folderName } -> MediaType.VIDEO
+ in MediaDirectory.Image.values().map { it.folderName } -> MediaType.IMAGE
+ in MediaDirectory.Audio.values().map { it.folderName } -> MediaType.AUDIO
+ in MediaDirectory.Video.values().map { it.folderName } -> MediaType.VIDEO
else -> return null
}
val subFolder = basePath.substringAfter('/', "")
@@ -87,14 +112,23 @@ object MediaStoreCompat {
return createMedia(context, mediaType, mediaFolder, file, mode)
}
- private fun createMedia(context: Context, mediaType: MediaType, folderName: String, file: FileDescription, mode: CreateMode): MediaFile? {
+ private fun createMedia(
+ context: Context,
+ mediaType: MediaType,
+ folderName: String,
+ file: FileDescription,
+ mode: CreateMode
+ ): MediaFile? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val fullName = file.fullName
val mimeType = file.mimeType
val baseName = MimeType.getBaseFileName(fullName)
val ext = MimeType.getExtensionFromFileName(fullName)
val contentValues = ContentValues().apply {
- put(MediaStore.MediaColumns.DISPLAY_NAME, if (mimeType == MimeType.BINARY_FILE) fullName else baseName)
+ put(
+ MediaStore.MediaColumns.DISPLAY_NAME,
+ if (mimeType == MimeType.BINARY_FILE) fullName else baseName
+ )
put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
val dateCreated = System.currentTimeMillis()
put(MediaStore.MediaColumns.DATE_ADDED, dateCreated)
@@ -123,14 +157,22 @@ object MediaStoreCompat {
// Android R+ already has this check, thus no need to check empty media files for reuse
val prefix = "$baseName ("
fromFileNameContains(context, mediaType, baseName).asSequence()
- .filter { relativePath.isBlank() || relativePath == it.relativePath.removeSuffix("/") }
+ .filter {
+ relativePath.isBlank() || relativePath == it.relativePath.removeSuffix(
+ "/"
+ )
+ }
.filter {
val name = it.name
if (name.isNullOrEmpty() || MimeType.getExtensionFromFileName(name) != ext)
false
else {
- name.startsWith(prefix) && (DocumentFileCompat.FILE_NAME_DUPLICATION_REGEX_WITH_EXTENSION.matches(name)
- || DocumentFileCompat.FILE_NAME_DUPLICATION_REGEX_WITHOUT_EXTENSION.matches(name))
+ name.startsWith(prefix) && (DocumentFileCompat.FILE_NAME_DUPLICATION_REGEX_WITH_EXTENSION.matches(
+ name
+ )
+ || DocumentFileCompat.FILE_NAME_DUPLICATION_REGEX_WITHOUT_EXTENSION.matches(
+ name
+ ))
}
}
// Use existing empty media file
@@ -140,10 +182,10 @@ object MediaStoreCompat {
tryInsertMediaFile(context, mediaType, contentValues)
}
+
else -> tryInsertMediaFile(context, mediaType, contentValues)
}
} else {
- @Suppress("DEPRECATION")
val publicDirectory = Environment.getExternalStoragePublicDirectory(folderName)
if (publicDirectory.canModify(context)) {
val filename = file.fullName
@@ -165,9 +207,16 @@ object MediaStoreCompat {
}
}
- private fun tryInsertMediaFile(context: Context, mediaType: MediaType, contentValues: ContentValues): MediaFile? {
+ private fun tryInsertMediaFile(
+ context: Context,
+ mediaType: MediaType,
+ contentValues: ContentValues
+ ): MediaFile? {
return try {
- MediaFile(context, context.contentResolver.insert(mediaType.writeUri!!, contentValues) ?: return null)
+ MediaFile(
+ context,
+ context.contentResolver.insert(mediaType.writeUri!!, contentValues) ?: return null
+ )
} catch (e: Exception) {
e.printStackTrace()
null
@@ -203,13 +252,18 @@ object MediaStoreCompat {
@JvmStatic
fun fromFileName(context: Context, mediaType: MediaType, name: String): MediaFile? {
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
- @Suppress("DEPRECATION")
File(PublicDirectory.DOWNLOADS.file, name).let {
if (it.isFile && it.canRead()) MediaFile(context, it) else null
}
} else {
val selection = "${MediaStore.MediaColumns.DISPLAY_NAME} = ?"
- context.contentResolver.query(mediaType.readUri ?: return null, arrayOf(BaseColumns._ID), selection, arrayOf(name), null)?.use {
+ context.contentResolver.query(
+ mediaType.readUri ?: return null,
+ arrayOf(BaseColumns._ID),
+ selection,
+ arrayOf(name),
+ null
+ )?.use {
fromCursorToMediaFile(context, mediaType, it)
}
}
@@ -223,16 +277,25 @@ object MediaStoreCompat {
fun fromBasePath(context: Context, mediaType: MediaType, basePath: String): MediaFile? {
val cleanBasePath = basePath.removeForbiddenCharsFromFilename().trimFileSeparator()
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
- @Suppress("DEPRECATION")
- File(Environment.getExternalStorageDirectory(), cleanBasePath).let { if (it.isFile && it.canRead()) MediaFile(context, it) else null }
+ File(
+ Environment.getExternalStorageDirectory(),
+ cleanBasePath
+ ).let { if (it.isFile && it.canRead()) MediaFile(context, it) else null }
} else {
val relativePath = cleanBasePath.substringBeforeLast('/', "")
if (relativePath.isEmpty()) {
return null
}
val filename = cleanBasePath.substringAfterLast('/')
- val selection = "${MediaStore.MediaColumns.DISPLAY_NAME} = ? AND ${MediaStore.MediaColumns.RELATIVE_PATH} = ?"
- context.contentResolver.query(mediaType.readUri ?: return null, arrayOf(BaseColumns._ID), selection, arrayOf(filename, "$relativePath/"), null)
+ val selection =
+ "${MediaStore.MediaColumns.DISPLAY_NAME} = ? AND ${MediaStore.MediaColumns.RELATIVE_PATH} = ?"
+ context.contentResolver.query(
+ mediaType.readUri ?: return null,
+ arrayOf(BaseColumns._ID),
+ selection,
+ arrayOf(filename, "$relativePath/"),
+ null
+ )
?.use { fromCursorToMediaFile(context, mediaType, it) }
}
}
@@ -243,6 +306,7 @@ object MediaStoreCompat {
Environment.DIRECTORY_MOVIES, Environment.DIRECTORY_DCIM -> MediaType.VIDEO
Environment.DIRECTORY_MUSIC, Environment.DIRECTORY_PODCASTS, Environment.DIRECTORY_RINGTONES,
Environment.DIRECTORY_ALARMS, Environment.DIRECTORY_NOTIFICATIONS -> MediaType.AUDIO
+
Environment.DIRECTORY_DOWNLOADS -> MediaType.DOWNLOADS
else -> null
}
@@ -251,7 +315,8 @@ object MediaStoreCompat {
* @see MediaStore.MediaColumns.RELATIVE_PATH
*/
@JvmStatic
- fun fromRelativePath(context: Context, publicDirectory: PublicDirectory) = fromRelativePath(context, publicDirectory.folderName)
+ fun fromRelativePath(context: Context, publicDirectory: PublicDirectory) =
+ fromRelativePath(context, publicDirectory.folderName)
/**
* @see MediaStore.MediaColumns.RELATIVE_PATH
@@ -260,8 +325,12 @@ object MediaStoreCompat {
fun fromRelativePath(context: Context, relativePath: String): List {
val cleanRelativePath = relativePath.trimFileSeparator()
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
- @Suppress("DEPRECATION")
- DocumentFile.fromFile(File(Environment.getExternalStorageDirectory(), cleanRelativePath))
+ DocumentFile.fromFile(
+ File(
+ Environment.getExternalStorageDirectory(),
+ cleanRelativePath
+ )
+ )
.search(true, DocumentFileType.FILE)
.map { MediaFile(context, File(it.uri.path!!)) }
} else {
@@ -269,7 +338,13 @@ object MediaStoreCompat {
val relativePathWithSlashSuffix = relativePath.trimEnd('/') + '/'
val selection = "${MediaStore.MediaColumns.RELATIVE_PATH} IN(?, ?)"
val selectionArgs = arrayOf(relativePathWithSlashSuffix, cleanRelativePath)
- return context.contentResolver.query(mediaType.readUri ?: return emptyList(), arrayOf(BaseColumns._ID), selection, selectionArgs, null)?.use {
+ return context.contentResolver.query(
+ mediaType.readUri ?: return emptyList(),
+ arrayOf(BaseColumns._ID),
+ selection,
+ selectionArgs,
+ null
+ )?.use {
fromCursorToMediaFiles(context, mediaType, it)
}.orEmpty()
}
@@ -282,34 +357,58 @@ object MediaStoreCompat {
fun fromRelativePath(context: Context, relativePath: String, name: String): MediaFile? {
val cleanRelativePath = relativePath.trimFileSeparator()
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
- @Suppress("DEPRECATION")
- DocumentFile.fromFile(File(Environment.getExternalStorageDirectory(), cleanRelativePath))
+ DocumentFile.fromFile(
+ File(
+ Environment.getExternalStorageDirectory(),
+ cleanRelativePath
+ )
+ )
.search(true, DocumentFileType.FILE, name = name)
.map { MediaFile(context, File(it.uri.path!!)) }
.firstOrNull()
} else {
val mediaType = mediaTypeFromRelativePath(cleanRelativePath) ?: return null
val relativePathWithSlashSuffix = relativePath.trimEnd('/') + '/'
- val selection = "${MediaStore.MediaColumns.DISPLAY_NAME} = ? AND ${MediaStore.MediaColumns.RELATIVE_PATH} IN(?, ?)"
+ val selection =
+ "${MediaStore.MediaColumns.DISPLAY_NAME} = ? AND ${MediaStore.MediaColumns.RELATIVE_PATH} IN(?, ?)"
val selectionArgs = arrayOf(name, relativePathWithSlashSuffix, cleanRelativePath)
- return context.contentResolver.query(mediaType.readUri ?: return null, arrayOf(BaseColumns._ID), selection, selectionArgs, null)?.use {
+ return context.contentResolver.query(
+ mediaType.readUri ?: return null,
+ arrayOf(BaseColumns._ID),
+ selection,
+ selectionArgs,
+ null
+ )?.use {
fromCursorToMediaFile(context, mediaType, it)
}
}
}
@JvmStatic
- fun fromFileNameContains(context: Context, mediaType: MediaType, containsName: String): List {
+ fun fromFileNameContains(
+ context: Context,
+ mediaType: MediaType,
+ containsName: String
+ ): List {
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
mediaType.directories.map { directory ->
- @Suppress("DEPRECATION")
DocumentFile.fromFile(directory)
- .search(true, regex = Regex("^.*$containsName.*\$"), mimeTypes = arrayOf(mediaType.mimeType))
+ .search(
+ true,
+ regex = Regex("^.*$containsName.*\$"),
+ mimeTypes = arrayOf(mediaType.mimeType)
+ )
.map { MediaFile(context, File(it.uri.path!!)) }
}.flatten()
} else {
val selection = "${MediaStore.MediaColumns.DISPLAY_NAME} LIKE '%$containsName%'"
- return context.contentResolver.query(mediaType.readUri ?: return emptyList(), arrayOf(BaseColumns._ID), selection, null, null)?.use {
+ return context.contentResolver.query(
+ mediaType.readUri ?: return emptyList(),
+ arrayOf(BaseColumns._ID),
+ selection,
+ null,
+ null
+ )?.use {
fromCursorToMediaFiles(context, mediaType, it)
}.orEmpty()
}
@@ -319,14 +418,19 @@ object MediaStoreCompat {
fun fromMimeType(context: Context, mediaType: MediaType, mimeType: String): List {
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
mediaType.directories.map { directory ->
- @Suppress("DEPRECATION")
DocumentFile.fromFile(directory)
.search(true, DocumentFileType.FILE, arrayOf(mimeType))
.map { MediaFile(context, File(it.uri.path!!)) }
}.flatten()
} else {
val selection = "${MediaStore.MediaColumns.MIME_TYPE} = ?"
- return context.contentResolver.query(mediaType.readUri ?: return emptyList(), arrayOf(BaseColumns._ID), selection, arrayOf(mimeType), null)?.use {
+ return context.contentResolver.query(
+ mediaType.readUri ?: return emptyList(),
+ arrayOf(BaseColumns._ID),
+ selection,
+ arrayOf(mimeType),
+ null
+ )?.use {
fromCursorToMediaFiles(context, mediaType, it)
}.orEmpty()
}
@@ -336,19 +440,28 @@ object MediaStoreCompat {
fun fromMediaType(context: Context, mediaType: MediaType): List {
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
mediaType.directories.map { directory ->
- @Suppress("DEPRECATION")
DocumentFile.fromFile(directory)
.search(true, mimeTypes = arrayOf(mediaType.mimeType))
.map { MediaFile(context, File(it.uri.path!!)) }
}.flatten()
} else {
- return context.contentResolver.query(mediaType.readUri ?: return emptyList(), arrayOf(BaseColumns._ID), null, null, null)?.use {
+ return context.contentResolver.query(
+ mediaType.readUri ?: return emptyList(),
+ arrayOf(BaseColumns._ID),
+ null,
+ null,
+ null
+ )?.use {
fromCursorToMediaFiles(context, mediaType, it)
}.orEmpty()
}
}
- private fun fromCursorToMediaFiles(context: Context, mediaType: MediaType, cursor: Cursor): List {
+ private fun fromCursorToMediaFiles(
+ context: Context,
+ mediaType: MediaType,
+ cursor: Cursor
+ ): List {
if (cursor.moveToFirst()) {
val mediaFiles = ArrayList(cursor.count)
do {
@@ -361,7 +474,11 @@ object MediaStoreCompat {
return emptyList()
}
- private fun fromCursorToMediaFile(context: Context, mediaType: MediaType, cursor: Cursor): MediaFile? {
+ private fun fromCursorToMediaFile(
+ context: Context,
+ mediaType: MediaType,
+ cursor: Cursor
+ ): MediaFile? {
return if (cursor.moveToFirst()) {
cursor.getString(BaseColumns._ID)?.let { fromMediaId(context, mediaType, it) }
} else null
diff --git a/storage/src/main/kotlin/com/anggrayudi/storage/media/MediaType.kt b/storage/src/main/kotlin/com/anggrayudi/storage/media/MediaType.kt
new file mode 100644
index 0000000..f0578c9
--- /dev/null
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/media/MediaType.kt
@@ -0,0 +1,55 @@
+package com.anggrayudi.storage.media
+
+import android.net.Uri
+import android.os.Build
+import android.os.Environment
+import android.provider.MediaStore
+import com.anggrayudi.storage.file.MimeType
+import com.anggrayudi.storage.file.PublicDirectory
+import java.io.File
+
+/**
+ * Created on 06/09/20
+ * @author Anggrayudi H
+ */
+enum class MediaType(val readUri: Uri?, val writeUri: Uri?, val mimeType: String) {
+ IMAGE(
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ MediaStore.Images.Media.getContentUri(MediaStoreCompat.volumeName),
+ MimeType.IMAGE
+ ),
+ AUDIO(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ MediaStore.Audio.Media.getContentUri(MediaStoreCompat.volumeName),
+ MimeType.AUDIO
+ ),
+ VIDEO(
+ MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
+ MediaStore.Video.Media.getContentUri(MediaStoreCompat.volumeName),
+ MimeType.VIDEO
+ ),
+ DOWNLOADS(
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) null else MediaStore.Downloads.EXTERNAL_CONTENT_URI,
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) null else MediaStore.Downloads.getContentUri(
+ MediaStoreCompat.volumeName
+ ),
+ MimeType.UNKNOWN
+ );
+
+ /**
+ * Directories associated with this media type.
+ */
+ val directories: List
+ get() = when (this) {
+ IMAGE -> MediaDirectory.Image.values()
+ .map { Environment.getExternalStoragePublicDirectory(it.folderName) }
+
+ AUDIO -> MediaDirectory.Audio.values()
+ .map { Environment.getExternalStoragePublicDirectory(it.folderName) }
+
+ VIDEO -> MediaDirectory.Video.values()
+ .map { Environment.getExternalStoragePublicDirectory(it.folderName) }
+
+ DOWNLOADS -> listOf(PublicDirectory.DOWNLOADS.file)
+ }
+}
\ No newline at end of file
diff --git a/storage/src/main/java/com/anggrayudi/storage/permission/ActivityPermissionRequest.kt b/storage/src/main/kotlin/com/anggrayudi/storage/permission/ActivityPermissionRequest.kt
similarity index 73%
rename from storage/src/main/java/com/anggrayudi/storage/permission/ActivityPermissionRequest.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/permission/ActivityPermissionRequest.kt
index c96833f..3f06faf 100644
--- a/storage/src/main/java/com/anggrayudi/storage/permission/ActivityPermissionRequest.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/permission/ActivityPermissionRequest.kt
@@ -18,12 +18,19 @@ class ActivityPermissionRequest private constructor(
private val callback: PermissionCallback
) : PermissionRequest {
- private val launcher = if (activity is ComponentActivity) activity.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
+ private val launcher = if (activity is ComponentActivity) activity.registerForActivityResult(
+ ActivityResultContracts.RequestMultiplePermissions()
+ ) {
onRequestPermissionsResult(it)
} else null
override fun check() {
- if (permissions.all { ContextCompat.checkSelfPermission(activity, it) == PackageManager.PERMISSION_GRANTED }) {
+ if (permissions.all {
+ ContextCompat.checkSelfPermission(
+ activity,
+ it
+ ) == PackageManager.PERMISSION_GRANTED
+ }) {
callback.onPermissionsChecked(
PermissionResult(permissions.map {
PermissionReport(it, isGranted = true, deniedPermanently = false)
@@ -49,14 +56,25 @@ class ActivityPermissionRequest private constructor(
val reports = permissions.mapIndexed { index, permission ->
val isGranted = grantResults[index] == PackageManager.PERMISSION_GRANTED
- PermissionReport(permission, isGranted, !isGranted && !ActivityCompat.shouldShowRequestPermissionRationale(activity, permission))
+ PermissionReport(
+ permission,
+ isGranted,
+ !isGranted && !ActivityCompat.shouldShowRequestPermissionRationale(
+ activity,
+ permission
+ )
+ )
}
reportResult(reports)
}
private fun onRequestPermissionsResult(result: Map) {
val reports = result.map {
- PermissionReport(it.key, it.value, !it.value && !ActivityCompat.shouldShowRequestPermissionRationale(activity, it.key))
+ PermissionReport(
+ it.key,
+ it.value,
+ !it.value && !ActivityCompat.shouldShowRequestPermissionRationale(activity, it.key)
+ )
}
reportResult(reports)
}
@@ -80,11 +98,20 @@ class ActivityPermissionRequest private constructor(
*/
override fun continueToPermissionRequest() {
permissions.forEach {
- if (ContextCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED) {
+ if (ContextCompat.checkSelfPermission(
+ activity,
+ it
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
if (launcher != null) {
launcher.launch(permissions)
} else {
- ActivityCompat.requestPermissions(activity, permissions, requestCode ?: throw IllegalStateException("Request code hasn't been set yet"))
+ ActivityCompat.requestPermissions(
+ activity,
+ permissions,
+ requestCode
+ ?: throw IllegalStateException("Request code hasn't been set yet")
+ )
}
return
}
@@ -123,7 +150,8 @@ class ActivityPermissionRequest private constructor(
this.callback = callback
}
- fun build() = ActivityPermissionRequest(activity, permissions.toTypedArray(), requestCode, callback!!)
+ fun build() =
+ ActivityPermissionRequest(activity, permissions.toTypedArray(), requestCode, callback!!)
fun check() = build().check()
}
diff --git a/storage/src/main/java/com/anggrayudi/storage/permission/FragmentPermissionRequest.kt b/storage/src/main/kotlin/com/anggrayudi/storage/permission/FragmentPermissionRequest.kt
similarity index 76%
rename from storage/src/main/java/com/anggrayudi/storage/permission/FragmentPermissionRequest.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/permission/FragmentPermissionRequest.kt
index b5f70d8..3039f1f 100644
--- a/storage/src/main/java/com/anggrayudi/storage/permission/FragmentPermissionRequest.kt
+++ b/storage/src/main/kotlin/com/anggrayudi/storage/permission/FragmentPermissionRequest.kt
@@ -18,13 +18,19 @@ class FragmentPermissionRequest private constructor(
private val callback: PermissionCallback
) : PermissionRequest {
- private val launcher = fragment.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
- onRequestPermissionsResult(it)
- }
+ private val launcher =
+ fragment.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
+ onRequestPermissionsResult(it)
+ }
override fun check() {
val context = fragment.requireContext()
- if (permissions.all { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED }) {
+ if (permissions.all {
+ ContextCompat.checkSelfPermission(
+ context,
+ it
+ ) == PackageManager.PERMISSION_GRANTED
+ }) {
callback.onPermissionsChecked(
PermissionResult(permissions.map {
PermissionReport(it, isGranted = true, deniedPermanently = false)
@@ -42,7 +48,11 @@ class FragmentPermissionRequest private constructor(
}
val activity = fragment.requireActivity()
val reports = result.map {
- PermissionReport(it.key, it.value, !it.value && !ActivityCompat.shouldShowRequestPermissionRationale(activity, it.key))
+ PermissionReport(
+ it.key,
+ it.value,
+ !it.value && !ActivityCompat.shouldShowRequestPermissionRationale(activity, it.key)
+ )
}
val blockedPermissions = reports.filter { it.deniedPermanently }
if (blockedPermissions.isEmpty()) {
@@ -59,7 +69,11 @@ class FragmentPermissionRequest private constructor(
override fun continueToPermissionRequest() {
val activity = fragment.requireActivity()
permissions.forEach {
- if (ContextCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED) {
+ if (ContextCompat.checkSelfPermission(
+ activity,
+ it
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
launcher.launch(permissions, options)
return
}
@@ -91,7 +105,8 @@ class FragmentPermissionRequest private constructor(
this.options = options
}
- fun build() = FragmentPermissionRequest(fragment, permissions.toTypedArray(), options, callback!!)
+ fun build() =
+ FragmentPermissionRequest(fragment, permissions.toTypedArray(), options, callback!!)
fun check() = build().check()
}
diff --git a/storage/src/main/java/com/anggrayudi/storage/permission/PermissionCallback.kt b/storage/src/main/kotlin/com/anggrayudi/storage/permission/PermissionCallback.kt
similarity index 100%
rename from storage/src/main/java/com/anggrayudi/storage/permission/PermissionCallback.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/permission/PermissionCallback.kt
diff --git a/storage/src/main/java/com/anggrayudi/storage/permission/PermissionReport.kt b/storage/src/main/kotlin/com/anggrayudi/storage/permission/PermissionReport.kt
similarity index 100%
rename from storage/src/main/java/com/anggrayudi/storage/permission/PermissionReport.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/permission/PermissionReport.kt
diff --git a/storage/src/main/java/com/anggrayudi/storage/permission/PermissionRequest.kt b/storage/src/main/kotlin/com/anggrayudi/storage/permission/PermissionRequest.kt
similarity index 100%
rename from storage/src/main/java/com/anggrayudi/storage/permission/PermissionRequest.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/permission/PermissionRequest.kt
diff --git a/storage/src/main/java/com/anggrayudi/storage/permission/PermissionResult.kt b/storage/src/main/kotlin/com/anggrayudi/storage/permission/PermissionResult.kt
similarity index 100%
rename from storage/src/main/java/com/anggrayudi/storage/permission/PermissionResult.kt
rename to storage/src/main/kotlin/com/anggrayudi/storage/permission/PermissionResult.kt
diff --git a/storage/src/test/java/com/anggrayudi/storage/ExampleUnitTest.kt b/storage/src/test/java/com/anggrayudi/storage/ExampleUnitTest.kt
deleted file mode 100644
index 14ce55c..0000000
--- a/storage/src/test/java/com/anggrayudi/storage/ExampleUnitTest.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.anggrayudi.storage
-
-import org.junit.Test
-
-import org.junit.Assert.*
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-class ExampleUnitTest {
- @Test
- fun addition_isCorrect() {
- assertEquals(4, 2 + 2)
- }
-}
\ No newline at end of file
diff --git a/storage/src/test/java/com/anggrayudi/storage/DocumentFileCompatTest.kt b/storage/src/test/kotlin/com/anggrayudi/storage/DocumentFileCompatTest.kt
similarity index 100%
rename from storage/src/test/java/com/anggrayudi/storage/DocumentFileCompatTest.kt
rename to storage/src/test/kotlin/com/anggrayudi/storage/DocumentFileCompatTest.kt
diff --git a/storage/src/test/java/com/anggrayudi/storage/SimpleStorageTest.kt b/storage/src/test/kotlin/com/anggrayudi/storage/SimpleStorageTest.kt
similarity index 95%
rename from storage/src/test/java/com/anggrayudi/storage/SimpleStorageTest.kt
rename to storage/src/test/kotlin/com/anggrayudi/storage/SimpleStorageTest.kt
index cd950d3..cc40e6b 100644
--- a/storage/src/test/java/com/anggrayudi/storage/SimpleStorageTest.kt
+++ b/storage/src/test/kotlin/com/anggrayudi/storage/SimpleStorageTest.kt
@@ -79,7 +79,12 @@ class SimpleStorageTest {
SimpleStorage.cleanupRedundantUriPermissions(context)
assertEquals(revokedUris, capturedUris)
- verify(exactly = revokedUris.size) { resolver.releasePersistableUriPermission(any(), any()) }
+ verify(exactly = revokedUris.size) {
+ resolver.releasePersistableUriPermission(
+ any(),
+ any()
+ )
+ }
verify { resolver.persistedUriPermissions }
confirmVerified(resolver)
}
diff --git a/storage/src/test/java/com/anggrayudi/storage/extension/TextExtKtTest.kt b/storage/src/test/kotlin/com/anggrayudi/storage/extension/TextExtKtTest.kt
similarity index 80%
rename from storage/src/test/java/com/anggrayudi/storage/extension/TextExtKtTest.kt
rename to storage/src/test/kotlin/com/anggrayudi/storage/extension/TextExtKtTest.kt
index f5b4fbc..bff56ba 100644
--- a/storage/src/test/java/com/anggrayudi/storage/extension/TextExtKtTest.kt
+++ b/storage/src/test/kotlin/com/anggrayudi/storage/extension/TextExtKtTest.kt
@@ -3,7 +3,9 @@ package com.anggrayudi.storage.extension
import android.os.Environment
import io.mockk.every
import io.mockk.mockkStatic
-import org.junit.Assert.*
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import java.io.File
@@ -27,7 +29,10 @@ class TextExtKtTest {
assertEquals(6, "87jkakkubaakjnaaa".count("a"))
assertEquals(0, "87jkakku baakjnaaa".count(""))
assertEquals(0, "87jka kkubaakjnaaa".count("abc"))
- assertEquals(1, "primary:DCIM/document/primary:DCIM/document/assas/document/as".count("/document/") % 2)
+ assertEquals(
+ 1,
+ "primary:DCIM/document/primary:DCIM/document/assas/document/as".count("/document/") % 2
+ )
}
fun String.splitToPairAt(text: String, occurence: Int): Pair? {
@@ -51,14 +56,20 @@ class TextExtKtTest {
@Test
fun splitAt() {
- assertEquals(Pair("asosdisf/doc", "safsfsfaf/doc/8hhyjbh"), "asosdisf/doc/safsfsfaf/doc/8hhyjbh".splitToPairAt("/", 2))
+ assertEquals(
+ Pair("asosdisf/doc", "safsfsfaf/doc/8hhyjbh"),
+ "asosdisf/doc/safsfsfaf/doc/8hhyjbh".splitToPairAt("/", 2)
+ )
}
@Test
fun replaceCompletely() {
assertEquals("/storage/ABC//Movie/", "/storage/ABC////Movie/".replace("//", "/"))
assertEquals("/storage/ABC/Movie/", "/storage/ABC///Movie/".replaceCompletely("//", "/"))
- assertEquals("/storage/ABC/Movie/", "/storage////ABC///Movie//".replaceCompletely("//", "/"))
+ assertEquals(
+ "/storage/ABC/Movie/",
+ "/storage////ABC///Movie//".replaceCompletely("//", "/")
+ )
assertEquals("BB", "aaaaaaaaBaaaaaaBa".replaceCompletely("a", ""))
}
@@ -89,7 +100,10 @@ class TextExtKtTest {
assertEquals("/storage/AAAA-BBBB", "/storage/AAAA-BBBB/abc.txt".parent())
assertEquals("", "/storage/AAAA-BBBB".parent())
- assertEquals("/storage/emulated/0/Download", "/storage/emulated/0/Download/abc.txt".parent())
+ assertEquals(
+ "/storage/emulated/0/Download",
+ "/storage/emulated/0/Download/abc.txt".parent()
+ )
assertEquals("/storage/emulated/0", "/storage/emulated/0/abc.txt".parent())
assertEquals("", "/storage/emulated/0".parent())
}
diff --git a/storage/src/test/java/com/anggrayudi/storage/file/DocumentFileCompatTest.kt b/storage/src/test/kotlin/com/anggrayudi/storage/file/DocumentFileCompatTest.kt
similarity index 93%
rename from storage/src/test/java/com/anggrayudi/storage/file/DocumentFileCompatTest.kt
rename to storage/src/test/kotlin/com/anggrayudi/storage/file/DocumentFileCompatTest.kt
index 58b6041..34c5064 100644
--- a/storage/src/test/java/com/anggrayudi/storage/file/DocumentFileCompatTest.kt
+++ b/storage/src/test/kotlin/com/anggrayudi/storage/file/DocumentFileCompatTest.kt
@@ -38,7 +38,10 @@ class DocumentFileCompatTest {
assertEquals(PRIMARY, DocumentFileCompat.getStorageId(context, "/storage/emulated/0"))
assertEquals(PRIMARY, DocumentFileCompat.getStorageId(context, "/storage/emulated/0/Music"))
assertEquals(PRIMARY, DocumentFileCompat.getStorageId(context, "primary:Music"))
- assertEquals("AAAA-BBBB", DocumentFileCompat.getStorageId(context, "/storage/AAAA-BBBB/Music"))
+ assertEquals(
+ "AAAA-BBBB",
+ DocumentFileCompat.getStorageId(context, "/storage/AAAA-BBBB/Music")
+ )
assertEquals("AAAA-BBBB", DocumentFileCompat.getStorageId(context, "AAAA-BBBB:Music"))
}
@@ -48,7 +51,10 @@ class DocumentFileCompatTest {
assertEquals("", DocumentFileCompat.getBasePath(context, "AAAA-BBBB:"))
assertEquals("Music", DocumentFileCompat.getBasePath(context, "/storage/emulated/0/Music"))
assertEquals("Music", DocumentFileCompat.getBasePath(context, "primary:Music"))
- assertEquals("Music/Pop", DocumentFileCompat.getBasePath(context, "/storage/AAAA-BBBB//Music///Pop/"))
+ assertEquals(
+ "Music/Pop",
+ DocumentFileCompat.getBasePath(context, "/storage/AAAA-BBBB//Music///Pop/")
+ )
assertEquals("Music", DocumentFileCompat.getBasePath(context, "AAAA-BBBB:Music"))
}
diff --git a/storage/src/test/java/com/anggrayudi/storage/file/FileExtKtTest.kt b/storage/src/test/kotlin/com/anggrayudi/storage/file/FileExtKtTest.kt
similarity index 95%
rename from storage/src/test/java/com/anggrayudi/storage/file/FileExtKtTest.kt
rename to storage/src/test/kotlin/com/anggrayudi/storage/file/FileExtKtTest.kt
index 51f4774..9542254 100644
--- a/storage/src/test/java/com/anggrayudi/storage/file/FileExtKtTest.kt
+++ b/storage/src/test/kotlin/com/anggrayudi/storage/file/FileExtKtTest.kt
@@ -92,8 +92,12 @@ class FileExtKtTest {
val ext = MimeType.getExtensionFromFileName(filename)
val prefix = "$baseName ("
var lastFileCount = list().orEmpty().filter {
- it.startsWith(prefix) && (DocumentFileCompat.FILE_NAME_DUPLICATION_REGEX_WITH_EXTENSION.matches(it)
- || DocumentFileCompat.FILE_NAME_DUPLICATION_REGEX_WITHOUT_EXTENSION.matches(it))
+ it.startsWith(prefix) && (DocumentFileCompat.FILE_NAME_DUPLICATION_REGEX_WITH_EXTENSION.matches(
+ it
+ )
+ || DocumentFileCompat.FILE_NAME_DUPLICATION_REGEX_WITHOUT_EXTENSION.matches(
+ it
+ ))
}.maxOfOrNull {
it.substringAfterLast('(', "")
.substringBefore(')', "")
diff --git a/storage/src/test/java/com/anggrayudi/storage/file/MimeTypeTest.kt b/storage/src/test/kotlin/com/anggrayudi/storage/file/MimeTypeTest.kt
similarity index 100%
rename from storage/src/test/java/com/anggrayudi/storage/file/MimeTypeTest.kt
rename to storage/src/test/kotlin/com/anggrayudi/storage/file/MimeTypeTest.kt
diff --git a/versions.gradle b/versions.gradle
index 9caa4ec..ad19ee9 100644
--- a/versions.gradle
+++ b/versions.gradle
@@ -7,7 +7,7 @@ def versions = [:]
versions.activity = "1.6.0"
versions.appcompat = "1.5.1"
versions.core_ktx = "1.9.0"
-versions.coroutines = "1.6.4"
+versions.coroutines = "1.8.1"
versions.documentfile = "1.0.1"
versions.fragment = "1.5.3"
versions.junit = "5.9.1"
@@ -31,9 +31,7 @@ deps.multidex = "androidx.multidex:multidex:$versions.multidex"
deps.documentfile = "androidx.documentfile:documentfile:$versions.documentfile"
def coroutines = [:]
-coroutines.core = "org.jetbrains.kotlinx:kotlinx-coroutines-core:$versions.coroutines"
coroutines.android = "org.jetbrains.kotlinx:kotlinx-coroutines-android:$versions.coroutines"
-coroutines.test = "org.jetbrains.kotlinx:kotlinx-coroutines-test:$versions.coroutines"
deps.coroutines = coroutines
// Testing ------------------------------
@@ -42,14 +40,14 @@ deps.robolectric = "org.robolectric:robolectric:$versions.robolectric"
deps.mockk = "io.mockk:mockk:$versions.mockk"
// Others -------------------------------
-deps.material_dialogs = "com.afollestad.material-dialogs:core:$versions.material_dialogs"
+deps.material_dialogs_files = "com.afollestad.material-dialogs:files:$versions.material_dialogs"
deps.material_progressbar = "me.zhanghai.android.materialprogressbar:library:$versions.material_progressbar"
deps.timber = "com.jakewharton.timber:timber:$versions.timber"
// End of dependencies ------------------
ext.deps = deps
-def addRepos(RepositoryHandler handler) {
+static def addRepos(RepositoryHandler handler) {
handler.google()
handler.mavenCentral()
handler.maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }