├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── scripts │ ├── copy_env_variables.sh │ └── deploy_snapshot.sh └── workflows │ └── android.yml ├── .gitignore ├── .run ├── publishToCentral.run.xml └── publishToLocal.run.xml ├── FAQ.md ├── JAVA_COMPATIBILITY.md ├── LICENSE ├── README.md ├── art ├── folder-content-conflict.png ├── getAccessibleAbsolutePaths.png ├── parent-folder-conflict.png └── terminology.png ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── sample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── anggrayudi │ │ │ └── storage │ │ │ └── sample │ │ │ ├── App.kt │ │ │ ├── StorageInfoAdapter.kt │ │ │ ├── activity │ │ │ ├── BaseActivity.kt │ │ │ ├── FileCompressionActivity.kt │ │ │ ├── FileDecompressionActivity.kt │ │ │ ├── JavaActivity.java │ │ │ ├── MainActivity.kt │ │ │ ├── SampleFragmentActivity.kt │ │ │ └── SettingsActivity.kt │ │ │ └── fragment │ │ │ ├── SampleFragment.kt │ │ │ └── SettingsFragment.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ └── outline_folder_24.xml │ │ ├── layout │ │ ├── activity_file_compression.xml │ │ ├── activity_file_decompression.xml │ │ ├── activity_main.xml │ │ ├── activity_sample_fragment.xml │ │ ├── dialog_copy_progress.xml │ │ ├── incl_base_operation.xml │ │ ├── view_divider.xml │ │ ├── view_file_picked.xml │ │ └── view_item_storage_info.xml │ │ ├── menu │ │ └── main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ ├── preferences.xml │ │ └── provider_paths.xml │ └── test │ └── java │ └── com │ └── anggrayudi │ └── storage │ └── sample │ └── ExampleUnitTest.kt ├── settings.gradle ├── storage ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── anggrayudi │ │ │ └── storage │ │ │ ├── ActivityWrapper.kt │ │ │ ├── ComponentActivityWrapper.kt │ │ │ ├── ComponentWrapper.kt │ │ │ ├── FileWrapper.kt │ │ │ ├── FragmentWrapper.kt │ │ │ ├── SimpleStorage.kt │ │ │ ├── SimpleStorageHelper.kt │ │ │ ├── callback │ │ │ ├── CreateFileCallback.kt │ │ │ ├── FilePickerCallback.kt │ │ │ ├── FileReceiverCallback.kt │ │ │ ├── FolderPickerCallback.kt │ │ │ ├── MultipleFilesConflictCallback.kt │ │ │ ├── SingleFileConflictCallback.kt │ │ │ ├── SingleFolderConflictCallback.kt │ │ │ └── StorageAccessCallback.kt │ │ │ ├── extension │ │ │ ├── ContextExt.kt │ │ │ ├── CoroutineExt.kt │ │ │ ├── IOExt.kt │ │ │ ├── PrimitivesExt.kt │ │ │ ├── TextExt.kt │ │ │ └── UriExt.kt │ │ │ ├── file │ │ │ ├── CreateMode.kt │ │ │ ├── DocumentFileCompat.kt │ │ │ ├── DocumentFileExt.kt │ │ │ ├── DocumentFileType.kt │ │ │ ├── FileExt.kt │ │ │ ├── FileFullPath.kt │ │ │ ├── FileSize.kt │ │ │ ├── MimeType.kt │ │ │ ├── PublicDirectory.kt │ │ │ ├── StorageId.kt │ │ │ └── StorageType.kt │ │ │ ├── media │ │ │ ├── AudioMediaDirectory.kt │ │ │ ├── FileDescription.kt │ │ │ ├── ImageMediaDirectory.kt │ │ │ ├── MediaFile.kt │ │ │ ├── MediaFileExt.kt │ │ │ ├── MediaStoreCompat.kt │ │ │ ├── MediaType.kt │ │ │ └── VideoMediaDirectory.kt │ │ │ ├── permission │ │ │ ├── ActivityPermissionRequest.kt │ │ │ ├── FragmentPermissionRequest.kt │ │ │ ├── PermissionCallback.kt │ │ │ ├── PermissionReport.kt │ │ │ ├── PermissionRequest.kt │ │ │ └── PermissionResult.kt │ │ │ └── result │ │ │ ├── FilePropertiesResult.kt │ │ │ ├── MultipleFilesResult.kt │ │ │ ├── SingleFileResult.kt │ │ │ ├── SingleFolderResult.kt │ │ │ ├── ZipCompressionResult.kt │ │ │ └── ZipDecompressionResult.kt │ └── res │ │ ├── values-in │ │ └── strings.xml │ │ ├── values-ja │ │ └── strings.xml │ │ ├── values-ru │ │ └── strings.xml │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── anggrayudi │ └── storage │ ├── DocumentFileCompatTest.kt │ ├── ExampleUnitTest.kt │ ├── SimpleStorageTest.kt │ ├── extension │ └── TextExtKtTest.kt │ └── file │ ├── DocumentFileCompatTest.kt │ ├── FileExtKtTest.kt │ └── MimeTypeTest.kt └── versions.gradle /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: [ paypal.me/hardiannicko, saweria.co/hardiannicko ] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | Library version: 1.x.x 11 | OS version: [Android 10] 12 | Device model: [Samsung S20] 13 | 14 | **Describe the bug** 15 | [A clear and concise description of what the bug is] 16 | 17 | **To Reproduce** 18 | [Explain the steps to reproduce] 19 | 20 | **Stacktrace** 21 | [If applicable] 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **What feature you want to suggest?** 11 | [Explain your idea and how it can help Simple Storage to be better] 12 | 13 | **Your solution** 14 | [Expected solution for the feature, if any] 15 | 16 | **Your current alternative** 17 | [Do you have any alternative or workaround to overcome the feature that's not available yet?] 18 | -------------------------------------------------------------------------------- /.github/scripts/copy_env_variables.sh: -------------------------------------------------------------------------------- 1 | # Append suffix -SNAPSHOT 2 | if [[ ! ($(grep "VERSION_NAME=" gradle.properties) == *"-SNAPSHOT") ]]; then 3 | sed -ie "s/VERSION_NAME.*$/&-SNAPSHOT/g" gradle.properties 4 | rm -f gradle.propertiese 5 | fi 6 | 7 | mkdir "$HOME/.android" 8 | keytool -genkey -v -keystore "$HOME/.android/debug.keystore" -storepass android -alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 -validity 10000 -dname "C=US, O=Android, CN=Android Debug" 9 | 10 | { 11 | printf "\nkeyAlias=androiddebugkey" 12 | printf "\nkeyPassword=android" 13 | printf "\nstorePassword=android" 14 | } >>local.properties 15 | -------------------------------------------------------------------------------- /.github/scripts/deploy_snapshot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SLUG="anggrayudi/SimpleStorage" 4 | 5 | set -e 6 | 7 | if [ "$GITHUB_REPOSITORY" != "$SLUG" ]; then 8 | echo "Skipping deployment: wrong repository. Expected '$SLUG' but was '$GITHUB_REPOSITORY'." 9 | elif [ "${GITHUB_REF##*/}" != "master" ]; then 10 | echo "Skipping deployment: wrong branch. Expected 'master' but was '${GITHUB_REF##*/}'." 11 | else 12 | echo "Deploying snapshot..." 13 | ./gradlew :storage:publishAllPublicationsToMavenCentral --no-daemon --no-parallel --stacktrace 14 | echo "Snapshot released!" 15 | fi 16 | -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | # Controls when the action will run. Triggers the workflow on push or pull request 4 | # events but only for the master branch 5 | on: 6 | push: 7 | branches: [ master ] 8 | pull_request: 9 | branches: 10 | - 'master' 11 | - 'release/**' 12 | 13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 14 | jobs: 15 | build: 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | 22 | - name: Setup JDK 17 23 | uses: actions/setup-java@v1 24 | with: 25 | java-version: 17 26 | 27 | - name: Copy environment variables 28 | env: 29 | RELEASE_KEYSTORE_BASE64: ${{ secrets.RELEASE_KEYSTORE_BASE64 }} 30 | RELEASE_KEYSTORE_ALIAS: ${{ secrets.RELEASE_KEYSTORE_ALIAS }} 31 | RELEASE_KEYSTORE_PASSWORD: ${{ secrets.RELEASE_KEYSTORE_PASSWORD }} 32 | run: ./.github/scripts/copy_env_variables.sh 33 | 34 | - name: Build with Gradle 35 | if: ${{ github.repository_owner == 'anggrayudi' }} 36 | env: 37 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.OSS_SONATYPE_NEXUS_USERNAME }} 38 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.OSS_SONATYPE_NEXUS_PASSWORD }} 39 | run: ./gradlew build test check 40 | 41 | - name: Build with Gradle (default) 42 | if: ${{ github.repository_owner != 'anggrayudi' }} 43 | env: 44 | ORG_GRADLE_PROJECT_mavenCentralUsername: 'abc' 45 | ORG_GRADLE_PROJECT_mavenCentralPassword: 'xyz' 46 | run: ./gradlew build test check 47 | 48 | - name: Upload snapshot archives 49 | if: ${{ github.repository_owner == 'anggrayudi' }} 50 | env: 51 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.OSS_SONATYPE_NEXUS_USERNAME }} 52 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.OSS_SONATYPE_NEXUS_PASSWORD }} 53 | run: ./.github/scripts/deploy_snapshot.sh 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated files 2 | bin/ 3 | gen/ 4 | *.apk 5 | *.aab 6 | *.class 7 | .DS_Store 8 | 9 | # Gradle files 10 | .gradle/ 11 | build/ 12 | /sample/release 13 | 14 | # Local configuration file (sdk path, etc) 15 | local.properties 16 | 17 | # Proguard folder generated by Eclipse 18 | proguard/ 19 | 20 | # Log Files 21 | *.log 22 | 23 | # Android Studio Navigation editor temp files 24 | .navigation/ 25 | 26 | # Android Studio captures folder 27 | captures/ 28 | 29 | # IntelliJ 30 | *.iml 31 | .idea 32 | .kotlin 33 | 34 | # Keystore files 35 | # Uncomment the following line if you do not want to check your keystore files in. 36 | *.jks 37 | *.keystore 38 | *.gpg 39 | 40 | # External native build folder generated in Android Studio 2.2 and later 41 | .externalNativeBuild 42 | 43 | # Google Services (e.g. APIs or Firebase) 44 | google-services.json 45 | 46 | # Freeline 47 | freeline.py 48 | freeline/ 49 | freeline_project_description.json 50 | 51 | # fastlane 52 | fastlane/report.xml 53 | fastlane/Preview.html 54 | fastlane/screenshots 55 | fastlane/test_output 56 | fastlane/readme.md 57 | 58 | # Java dump memory 59 | *.hprof -------------------------------------------------------------------------------- /.run/publishToCentral.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | -------------------------------------------------------------------------------- /.run/publishToLocal.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | ### The app is not responding when copy, move, and other IO tasks 4 | Read the quick documentation, Javadoc or go to the source code. 5 | All functions annotated by `@WorkerThread` must be called in the background thread, 6 | otherwise `@UiThread` must be called in the main thread. 7 | If you ignore the annotation, your apps will lead to [ANR](https://developer.android.com/topic/performance/vitals/anr). 8 | 9 | ### How to open quick documentation? 10 | Use keyboard shortcut Control + Q on Windows or Control + J on MacOS. 11 | More shortcuts can be found on [Android Studio keyboard shortcuts](https://developer.android.com/studio/intro/keyboard-shortcuts). 12 | 13 | ### Why permission dialog is not shown on API 29+? 14 | No runtime permission is required to be prompted on scoped storage. 15 | 16 | ### How to upload the `DocumentFile` and `MediaFile` to server? 17 | Read the input stream with extension function `openInputStream()` and upload it as Base64 text. 18 | 19 | ### File path returns empty string 20 | 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`: 21 | ``` 22 | content://com.android.providers.downloads.documents/document/9 23 | content://com.android.providers.media.documents/document/document%3A34 24 | ``` 25 | Here're some notes: 26 | * Empty file path is not this library's limitation, but Android OS itself. 27 | * To check if the file has guaranteed direct file path, extension function `DocumentFile.isTreeDocumentFile` will return `true`. 28 | * You can convert `SingleDocumentFile` to `MediaFile` and use `MediaFile.absolutePath`. If this still does not work, then there's no other way. 29 | * 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. 30 | 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`. 31 | 32 | ### How to check if a folder/file is writable? 33 | Use `isWritable()` extension function, because `DocumentFile.canWrite()` sometimes buggy on API 30. 34 | 35 | ### Which paths are writable with `java.io.File` on scoped storage? 36 | Accessing files in scoped storage requires URI, but the following paths are exception and no storage permission needed: 37 | * `/storage/emulated/0/Android/data/` 38 | * `/storage//Android/data/` 39 | * `/data/user/0/` (API 24+) 40 | * `/data/data/` (API 23-) 41 | 42 | ### What is the target branch for pull requests? 43 | Use branch `release/*` if exists, or use `master` instead. 44 | 45 | ### I have Java projects, but this library is built in Kotlin. How can I use it? 46 | Kotlin is compatible with Java. You can read Kotlin functions as Java methods. 47 | Read: [Java Compatibility](https://github.com/anggrayudi/SimpleStorage/blob/master/JAVA_COMPATIBILITY.md) 48 | 49 | ### Why does SimpleStorage use Kotlin? 50 | The main reasons why this library really needs Kotlin: 51 | * SimpleStorage requires thread suspension feature, but this feature is only provided by [Kotlin Coroutines](https://github.com/Kotlin/kotlinx.coroutines). 52 | * SimpleStorage contains many `String` & `Collection` manipulations, and Kotlin can overcome them in simple and easy ways. 53 | 54 | Other reasons are: 55 | * Kotlin can shorten and simplify your code. 56 | * Writing code in Kotlin is faster, thus it saves your time and improves your productivity. 57 | * [Google is Kotlin first](https://techcrunch.com/2019/05/07/kotlin-is-now-googles-preferred-language-for-android-app-development/) now. 58 | 59 | ### What are SimpleStorage alternatives? 60 | 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. 61 | Learn Kotlin, or Google will leave you far behind. 62 | 63 | **We have no intention to create Java version of SimpleStorage.** It will double our works and requires a lot of effort. 64 | Keep in mind that we don't want to archive this library, even though Google has released the stable version of ModernStorage. 65 | This library has rich features that Google may not covers, e.g. moving, copying, compressing and scanning folders. 66 | -------------------------------------------------------------------------------- /JAVA_COMPATIBILITY.md: -------------------------------------------------------------------------------- 1 | # Java Compatibility 2 | 3 | Kotlin is compatible with Java, meaning that Kotlin code is readable in Java. 4 | 5 | ## How to use? 6 | 7 | Simple Storage contains utility functions stored in `object` class, e.g. `DocumentFileCompat` and `MediaStoreCompat`. 8 | These classes contain only static functions. 9 | 10 | Additionally, this library also has extension functions, e.g. `DocumentFileExtKt` and `FileExtKt`. 11 | You can learn it [here](https://www.raywenderlich.com/10986797-extension-functions-and-properties-in-kotlin). 12 | 13 | ### Extension Functions 14 | 15 | Common extension functions are stored in package `com.anggrayudi.storage.extension`. The others are in `com.anggrayudi.storage.file`. 16 | You'll find that the most useful extension functions come from `DocumentFileExtKt` and `FileExtKt`. They are: 17 | * `DocumentFile.getStorageId()` and `File.getStorageId()` → Get storage ID. Returns `primary` for external storage and something like `AAAA-BBBB` for SD card. 18 | * `DocumentFile.getAbsolutePath()` → Get file's absolute path. Returns something like `/storage/AAAA-BBBB/Music/My Love.mp3`. 19 | * `DocumentFile.copyFileTo()` and `File.copyFileTo()` 20 | * `DocumentFile.search()` and `File.search()`, etc. 21 | 22 | Note that some long-running functions like copy, move, search, compress, and unzip are now only available in Kotlin. 23 | You can still use these Java features in your project, but you will need [v1.5.6](https://github.com/anggrayudi/SimpleStorage/releases/tag/1.5.6) which is the latest version that 24 | supports Java. 25 | 26 | Suppose that you want to get storage ID of the file: 27 | 28 | #### In Kotlin 29 | 30 | ```kotlin 31 | val file = ... 32 | val storageId = file.getStorageId(context) 33 | ``` 34 | 35 | #### In Java 36 | 37 | ```java 38 | DocumentFile file = ... 39 | String storageId = DocumentFileUtils.getStorageId(file, context); 40 | ``` 41 | 42 | All extension functions work like static methods in Java. Note that since `0.4.2`, 43 | their class names are renamed from using suffix `ExtKt` to `Utils`. 44 | 45 | ### Utility Functions 46 | 47 | I will refer to utility functions stored in Kotlin `object` class so you can understand it easily. 48 | You can find the most useful utility functions in `DocumentFileCompat` and `MediaStoreCompat`. 49 | 50 | Suppose that I want to get file from SD card with the following simple path: `AAAA-BBBB:Music/My Love.mp3`. 51 | BTW, `AAAA-BBBB` is the SD card's storage ID for this example. 52 | 53 | #### In Kotlin 54 | 55 | ```kotlin 56 | val file = DocumentFileCompat.fromSimplePath(context, "AAAA-BBBB", "Music/My Love.mp3") 57 | ``` 58 | 59 | #### In Java 60 | 61 | ```java 62 | DocumentFile file = DocumentFileCompat.INSTANCE.fromSimplePath(context, "AAAA-BBBB", "Music/My Love.mp3"); 63 | ``` 64 | 65 | In Java, you need to append `INSTANCE` after the utility class name. 66 | Anyway, if the function is annotated with `@JvmStatic`, you don't need to append `INSTANCE`. 67 | Just go to the source code to check whether it has the annotation. 68 | 69 | ## Sample Code 70 | 71 | * More sample code in Java can be found in 72 | [`JavaActivity`](https://github.com/anggrayudi/SimpleStorage/blob/master/sample/src/main/java/com/anggrayudi/storage/sample/activity/JavaActivity.java) 73 | * Learn Kotlin on [Udacity](https://classroom.udacity.com/courses/ud9011). It's easy and free! -------------------------------------------------------------------------------- /art/folder-content-conflict.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anggrayudi/SimpleStorage/3024a93489af334f9f060cafaf3c287839460916/art/folder-content-conflict.png -------------------------------------------------------------------------------- /art/getAccessibleAbsolutePaths.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anggrayudi/SimpleStorage/3024a93489af334f9f060cafaf3c287839460916/art/getAccessibleAbsolutePaths.png -------------------------------------------------------------------------------- /art/parent-folder-conflict.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anggrayudi/SimpleStorage/3024a93489af334f9f060cafaf3c287839460916/art/parent-folder-conflict.png -------------------------------------------------------------------------------- /art/terminology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anggrayudi/SimpleStorage/3024a93489af334f9f060cafaf3c287839460916/art/terminology.png -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | apply from: 'versions.gradle' 4 | 5 | addRepos(repositories) 6 | 7 | ext.kotlin_version = '2.0.0' 8 | 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:8.4.1' 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | classpath 'com.vanniktech:gradle-maven-publish-plugin:0.22.0' 13 | classpath 'org.jetbrains.dokka:dokka-gradle-plugin:1.7.20' 14 | } 15 | } 16 | 17 | allprojects { 18 | addRepos(repositories) 19 | 20 | //Support @JvmDefault 21 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { 22 | kotlinOptions { 23 | freeCompilerArgs = ['-Xjvm-default=all', '-opt-in=kotlin.RequiresOptIn'] 24 | jvmTarget = '1.8' 25 | } 26 | } 27 | } 28 | 29 | subprojects { 30 | if (name != 'sample') { 31 | apply plugin: "com.vanniktech.maven.publish" 32 | 33 | repositories { 34 | maven { 35 | url = version.toString().endsWith("SNAPSHOT") 36 | ? 'https://oss.sonatype.org/content/repositories/snapshots/' 37 | : 'https://oss.sonatype.org/service/local/staging/deploy/maven2' 38 | } 39 | } 40 | } 41 | 42 | afterEvaluate { 43 | android { 44 | compileSdkVersion 34 45 | 46 | defaultConfig { 47 | minSdkVersion 21 48 | targetSdkVersion 34 49 | versionCode 1 50 | versionName "$VERSION_NAME" 51 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 52 | vectorDrawables.useSupportLibrary = true 53 | } 54 | 55 | compileOptions { 56 | sourceCompatibility JavaVersion.VERSION_1_8 57 | targetCompatibility JavaVersion.VERSION_1_8 58 | } 59 | 60 | buildFeatures { 61 | buildConfig true 62 | } 63 | } 64 | configurations.configureEach { 65 | resolutionStrategy { 66 | // Force Kotlin to use current version 67 | force "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 68 | force "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 69 | force "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 70 | force "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlin_version" 71 | force "org.jetbrains.kotlin:kotlin-android-extensions-runtime:$kotlin_version" 72 | } 73 | } 74 | // global dependencies for all modules 75 | dependencies { 76 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 77 | testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" 78 | } 79 | } 80 | } 81 | 82 | tasks.register('clean', Delete) { 83 | delete rootProject.buildDir 84 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2G 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | # For publishing: 23 | GROUP=com.anggrayudi 24 | POM_ARTIFACT_ID=storage 25 | VERSION_NAME=2.0.0-SNAPSHOT 26 | RELEASE_SIGNING_ENABLED=false 27 | SONATYPE_AUTOMATIC_RELEASE=true 28 | SONATYPE_HOST=DEFAULT 29 | POM_NAME=storage 30 | POM_DESCRIPTION=Simplify Android Storage Access Framework for file management across API levels. 31 | POM_INCEPTION_YEAR=2020 32 | POM_URL=https://github.com/anggrayudi/SimpleStorage 33 | POM_LICENSE_NAME=The Apache Software License, Version 2.0 34 | POM_LICENSE_URL=https://github.com/anggrayudi/SimpleStorage/blob/master/LICENSE 35 | POM_LICENSE_DIST=https://www.apache.org/licenses/LICENSE-2.0.txt 36 | POM_SCM_URL=https://github.com/anggrayudi/SimpleStorage 37 | POM_SCM_CONNECTION=scm:git:git://github.com/anggrayudi/SimpleStorage.git 38 | POM_SCM_DEV_CONNECTION=scm:git:ssh://github.com:anggrayudi/SimpleStorage.git 39 | POM_DEVELOPER_ID=anggrayudi 40 | POM_DEVELOPER_NAME=Anggrayudi H 41 | POM_DEVELOPER_URL=https://github.com/anggrayudi/ 42 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anggrayudi/SimpleStorage/3024a93489af334f9f060cafaf3c287839460916/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Dec 01 18:57:30 WIB 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /.externalNativeBuild 3 | /release 4 | /local -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | 4 | def properties = new Properties() 5 | properties.load(rootProject.file('local.properties').newDataInputStream()) 6 | 7 | android { 8 | signingConfigs { 9 | def debugKeystore = file("${System.properties['user.home']}${File.separator}.android${File.separator}debug.keystore") 10 | debug { 11 | keyAlias 'androiddebugkey' 12 | keyPassword 'android' 13 | storePassword 'android' 14 | storeFile debugKeystore 15 | } 16 | release { 17 | keyAlias 'androiddebugkey' 18 | keyPassword 'android' 19 | storePassword 'android' 20 | storeFile debugKeystore 21 | } 22 | } 23 | 24 | namespace 'com.anggrayudi.storage.sample' 25 | defaultConfig { 26 | applicationId "com.anggrayudi.storage.sample" 27 | multiDexEnabled true 28 | } 29 | 30 | buildTypes { 31 | debug { 32 | minifyEnabled false 33 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 34 | signingConfig signingConfigs.debug 35 | } 36 | release { 37 | minifyEnabled true 38 | shrinkResources false 39 | zipAlignEnabled true 40 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 41 | signingConfig signingConfigs.release 42 | } 43 | } 44 | 45 | lint { 46 | abortOnError false 47 | } 48 | 49 | buildFeatures { 50 | viewBinding = true 51 | } 52 | 53 | flavorDimensions "libSource" 54 | productFlavors { 55 | local { 56 | dimension "libSource" 57 | getIsDefault().set(true) 58 | } 59 | maven { 60 | dimension "libSource" 61 | configurations.all { 62 | // Check for updates every build 63 | resolutionStrategy.cacheChangingModulesFor 0, 'seconds' 64 | } 65 | } 66 | } 67 | } 68 | 69 | dependencies { 70 | implementation fileTree(dir: "libs", include: ["*.jar"]) 71 | implementation project(":storage") 72 | // localImplementation project(":storage") 73 | // mavenImplementation("$GROUP:$POM_ARTIFACT_ID:$VERSION_NAME") { changing = true } 74 | 75 | implementation deps.core_ktx 76 | implementation deps.appcompat 77 | implementation deps.multidex 78 | 79 | implementation deps.timber 80 | implementation deps.material_progressbar 81 | implementation 'androidx.preference:preference-ktx:1.2.1' 82 | implementation 'com.afollestad.material-dialogs:files:3.3.0' 83 | 84 | //test 85 | testImplementation deps.junit 86 | testImplementation deps.mockk 87 | } -------------------------------------------------------------------------------- /sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 9 | 10 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 42 | 43 | 46 | 47 | 50 | 51 | 54 | 55 | 60 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /sample/src/main/java/com/anggrayudi/storage/sample/App.kt: -------------------------------------------------------------------------------- 1 | package com.anggrayudi.storage.sample 2 | 3 | import androidx.multidex.MultiDexApplication 4 | import timber.log.Timber 5 | 6 | /** 7 | * @author Anggrayudi Hardiannico A. (anggrayudi.hardiannico@dana.id) 8 | * @version App, v 0.0.1 10/08/20 00.39 by Anggrayudi Hardiannico A. 9 | */ 10 | class App : MultiDexApplication() { 11 | 12 | override fun onCreate() { 13 | super.onCreate() 14 | Timber.plant(Timber.DebugTree()) 15 | } 16 | } -------------------------------------------------------------------------------- /sample/src/main/java/com/anggrayudi/storage/sample/StorageInfoAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.anggrayudi.storage.sample 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.os.Build 6 | import android.text.format.Formatter 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import android.widget.Button 11 | import android.widget.TextView 12 | import androidx.recyclerview.widget.RecyclerView 13 | import com.afollestad.materialdialogs.MaterialDialog 14 | import com.afollestad.materialdialogs.list.listItems 15 | import com.anggrayudi.storage.file.DocumentFileCompat 16 | import com.anggrayudi.storage.file.StorageId.PRIMARY 17 | import kotlinx.coroutines.CoroutineScope 18 | import kotlinx.coroutines.launch 19 | 20 | /** 21 | * Created on 12/14/20 22 | * @author Anggrayudi H 23 | */ 24 | class StorageInfoAdapter( 25 | private val context: Context, 26 | private val ioScope: CoroutineScope, 27 | private val uiScope: CoroutineScope 28 | ) : RecyclerView.Adapter() { 29 | 30 | private val storageIds = DocumentFileCompat.getStorageIds(context) 31 | 32 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 33 | return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.view_item_storage_info, parent, false)) 34 | } 35 | 36 | @SuppressLint("SetTextI18n") 37 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 38 | ioScope.launch { 39 | val storageId = storageIds[position] 40 | val storageName = if (storageId == PRIMARY) "External Storage" else storageId 41 | val storageCapacity = Formatter.formatFileSize(context, DocumentFileCompat.getStorageCapacity(context, storageId)) 42 | val storageUsedSpace = Formatter.formatFileSize(context, DocumentFileCompat.getUsedSpace(context, storageId)) 43 | val storageFreeSpace = Formatter.formatFileSize(context, DocumentFileCompat.getFreeSpace(context, storageId)) 44 | uiScope.launch { 45 | holder.run { 46 | tvStorageName.text = storageName 47 | tvStorageCapacity.text = "Capacity: $storageCapacity" 48 | tvStorageUsedSpace.text = "Used Space: $storageUsedSpace" 49 | tvStorageFreeSpace.text = "Free Space: $storageFreeSpace" 50 | btnShowGrantedUri.setOnClickListener { showGrantedUris(it.context, storageId) } 51 | if (storageId == PRIMARY && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { 52 | // No URI permission required for external storage 53 | btnShowGrantedUri.visibility = View.GONE 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | /** 61 | * A storageId may contains more than one granted URIs 62 | */ 63 | @SuppressLint("NewApi") 64 | private fun showGrantedUris(context: Context, filterStorageId: String) { 65 | val grantedPaths = DocumentFileCompat.getAccessibleAbsolutePaths(context)[filterStorageId] 66 | if (grantedPaths == null) { 67 | MaterialDialog(context) 68 | .message(text = "No permission granted on storage ID \"$filterStorageId\"") 69 | .positiveButton() 70 | .show() 71 | } else { 72 | MaterialDialog(context) 73 | .title(text = "Granted paths for \"$filterStorageId\"") 74 | .listItems(items = grantedPaths.toList().sorted()) 75 | .show() 76 | } 77 | } 78 | 79 | override fun getItemCount() = storageIds.size 80 | 81 | class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { 82 | 83 | internal val tvStorageName = view.findViewById(R.id.tvStorageName) 84 | internal val tvStorageCapacity = view.findViewById(R.id.tvStorageCapacity) 85 | internal val tvStorageUsedSpace = view.findViewById(R.id.tvStorageUsedSpace) 86 | internal val tvStorageFreeSpace = view.findViewById(R.id.tvStorageFreeSpace) 87 | internal val btnShowGrantedUri = view.findViewById