├── .github ├── CODEOWNERS ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── filepickerlibrary ├── .gitignore ├── build.gradle.kts ├── config │ └── detekt │ │ └── detekt.yml ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── nareshchocha │ │ └── filepickerlibrary │ │ └── ui │ │ └── FilePickerBuilderTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── nareshchocha │ │ │ └── filepickerlibrary │ │ │ ├── initializer │ │ │ └── TimberInitializer.kt │ │ │ ├── models │ │ │ ├── BaseConfig.kt │ │ │ └── PickerData.kt │ │ │ ├── picker │ │ │ └── PickerUtils.kt │ │ │ ├── ui │ │ │ ├── FilePicker.kt │ │ │ ├── activitys │ │ │ │ ├── DocumentFilePickerActivity.kt │ │ │ │ ├── ImageCaptureActivity.kt │ │ │ │ ├── MediaFilePickerActivity.kt │ │ │ │ ├── PopUpActivity.kt │ │ │ │ └── VideoCaptureActivity.kt │ │ │ └── components │ │ │ │ └── dialogs │ │ │ │ ├── AppAlertDialog.kt │ │ │ │ └── AppDialog.kt │ │ │ └── utilities │ │ │ ├── CustomFileProvider.kt │ │ │ ├── FileUtils.kt │ │ │ ├── appConst │ │ │ └── Const.kt │ │ │ └── extentions │ │ │ ├── Extentions.kt │ │ │ └── FilePathExtentions.kt │ └── res │ │ ├── drawable-hdpi │ │ └── ic_media.webp │ │ ├── drawable-mdpi │ │ └── ic_media.webp │ │ ├── drawable-xhdpi │ │ └── ic_media.webp │ │ ├── drawable-xxhdpi │ │ └── ic_media.webp │ │ ├── drawable-xxxhdpi │ │ └── ic_media.webp │ │ ├── drawable │ │ ├── ic_camera.xml │ │ ├── ic_file.xml │ │ ├── ic_video.xml │ │ └── transparent.xml │ │ ├── values │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ └── file_picker_library_provider.xml │ └── test │ └── java │ └── com │ └── nareshchocha │ └── filepickerlibrary │ └── ExampleUnitTest.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── sample ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── nareshchocha │ │ └── filepicker │ │ ├── AppUITest.kt │ │ ├── TestConst.kt │ │ └── TestExtentions.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── nareshchocha │ │ │ └── filepicker │ │ │ └── MainActivity.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ ├── data_extraction_rules.xml │ │ └── file_library_provider.xml │ └── test │ └── java │ └── com │ └── nareshchocha │ └── filepicker │ └── ExampleUnitTest.kt ├── secring.gpg └── settings.gradle.kts /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @ChochaNaresh 2 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to FilePicker 2 | 3 | We welcome pull requests! Please follow these guidelines: 4 | 5 | - Create issues for bugs and features. 6 | - Fork and create feature branches (e.g., `feature/pick-*`). 7 | - Follow existing code style and naming conventions. 8 | - Run tests with `./gradlew test` before submitting a PR. 9 | - Add documentation for any new public APIs. 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ChochaNaresh 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: [bug] 5 | assignees: [] 6 | 7 | body: 8 | - type: textarea 9 | attributes: 10 | label: Describe the bug 11 | description: A clear and concise description of what the bug is. 12 | placeholder: The app crashes when... 13 | validations: 14 | required: true 15 | 16 | - type: input 17 | attributes: 18 | label: Library Version 19 | placeholder: e.g. 1.0.0-beta.1 20 | validations: 21 | required: true 22 | 23 | - type: textarea 24 | attributes: 25 | label: Steps to reproduce 26 | placeholder: | 27 | 1. Import library 28 | 2. Call pickFile() 29 | 3. App crashes 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a Question 4 | url: https://github.com/ChochaNaresh/FilePicker/discussions 5 | about: Please ask and answer questions in GitHub Discussions. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest a new feature or improvement 3 | title: "[Feature]: " 4 | labels: [enhancement] 5 | assignees: [] 6 | 7 | body: 8 | - type: textarea 9 | attributes: 10 | label: Feature Description 11 | description: What feature would you like to see? 12 | validations: 13 | required: true 14 | 15 | - type: textarea 16 | attributes: 17 | label: Use case or motivation 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## ✨ What's Changed 2 | 3 | Describe the changes you made in this PR. 4 | 5 | ## ✅ Checklist 6 | 7 | - [ ] I've tested this on a device/emulator 8 | - [ ] I've added tests where applicable 9 | - [ ] I've updated documentation (if needed) 10 | - [ ] This PR follows the [CONTRIBUTING.md](./CONTRIBUTING.md) guidelines 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Android CI/CD 2 | 3 | on: 4 | push: 5 | branches: [main, master] 6 | tags: 7 | - '[0-9]*' # matches 1.0.0, 0.1.0-alpha etc. 8 | pull_request: 9 | branches: [main, master] 10 | 11 | permissions: 12 | contents: write # Required to create GitHub Releases 13 | 14 | jobs: 15 | build: 16 | name: Build & Test 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout source 21 | uses: actions/checkout@v4 22 | 23 | - name: Set up JDK 17 24 | uses: actions/setup-java@v4 25 | with: 26 | distribution: 'temurin' 27 | java-version: '17' 28 | 29 | - name: Cache Gradle 30 | uses: actions/cache@v3 31 | with: 32 | path: | 33 | ~/.gradle/caches 34 | ~/.gradle/wrapper 35 | key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 36 | restore-keys: gradle-${{ runner.os }}- 37 | 38 | - name: Make gradlew executable 39 | run: chmod +x ./gradlew 40 | 41 | - name: Run unit tests 42 | run: ./gradlew filepickerlibrary:testDebugUnitTest 43 | 44 | - name: Upload test results 45 | if: always() 46 | uses: actions/upload-artifact@v4 47 | with: 48 | name: test-results 49 | path: '**/build/test-results/testDebugUnitTest/' 50 | 51 | - name: Build library 52 | run: ./gradlew filepickerlibrary:build 53 | 54 | - name: Upload build artifacts 55 | uses: actions/upload-artifact@v4 56 | with: 57 | name: build-artifacts 58 | path: | 59 | **/build/outputs/aar/*.aar 60 | **/build/libs/*.jar 61 | 62 | publish: 63 | name: Publish Release 64 | needs: build 65 | runs-on: ubuntu-latest 66 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') 67 | 68 | steps: 69 | - name: Checkout code 70 | uses: actions/checkout@v4 71 | 72 | - name: Set up JDK 17 73 | uses: actions/setup-java@v4 74 | with: 75 | distribution: 'temurin' 76 | java-version: '17' 77 | 78 | - name: Make gradlew executable 79 | run: chmod +x ./gradlew 80 | 81 | - name: Extract version from Git tag 82 | id: version 83 | run: | 84 | TAG=${GITHUB_REF#refs/tags/} 85 | VERSION_NAME=${TAG#v} 86 | echo "VERSION_NAME=$VERSION_NAME" >> $GITHUB_ENV 87 | 88 | - name: Detect release type 89 | id: release_type 90 | run: | 91 | if [[ "${GITHUB_REF}" == *alpha* ]]; then 92 | echo "release_stage=alpha" >> $GITHUB_OUTPUT 93 | elif [[ "${GITHUB_REF}" == *beta* ]]; then 94 | echo "release_stage=beta" >> $GITHUB_OUTPUT 95 | else 96 | echo "release_stage=stable" >> $GITHUB_OUTPUT 97 | fi 98 | - name: Restore gradle.properties 99 | env: 100 | GRADLE_PROPERTIES: ${{ secrets.GRADLE_PROPERTIES }} 101 | shell: bash 102 | run: | 103 | mkdir -p ~/.gradle/ 104 | echo "GRADLE_USER_HOME=${HOME}/.gradle" >> $GITHUB_ENV 105 | echo "${GRADLE_PROPERTIES}" > ~/.gradle/gradle.properties 106 | - name: Import GPG key 107 | uses: crazy-max/ghaction-import-gpg@v6 108 | with: 109 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 110 | passphrase: ${{ secrets.PASSPHRASE }} 111 | 112 | - name: Publish to Maven Central 113 | run: ./gradlew filepickerlibrary:publishAllPublicationsToMavenCentral --no-configuration-cache -PVERSION_NAME=${{ env.VERSION_NAME }} 114 | 115 | - name: Upload GitHub Release 116 | uses: softprops/action-gh-release@v1 117 | with: 118 | tag_name: ${{ github.ref_name }} 119 | name: Release ${{ github.ref_name }} 120 | body: | 121 | 🚀 **Release:** `${{ github.ref_name }}` 122 | 🏷 **Type:** `${{ steps.release_type.outputs.release_stage }}` 123 | 124 | This release was automatically published from CI. 125 | draft: false 126 | prerelease: ${{ steps.release_type.outputs.release_stage != 'stable' }} 127 | files: | 128 | **/build/outputs/aar/*.aar 129 | **/build/libs/*.jar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /.idea/ 4 | /local.properties 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | local.properties 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Maven Git Versioning Extension Changelog 2 | 3 | [![Maven Central](https://img.shields.io/maven-central/v/io.github.chochanaresh/filepicker.svg)](https://search.maven.org/artifact/io.github.chochanaresh/filepicker) 4 | 5 | # Changelog 6 | 7 | ## 0.2.1 8 | 9 | ##### Initial 10 | - initial release in Maven Central -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## File Picker Library for Android 3 | This library is designed to simplify the process of selecting and retrieving media files from an Android device, and supports media capture for images and videos. 4 | 5 | [![Maven Central](https://img.shields.io/maven-central/v/io.github.chochanaresh/filepicker.svg)](https://search.maven.org/artifact/io.github.chochanaresh/filepicker) 6 | ![Build Workflow](https://github.com/ChochaNaresh/FilePicker/actions/workflows/ci.yml/badge.svg) 7 | [![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21) 8 | ![Language](https://img.shields.io/badge/language-Kotlin-orange.svg) 9 | ![Language](https://img.shields.io/badge/Kotlin-2.1.0-blue) 10 | 11 | ### How to use 12 | **How to add dependencies** 13 | 14 | **Groovy** 15 | ```groovy 16 | allprojects { 17 | repositories { 18 | mavenCentral() // For FilePicker library, this line is enough. 19 | } 20 | } 21 | ``` 22 | 23 | ```groovy 24 | dependencies { 25 | // ... 26 | implementation 'io.github.chochanaresh:filepicker:$libVersion' 27 | // ... 28 | } 29 | ``` 30 | 31 | **kts** 32 | ```kotlin 33 | allprojects { 34 | repositories { 35 | mavenCentral() // For FilePicker library, this line is enough. 36 | } 37 | } 38 | ``` 39 | 40 | ```kotlin 41 | dependencies { 42 | // ... 43 | implementation ("io.github.chochanaresh:filepicker:$libVersion") 44 | // ... 45 | } 46 | ``` 47 | **libs.versions.toml** 48 | ```toml 49 | [versions] 50 | filepicker = "$libVersion" 51 | 52 | [libraries] 53 | filepicker = { group = "io.github.chochanaresh", name = "filepicker", version.ref = "filepicker" } 54 | ```` 55 | 56 | ```kotlin 57 | dependencies { 58 | // ... 59 | implementation(libs.filepicker) 60 | // ... 61 | } 62 | ``` 63 | ## Version 64 | Where `$libVersion` = [![libVersion](https://img.shields.io/maven-central/v/io.github.chochanaresh/filepicker.svg)](https://central.sonatype.com/artifact/io.github.chochanaresh/filepicker/versions) 65 | 66 | ## How to get result 67 | 68 | ##### Kotlin 69 | ```kotlin 70 | private val launcher = 71 | registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { 72 | if (it.resultCode == Activity.RESULT_OK) { 73 | // Use the uri to load the image 74 | val uri = it.data?.data!! 75 | // Use the file path to set image or upload 76 | val filePath= it.data.getStringExtra(Const.BundleExtras.FILE_PATH) 77 | //... 78 | 79 | // for Multiple picks 80 | // first item 81 | val first = it.data?.data!! 82 | // other items 83 | val clipData = it.data?.clipData 84 | // Multiple file paths list 85 | val filePaths = result.data?.getStringArrayListExtra(Const.BundleExtras.FILE_PATH_LIST) 86 | //... 87 | } 88 | } 89 | ``` 90 | 91 | 92 | ##### Java 93 | ```java 94 | private ActivityResultLauncher launcher = 95 | registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), 96 | new ActivityResultCallback() { 97 | @Override 98 | public void onActivityResult(ActivityResult result) { 99 | if (result.getResultCode() == Activity.RESULT_OK) { 100 | // Use the uri to load the image 101 | Uri uri = result.getData().getData(); 102 | // Use the file path to set image or upload 103 | String filePath = result.getData().getStringExtra(Const.BundleExtras.FILE_PATH); 104 | //... 105 | 106 | // for Multiple picks 107 | // first item 108 | Uri first = result.getData().getData(); 109 | // other items 110 | ClipData clipData = result.getData().getClipData(); 111 | // Multiple file paths list 112 | ArrayList filePaths = result.getData().getStringArrayListExtra(Const.BundleExtras.FILE_PATH_LIST); 113 | //... 114 | } 115 | } 116 | }); 117 | ``` 118 | ## Customization 119 | 120 | **Multiple option with BottomSheet Or Dialog** 121 | 122 | ##### Kotlin 123 | ```kotlin 124 | FilePicker.Builder(this) 125 | .setPopUpConfig() 126 | .addPickDocumentFile() 127 | .addImageCapture() 128 | .addVideoCapture() 129 | .addPickMedia() 130 | .build() 131 | ``` 132 | ##### Java 133 | ```java 134 | new FilePicker.Builder(this) 135 | .setPopUpConfig(null) 136 | .addPickDocumentFile(null) 137 | .addImageCapture(null) 138 | .addVideoCapture(null) 139 | .addPickMedia(null) 140 | .build(); 141 | ``` 142 | **Customize popup** 143 | 144 | ##### Kotlin 145 | ```kotlin 146 | //.. 147 | setPopUpConfig( 148 | PopUpConfig( 149 | chooserTitle = "Choose Profile", 150 | // layoutId = 0, custom layout 151 | mPopUpType = PopUpType.BOTTOM_SHEET,// PopUpType.BOTTOM_SHEET Or PopUpType.DIALOG 152 | mOrientation = RecyclerView.VERTICAL // RecyclerView.VERTICAL or RecyclerView.HORIZONTAL 153 | ) 154 | ) 155 | //.. 156 | .build() 157 | ``` 158 | 159 | ##### Java 160 | ```java 161 | //.. 162 | setPopUpConfig( 163 | new PopUpConfig( 164 | "Choose Profile", 165 | null,// custom layout 166 | PopUpType.BOTTOM_SHEET, // PopUpType.BOTTOM_SHEET Or PopUpType.DIALOG 167 | RecyclerView.VERTICAL // RecyclerView.VERTICAL or RecyclerView.HORIZONTAL 168 | ) 169 | ) 170 | //.. 171 | .build() 172 | ``` 173 | ### Pick Document Config 174 | ##### Kotlin 175 | ```kotlin 176 | //.. 177 | addPickDocumentFile( 178 | DocumentFilePickerConfig( 179 | popUpIcon = R.drawable.ic_file,// DrawableRes Id 180 | popUpText = "File Media", 181 | allowMultiple = false,// set Multiple pick file 182 | maxFiles = 0,// max files working only in android latest version 183 | mMimeTypes = listOf("*/*"),// added Multiple MimeTypes 184 | askPermissionTitle = null, // set Permission ask Title 185 | askPermissionMessage = null,// set Permission ask Message 186 | settingPermissionTitle = null,// set Permission setting Title 187 | settingPermissionMessage = null,// set Permission setting Messag 188 | ), 189 | ) 190 | //.. 191 | .build() 192 | ``` 193 | 194 | ##### Java 195 | ```java 196 | //.. 197 | addPickDocumentFile( 198 | new DocumentFilePickerConfig( 199 | null, // DrawableRes Id 200 | null,// Title for pop item 201 | true, // set Multiple pick file 202 | null, // max files working only in android latest version 203 | mMimeTypesList, // added Multiple MimeTypes 204 | null, // set Permission ask Title 205 | null, // set Permission ask Message 206 | null, // set Permission setting Title 207 | null // set Permission setting Messag 208 | ) 209 | ) 210 | //.. 211 | .build() 212 | ``` 213 | 214 | ### Image Capture Config 215 | 216 | ##### kotlin 217 | ```kotlin 218 | //.. 219 | addImageCapture( 220 | ImageCaptureConfig( 221 | popUpIcon = R.drawable.ic_camera,// DrawableRes Id 222 | popUpText = "Camera", 223 | mFolder = File(),// set custom folder with write file permission 224 | fileName = "image.jpg", 225 | // It is not working correctly. However, it will Work on the same devices. 226 | isUseRearCamera = true, // setting camera facing 227 | askPermissionTitle = null, // set Permission ask Title 228 | askPermissionMessage = null,// set Permission ask Message 229 | settingPermissionTitle = null,// set Permission setting Title 230 | settingPermissionMessage = null,// set Permission setting Messag 231 | ), 232 | ) 233 | //.. 234 | .build() 235 | ``` 236 | 237 | ##### Java 238 | ```java 239 | //.. 240 | addImageCapture( 241 | new ImageCaptureConfig( 242 | R.drawable.ic_camera, // DrawableRes Id 243 | null, // Title for pop item 244 | null, // set custom folder with write file permission 245 | null, // set custom File name 246 | // It is not working correctly. However, it will Work on the same devices. 247 | isUseRearCamera = true, // setting camera facing 248 | askPermissionTitle = null, // set Permission ask Title 249 | askPermissionMessage = null,// set Permission ask Message 250 | settingPermissionTitle = null,// set Permission setting Title 251 | settingPermissionMessage = null,// set Permission setting Messag 252 | ) 253 | ) 254 | //.. 255 | .build() 256 | ``` 257 | 258 | ### Video Capture Config 259 | 260 | ##### kotlin 261 | ```kotlin 262 | //.. 263 | addVideoCapture( 264 | VideoCaptureConfig( 265 | popUpIcon = R.drawable.ic_video,// DrawableRes Id 266 | popUpText = "Video", 267 | mFolder=File(),// set custom folder with write file permission 268 | fileName = "video.mp4", 269 | maxSeconds = null,// set video duration in seconds 270 | maxSizeLimit = null,// set size limit 271 | isHighQuality = null,// set isHighQuality true/false 272 | askPermissionTitle = null, // set Permission ask Title 273 | askPermissionMessage = null,// set Permission ask Message 274 | settingPermissionTitle = null,// set Permission setting Title 275 | settingPermissionMessage = null,// set Permission setting Messag 276 | ), 277 | ) 278 | //.. 279 | .build() 280 | ``` 281 | 282 | ##### Java 283 | ```java 284 | //.. 285 | addVideoCapture( 286 | new VideoCaptureConfig( 287 | R.drawable.ic_video,// DrawableRes Id 288 | null, // Title for pop item 289 | null, // set custom folder with write file permission 290 | null, // set custom File name 291 | null, // set video duration in seconds 292 | null, // set size limit 293 | null, // set isHighQuality true/false 294 | null, // set Permission ask Title 295 | null, // set Permission ask Message 296 | null, // set Permission setting Title 297 | null, // set Permission setting Messag 298 | ) 299 | ) 300 | //.. 301 | .build() 302 | ``` 303 | 304 | ### Pick Media Config 305 | ##### kotlin 306 | ```kotlin 307 | //.. 308 | addPickMedia( 309 | PickMediaConfig( 310 | popUpIcon = R.drawable.ic_media,// DrawableRes Id 311 | popUpText = "Video", 312 | allowMultiple = false,// set Multiple pick file 313 | maxFiles = 0,// max files working only in android latest version 314 | mPickMediaType = ImageAndVideo, 315 | askPermissionTitle = null, // set Permission ask Title 316 | askPermissionMessage = null,// set Permission ask Message 317 | settingPermissionTitle = null,// set Permission setting Title 318 | settingPermissionMessage = null,// set Permission setting Messag 319 | ), 320 | ) 321 | //.. 322 | .build() 323 | ``` 324 | 325 | ##### Java 326 | ```java 327 | //.. 328 | addPickMedia( 329 | new PickMediaConfig( 330 | R.drawable.ic_media,// DrawableRes Id 331 | null, // Title for pop item 332 | null, // set Multiple pick file 333 | null, // max files working only in android latest version 334 | null, // set PickMediaTypes 335 | null, // set Permission ask Title 336 | null, // set Permission ask Message 337 | null, // set Permission setting Title 338 | null, // set Permission setting Messag 339 | ) 340 | ) 341 | //.. 342 | .build() 343 | ``` 344 | 345 | ### Pick Media Types 346 | 347 | ##### kotlin 348 | ```kotlin 349 | import com.nareshchocha.filepickerlibrary.models.PickMediaType 350 | ``` 351 | 352 | ##### Java 353 | ```java 354 | import com.nareshchocha.filepickerlibrary.models.PickMediaType; 355 | ``` 356 | 357 | * PickMediaType.ImageOnly 358 | * PickMediaType.VideoOnly 359 | * PickMediaType.ImageAndVideo 360 | ## Use directly 361 | ### Pick Document 362 | 363 | ##### Kotlin 364 | ```kotlin 365 | FilePicker.Builder(this) 366 | .pickDocumentFileBuild(DocumentFilePickerConfig()) 367 | ``` 368 | Customization 369 | [**DocumentFilePickerConfig**](#kotlin-3) 370 | 371 | ##### Java 372 | ```java 373 | new FilePicker.Builder(this) 374 | .pickDocumentFileBuild(new DocumentFilePickerConfig(null)); 375 | ``` 376 | Customization 377 | [**DocumentFilePickerConfig**](#java-3) 378 | 379 | ### Image Capture 380 | 381 | ##### Kotlin 382 | ```kotlin 383 | FilePicker.Builder(this) 384 | .imageCaptureBuild(ImageCaptureConfig()) 385 | ``` 386 | Customization 387 | [**ImageCaptureConfig**](#kotlin-4) 388 | 389 | ##### Java 390 | ```java 391 | new FilePicker.Builder(this) 392 | .imageCaptureBuild(new ImageCaptureConfig(null)); 393 | ``` 394 | Customization 395 | [**ImageCaptureConfig**](#java-4) 396 | 397 | ### Video Capture 398 | 399 | ##### Kotlin 400 | ```kotlin 401 | FilePicker.Builder(this) 402 | .videoCaptureBuild(VideoCaptureConfig()) 403 | ``` 404 | Customization 405 | [**VideoCaptureConfig**](#kotlin-5) 406 | 407 | ##### Java 408 | ```java 409 | new FilePicker.Builder(this) 410 | .videoCaptureBuild(new VideoCaptureConfig(null)); 411 | ``` 412 | Customization 413 | [**VideoCaptureConfig**](#java-5) 414 | 415 | ### Pick Media 416 | 417 | ##### Kotlin 418 | ```kotlin 419 | FilePicker.Builder(this) 420 | .pickMediaBuild(PickMediaConfig()) 421 | ``` 422 | Customization 423 | [**PickMediaConfig**](#kotlin-6) 424 | 425 | ##### Java 426 | ```java 427 | new FilePicker.Builder(this) 428 | .pickMediaBuild(new PickMediaConfig(null)); 429 | ``` 430 | Customization 431 | [**PickMediaConfig**](#java-6) 432 | ## Proguard rules 433 | ```text 434 | -keepclasseswithmembernames class com.nareshchocha.filepickerlibrary.models.**{ 435 | *; 436 | } 437 | -keepclassmembers class * extends androidx.appcompat.app.AppCompatActivity { 438 | *; 439 | } 440 | ``` 441 | ## Compatibility 442 | * Library - Android Lollipop 5.0+ (API 21) 443 | * Sample - Android Lollipop 5.0+ (API 21) 444 | ## Contributing 445 | 446 | Contributions are always welcome! 447 | ## Support 448 | 449 | For support, email chochanaresh0@gmail.com or join our Slack channel. 450 | 451 | [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/chochanaresh) 452 | 453 | ## License 454 | ```text 455 | Copyright 2023 Naresh chocha 456 | 457 | Licensed under the Apache License, Version 2.0 (the "License"); 458 | you may not use this file except in compliance with the License. 459 | You may obtain a copy of the License at 460 | 461 | http://www.apache.org/licenses/LICENSE-2.0 462 | 463 | Unless required by applicable law or agreed to in writing, software 464 | distributed under the License is distributed on an "AS IS" BASIS, 465 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 466 | See the License for the specific language governing permissions and 467 | limitations under the License. 468 | ``` 469 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | plugins { 3 | alias(libs.plugins.android.application) apply false 4 | alias(libs.plugins.android.library) apply false 5 | alias(libs.plugins.kotlin.android) apply false 6 | // compose 7 | alias(libs.plugins.kotlin.compose) apply false 8 | // parcelize 9 | alias(libs.plugins.kotlin.parcelize) apply false 10 | // code analyzer for Kotlin 11 | alias(libs.plugins.arturbosch.detekt) apply false 12 | alias(libs.plugins.maven.publish) apply false 13 | } 14 | 15 | tasks.register("clean", Delete::class) { 16 | delete(rootProject.layout.buildDirectory) 17 | } 18 | -------------------------------------------------------------------------------- /filepickerlibrary/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /filepickerlibrary/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.vanniktech.maven.publish.SonatypeHost 2 | 3 | plugins { 4 | alias(libs.plugins.android.library) 5 | alias(libs.plugins.kotlin.android) 6 | alias(libs.plugins.kotlin.compose) 7 | alias(libs.plugins.arturbosch.detekt) 8 | alias(libs.plugins.maven.publish) 9 | alias(libs.plugins.kotlin.parcelize) 10 | alias(libs.plugins.signing) 11 | } 12 | 13 | val versionName = project.findProperty("VERSION_NAME") as String? ?: "0.0.1" 14 | android { 15 | namespace = "com.nareshchocha.filepickerlibrary" 16 | compileSdk = libs.versions.compileSdk.get().toInt() 17 | 18 | defaultConfig { 19 | minSdk = libs.versions.minSdk.get().toInt() 20 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 21 | } 22 | buildTypes { 23 | release { 24 | isMinifyEnabled = false 25 | proguardFiles( 26 | getDefaultProguardFile("proguard-android-optimize.txt"), 27 | "proguard-rules.pro" 28 | ) 29 | } 30 | } 31 | 32 | buildFeatures { 33 | compose = true 34 | aidl = false 35 | buildConfig = false 36 | renderScript = false 37 | shaders = false 38 | } 39 | compileOptions { 40 | sourceCompatibility = JavaVersion.valueOf(libs.versions.jdkVersion.get()) 41 | targetCompatibility = JavaVersion.valueOf(libs.versions.jdkVersion.get()) 42 | } 43 | 44 | kotlinOptions { 45 | jvmTarget = JavaVersion.valueOf(libs.versions.jdkVersion.get()).toString() 46 | } 47 | } 48 | 49 | dependencies { 50 | 51 | implementation(libs.androidx.startup.runtime) 52 | // compose 53 | implementation(libs.androidx.activity.compose) 54 | implementation(platform(libs.androidx.compose.bom)) 55 | implementation(libs.androidx.ui) 56 | implementation(libs.androidx.ui.graphics) 57 | implementation(libs.androidx.ui.tooling.preview) 58 | implementation(libs.androidx.material3) 59 | implementation(libs.androidx.material.icons.extended) 60 | 61 | // timber 62 | implementation(libs.timber) 63 | 64 | 65 | // testing 66 | testImplementation(libs.junit) 67 | androidTestImplementation(libs.androidx.junit) 68 | androidTestImplementation(libs.androidx.espresso.core) 69 | testImplementation(libs.truth) 70 | androidTestImplementation(libs.truth) 71 | 72 | 73 | // testing compose 74 | androidTestImplementation(platform(libs.androidx.compose.bom)) 75 | androidTestImplementation(libs.androidx.ui.test.junit4) 76 | debugImplementation(libs.androidx.ui.tooling) 77 | debugImplementation(libs.androidx.ui.test.manifest) 78 | } 79 | 80 | detekt { 81 | toolVersion = libs.versions.detekt.get() 82 | config.setFrom("$projectDir/config/detekt/detekt.yml") 83 | buildUponDefaultConfig = true 84 | } 85 | 86 | mavenPublishing { 87 | publishToMavenCentral(SonatypeHost.S01,true) 88 | signAllPublications() 89 | coordinates("io.github.chochanaresh", "filepicker", versionName) 90 | 91 | pom { 92 | name.set("filepicker") 93 | description.set("All file and media picker library for android. This library is designed to simplify the process of selecting and retrieving media files from an Android device, and supports media capture for images and videos.") 94 | inceptionYear.set("2023") 95 | url.set("https://github.com/ChochaNaresh/FilePicker") 96 | licenses { 97 | license { 98 | name.set("The Apache License, Version 2.0") 99 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 100 | distribution.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 101 | } 102 | } 103 | developers { 104 | developer { 105 | id.set("ChochaNaresh") 106 | name.set("Naresh Chocha") 107 | url.set("https://github.com/ChochaNaresh") 108 | } 109 | } 110 | scm { 111 | url.set("https://github.com/ChochaNaresh/FilePicker") 112 | connection.set("scm:git:git://github.com/ChochaNaresh/FilePicker.git") 113 | developerConnection.set("scm:git:ssh://git@github.com/ChochaNaresh/FilePicker.git") 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /filepickerlibrary/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.kts. 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 22 | -keepclasseswithmembernames class com.nareshchocha.filepickerlibrary.models.**{ 23 | *; 24 | } 25 | #-keepclassmembers class * extends com.nareshchocha.filepickerlibrary.models.BaseConfig { 26 | # *; 27 | #} 28 | #-keepclassmembers class * extends com.nareshchocha.filepickerlibrary.models.PickMediaType { 29 | # *; 30 | #} 31 | 32 | -keepclassmembers class * extends androidx.appcompat.app.AppCompatActivity { 33 | *; 34 | } 35 | -------------------------------------------------------------------------------- /filepickerlibrary/src/androidTest/java/com/nareshchocha/filepickerlibrary/ui/FilePickerBuilderTest.kt: -------------------------------------------------------------------------------- 1 | package com.nareshchocha.filepickerlibrary.ui 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import android.util.Log 6 | import androidx.test.filters.SmallTest 7 | import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner 8 | import androidx.test.platform.app.InstrumentationRegistry 9 | import com.google.common.truth.Truth.assertThat 10 | import com.nareshchocha.filepickerlibrary.models.DocumentFilePickerConfig 11 | import com.nareshchocha.filepickerlibrary.models.ImageCaptureConfig 12 | import com.nareshchocha.filepickerlibrary.models.PickMediaConfig 13 | import com.nareshchocha.filepickerlibrary.models.VideoCaptureConfig 14 | import com.nareshchocha.filepickerlibrary.utilities.appConst.Const 15 | import org.junit.Before 16 | import org.junit.Test 17 | import org.junit.runner.RunWith 18 | 19 | @RunWith(AndroidJUnit4ClassRunner::class) 20 | @SmallTest 21 | class FilePickerBuilderTest { 22 | lateinit var appContext: Context 23 | 24 | @Before 25 | fun setup() { 26 | appContext = InstrumentationRegistry.getInstrumentation().targetContext 27 | } 28 | 29 | 30 | 31 | @Test 32 | fun testImageCaptureBuild() { 33 | val mImageCaptureConfig = ImageCaptureConfig() 34 | val intentActivity = FilePicker.Builder(appContext).imageCaptureBuild(mImageCaptureConfig) 35 | val data = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 36 | intentActivity.getParcelableExtra( 37 | Const.BundleInternalExtras.IMAGE_CAPTURE, 38 | ImageCaptureConfig::class.java, 39 | ) 40 | } else { 41 | @Suppress("DEPRECATION") 42 | intentActivity.getParcelableExtra(Const.BundleInternalExtras.IMAGE_CAPTURE) as ImageCaptureConfig? 43 | } 44 | assertThat(data).isSameInstanceAs(mImageCaptureConfig) 45 | } 46 | 47 | @Test 48 | fun testVideoCaptureBuild() { 49 | val mVideoCaptureConfig = VideoCaptureConfig() 50 | val intentActivity = FilePicker.Builder(appContext).videoCaptureBuild(mVideoCaptureConfig) 51 | val data = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 52 | intentActivity.getParcelableExtra( 53 | Const.BundleInternalExtras.VIDEO_CAPTURE, 54 | VideoCaptureConfig::class.java, 55 | ) 56 | } else { 57 | @Suppress("DEPRECATION") 58 | intentActivity.getParcelableExtra(Const.BundleInternalExtras.VIDEO_CAPTURE) as VideoCaptureConfig? 59 | } 60 | assertThat(data).isSameInstanceAs(mVideoCaptureConfig) 61 | } 62 | 63 | @Test 64 | fun testPickMediaBuild() { 65 | val mPickMediaConfig = PickMediaConfig() 66 | val intentActivity = FilePicker.Builder(appContext).pickMediaBuild(mPickMediaConfig) 67 | val data = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 68 | intentActivity.getParcelableExtra( 69 | Const.BundleInternalExtras.PICK_MEDIA, 70 | PickMediaConfig::class.java, 71 | ) 72 | } else { 73 | @Suppress("DEPRECATION") 74 | intentActivity.getParcelableExtra(Const.BundleInternalExtras.PICK_MEDIA) as PickMediaConfig? 75 | } 76 | assertThat(data).isSameInstanceAs(mPickMediaConfig) 77 | } 78 | 79 | @Test 80 | fun testDocumentFilePickerBuild() { 81 | val mDocumentFilePickerConfig = DocumentFilePickerConfig() 82 | val intentActivity = 83 | FilePicker.Builder(appContext).pickDocumentFileBuild(mDocumentFilePickerConfig) 84 | val data = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 85 | intentActivity.getParcelableExtra( 86 | Const.BundleInternalExtras.PICK_DOCUMENT, 87 | DocumentFilePickerConfig::class.java, 88 | ) 89 | } else { 90 | @Suppress("DEPRECATION") 91 | intentActivity.getParcelableExtra(Const.BundleInternalExtras.PICK_DOCUMENT) as DocumentFilePickerConfig? 92 | } 93 | assertThat(data).isSameInstanceAs(mDocumentFilePickerConfig) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /filepickerlibrary/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 15 | 17 | 18 | 23 | 27 | 31 | 35 | 39 | 43 | 47 | 51 | 55 | 59 | 63 | 64 | 71 | 78 | 81 | 82 | 86 | 91 | 95 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /filepickerlibrary/src/main/java/com/nareshchocha/filepickerlibrary/initializer/TimberInitializer.kt: -------------------------------------------------------------------------------- 1 | package com.nareshchocha.filepickerlibrary.initializer 2 | 3 | import android.content.Context 4 | import androidx.startup.Initializer 5 | import timber.log.Timber 6 | 7 | class TimberInitializer : Initializer { 8 | companion object { 9 | private val TAG: String = TimberInitializer::class.java.name 10 | } 11 | 12 | override fun create(context: Context) { 13 | // if (BuildConfig.DEBUG) { 14 | Timber.plant(Timber.DebugTree()) 15 | Timber.tag(TAG).v("TimberInitializer is initialized.") 16 | //Timber.tag(TAG).v("Delete copy Files") 17 | /*GlobalScope.launch(Dispatchers.IO) { 18 | try { 19 | deleteFiles(context.cacheDir.path+"/"+ copyFileFolder) 20 | *//*val isDeleted = File(context.cacheDir, copyFileFolder).deleteOnExit() 21 | Timber.tag(TAG).e("Delete Files isDeleted:: $isDeleted")*//* 22 | } catch (e: Exception) { 23 | Timber.tag(TAG).e("Delete Files Exception :: $e") 24 | } 25 | this.cancel() 26 | }.start()*/ 27 | } 28 | /*fun deleteFiles(path: String) { 29 | val file = File(path) 30 | if (file.exists()) { 31 | val deleteCmd = "rm -r $path" 32 | val runtime = Runtime.getRuntime() 33 | try { 34 | Timber.tag(TAG).e(" BeFear Files Size:: ${File(path).list()?.size}") 35 | runtime.exec(deleteCmd) 36 | Timber.tag(TAG).e(" After Files Size:: ${File(path).list()?.size}") 37 | Timber.tag(TAG).e("Delete Files isDeleted:: Done") 38 | } catch (e: IOException) { 39 | Timber.tag(TAG).e("deleteFiles Exception :: $e") 40 | } 41 | } 42 | }*/ 43 | 44 | override fun dependencies(): List>> = emptyList() 45 | } 46 | -------------------------------------------------------------------------------- /filepickerlibrary/src/main/java/com/nareshchocha/filepickerlibrary/models/BaseConfig.kt: -------------------------------------------------------------------------------- 1 | package com.nareshchocha.filepickerlibrary.models 2 | 3 | import android.os.Parcelable 4 | import androidx.annotation.Keep 5 | import kotlinx.parcelize.Parcelize 6 | 7 | @Keep() 8 | @Parcelize 9 | open class BaseConfig( 10 | open val popUpIcon: Int?, 11 | open val popUpText: String?, 12 | open val askPermissionTitle: String?, 13 | open val askPermissionMessage: String?, 14 | open val settingPermissionTitle: String?, 15 | open val settingPermissionMessage: String?, 16 | ) : Parcelable 17 | -------------------------------------------------------------------------------- /filepickerlibrary/src/main/java/com/nareshchocha/filepickerlibrary/models/PickerData.kt: -------------------------------------------------------------------------------- 1 | package com.nareshchocha.filepickerlibrary.models 2 | 3 | import android.graphics.drawable.shapes.Shape 4 | import android.os.Build 5 | import android.os.Parcelable 6 | import android.provider.MediaStore 7 | import androidx.annotation.DrawableRes 8 | import androidx.annotation.Keep 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.unit.Dp 11 | import com.nareshchocha.filepickerlibrary.R 12 | import com.nareshchocha.filepickerlibrary.utilities.appConst.Const 13 | import kotlinx.parcelize.Parcelize 14 | import java.io.File 15 | 16 | @Keep 17 | @Parcelize 18 | data class PickerData( 19 | val mPopUpConfig: PopUpConfig? = null, 20 | val listIntents: List = emptyList(), 21 | ) : Parcelable 22 | 23 | @Keep 24 | @Parcelize 25 | data class PopUpConfig( 26 | val chooserTitle: String? = null, 27 | val mPopUpType: PopUpType? = null, 28 | val mOrientation: Orientation? = null, 29 | val cornerSize: Float = 8f, 30 | val title: @Composable ((title: String) -> Unit)? = null, 31 | val item: @Composable ((item: BaseConfig) -> Unit)? = null, 32 | ) : Parcelable 33 | 34 | enum class Orientation { 35 | HORIZONTAL, 36 | VERTICAL; 37 | 38 | fun isHorizontal() = this == HORIZONTAL 39 | fun isVertical() = this == VERTICAL 40 | } 41 | 42 | @Parcelize 43 | data class ImageCaptureConfig( 44 | @DrawableRes override val popUpIcon: Int? = R.drawable.ic_camera, 45 | override val popUpText: String? = "Camera", 46 | val mFolder: File? = null, 47 | val fileName: String? = Const.DefaultPaths.defaultImageFile(), 48 | /** 49 | * It is not working correctly. However, it will Work on the same devices. 50 | */ 51 | val isUseRearCamera: Boolean? = true, 52 | override val askPermissionTitle: String? = null, 53 | override val askPermissionMessage: String? = null, 54 | override val settingPermissionTitle: String? = null, 55 | override val settingPermissionMessage: String? = null, 56 | ) : Parcelable, BaseConfig( 57 | popUpIcon, 58 | popUpText, 59 | askPermissionTitle, 60 | askPermissionMessage, 61 | settingPermissionTitle, 62 | settingPermissionMessage, 63 | ) 64 | 65 | @Parcelize 66 | data class VideoCaptureConfig( 67 | @DrawableRes override val popUpIcon: Int? = R.drawable.ic_video, 68 | override val popUpText: String? = "Video", 69 | val mFolder: File? = null, 70 | val fileName: String? = Const.DefaultPaths.defaultVideoFile(), 71 | val maxSeconds: Int? = null, 72 | val maxSizeLimit: Long? = null, 73 | val isHighQuality: Boolean? = null, 74 | override val askPermissionTitle: String? = null, 75 | override val askPermissionMessage: String? = null, 76 | override val settingPermissionTitle: String? = null, 77 | override val settingPermissionMessage: String? = null, 78 | ) : Parcelable, BaseConfig( 79 | popUpIcon, 80 | popUpText, 81 | askPermissionTitle, 82 | askPermissionMessage, 83 | settingPermissionTitle, 84 | settingPermissionMessage, 85 | ) 86 | 87 | @Parcelize 88 | data class PickMediaConfig( 89 | @DrawableRes override val popUpIcon: Int? = R.drawable.ic_media, 90 | override val popUpText: String? = "Pick Media", 91 | val allowMultiple: Boolean? = false, 92 | /** 93 | * MaxFiles work after SDK 33 or above versions 94 | */ 95 | val maxFiles: Int? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 96 | MediaStore.getPickImagesMaxLimit() 97 | } else { 98 | Int.MAX_VALUE 99 | }, 100 | val mPickMediaType: PickMediaType? = PickMediaType.ImageAndVideo, 101 | override val askPermissionTitle: String? = null, 102 | override val askPermissionMessage: String? = null, 103 | override val settingPermissionTitle: String? = null, 104 | override val settingPermissionMessage: String? = null, 105 | ) : Parcelable, BaseConfig( 106 | popUpIcon, 107 | popUpText, 108 | askPermissionTitle, 109 | askPermissionMessage, 110 | settingPermissionTitle, 111 | settingPermissionMessage, 112 | ) { 113 | fun getPickMediaType(input: PickMediaType?): String? { 114 | return when (input) { 115 | PickMediaType.ImageOnly -> "image/*" 116 | PickMediaType.VideoOnly -> "video/*" 117 | PickMediaType.ImageAndVideo -> null 118 | else -> null 119 | } 120 | } 121 | } 122 | 123 | @Parcelize 124 | data class DocumentFilePickerConfig( 125 | @DrawableRes override val popUpIcon: Int? = R.drawable.ic_file, 126 | override val popUpText: String? = "File Media", 127 | val allowMultiple: Boolean? = false, 128 | /** 129 | * MaxFiles work after SDK 33 or above versions 130 | */ 131 | val maxFiles: Int? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 132 | MediaStore.getPickImagesMaxLimit() 133 | } else { 134 | Int.MAX_VALUE 135 | }, 136 | val mMimeTypes: List? = listOf("*/*"), 137 | override val askPermissionTitle: String? = null, 138 | override val askPermissionMessage: String? = null, 139 | override val settingPermissionTitle: String? = null, 140 | override val settingPermissionMessage: String? = null, 141 | ) : Parcelable, BaseConfig( 142 | popUpIcon, 143 | popUpText, 144 | askPermissionTitle, 145 | askPermissionMessage, 146 | settingPermissionTitle, 147 | settingPermissionMessage, 148 | ) 149 | 150 | @Keep 151 | @Parcelize 152 | enum class PopUpType : Parcelable { 153 | BOTTOM_SHEET, DIALOG; 154 | 155 | fun isDialog() = this == DIALOG 156 | } 157 | 158 | @Parcelize 159 | enum class PickMediaType : Parcelable { 160 | ImageOnly, VideoOnly, ImageAndVideo 161 | } 162 | -------------------------------------------------------------------------------- /filepickerlibrary/src/main/java/com/nareshchocha/filepickerlibrary/picker/PickerUtils.kt: -------------------------------------------------------------------------------- 1 | package com.nareshchocha.filepickerlibrary.picker 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import androidx.annotation.Keep 6 | import androidx.core.content.FileProvider 7 | import com.nareshchocha.filepickerlibrary.utilities.appConst.Const 8 | import timber.log.Timber 9 | import java.io.File 10 | import java.io.IOException 11 | 12 | /** 13 | * Utility object that provides helper functions for file operations in the FilePicker library. 14 | * Contains methods for creating file directories, generating files, and obtaining content URIs. 15 | */ 16 | internal object PickerUtils { 17 | 18 | /** 19 | * Creates a file within the specified folder directory. 20 | * 21 | * @param folderFile The directory where the file should be created 22 | * @param fileName The name of the file to be created 23 | * @return A File object representing the newly created file path 24 | */ 25 | @Keep 26 | fun createMediaFileFolder( 27 | folderFile: File, 28 | fileName: String, 29 | ): File { 30 | if (!folderFile.exists()) { 31 | try { 32 | if (!folderFile.mkdirs()) { 33 | Timber.tag(Const.LogTag.FILE_PICKER_ERROR) 34 | .e("Failed to create directory: ${folderFile.path}") 35 | } 36 | } catch (e: SecurityException) { 37 | Timber.tag(Const.LogTag.FILE_PICKER_ERROR) 38 | .e(e, "Security exception creating directory: ${folderFile.path}") 39 | } 40 | } 41 | return File(folderFile.path + File.separator + fileName) 42 | } 43 | 44 | /** 45 | * Creates a file if it doesn't exist and returns its content URI. 46 | * The URI is created using a FileProvider for secure file sharing. 47 | * 48 | * @param mFile The File object to create and get URI for 49 | * @return A content URI for the file or null if creation or URI generation fails 50 | */ 51 | @Keep 52 | fun Context.createFileGetUri(mFile: File): Uri? { 53 | var uri: Uri? = null 54 | if (!mFile.exists()) { 55 | try { 56 | mFile.createNewFile() 57 | } catch (e: IOException) { 58 | Timber.tag(Const.LogTag.FILE_PICKER_ERROR) 59 | .e(e, "IO exception creating file: ${mFile.path}") 60 | } catch (e: SecurityException) { 61 | Timber.tag(Const.LogTag.FILE_PICKER_ERROR) 62 | .e(e, "Security exception creating file: ${mFile.path}") 63 | } 64 | } 65 | if (mFile.exists()) { 66 | try { 67 | uri = getUriForFile(mFile) 68 | } catch (e: IOException) { 69 | Timber.tag(Const.LogTag.FILE_PICKER_ERROR) 70 | .e(e, "Failed to get URI for file: ${mFile.path}") 71 | } 72 | } 73 | return uri 74 | } 75 | 76 | /** 77 | * Gets a content URI for a file using FileProvider. 78 | * 79 | * @param mFile The File object to get a URI for 80 | * @return A content URI for the file 81 | * @throws IllegalArgumentException if the file cannot be shared 82 | */ 83 | private fun Context.getUriForFile(mFile: File): Uri? { 84 | return FileProvider.getUriForFile( 85 | this.applicationContext, 86 | applicationContext.packageName + 87 | Const.AUTHORITY, 88 | mFile, 89 | ) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /filepickerlibrary/src/main/java/com/nareshchocha/filepickerlibrary/ui/FilePicker.kt: -------------------------------------------------------------------------------- 1 | package com.nareshchocha.filepickerlibrary.ui 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Build 6 | import android.provider.MediaStore 7 | import androidx.annotation.Keep 8 | import com.nareshchocha.filepickerlibrary.R 9 | import com.nareshchocha.filepickerlibrary.models.BaseConfig 10 | import com.nareshchocha.filepickerlibrary.models.DocumentFilePickerConfig 11 | import com.nareshchocha.filepickerlibrary.models.ImageCaptureConfig 12 | import com.nareshchocha.filepickerlibrary.models.Orientation 13 | import com.nareshchocha.filepickerlibrary.models.PickMediaConfig 14 | import com.nareshchocha.filepickerlibrary.models.PickMediaType 15 | import com.nareshchocha.filepickerlibrary.models.PickerData 16 | import com.nareshchocha.filepickerlibrary.models.PopUpConfig 17 | import com.nareshchocha.filepickerlibrary.models.PopUpType 18 | import com.nareshchocha.filepickerlibrary.models.VideoCaptureConfig 19 | import com.nareshchocha.filepickerlibrary.ui.activitys.DocumentFilePickerActivity 20 | import com.nareshchocha.filepickerlibrary.ui.activitys.ImageCaptureActivity 21 | import com.nareshchocha.filepickerlibrary.ui.activitys.MediaFilePickerActivity 22 | import com.nareshchocha.filepickerlibrary.ui.activitys.PopUpActivity 23 | import com.nareshchocha.filepickerlibrary.ui.activitys.VideoCaptureActivity 24 | import com.nareshchocha.filepickerlibrary.utilities.appConst.Const 25 | 26 | @Keep 27 | class FilePicker private constructor() { 28 | 29 | @Keep 30 | class Builder(private val context: Context) { 31 | private val listIntents: ArrayList = ArrayList() 32 | private var mPopUpConfig: PopUpConfig? = null 33 | 34 | @Keep 35 | fun setPopUpConfig(mPopUpConfig: PopUpConfig? = null): Builder { 36 | this.mPopUpConfig = PopUpConfig( 37 | chooserTitle = mPopUpConfig?.chooserTitle ?: "Choose Option", 38 | mPopUpType = mPopUpConfig?.mPopUpType ?: PopUpType.BOTTOM_SHEET, 39 | mOrientation = mPopUpConfig?.mOrientation ?: Orientation.VERTICAL, 40 | ) 41 | return this 42 | } 43 | 44 | @Keep 45 | fun addImageCapture(mImageCaptureConfig: ImageCaptureConfig? = null): Builder { 46 | listIntents.add( 47 | mImageCaptureConfig?: ImageCaptureConfig(), 48 | ) 49 | return this 50 | } 51 | 52 | @Keep 53 | fun addVideoCapture(mVideoCaptureConfig: VideoCaptureConfig? = null): Builder { 54 | listIntents.add( 55 | mVideoCaptureConfig?:VideoCaptureConfig(), 56 | ) 57 | return this 58 | } 59 | 60 | @Keep 61 | fun addPickMedia(mPickMediaConfig: PickMediaConfig? = null): Builder { 62 | listIntents.add( 63 | mPickMediaConfig ?: PickMediaConfig(), 64 | ) 65 | return this 66 | } 67 | 68 | @Keep 69 | fun addPickDocumentFile(mDocumentFilePickerConfig: DocumentFilePickerConfig? = null): Builder { 70 | listIntents.add(mDocumentFilePickerConfig ?: DocumentFilePickerConfig()) 71 | return this 72 | } 73 | 74 | @Keep 75 | fun imageCaptureBuild(mImageCaptureConfig: ImageCaptureConfig? = null): Intent = 76 | ImageCaptureActivity.getInstance( 77 | context, 78 | mImageCaptureConfig, 79 | ) 80 | 81 | @Keep 82 | fun videoCaptureBuild(mVideoCaptureConfig: VideoCaptureConfig? = null): Intent = 83 | VideoCaptureActivity.getInstance( 84 | context, 85 | mVideoCaptureConfig, 86 | ) 87 | 88 | @Keep 89 | fun pickMediaBuild(mPickMediaConfig: PickMediaConfig? = null): Intent = 90 | MediaFilePickerActivity.getInstance( 91 | context, 92 | mPickMediaConfig, 93 | ) 94 | 95 | @Keep 96 | fun pickDocumentFileBuild(mDocumentFilePickerConfig: DocumentFilePickerConfig? = null): Intent = 97 | DocumentFilePickerActivity.getInstance( 98 | context, 99 | mDocumentFilePickerConfig, 100 | ) 101 | 102 | @Keep 103 | fun build(): Intent = PopUpActivity.getInstance( 104 | context, 105 | PickerData(mPopUpConfig = mPopUpConfig, listIntents = listIntents), 106 | ) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /filepickerlibrary/src/main/java/com/nareshchocha/filepickerlibrary/ui/activitys/DocumentFilePickerActivity.kt: -------------------------------------------------------------------------------- 1 | package com.nareshchocha.filepickerlibrary.ui.activitys 2 | 3 | import android.Manifest 4 | import android.annotation.SuppressLint 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.pm.PackageManager 8 | import android.net.Uri 9 | import android.os.Build 10 | import android.os.Bundle 11 | import androidx.activity.ComponentActivity 12 | import androidx.activity.compose.ManagedActivityResultLauncher 13 | import androidx.activity.compose.rememberLauncherForActivityResult 14 | import androidx.activity.compose.setContent 15 | import androidx.activity.result.ActivityResult 16 | import androidx.activity.result.contract.ActivityResultContracts 17 | import androidx.annotation.Keep 18 | import androidx.compose.material3.AlertDialog 19 | import androidx.compose.material3.ExperimentalMaterial3Api 20 | import androidx.compose.material3.Text 21 | import androidx.compose.material3.TextButton 22 | import androidx.compose.runtime.Composable 23 | import androidx.compose.runtime.LaunchedEffect 24 | import androidx.compose.runtime.getValue 25 | import androidx.compose.runtime.mutableStateOf 26 | import androidx.compose.runtime.remember 27 | import androidx.compose.runtime.setValue 28 | import androidx.compose.ui.platform.LocalContext 29 | import androidx.compose.ui.res.stringResource 30 | import androidx.core.app.ActivityCompat 31 | import com.nareshchocha.filepickerlibrary.R 32 | import com.nareshchocha.filepickerlibrary.models.DocumentFilePickerConfig 33 | import com.nareshchocha.filepickerlibrary.utilities.FileUtils 34 | import com.nareshchocha.filepickerlibrary.utilities.appConst.Const 35 | import com.nareshchocha.filepickerlibrary.utilities.extentions.getDocumentFilePick 36 | import com.nareshchocha.filepickerlibrary.utilities.extentions.getRequestedPermissions 37 | import com.nareshchocha.filepickerlibrary.utilities.extentions.getSettingIntent 38 | import com.nareshchocha.filepickerlibrary.utilities.extentions.setCanceledResult 39 | import com.nareshchocha.filepickerlibrary.utilities.extentions.setSuccessResult 40 | import timber.log.Timber 41 | 42 | @OptIn(ExperimentalMaterial3Api::class) 43 | internal class DocumentFilePickerActivity : ComponentActivity() { 44 | 45 | private val mDocumentFilePickerConfig: DocumentFilePickerConfig? by lazy { 46 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 47 | intent.getParcelableExtra( 48 | Const.BundleInternalExtras.PICK_DOCUMENT, 49 | DocumentFilePickerConfig::class.java, 50 | ) 51 | } else { 52 | @Suppress("DEPRECATION") 53 | intent.getParcelableExtra(Const.BundleInternalExtras.PICK_DOCUMENT) as DocumentFilePickerConfig? 54 | } 55 | } 56 | 57 | override fun onCreate(savedInstanceState: Bundle?) { 58 | super.onCreate(savedInstanceState) 59 | setContent { 60 | StatingDocument() 61 | } 62 | } 63 | 64 | @Composable 65 | fun StatingDocument() { 66 | var showAskDialog by remember { mutableStateOf(false) } 67 | var showGotoSettingDialog by remember { mutableStateOf(false) } 68 | var permissionRequested by remember { mutableStateOf(false) } 69 | 70 | val filePickerLauncher = rememberFilePickerLauncher() 71 | val permissionLauncher = rememberPermissionLauncher( 72 | onGranted = { launchFilePicker(filePickerLauncher) }, 73 | onShowRationale = { showAskDialog = true }, 74 | onDenied = { showGotoSettingDialog = true } 75 | ) 76 | val settingLauncher = rememberSettingsLauncher( 77 | onGranted = { launchFilePicker(filePickerLauncher) }, 78 | onDenied = { 79 | setCanceledResult(getString(R.string.err_permission_result)) 80 | finish() 81 | } 82 | ) 83 | 84 | // Initial permission check 85 | CheckInitialPermissions( 86 | permissionRequested = permissionRequested, 87 | onPermissionRequested = { permissionRequested = true }, 88 | onCheckPermission = { checkPermissionFlow(permissionLauncher) }, 89 | onLaunchPicker = { launchFilePicker(filePickerLauncher) } 90 | ) 91 | 92 | // Permission dialogs 93 | if (showAskDialog) { 94 | ShowAskPermissionDialog( 95 | mDocumentFilePickerConfig = mDocumentFilePickerConfig, 96 | onConfirm = { 97 | showAskDialog = false 98 | permissionLauncher.launch(getPermission()) 99 | }, 100 | onDismiss = { 101 | setCanceledResult(getString(R.string.err_permission_result)) 102 | finish() 103 | } 104 | ) 105 | } 106 | 107 | if (showGotoSettingDialog) { 108 | ShowSettingsPermissionDialog( 109 | onConfirm = { 110 | showGotoSettingDialog = false 111 | settingLauncher.launch(getSettingIntent()) 112 | }, 113 | onDismiss = { 114 | setCanceledResult(getString(R.string.err_permission_result)) 115 | finish() 116 | } 117 | ) 118 | } 119 | } 120 | 121 | @Composable 122 | private fun rememberFilePickerLauncher(): ManagedActivityResultLauncher { 123 | return rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> 124 | handleFilePickerResult(result) 125 | } 126 | } 127 | 128 | 129 | @Composable 130 | private fun CheckInitialPermissions( 131 | permissionRequested: Boolean, 132 | onPermissionRequested: () -> Unit, 133 | onCheckPermission: () -> Unit, 134 | onLaunchPicker: () -> Unit 135 | ) { 136 | LaunchedEffect(Unit) { 137 | if (!permissionRequested) { 138 | onPermissionRequested() 139 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 140 | onCheckPermission() 141 | } else { 142 | onLaunchPicker() 143 | } 144 | } 145 | } 146 | } 147 | 148 | 149 | @Composable 150 | private fun ShowSettingsPermissionDialog( 151 | onConfirm: () -> Unit, 152 | onDismiss: () -> Unit 153 | ) { 154 | AlertDialog( 155 | onDismissRequest = onDismiss, 156 | title = { 157 | Text( 158 | mDocumentFilePickerConfig?.settingPermissionTitle 159 | ?: getString(R.string.err_permission_denied) 160 | ) 161 | }, 162 | text = { 163 | Text( 164 | mDocumentFilePickerConfig?.settingPermissionMessage ?: getString( 165 | R.string.err_write_storage_setting, 166 | getPermission().split(".").lastOrNull() ?: "", 167 | ) 168 | ) 169 | }, 170 | confirmButton = { 171 | TextButton(onClick = onConfirm) { Text(getString(R.string.str_go_to_setting)) } 172 | }, 173 | dismissButton = { 174 | TextButton(onClick = onDismiss) { Text(getString(android.R.string.cancel)) } 175 | } 176 | ) 177 | } 178 | 179 | private fun checkPermissionFlow(permissionLauncher: ManagedActivityResultLauncher) { 180 | if (mDocumentFilePickerConfig != null) { 181 | val list = getPermissionManifestCheck(this) 182 | if (list.isEmpty()) { 183 | setCanceledResult(getString(R.string.permission_not_found)) 184 | finish() 185 | } else { 186 | permissionLauncher.launch(getPermission()) 187 | } 188 | } else { 189 | setCanceledResult( 190 | getString( 191 | R.string.err_config_null, 192 | this::mDocumentFilePickerConfig::class.java.name 193 | ) 194 | ) 195 | finish() 196 | } 197 | } 198 | 199 | private fun launchFilePicker(filePickerLauncher: ManagedActivityResultLauncher) { 200 | if (mDocumentFilePickerConfig != null) { 201 | filePickerLauncher.launch(getDocumentFilePick(mDocumentFilePickerConfig!!)) 202 | } else { 203 | setCanceledResult( 204 | getString( 205 | R.string.err_config_null, 206 | this::mDocumentFilePickerConfig::class.java.name 207 | ) 208 | ) 209 | finish() 210 | } 211 | } 212 | 213 | private fun handleFilePickerResult(result: ActivityResult) { 214 | if (result.resultCode == RESULT_OK && result.data != null) { 215 | if (mDocumentFilePickerConfig?.allowMultiple == true && result.data?.clipData != null) { 216 | val uris = result.data?.getClipDataUris() 217 | val filePaths = uris?.getFilePathList(this) 218 | Timber.tag(Const.LogTag.FILE_RESULT).v("File Uri ::: $uris") 219 | Timber.tag(Const.LogTag.FILE_RESULT).v("filePath ::: $filePaths") 220 | setSuccessResult(uris, filePath = filePaths) 221 | } else if (result.data?.data != null) { 222 | val data = result.data?.data 223 | val filePath = data?.let { FileUtils.getRealPath(this, it) } 224 | Timber.tag(Const.LogTag.FILE_RESULT).v("File Uri ::: ${data?.toString()}") 225 | Timber.tag(Const.LogTag.FILE_RESULT).v("filePath ::: $filePath") 226 | setSuccessResult(data, filePath) 227 | } 228 | } else { 229 | Timber.tag(Const.LogTag.FILE_PICKER_ERROR) 230 | .v(getString(R.string.err_document_pick_error)) 231 | setCanceledResult(getString(R.string.err_document_pick_error)) 232 | } 233 | finish() 234 | } 235 | 236 | private fun Intent.getClipDataUris(): ArrayList { 237 | val resultSet = LinkedHashSet() 238 | data?.let { resultSet.add(it) } 239 | val clipData = clipData 240 | if (clipData == null && resultSet.isEmpty()) { 241 | return ArrayList() 242 | } else if (clipData != null) { 243 | for (i in 0 until clipData.itemCount) { 244 | val uri = clipData.getItemAt(i).uri 245 | if (uri != null) { 246 | resultSet.add(uri) 247 | } 248 | } 249 | } 250 | return ArrayList(resultSet) 251 | } 252 | 253 | private fun List.getFilePathList(context: Context): ArrayList { 254 | val filePathList = ArrayList() 255 | forEach { uri -> 256 | FileUtils.getRealPath(context, uri)?.also { filePath -> 257 | filePathList.add(filePath) 258 | } 259 | } 260 | return filePathList 261 | } 262 | 263 | companion object { 264 | private fun getPermissionManifestCheck(context: Context) = ArrayList().also { 265 | val permissions = context.getRequestedPermissions() 266 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 267 | if (permissions?.contains(Manifest.permission.READ_MEDIA_VIDEO) == true) { 268 | it.add(Manifest.permission.READ_MEDIA_VIDEO) 269 | } 270 | } else { 271 | if (permissions?.contains(Manifest.permission.READ_EXTERNAL_STORAGE) == true) { 272 | it.add(Manifest.permission.READ_EXTERNAL_STORAGE) 273 | } 274 | } 275 | } 276 | 277 | 278 | @Keep 279 | fun getInstance( 280 | mContext: Context, 281 | mDocumentFilePickerConfig: DocumentFilePickerConfig?, 282 | ): Intent { 283 | val filePickerIntent = Intent(mContext, DocumentFilePickerActivity::class.java) 284 | mDocumentFilePickerConfig?.let { 285 | filePickerIntent.putExtra(Const.BundleInternalExtras.PICK_DOCUMENT, it) 286 | } 287 | return filePickerIntent 288 | } 289 | } 290 | } 291 | 292 | @SuppressLint("ContextCastToActivity") 293 | @Composable 294 | private fun rememberPermissionLauncher( 295 | onGranted: () -> Unit, 296 | onShowRationale: () -> Unit, 297 | onDenied: () -> Unit 298 | ): ManagedActivityResultLauncher { 299 | val activity = LocalContext.current as ComponentActivity 300 | return rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> 301 | if (granted) { 302 | onGranted() 303 | } else if (ActivityCompat.shouldShowRequestPermissionRationale( 304 | activity, 305 | getPermission() 306 | ) 307 | ) { 308 | onShowRationale() 309 | } else { 310 | onDenied() 311 | } 312 | } 313 | } 314 | 315 | @Composable 316 | private fun rememberSettingsLauncher( 317 | onGranted: () -> Unit, 318 | onDenied: () -> Unit 319 | ): ManagedActivityResultLauncher { 320 | val context = LocalContext.current 321 | return rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { 322 | if (ActivityCompat.checkSelfPermission( 323 | context, 324 | getPermission() 325 | ) == PackageManager.PERMISSION_GRANTED 326 | ) { 327 | onGranted() 328 | } else { 329 | onDenied() 330 | } 331 | } 332 | } 333 | 334 | private fun getPermission(): String { 335 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 336 | Manifest.permission.READ_MEDIA_VIDEO 337 | } else { 338 | Manifest.permission.READ_EXTERNAL_STORAGE 339 | } 340 | } 341 | 342 | @Composable 343 | private fun ShowAskPermissionDialog( 344 | mDocumentFilePickerConfig: DocumentFilePickerConfig? = null, 345 | onConfirm: () -> Unit, 346 | onDismiss: () -> Unit 347 | ) { 348 | AlertDialog( 349 | onDismissRequest = onDismiss, 350 | title = { 351 | Text( 352 | mDocumentFilePickerConfig?.askPermissionTitle 353 | ?: stringResource(R.string.err_permission_denied) 354 | ) 355 | }, 356 | text = { 357 | Text( 358 | mDocumentFilePickerConfig?.askPermissionMessage ?: stringResource( 359 | R.string.err_write_storage_permission, 360 | getPermission().split(".").lastOrNull() ?: "", 361 | ) 362 | ) 363 | }, 364 | confirmButton = { 365 | TextButton(onClick = onConfirm) { Text(stringResource(android.R.string.ok)) } 366 | }, 367 | dismissButton = { 368 | TextButton(onClick = onDismiss) { Text(stringResource(android.R.string.cancel)) } 369 | } 370 | ) 371 | } 372 | -------------------------------------------------------------------------------- /filepickerlibrary/src/main/java/com/nareshchocha/filepickerlibrary/ui/activitys/ImageCaptureActivity.kt: -------------------------------------------------------------------------------- 1 | package com.nareshchocha.filepickerlibrary.ui.activitys 2 | 3 | import android.Manifest 4 | import android.app.Activity 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.pm.PackageManager 8 | import android.net.Uri 9 | import android.os.Build 10 | import android.os.Bundle 11 | import androidx.activity.ComponentActivity 12 | import androidx.activity.compose.rememberLauncherForActivityResult 13 | import androidx.activity.compose.setContent 14 | import androidx.activity.result.ActivityResultLauncher 15 | import androidx.activity.result.contract.ActivityResultContracts 16 | import androidx.annotation.Keep 17 | import androidx.compose.material3.ExperimentalMaterial3Api 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.LaunchedEffect 20 | import androidx.compose.runtime.getValue 21 | import androidx.compose.runtime.mutableStateOf 22 | import androidx.compose.runtime.remember 23 | import androidx.compose.runtime.setValue 24 | import androidx.compose.ui.Modifier 25 | import androidx.compose.ui.platform.LocalContext 26 | import androidx.compose.ui.res.stringResource 27 | import androidx.core.app.ActivityCompat 28 | import androidx.core.content.ContextCompat 29 | import com.nareshchocha.filepickerlibrary.R 30 | import com.nareshchocha.filepickerlibrary.models.ImageCaptureConfig 31 | import com.nareshchocha.filepickerlibrary.picker.PickerUtils.createFileGetUri 32 | import com.nareshchocha.filepickerlibrary.picker.PickerUtils.createMediaFileFolder 33 | import com.nareshchocha.filepickerlibrary.ui.components.dialogs.AppAlertDialog 34 | import com.nareshchocha.filepickerlibrary.utilities.appConst.Const 35 | import com.nareshchocha.filepickerlibrary.utilities.appConst.Const.DefaultPaths.defaultFolder 36 | import com.nareshchocha.filepickerlibrary.utilities.extentions.getImageCaptureIntent 37 | import com.nareshchocha.filepickerlibrary.utilities.extentions.getRequestedPermissions 38 | import com.nareshchocha.filepickerlibrary.utilities.extentions.getSettingIntent 39 | import com.nareshchocha.filepickerlibrary.utilities.extentions.setCanceledResult 40 | import com.nareshchocha.filepickerlibrary.utilities.extentions.setSuccessResult 41 | import timber.log.Timber 42 | import java.io.File 43 | 44 | @OptIn(ExperimentalMaterial3Api::class) 45 | internal class ImageCaptureActivity : ComponentActivity() { 46 | private var imageFileUri: Uri? = null 47 | private var imageFile: File? = null 48 | 49 | private val mImageCaptureConfig: ImageCaptureConfig? by lazy { 50 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 51 | intent.getParcelableExtra( 52 | Const.BundleInternalExtras.IMAGE_CAPTURE, 53 | ImageCaptureConfig::class.java, 54 | ) 55 | } else { 56 | @Suppress("DEPRECATION") 57 | intent.getParcelableExtra(Const.BundleInternalExtras.IMAGE_CAPTURE) as ImageCaptureConfig? 58 | } 59 | } 60 | 61 | override fun onCreate(savedInstanceState: Bundle?) { 62 | super.onCreate(savedInstanceState) 63 | setContent { 64 | SetUI() 65 | } 66 | } 67 | 68 | 69 | @Composable 70 | fun SetUI() { 71 | val context = LocalContext.current 72 | var showAskDialog by remember { mutableStateOf(false) } 73 | var showGotoSettingDialog by remember { mutableStateOf(false) } 74 | var permissionRequested by remember { mutableStateOf(false) } 75 | 76 | val imageCaptureLauncher = 77 | rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> 78 | handleImageCaptureResult(result.resultCode) 79 | } 80 | 81 | val permissionLauncher = 82 | rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result -> 83 | handlePermissionResult( 84 | result, 85 | context, 86 | imageCaptureLauncher 87 | ) { askDialog, settingDialog -> 88 | showAskDialog = askDialog 89 | showGotoSettingDialog = settingDialog 90 | } 91 | } 92 | 93 | val settingLauncher = 94 | rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { 95 | checkPermissionsAndLaunchCamera(context, imageCaptureLauncher) 96 | } 97 | 98 | // Initial permission check 99 | LaunchedEffect(Unit) { 100 | if (!permissionRequested) { 101 | permissionRequested = true 102 | checkAndRequestPermissions( 103 | context, 104 | permissionLauncher = { 105 | permissionLauncher.launch(getPermissionsList(context).toTypedArray()) 106 | }, 107 | launchCamera = { 108 | launchCamera(context, imageCaptureLauncher) 109 | }, 110 | ) 111 | } 112 | } 113 | // Ask Permission Dialog 114 | if (showAskDialog) { 115 | PermissionDialog(context, onConfirm = { 116 | showAskDialog = false 117 | permissionLauncher.launch(getPermissionsList(context).toTypedArray()) 118 | }, onDismiss = { 119 | setCanceledResult(getString(R.string.err_permission_result)) 120 | finish() 121 | }) 122 | } 123 | 124 | // Go to Setting Dialog 125 | if (showGotoSettingDialog) { 126 | SettingDialog(context, onConfirm = { 127 | showGotoSettingDialog = false 128 | settingLauncher.launch(getSettingIntent()) 129 | }, onDismiss = { 130 | setCanceledResult(getString(R.string.err_permission_result)) 131 | finish() 132 | }) 133 | } 134 | 135 | } 136 | 137 | private fun checkAndRequestPermissions( 138 | context: Context, 139 | permissionLauncher: (Array) -> Unit, 140 | launchCamera: () -> Unit 141 | ) { 142 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 143 | val permissionsList = getPermissionsList(context) 144 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q || permissionsList.isNotEmpty()) { 145 | permissionLauncher(permissionsList.toTypedArray()) 146 | } else { 147 | launchCamera() 148 | } 149 | } else { 150 | launchCamera() 151 | } 152 | } 153 | 154 | private fun checkPermissionsAndLaunchCamera( 155 | context: Context, 156 | imageCaptureLauncher: ActivityResultLauncher 157 | ) { 158 | val allGranted = getPermissionsList(context).all { permission -> 159 | ContextCompat.checkSelfPermission( 160 | context, 161 | permission, 162 | ) == PackageManager.PERMISSION_GRANTED 163 | } 164 | 165 | if (allGranted) { 166 | launchCamera(context, imageCaptureLauncher) 167 | } else { 168 | setCanceledResult(getString(R.string.err_permission_result)) 169 | finish() 170 | } 171 | } 172 | 173 | private fun handlePermissionResult( 174 | result: Map, 175 | context: Context, 176 | imageCaptureLauncher: ActivityResultLauncher, 177 | onShowDialog: (askDialog: Boolean, settingDialog: Boolean) -> Unit 178 | ) { 179 | val allGranted = result.all { permission -> 180 | ContextCompat.checkSelfPermission( 181 | context, 182 | permission.key, 183 | ) == PackageManager.PERMISSION_GRANTED 184 | } 185 | 186 | val isShowRationale = result.any { permission -> 187 | ActivityCompat.shouldShowRequestPermissionRationale( 188 | this, 189 | permission.key, 190 | ) 191 | } 192 | 193 | if (allGranted) { 194 | launchCamera(context, imageCaptureLauncher) 195 | } else if (isShowRationale) { 196 | onShowDialog(true, false) 197 | } else { 198 | onShowDialog(false, true) 199 | } 200 | } 201 | 202 | private fun handleImageCaptureResult(resultCode: Int) { 203 | if (resultCode == RESULT_OK) { 204 | Timber.tag(Const.LogTag.FILE_RESULT) 205 | .v("File Uri ::: ${imageFileUri?.toString()}") 206 | Timber.tag(Const.LogTag.FILE_RESULT) 207 | .v("filePath ::: ${imageFile?.absoluteFile}") 208 | Timber.tag(Const.LogTag.FILE_RESULT) 209 | .v("file read:: ${imageFile?.canRead()}") 210 | setSuccessResult(imageFileUri, imageFile?.absolutePath, true) 211 | } else { 212 | Timber.tag(Const.LogTag.FILE_PICKER_ERROR) 213 | .v(getString(R.string.err_capture_error, "imageCapture")) 214 | setCanceledResult(getString(R.string.err_capture_error, "imageCapture")) 215 | } 216 | finish() 217 | } 218 | 219 | @Composable 220 | private fun PermissionDialog( 221 | context: Context, 222 | onConfirm: () -> Unit, 223 | onDismiss: () -> Unit, 224 | ) { 225 | AppAlertDialog( 226 | onDismissRequest = { 227 | setCanceledResult(getString(R.string.err_permission_result)) 228 | finish() 229 | }, 230 | title = mImageCaptureConfig?.askPermissionTitle 231 | ?: stringResource(R.string.err_permission_denied), 232 | message = mImageCaptureConfig?.askPermissionMessage ?: stringResource( 233 | R.string.err_write_storage_permission, 234 | getPermissionsListString(context), 235 | ), 236 | onConfirm = onConfirm, 237 | onDismiss = onDismiss 238 | ) 239 | } 240 | 241 | @Composable 242 | private fun SettingDialog( 243 | context: Context, 244 | onConfirm: () -> Unit, 245 | onDismiss: () -> Unit, 246 | ) { 247 | AppAlertDialog( 248 | onDismissRequest = { 249 | setCanceledResult(getString(R.string.err_permission_result)) 250 | finish() 251 | }, 252 | title = mImageCaptureConfig?.settingPermissionTitle 253 | ?: stringResource(R.string.err_permission_denied), 254 | message = mImageCaptureConfig?.settingPermissionMessage ?: getString( 255 | R.string.err_write_storage_setting, 256 | getPermissionsListString(context), 257 | ), 258 | onConfirm = onConfirm, 259 | onDismiss = onDismiss 260 | ) 261 | } 262 | 263 | 264 | private fun launchCamera( 265 | context: Context, 266 | imageCaptureLauncher: ActivityResultLauncher 267 | ) { 268 | imageFileUri = if (mImageCaptureConfig != null) { 269 | imageFile = createMediaFileFolder( 270 | folderFile = mImageCaptureConfig!!.mFolder ?: context.defaultFolder(), 271 | fileName = mImageCaptureConfig!!.fileName ?: Const.DefaultPaths.defaultImageFile(), 272 | ) 273 | context.createFileGetUri(imageFile!!) 274 | } else { 275 | imageFile = createMediaFileFolder( 276 | folderFile = context.defaultFolder(), 277 | fileName = Const.DefaultPaths.defaultImageFile(), 278 | ) 279 | context.createFileGetUri(imageFile!!) 280 | } 281 | imageFileUri?.let { 282 | imageCaptureLauncher.launch( 283 | context.getImageCaptureIntent( 284 | it, 285 | mImageCaptureConfig?.isUseRearCamera ?: true 286 | ) 287 | ) 288 | } 289 | } 290 | 291 | private fun getPermissionsListString(context: Context): String { 292 | val listString = getPermissionsList(context).map { 293 | it.split(".").lastOrNull() ?: "" 294 | }.toString() 295 | return listString.substring(1, listString.length - 1).replace(",", " and ") 296 | } 297 | 298 | companion object { 299 | @Keep 300 | private fun getPermissionsList(context: Context) = ArrayList().also { 301 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { 302 | it.add(Manifest.permission.WRITE_EXTERNAL_STORAGE) 303 | } 304 | val permissions = context.getRequestedPermissions() 305 | if (permissions?.contains(Manifest.permission.CAMERA) == true) { 306 | it.add(Manifest.permission.CAMERA) 307 | } 308 | } 309 | 310 | @Keep 311 | fun getInstance(mContext: Context, mImageCaptureConfig: ImageCaptureConfig?): Intent { 312 | val filePickerIntent = Intent(mContext, ImageCaptureActivity::class.java) 313 | mImageCaptureConfig?.let { 314 | filePickerIntent.putExtra(Const.BundleInternalExtras.IMAGE_CAPTURE, it) 315 | } 316 | return filePickerIntent 317 | } 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /filepickerlibrary/src/main/java/com/nareshchocha/filepickerlibrary/ui/activitys/PopUpActivity.kt: -------------------------------------------------------------------------------- 1 | package com.nareshchocha.filepickerlibrary.ui.activitys 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Build 6 | import android.os.Bundle 7 | import androidx.activity.ComponentActivity 8 | import androidx.activity.compose.setContent 9 | import androidx.activity.result.contract.ActivityResultContracts 10 | import androidx.compose.foundation.Image 11 | import androidx.compose.foundation.background 12 | import androidx.compose.foundation.horizontalScroll 13 | import androidx.compose.foundation.layout.Column 14 | import androidx.compose.foundation.layout.Row 15 | import androidx.compose.foundation.layout.fillMaxWidth 16 | import androidx.compose.foundation.layout.height 17 | import androidx.compose.foundation.layout.navigationBarsPadding 18 | import androidx.compose.foundation.layout.padding 19 | import androidx.compose.foundation.rememberScrollState 20 | import androidx.compose.foundation.shape.RoundedCornerShape 21 | import androidx.compose.foundation.verticalScroll 22 | import androidx.compose.material3.ExperimentalMaterial3Api 23 | import androidx.compose.material3.MaterialTheme 24 | import androidx.compose.material3.ModalBottomSheet 25 | import androidx.compose.material3.Text 26 | import androidx.compose.material3.TextButton 27 | import androidx.compose.material3.rememberModalBottomSheetState 28 | import androidx.compose.runtime.Composable 29 | import androidx.compose.ui.Modifier 30 | import androidx.compose.ui.platform.LocalContext 31 | import androidx.compose.ui.res.painterResource 32 | import androidx.compose.ui.res.stringResource 33 | import androidx.compose.ui.unit.dp 34 | import com.nareshchocha.filepickerlibrary.R 35 | import com.nareshchocha.filepickerlibrary.models.BaseConfig 36 | import com.nareshchocha.filepickerlibrary.models.DocumentFilePickerConfig 37 | import com.nareshchocha.filepickerlibrary.models.ImageCaptureConfig 38 | import com.nareshchocha.filepickerlibrary.models.Orientation 39 | import com.nareshchocha.filepickerlibrary.models.PickMediaConfig 40 | import com.nareshchocha.filepickerlibrary.models.PickerData 41 | import com.nareshchocha.filepickerlibrary.models.PopUpConfig 42 | import com.nareshchocha.filepickerlibrary.models.VideoCaptureConfig 43 | import com.nareshchocha.filepickerlibrary.ui.FilePicker 44 | import com.nareshchocha.filepickerlibrary.ui.components.dialogs.AppDialog 45 | import com.nareshchocha.filepickerlibrary.utilities.appConst.Const 46 | import com.nareshchocha.filepickerlibrary.utilities.extentions.setCanceledResult 47 | 48 | @OptIn(ExperimentalMaterial3Api::class) 49 | internal class PopUpActivity : ComponentActivity() { 50 | private val mPickerData: PickerData? by lazy { 51 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 52 | intent.getParcelableExtra( 53 | Const.BundleInternalExtras.PICKER_DATA, 54 | PickerData::class.java, 55 | ) 56 | } else { 57 | @Suppress("DEPRECATION") 58 | intent.getParcelableExtra(Const.BundleInternalExtras.PICKER_DATA) as PickerData? 59 | } 60 | } 61 | 62 | private val intentResultLauncher = 63 | registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> 64 | setResult(result.resultCode, result.data) 65 | finish() 66 | } 67 | 68 | override fun onCreate(savedInstanceState: Bundle?) { 69 | super.onCreate(savedInstanceState) 70 | setContent { 71 | if (mPickerData == null) { 72 | setCanceledResult() 73 | finish() 74 | return@setContent 75 | } 76 | val context = LocalContext.current 77 | val items = mPickerData?.listIntents ?: emptyList() 78 | 79 | if (mPickerData?.mPopUpConfig?.mPopUpType?.isDialog() == true) { 80 | LoadDialogUI( 81 | mPopUpConfig = mPickerData?.mPopUpConfig ?: PopUpConfig(), 82 | items = items, 83 | onItemClick = { item -> 84 | handleItemClick(context, item) 85 | }) 86 | } else { 87 | LoadBottomSheetUI( 88 | mPopUpConfig = mPickerData?.mPopUpConfig ?: PopUpConfig(), 89 | items = items, 90 | onItemClick = { item -> 91 | handleItemClick(context, item) 92 | } 93 | ) 94 | } 95 | } 96 | } 97 | 98 | @Composable 99 | fun LoadDialogUI( 100 | mPopUpConfig: PopUpConfig, 101 | items: List = emptyList(), 102 | onItemClick: (BaseConfig) -> Unit 103 | ) { 104 | val title = mPopUpConfig.chooserTitle ?: stringResource(R.string.str_choose_option) 105 | AppDialog( 106 | modifier = Modifier 107 | .background( 108 | color = MaterialTheme.colorScheme.background, 109 | shape = RoundedCornerShape(mPopUpConfig.cornerSize.dp) 110 | ), 111 | onDismissRequest = { 112 | setCanceledResult() 113 | finish() 114 | }, 115 | ) { 116 | Column { 117 | if (mPopUpConfig.title != null) { 118 | mPopUpConfig.title(title) 119 | } else { 120 | Text( 121 | text = title, 122 | style = MaterialTheme.typography.titleLarge, 123 | modifier = Modifier.padding(16.dp) 124 | ) 125 | } 126 | ItemList( 127 | orientation = mPopUpConfig.mOrientation ?: Orientation.VERTICAL, 128 | items = items, 129 | item = mPopUpConfig.item, 130 | onItemClick = onItemClick 131 | ) 132 | } 133 | 134 | } 135 | } 136 | 137 | @Composable 138 | fun LoadBottomSheetUI( 139 | mPopUpConfig: PopUpConfig, 140 | items: List = emptyList(), 141 | onItemClick: (BaseConfig) -> Unit 142 | ) { 143 | val sheetState = rememberModalBottomSheetState() 144 | val title = mPopUpConfig.chooserTitle ?: stringResource(R.string.str_choose_option) 145 | ModalBottomSheet( 146 | onDismissRequest = { 147 | setCanceledResult() 148 | finish() 149 | }, 150 | dragHandle = null, 151 | sheetState = sheetState, 152 | shape = RoundedCornerShape( 153 | topStart = mPopUpConfig.cornerSize.dp, 154 | topEnd = mPopUpConfig.cornerSize.dp 155 | ) 156 | ) { 157 | Column(modifier = Modifier) { 158 | if (mPopUpConfig.title != null) { 159 | mPopUpConfig.title(title) 160 | } else { 161 | Text( 162 | text = title, 163 | style = MaterialTheme.typography.titleLarge, 164 | modifier = Modifier.padding(16.dp) 165 | ) 166 | } 167 | ItemList( 168 | orientation = mPopUpConfig.mOrientation ?: Orientation.VERTICAL, 169 | items = items, 170 | item = mPopUpConfig.item, 171 | onItemClick = onItemClick 172 | ) 173 | } 174 | } 175 | 176 | } 177 | 178 | 179 | @Composable 180 | private fun ItemList( 181 | modifier: Modifier = Modifier, 182 | orientation: Orientation, items: List, 183 | item: @Composable ((item: BaseConfig) -> Unit)? = null, 184 | onItemClick: (BaseConfig) -> Unit 185 | ) { 186 | if (orientation.isHorizontal()) { 187 | Row( 188 | modifier = modifier 189 | .fillMaxWidth() 190 | .padding(bottom = 6.dp) 191 | .horizontalScroll(rememberScrollState()) 192 | ) { 193 | ItemList(items, item, onItemClick) 194 | } 195 | } else { 196 | Column( 197 | modifier = modifier 198 | .fillMaxWidth() 199 | .padding(horizontal = 6.dp) 200 | .verticalScroll(rememberScrollState()) 201 | ) { 202 | ItemList(items, item, onItemClick) 203 | } 204 | } 205 | } 206 | 207 | 208 | @Composable 209 | private fun ItemList( 210 | items: List, 211 | itemView: @Composable ((item: BaseConfig) -> Unit)? = null, 212 | onItemClick: (BaseConfig) -> Unit 213 | ) { 214 | items.forEach { item -> 215 | if (itemView != null) { 216 | itemView(item) 217 | } else { 218 | TextButton( 219 | onClick = { onItemClick(item) }, 220 | ) { 221 | Row { 222 | val iconRes = item.popUpIcon 223 | if (iconRes != null && iconRes != 0) { 224 | Image( 225 | painter = painterResource(id = iconRes), 226 | contentDescription = null, 227 | modifier = Modifier 228 | .padding(end = 8.dp) 229 | .height(24.dp) 230 | ) 231 | } 232 | Text(item.popUpText ?: "") 233 | } 234 | } 235 | } 236 | } 237 | } 238 | 239 | private fun handleItemClick(context: Context, item: BaseConfig) { 240 | when (item) { 241 | is ImageCaptureConfig -> { 242 | intentResultLauncher.launch(FilePicker.Builder(context).imageCaptureBuild(item)) 243 | } 244 | 245 | is VideoCaptureConfig -> { 246 | intentResultLauncher.launch(FilePicker.Builder(context).videoCaptureBuild(item)) 247 | } 248 | 249 | is PickMediaConfig -> { 250 | intentResultLauncher.launch(FilePicker.Builder(context).pickMediaBuild(item)) 251 | } 252 | 253 | is DocumentFilePickerConfig -> { 254 | intentResultLauncher.launch(FilePicker.Builder(context).pickDocumentFileBuild(item)) 255 | } 256 | } 257 | } 258 | 259 | companion object { 260 | fun getInstance(mContext: Context, mPickerData: PickerData?): Intent { 261 | val filePickerIntent = Intent(mContext, PopUpActivity::class.java) 262 | mPickerData?.let { 263 | filePickerIntent.putExtra(Const.BundleInternalExtras.PICKER_DATA, it) 264 | } 265 | filePickerIntent.flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION 266 | return filePickerIntent 267 | } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /filepickerlibrary/src/main/java/com/nareshchocha/filepickerlibrary/ui/activitys/VideoCaptureActivity.kt: -------------------------------------------------------------------------------- 1 | package com.nareshchocha.filepickerlibrary.ui.activitys 2 | 3 | import android.Manifest 4 | import android.app.Activity 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.pm.PackageManager 8 | import android.net.Uri 9 | import android.os.Build 10 | import android.os.Bundle 11 | import androidx.activity.ComponentActivity 12 | import androidx.activity.compose.rememberLauncherForActivityResult 13 | import androidx.activity.compose.setContent 14 | import androidx.activity.result.ActivityResultLauncher 15 | import androidx.activity.result.contract.ActivityResultContracts 16 | import androidx.annotation.Keep 17 | import androidx.compose.material3.AlertDialog 18 | import androidx.compose.material3.ExperimentalMaterial3Api 19 | import androidx.compose.material3.Text 20 | import androidx.compose.material3.TextButton 21 | import androidx.compose.runtime.Composable 22 | import androidx.compose.runtime.LaunchedEffect 23 | import androidx.compose.runtime.getValue 24 | import androidx.compose.runtime.mutableStateOf 25 | import androidx.compose.runtime.remember 26 | import androidx.compose.runtime.setValue 27 | import androidx.compose.ui.Modifier 28 | import androidx.compose.ui.platform.LocalContext 29 | import androidx.core.app.ActivityCompat 30 | import androidx.core.content.ContextCompat 31 | import com.nareshchocha.filepickerlibrary.R 32 | import com.nareshchocha.filepickerlibrary.models.VideoCaptureConfig 33 | import com.nareshchocha.filepickerlibrary.picker.PickerUtils.createFileGetUri 34 | import com.nareshchocha.filepickerlibrary.picker.PickerUtils.createMediaFileFolder 35 | import com.nareshchocha.filepickerlibrary.utilities.appConst.Const 36 | import com.nareshchocha.filepickerlibrary.utilities.appConst.Const.DefaultPaths.defaultFolder 37 | import com.nareshchocha.filepickerlibrary.utilities.extentions.getSettingIntent 38 | import com.nareshchocha.filepickerlibrary.utilities.extentions.getVideoCaptureIntent 39 | import com.nareshchocha.filepickerlibrary.utilities.extentions.setCanceledResult 40 | import com.nareshchocha.filepickerlibrary.utilities.extentions.setSuccessResult 41 | import timber.log.Timber 42 | import java.io.File 43 | 44 | @OptIn(ExperimentalMaterial3Api::class) 45 | internal class VideoCaptureActivity : ComponentActivity() { 46 | private var videoFileUri: Uri? = null 47 | private var videoFile: File? = null 48 | 49 | private val mVideoCaptureConfig: VideoCaptureConfig? by lazy { 50 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 51 | intent.getParcelableExtra( 52 | Const.BundleInternalExtras.VIDEO_CAPTURE, 53 | VideoCaptureConfig::class.java, 54 | ) 55 | } else { 56 | @Suppress("DEPRECATION") 57 | intent.getParcelableExtra(Const.BundleInternalExtras.VIDEO_CAPTURE) as VideoCaptureConfig? 58 | } 59 | } 60 | 61 | override fun onCreate(savedInstanceState: Bundle?) { 62 | super.onCreate(savedInstanceState) 63 | setContent { 64 | StaringVideoCapture() 65 | } 66 | } 67 | 68 | @Composable 69 | fun StaringVideoCapture() { 70 | val context = LocalContext.current 71 | var showAskDialog by remember { mutableStateOf(false) } 72 | var showGotoSettingDialog by remember { mutableStateOf(false) } 73 | var permissionRequested by remember { mutableStateOf(false) } 74 | 75 | val videoCaptureLauncher = rememberVideoCaptureResultLauncher() 76 | val permissionLauncher = rememberPermissionResultLauncher { allGranted, isShowRationale -> 77 | when { 78 | allGranted -> launchCamera(context, videoCaptureLauncher) 79 | isShowRationale -> showAskDialog = true 80 | else -> showGotoSettingDialog = true 81 | } 82 | } 83 | val settingLauncher = rememberSettingResultLauncher( 84 | onGranted = { launchCamera(context, videoCaptureLauncher) }, 85 | onDenied = { 86 | setCanceledResult(getString(R.string.err_permission_result)) 87 | finish() 88 | } 89 | ) 90 | // Initial permission check 91 | CheckInitialPermissions( 92 | permissionRequested = permissionRequested, 93 | onPermissionRequested = { permissionRequested = true }, 94 | onRequestPermission = { permissionLauncher.launch(getPermissionsList(context).toTypedArray()) }, 95 | onLaunchCamera = { launchCamera(context, videoCaptureLauncher) } 96 | ) 97 | 98 | // Show dialogs if needed 99 | if (showAskDialog) { 100 | ShowAskPermissionDialog( 101 | context = context, 102 | onConfirm = { 103 | showAskDialog = false 104 | permissionLauncher.launch(getPermissionsList(context).toTypedArray()) 105 | } 106 | ) 107 | } 108 | 109 | if (showGotoSettingDialog) { 110 | ShowSettingsDialog( 111 | context = context, 112 | onConfirm = { 113 | showGotoSettingDialog = false 114 | settingLauncher.launch(getSettingIntent()) 115 | } 116 | ) 117 | } 118 | } 119 | 120 | @Composable 121 | private fun rememberVideoCaptureResultLauncher(): ActivityResultLauncher { 122 | return rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> 123 | if (result.resultCode == Activity.RESULT_OK) { 124 | Timber.tag(Const.LogTag.FILE_RESULT) 125 | .v("File Uri ::: ${videoFileUri?.toString()}") 126 | Timber.tag(Const.LogTag.FILE_RESULT) 127 | .v("filePath ::: ${videoFile?.absoluteFile}") 128 | setSuccessResult(videoFileUri, videoFile?.absolutePath, true) 129 | } else { 130 | Timber.tag(Const.LogTag.FILE_PICKER_ERROR) 131 | .v(getString(R.string.err_capture_error, "videoCapture")) 132 | setCanceledResult(getString(R.string.err_capture_error, "videoCapture")) 133 | } 134 | finish() 135 | } 136 | } 137 | 138 | @Composable 139 | private fun rememberPermissionResultLauncher( 140 | onResult: (allGranted: Boolean, isShowRationale: Boolean) -> Unit 141 | ): ActivityResultLauncher> { 142 | val context = LocalContext.current 143 | return rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result -> 144 | val allGranted = result.all { permission -> 145 | ContextCompat.checkSelfPermission( 146 | context, 147 | permission.key, 148 | ) == PackageManager.PERMISSION_GRANTED 149 | } 150 | val isShowRationale = result.any { permission -> 151 | ActivityCompat.shouldShowRequestPermissionRationale( 152 | context as Activity, 153 | permission.key, 154 | ) 155 | } 156 | onResult(allGranted, isShowRationale) 157 | } 158 | } 159 | 160 | @Composable 161 | private fun rememberSettingResultLauncher( 162 | onGranted: () -> Unit, 163 | onDenied: () -> Unit 164 | ): ActivityResultLauncher { 165 | val context = LocalContext.current 166 | return rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { 167 | val allGranted = getPermissionsList(context).all { permission -> 168 | ContextCompat.checkSelfPermission( 169 | context, 170 | permission, 171 | ) == PackageManager.PERMISSION_GRANTED 172 | } 173 | if (allGranted) onGranted() else onDenied() 174 | } 175 | } 176 | 177 | @Composable 178 | private fun CheckInitialPermissions( 179 | permissionRequested: Boolean, 180 | onPermissionRequested: () -> Unit, 181 | onRequestPermission: () -> Unit, 182 | onLaunchCamera: () -> Unit 183 | ) { 184 | val context = LocalContext.current 185 | 186 | LaunchedEffect(Unit) { 187 | if (!permissionRequested) { 188 | onPermissionRequested() 189 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 190 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q || getPermissionsList(context).isNotEmpty()) { 191 | onRequestPermission() 192 | } else { 193 | onLaunchCamera() 194 | } 195 | } else { 196 | onLaunchCamera() 197 | } 198 | } 199 | } 200 | } 201 | 202 | @Composable 203 | private fun ShowAskPermissionDialog( 204 | context: Context, 205 | onConfirm: () -> Unit 206 | ) { 207 | AlertDialog( 208 | onDismissRequest = { 209 | setCanceledResult(getString(R.string.err_permission_result)) 210 | finish() 211 | }, 212 | title = { 213 | Text( 214 | mVideoCaptureConfig?.askPermissionTitle 215 | ?: getString(R.string.err_permission_denied) 216 | ) 217 | }, 218 | text = { 219 | Text( 220 | mVideoCaptureConfig?.askPermissionMessage ?: getString( 221 | R.string.err_write_storage_permission, 222 | getPermissionsListString(context), 223 | ) 224 | ) 225 | }, 226 | confirmButton = { 227 | TextButton(onClick = onConfirm) { 228 | Text(getString(android.R.string.ok)) 229 | } 230 | }, 231 | dismissButton = { 232 | TextButton(onClick = { 233 | setCanceledResult(getString(R.string.err_permission_result)) 234 | finish() 235 | }) { 236 | Text(getString(android.R.string.cancel)) 237 | } 238 | } 239 | ) 240 | } 241 | 242 | @Composable 243 | private fun ShowSettingsDialog( 244 | context: Context, 245 | onConfirm: () -> Unit 246 | ) { 247 | AlertDialog( 248 | onDismissRequest = { 249 | setCanceledResult(getString(R.string.err_permission_result)) 250 | finish() 251 | }, 252 | title = { 253 | Text( 254 | mVideoCaptureConfig?.settingPermissionTitle 255 | ?: getString(R.string.err_permission_denied) 256 | ) 257 | }, 258 | text = { 259 | Text( 260 | mVideoCaptureConfig?.settingPermissionMessage ?: getString( 261 | R.string.err_write_storage_setting, 262 | getPermissionsListString(context), 263 | ) 264 | ) 265 | }, 266 | confirmButton = { 267 | TextButton(onClick = onConfirm) { 268 | Text(getString(R.string.str_go_to_setting)) 269 | } 270 | }, 271 | dismissButton = { 272 | TextButton(onClick = { 273 | setCanceledResult(getString(R.string.err_permission_result)) 274 | finish() 275 | }) { 276 | Text(getString(android.R.string.cancel)) 277 | } 278 | } 279 | ) 280 | } 281 | 282 | 283 | private fun launchCamera( 284 | context: Context, 285 | videoCaptureLauncher: androidx.activity.result.ActivityResultLauncher 286 | ) { 287 | videoFileUri = if (mVideoCaptureConfig != null) { 288 | videoFile = createMediaFileFolder( 289 | folderFile = mVideoCaptureConfig!!.mFolder ?: context.defaultFolder(), 290 | fileName = mVideoCaptureConfig!!.fileName ?: Const.DefaultPaths.defaultVideoFile(), 291 | ) 292 | createFileGetUri(videoFile!!) 293 | } else { 294 | videoFile = createMediaFileFolder( 295 | folderFile = context.defaultFolder(), 296 | fileName = Const.DefaultPaths.defaultVideoFile(), 297 | ) 298 | createFileGetUri(videoFile!!) 299 | } 300 | videoFileUri?.let { 301 | videoCaptureLauncher.launch( 302 | getVideoCaptureIntent( 303 | it, 304 | maxSeconds = mVideoCaptureConfig?.maxSeconds, 305 | maxSizeLimit = mVideoCaptureConfig?.maxSizeLimit, 306 | isHighQuality = mVideoCaptureConfig?.isHighQuality, 307 | ), 308 | ) 309 | } 310 | } 311 | 312 | private fun getPermissionsListString(context: Context): String { 313 | val listString = getPermissionsList(context).map { 314 | it.split(".").lastOrNull() ?: "" 315 | }.toString() 316 | return listString.substring(1, listString.length - 1).replace(",", " and ") 317 | } 318 | 319 | companion object { 320 | @Keep 321 | private fun getPermissionsList(context: Context) = ArrayList().also { 322 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { 323 | it.add(Manifest.permission.WRITE_EXTERNAL_STORAGE) 324 | } 325 | val permissions: Array? = 326 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 327 | context.packageManager.getPackageInfo( 328 | context.packageName, 329 | PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong()) 330 | ).requestedPermissions 331 | } else { 332 | @Suppress("DEPRECATION") 333 | context.packageManager.getPackageInfo( 334 | context.packageName, 335 | PackageManager.GET_PERMISSIONS 336 | ).requestedPermissions 337 | } 338 | if (permissions?.contains(Manifest.permission.CAMERA) == true) { 339 | it.add(Manifest.permission.CAMERA) 340 | } 341 | } 342 | 343 | @Keep 344 | fun getInstance(mContext: Context, mVideoCaptureConfig: VideoCaptureConfig?): Intent { 345 | val filePickerIntent = Intent(mContext, VideoCaptureActivity::class.java) 346 | mVideoCaptureConfig?.let { 347 | filePickerIntent.putExtra(Const.BundleInternalExtras.VIDEO_CAPTURE, it) 348 | } 349 | filePickerIntent.flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION 350 | return filePickerIntent 351 | } 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /filepickerlibrary/src/main/java/com/nareshchocha/filepickerlibrary/ui/components/dialogs/AppAlertDialog.kt: -------------------------------------------------------------------------------- 1 | package com.nareshchocha.filepickerlibrary.ui.components.dialogs 2 | 3 | import androidx.compose.material3.AlertDialog 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.Text 6 | import androidx.compose.material3.TextButton 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.res.stringResource 9 | import com.nareshchocha.filepickerlibrary.R 10 | 11 | @Composable 12 | fun AppAlertDialog( 13 | title: String, 14 | message: String, 15 | confirmText: String = stringResource(R.string.str_ok), 16 | dismissText: String = stringResource(R.string.str_cancel), 17 | onConfirm: () -> Unit, 18 | onDismiss: () -> Unit, 19 | onDismissRequest: () -> Unit = onDismiss, 20 | ) { 21 | AlertDialog( 22 | onDismissRequest = onDismissRequest, 23 | title = { Text(title, style = MaterialTheme.typography.titleLarge) }, 24 | text = { Text(message, style = MaterialTheme.typography.bodyMedium) }, 25 | confirmButton = { 26 | TextButton(onClick = onConfirm) { 27 | Text(confirmText) 28 | } 29 | }, 30 | dismissButton = { 31 | TextButton(onClick = onDismiss) { 32 | Text(dismissText) 33 | } 34 | } 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /filepickerlibrary/src/main/java/com/nareshchocha/filepickerlibrary/ui/components/dialogs/AppDialog.kt: -------------------------------------------------------------------------------- 1 | package com.nareshchocha.filepickerlibrary.ui.components.dialogs 2 | 3 | import androidx.compose.material3.BasicAlertDialog 4 | import androidx.compose.material3.ExperimentalMaterial3Api 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.window.DialogProperties 8 | 9 | @OptIn(ExperimentalMaterial3Api::class) 10 | @Composable 11 | fun AppDialog( 12 | modifier: Modifier= Modifier, 13 | onDismissRequest: () -> Unit, 14 | properties: DialogProperties = DialogProperties(), 15 | content: @Composable () -> Unit 16 | ) { 17 | BasicAlertDialog( 18 | modifier = modifier, 19 | onDismissRequest = onDismissRequest, 20 | properties = properties, 21 | content = content 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /filepickerlibrary/src/main/java/com/nareshchocha/filepickerlibrary/utilities/CustomFileProvider.kt: -------------------------------------------------------------------------------- 1 | package com.nareshchocha.filepickerlibrary.utilities 2 | 3 | import androidx.core.content.FileProvider 4 | 5 | class CustomFileProvider : FileProvider() 6 | -------------------------------------------------------------------------------- /filepickerlibrary/src/main/java/com/nareshchocha/filepickerlibrary/utilities/FileUtils.kt: -------------------------------------------------------------------------------- 1 | package com.nareshchocha.filepickerlibrary.utilities 2 | 3 | import android.content.ContentUris 4 | import android.content.Context 5 | import android.database.Cursor 6 | import android.net.Uri 7 | import android.os.Environment 8 | import android.provider.DocumentsContract 9 | import android.provider.MediaStore 10 | import androidx.annotation.Keep 11 | import com.nareshchocha.filepickerlibrary.utilities.extentions.copyFileToInternalStorage 12 | import com.nareshchocha.filepickerlibrary.utilities.extentions.getDataColumn 13 | import com.nareshchocha.filepickerlibrary.utilities.extentions.getDriveFilePath 14 | import com.nareshchocha.filepickerlibrary.utilities.extentions.getMediaDocumentPath 15 | import com.nareshchocha.filepickerlibrary.utilities.extentions.isDownloadsDocument 16 | import com.nareshchocha.filepickerlibrary.utilities.extentions.isExternalStorageDocument 17 | import com.nareshchocha.filepickerlibrary.utilities.extentions.isGoogleDriveUri 18 | import com.nareshchocha.filepickerlibrary.utilities.extentions.isGooglePhotosUri 19 | import com.nareshchocha.filepickerlibrary.utilities.extentions.isMediaDocument 20 | import java.io.File 21 | import androidx.core.net.toUri 22 | 23 | @Keep 24 | internal object FileUtils { 25 | @Keep 26 | fun getRealPath(context: Context, fileUri: Uri): String? { 27 | return pathFromURI(context, fileUri) 28 | } 29 | 30 | @Keep 31 | private fun pathFromURI(context: Context, uri: Uri): String? { 32 | val filePath = when { 33 | DocumentsContract.isDocumentUri(context, uri) -> { 34 | getDocumentUri(context, uri) 35 | } 36 | 37 | uri.isGoogleDriveUri() -> { 38 | context.getDriveFilePath(uri) 39 | } 40 | 41 | "content".equals(uri.scheme, ignoreCase = true) -> { 42 | // Return the remote address 43 | if (uri.isGooglePhotosUri()) { 44 | uri.lastPathSegment 45 | } else if (uri.isGoogleDriveUri()) { 46 | return context.getDriveFilePath(uri) 47 | } else { 48 | getDataColumn( 49 | context, 50 | uri, 51 | null, 52 | null, 53 | ) ?: context.copyFileToInternalStorage(uri) 54 | } 55 | } 56 | 57 | "file".equals(uri.scheme, ignoreCase = true) -> { 58 | uri.path 59 | } 60 | 61 | else -> { 62 | context.copyFileToInternalStorage(uri) 63 | } 64 | } 65 | return if (filePath != null) { 66 | val file = File(filePath) 67 | if (file.canRead()) { 68 | file.absolutePath 69 | } else { 70 | context.copyFileToInternalStorage(uri) 71 | } 72 | } else { 73 | null 74 | } 75 | } 76 | 77 | private fun getDocumentUri(context: Context, uri: Uri) = when { 78 | uri.isExternalStorageDocument() -> { 79 | getExternalDocumentPath(uri) 80 | } 81 | 82 | uri.isDownloadsDocument() -> { 83 | getFileName(context, uri)?.let { 84 | Environment.getExternalStorageDirectory() 85 | .toString() + "/Download/" + it 86 | } ?: context.getDownloadsDocumentPath(uri) ?: context.copyFileToInternalStorage( 87 | uri 88 | ) 89 | } 90 | 91 | uri.isMediaDocument() -> { 92 | context.getMediaDocumentPath(uri) ?: context.copyFileToInternalStorage(uri) 93 | } 94 | 95 | else -> { 96 | context.copyFileToInternalStorage(uri) 97 | } 98 | } 99 | 100 | 101 | @Keep 102 | private fun Context.getDownloadsDocumentPath(uri: Uri): String? { 103 | /*val fileName = getFileName(this, uri) 104 | if (fileName != null) { 105 | return Environment.getExternalStorageDirectory() 106 | .toString() + "/Download/" + fileName 107 | }*/ 108 | 109 | var id = DocumentsContract.getDocumentId(uri) 110 | if (id.startsWith("raw:")) { 111 | id = id.replaceFirst("raw:".toRegex(), "") 112 | val file = File(id) 113 | if (file.exists()) return id 114 | } 115 | val contentUriPrefixesToTry = arrayOf( 116 | "content://downloads/public_downloads", 117 | "content://downloads/my_downloads" 118 | ) 119 | var filePath: String? = null 120 | for (contentUriPrefix in contentUriPrefixesToTry) { 121 | try { 122 | val contentUri = ContentUris.withAppendedId( 123 | contentUriPrefix.toUri(), 124 | java.lang.Long.valueOf(id) 125 | ) 126 | filePath = getDataColumn(this, contentUri, null, null) 127 | if (filePath != null) { 128 | break 129 | } 130 | } catch (e: NumberFormatException) { 131 | filePath = null 132 | } 133 | } 134 | return filePath ?: uri.path?.replaceFirst("^/document/raw:", "")?.replaceFirst("^raw:", "") 135 | } 136 | 137 | @Keep 138 | private fun getExternalDocumentPath(uri: Uri): String { 139 | val docId = DocumentsContract.getDocumentId(uri) 140 | val split = 141 | docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() 142 | val type = split[0] 143 | 144 | // This is for checking Main Memory 145 | return if ("primary".equals(type, ignoreCase = true)) { 146 | if (split.size > 1) { 147 | Environment.getExternalStorageDirectory() 148 | .toString() + "/" + split[1] 149 | } else { 150 | Environment.getExternalStorageDirectory().toString() + "/" 151 | } 152 | // This is for checking SD Card 153 | } else { 154 | "storage" + "/" + docId.replace(":", "/") 155 | } 156 | } 157 | 158 | 159 | @Keep 160 | private fun getFileName(context: Context, uri: Uri?): String? { 161 | var cursor: Cursor? = null 162 | val projection = arrayOf( 163 | MediaStore.MediaColumns.DISPLAY_NAME, 164 | ) 165 | try { 166 | cursor = context.contentResolver.query( 167 | uri!!, 168 | projection, 169 | null, 170 | null, 171 | null, 172 | ) 173 | if (cursor != null && cursor.moveToFirst()) { 174 | val index = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME) 175 | return cursor.getString(index) 176 | } 177 | } finally { 178 | cursor?.close() 179 | } 180 | return null 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /filepickerlibrary/src/main/java/com/nareshchocha/filepickerlibrary/utilities/appConst/Const.kt: -------------------------------------------------------------------------------- 1 | package com.nareshchocha.filepickerlibrary.utilities.appConst 2 | 3 | import android.content.Context 4 | import android.os.Environment 5 | import androidx.annotation.Keep 6 | import com.nareshchocha.filepickerlibrary.R 7 | import java.io.File 8 | 9 | 10 | /** 11 | * Contains constants and utility objects used throughout the File Picker library. 12 | */ 13 | @Keep 14 | object Const { 15 | /** Default card corner radius. */ 16 | internal const val CARD_RADIUS = 10f 17 | 18 | /** FileProvider authority suffix. */ 19 | internal const val AUTHORITY = ".library.fileprovider" 20 | 21 | /** Folder name for copying files to internal storage. */ 22 | internal const val copyFileFolder = "copyFileToInternalStorage" 23 | 24 | /** Maximum buffer size for file operations (1 MB). */ 25 | internal const val MAX_BUFFER_SIZE = 1 * 1024 * 1024 26 | 27 | /** Default buffer size for file operations (1 KB). */ 28 | internal const val BUFFER_SIZE = 1024 29 | 30 | /** 31 | * Logging tags used for debugging and error reporting. 32 | */ 33 | internal object LogTag { 34 | /** Tag for file result logs. */ 35 | const val FILE_RESULT = "FILE_RESULT ::" 36 | /** Tag for file picker error logs. */ 37 | const val FILE_PICKER_ERROR = "FILE_PICKER_ERROR ::" 38 | /** Tag for file picker exception logs. */ 39 | const val FILE_PICKER_EXCEPTION = "FILE_PICKER_EXCEPTION :" 40 | } 41 | 42 | /** 43 | * Provides default file and folder paths for media operations. 44 | */ 45 | internal object DefaultPaths { 46 | /** 47 | * Generates a default image file name with a timestamp. 48 | * @return The generated image file name. 49 | */ 50 | fun defaultImageFile() = "tempImage_${System.currentTimeMillis()}.jpg" 51 | 52 | /** 53 | * Generates a default video file name with a timestamp. 54 | * @return The generated video file name. 55 | */ 56 | fun defaultVideoFile() = "tempVideo_${System.currentTimeMillis()}.mp4" 57 | 58 | /** 59 | * Returns the default folder for storing files, located in the DCIM directory. 60 | * @receiver Context used to access resources. 61 | * @return The default folder as a [File] object. 62 | */ 63 | fun Context.defaultFolder() = File( 64 | Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), 65 | getString(R.string.app_name) 66 | ) 67 | } 68 | 69 | /** 70 | * Keys for internal bundle extras used within the library. 71 | */ 72 | internal object BundleInternalExtras { 73 | /** Key for picker data. */ 74 | const val PICKER_DATA = "PICKER_DATA" 75 | /** Key for image capture. */ 76 | const val IMAGE_CAPTURE = "IMAGE_CAPTURE" 77 | /** Key for video capture. */ 78 | const val VIDEO_CAPTURE = "VIDEO_CAPTURE" 79 | /** Key for picking media. */ 80 | const val PICK_MEDIA = "PICK_MEDIA" 81 | /** Key for picking documents. */ 82 | const val PICK_DOCUMENT = "PICK_DOCUMENT" 83 | } 84 | 85 | /** 86 | * Keys for bundle extras exposed to external consumers. 87 | */ 88 | @Keep 89 | object BundleExtras { 90 | /** Indicates if the file is from capture. */ 91 | const val FROM_CAPTURE = "isFromCapture" 92 | /** Key for a single file path. */ 93 | const val FILE_PATH = "FILE_PATH" 94 | /** Key for a list of file paths. */ 95 | const val FILE_PATH_LIST = "FILE_PATH_LIST" 96 | /** Key for error messages. */ 97 | const val ERROR = "ERROR" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /filepickerlibrary/src/main/java/com/nareshchocha/filepickerlibrary/utilities/extentions/Extentions.kt: -------------------------------------------------------------------------------- 1 | package com.nareshchocha.filepickerlibrary.utilities.extentions 2 | 3 | import android.app.Activity 4 | import android.content.ClipData 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.pm.PackageInfo 8 | import android.content.pm.PackageManager 9 | import android.content.res.Configuration 10 | import android.net.Uri 11 | import android.os.Build 12 | import android.provider.MediaStore 13 | import android.provider.Settings 14 | import androidx.activity.result.PickVisualMediaRequest 15 | import androidx.activity.result.contract.ActivityResultContracts 16 | import com.nareshchocha.filepickerlibrary.models.DocumentFilePickerConfig 17 | import com.nareshchocha.filepickerlibrary.models.PickMediaConfig 18 | import com.nareshchocha.filepickerlibrary.utilities.appConst.Const 19 | import timber.log.Timber 20 | 21 | 22 | fun Context.getRequestedPermissions(): Array? { 23 | val info: PackageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 24 | packageManager.getPackageInfo( 25 | packageName, 26 | PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong()), 27 | ) 28 | } else { 29 | packageManager.getPackageInfo( 30 | packageName, 31 | PackageManager.GET_PERMISSIONS, 32 | ) 33 | } 34 | return info.requestedPermissions 35 | } 36 | 37 | internal fun Context.getImageCaptureIntent( 38 | outputFileUri: Uri, 39 | useRearCamera: Boolean 40 | ): Intent { 41 | return Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { 42 | it.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) 43 | it.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri) 44 | if (useRearCamera) { 45 | it.putExtra("android.intent.extras.LENS_FACING_BACK", 1) 46 | it.putExtra("android.intent.extras.LENS_FACING_FRONT", 0) 47 | it.putExtra("android.intent.extras.CAMERA_FACING", 0) 48 | it.putExtra("android.intent.extra.USE_FRONT_CAMERA", false) 49 | } else { 50 | it.putExtra("android.intent.extras.LENS_FACING_FRONT", 1) 51 | it.putExtra("android.intent.extras.LENS_FACING_BACK", 0) 52 | it.putExtra("android.intent.extras.CAMERA_FACING", 1) 53 | it.putExtra("android.intent.extra.USE_FRONT_CAMERA", true) 54 | } 55 | } 56 | } 57 | 58 | internal fun Context.getVideoCaptureIntent( 59 | outputFileUri: Uri, 60 | maxSeconds: Int? = null, 61 | maxSizeLimit: Long? = null, 62 | isHighQuality: Boolean? = null, 63 | ): Intent { 64 | return Intent(MediaStore.ACTION_VIDEO_CAPTURE).also { 65 | maxSeconds?.let { seconds -> it.putExtra(MediaStore.EXTRA_DURATION_LIMIT, seconds) } 66 | isHighQuality?.let { isHighQuality -> 67 | it.putExtra( 68 | MediaStore.EXTRA_VIDEO_QUALITY, 69 | if (isHighQuality) 1 else 0, 70 | ) 71 | } 72 | maxSizeLimit?.let { maxSizeLimit -> 73 | it.putExtra( 74 | MediaStore.EXTRA_SIZE_LIMIT, 75 | maxSizeLimit, 76 | ) 77 | } 78 | it.flags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION + Intent.FLAG_GRANT_READ_URI_PERMISSION 79 | it.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri) 80 | /*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 81 | it.clipData = ClipData.newUri(contentResolver, "Video", outputFileUri) 82 | }*/ 83 | } 84 | } 85 | 86 | internal fun getDocumentFilePick(mDocumentFilePickerConfig: DocumentFilePickerConfig): Intent { 87 | return Intent(Intent.ACTION_OPEN_DOCUMENT).apply { 88 | type = "*/*" 89 | putExtra(Intent.EXTRA_ALLOW_MULTIPLE, mDocumentFilePickerConfig.allowMultiple) 90 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && 91 | (mDocumentFilePickerConfig.allowMultiple == true) 92 | ) { 93 | putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, mDocumentFilePickerConfig.maxFiles) 94 | } 95 | if (mDocumentFilePickerConfig.mMimeTypes != null) { 96 | putExtra(Intent.EXTRA_MIME_TYPES, mDocumentFilePickerConfig.mMimeTypes.toTypedArray()) 97 | } 98 | } 99 | } 100 | 101 | internal fun Context.getMediaIntent(mPickMediaConfig: PickMediaConfig): Intent { 102 | val mPickMediaType = mPickMediaConfig.getPickMediaType(mPickMediaConfig.mPickMediaType) 103 | return if (mPickMediaConfig.allowMultiple == true) { 104 | ActivityResultContracts.PickMultipleVisualMedia( 105 | mPickMediaConfig.maxFiles 106 | ?: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 107 | MediaStore.getPickImagesMaxLimit() 108 | } else { 109 | Int.MAX_VALUE 110 | }, 111 | ).createIntent( 112 | this, 113 | PickVisualMediaRequest( 114 | if (mPickMediaType != null) { 115 | ActivityResultContracts.PickVisualMedia.SingleMimeType( 116 | mPickMediaType, 117 | ) 118 | } else { 119 | ActivityResultContracts.PickVisualMedia.ImageAndVideo 120 | }, 121 | ), 122 | ) 123 | } else { 124 | ActivityResultContracts.PickVisualMedia().createIntent( 125 | this, 126 | PickVisualMediaRequest( 127 | if (mPickMediaType != null) { 128 | ActivityResultContracts.PickVisualMedia.SingleMimeType( 129 | mPickMediaType, 130 | ) 131 | } else { 132 | ActivityResultContracts.PickVisualMedia.ImageAndVideo 133 | }, 134 | ), 135 | ) 136 | } 137 | } 138 | 139 | internal fun Context.getSettingIntent(): Intent { 140 | val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) 141 | val uri = Uri.fromParts("package", packageName, null) 142 | intent.data = uri 143 | return intent 144 | } 145 | 146 | internal fun Activity.setSuccessResult( 147 | fileUri: Uri?, 148 | filePath: String? = null, 149 | isFromCapture: Boolean = false, 150 | ) { 151 | setResult( 152 | Activity.RESULT_OK, 153 | Intent().also { mIntent -> 154 | mIntent.flags = 155 | if (isFromCapture) { 156 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION + Intent.FLAG_GRANT_READ_URI_PERMISSION 157 | } else { 158 | Intent.FLAG_GRANT_READ_URI_PERMISSION 159 | } 160 | 161 | fileUri?.let { mIntent.data = fileUri } 162 | if (isFromCapture) { 163 | mIntent.putExtra(Const.BundleExtras.FROM_CAPTURE, true) 164 | } 165 | filePath?.let { mIntent.putExtra(Const.BundleExtras.FILE_PATH, it) } 166 | }, 167 | ) 168 | finish() 169 | } 170 | 171 | internal fun Activity.setSuccessResult( 172 | fileUri: List?, 173 | filePath: ArrayList? = null, 174 | isFromCapture: Boolean = false, 175 | ) { 176 | setResult( 177 | Activity.RESULT_OK, 178 | Intent().also { mIntent -> 179 | mIntent.flags = 180 | if (isFromCapture) { 181 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION + Intent.FLAG_GRANT_READ_URI_PERMISSION 182 | } else { 183 | Intent.FLAG_GRANT_READ_URI_PERMISSION 184 | } 185 | if (isFromCapture) { 186 | mIntent.putExtra(Const.BundleExtras.FROM_CAPTURE, true) 187 | } 188 | if (!fileUri.isNullOrEmpty()) { 189 | val mClipData = ClipData.newUri(contentResolver, "uris", fileUri.first()) 190 | fileUri.subList(1, fileUri.size).forEach { 191 | mClipData.addItem(ClipData.Item(it)) 192 | } 193 | mIntent.clipData = mClipData 194 | filePath?.let { 195 | mIntent.putStringArrayListExtra( 196 | Const.BundleExtras.FILE_PATH_LIST, 197 | it, 198 | ) 199 | } 200 | } 201 | }, 202 | ) 203 | finish() 204 | } 205 | 206 | internal fun Activity.setCanceledResult(error: String? = null) { 207 | Timber.tag("FILE_RESULT").e("ERROR: $error") 208 | setResult( 209 | Activity.RESULT_CANCELED, 210 | Intent().also { mIntent -> 211 | error?.let { mIntent.putExtra(Const.BundleExtras.ERROR, it) } 212 | }, 213 | ) 214 | finish() 215 | } 216 | 217 | 218 | fun Context.isDarkMode(): Boolean { 219 | return when (resources.configuration.uiMode and 220 | Configuration.UI_MODE_NIGHT_MASK) { 221 | Configuration.UI_MODE_NIGHT_YES -> true 222 | Configuration.UI_MODE_NIGHT_NO -> false 223 | Configuration.UI_MODE_NIGHT_UNDEFINED -> false 224 | else -> false 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /filepickerlibrary/src/main/java/com/nareshchocha/filepickerlibrary/utilities/extentions/FilePathExtentions.kt: -------------------------------------------------------------------------------- 1 | package com.nareshchocha.filepickerlibrary.utilities.extentions 2 | 3 | import android.content.Context 4 | import android.database.Cursor 5 | import android.net.Uri 6 | import android.provider.DocumentsContract 7 | import android.provider.MediaStore 8 | import android.provider.OpenableColumns 9 | import androidx.annotation.Keep 10 | import com.nareshchocha.filepickerlibrary.utilities.appConst.Const 11 | import timber.log.Timber 12 | import java.io.File 13 | import java.io.FileOutputStream 14 | import java.io.IOException 15 | import java.io.InputStream 16 | 17 | /** 18 | * @return Whether the Uri authority is ExternalStorageProvider. 19 | */ 20 | internal fun Uri.isExternalStorageDocument(): Boolean { 21 | return "com.android.externalstorage.documents" == authority 22 | } 23 | 24 | /** 25 | * @return Whether the Uri authority is DownloadsProvider. 26 | */ 27 | internal fun Uri.isDownloadsDocument(): Boolean { 28 | return "com.android.providers.downloads.documents" == authority 29 | } 30 | 31 | /** 32 | * @return Whether the Uri authority is MediaProvider. 33 | */ 34 | internal fun Uri.isMediaDocument(): Boolean { 35 | return "com.android.providers.media.documents" == authority 36 | } 37 | 38 | /** 39 | * @return Whether the Uri authority is Google Photos. 40 | */ 41 | internal fun Uri.isGooglePhotosUri(): Boolean { 42 | return "com.google.android.apps.photos.content" == authority 43 | } 44 | 45 | internal fun Uri.isGoogleDriveUri(): Boolean { 46 | return "com.google.android.apps.docs.storage" == authority || 47 | "com.google.android.apps.docs.storage.legacy" == authority 48 | } 49 | 50 | @Keep 51 | internal fun getDataColumn( 52 | context: Context, 53 | uri: Uri?, 54 | selection: String?, 55 | selectionArgs: Array?, 56 | ): String? { 57 | var cursor: Cursor? = null 58 | val column = "_data" 59 | val projection = arrayOf( 60 | column, 61 | ) 62 | try { 63 | cursor = context.contentResolver.query( 64 | uri!!, 65 | projection, 66 | selection, 67 | selectionArgs, 68 | null, 69 | ) 70 | if (cursor != null && cursor.moveToFirst()) { 71 | val index = cursor.getColumnIndexOrThrow(column) 72 | return cursor.getString(index) 73 | } 74 | } finally { 75 | cursor?.close() 76 | } 77 | return null 78 | } 79 | 80 | 81 | @Keep 82 | internal fun Context.getMediaDocumentPath(uri: Uri): String? { 83 | val docId = DocumentsContract.getDocumentId(uri) 84 | val split = 85 | docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() 86 | val type = split[0] 87 | val contentUri: Uri? 88 | when (type) { 89 | "image" -> { 90 | contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI 91 | } 92 | 93 | "video" -> { 94 | contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI 95 | } 96 | 97 | "audio" -> { 98 | contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 99 | } 100 | 101 | else -> { 102 | contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 103 | } 104 | } 105 | val selection = "_id=?" 106 | val selectionArgs = arrayOf( 107 | split[1], 108 | ) 109 | return getDataColumn(this, contentUri, selection, selectionArgs) 110 | } 111 | 112 | internal fun Context.getDriveFilePath(uri: Uri): String? { 113 | val returnCursor: Cursor? = contentResolver.query( 114 | uri, 115 | null, 116 | null, 117 | null, 118 | null 119 | ) 120 | val nameIndex = returnCursor?.getColumnIndex(OpenableColumns.DISPLAY_NAME) ?: return null 121 | returnCursor.moveToFirst() 122 | val name = returnCursor.getString(nameIndex) 123 | val file = File(cacheDir, sanitizeFileName(name)) 124 | returnCursor.close() 125 | return try { 126 | val inputStream: InputStream? = contentResolver.openInputStream(uri) 127 | val outputStream = FileOutputStream(file) 128 | var read: Int? 129 | val bytesAvailable = inputStream?.available() ?: 0 130 | val bufferSize = bytesAvailable.coerceAtMost(Const.MAX_BUFFER_SIZE) 131 | val buffers = ByteArray(bufferSize) 132 | while (inputStream?.read(buffers).also { read = it } != -1) { 133 | read?.let { outputStream.write(buffers, 0, it) } 134 | } 135 | inputStream?.close() 136 | outputStream.close() 137 | file.path 138 | } catch (e: IOException) { 139 | Timber.tag(Const.LogTag.FILE_PICKER_EXCEPTION).e(e.message ?: "") 140 | null 141 | } 142 | 143 | } 144 | 145 | /** 146 | * Sanitizes a filename to prevent directory traversal attacks and other security issues 147 | */ 148 | private fun sanitizeFileName(fileName: String): String { 149 | // Remove any path components that could cause directory traversal 150 | val name = fileName.replace("[\\\\/:*?\"<>|]".toRegex(), "_") 151 | 152 | // Limit filename length if needed 153 | return if (name.length > FILE_NAME_LENGTH) { 154 | val extension = name.substringAfterLast('.', "") 155 | val baseName = name.substringBeforeLast('.') 156 | val maxBaseLength = 157 | FILE_NAME_LENGTH - extension.length - (if (extension.isNotEmpty()) 1 else 0) 158 | if (extension.isNotEmpty()) { 159 | "${baseName.take(maxBaseLength)}.$extension" 160 | } else { 161 | baseName.take(maxBaseLength) 162 | } 163 | } else { 164 | name 165 | } 166 | } 167 | 168 | const val FILE_NAME_LENGTH = 255 169 | 170 | 171 | /*** 172 | * Used for Android Q+ 173 | * @param uri 174 | * @param newDirName if you want to create a directory, you can set this variable 175 | * @return 176 | */ 177 | internal fun Context.copyFileToInternalStorage( 178 | uri: Uri, 179 | newDirName: String = Const.copyFileFolder 180 | ): String? { 181 | val returnCursor: Cursor? = this.contentResolver.query( 182 | uri, arrayOf( 183 | OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE 184 | ), null, null, null 185 | ) 186 | val nameIndex = returnCursor?.getColumnIndex(OpenableColumns.DISPLAY_NAME) ?: return null 187 | returnCursor.moveToFirst() 188 | val name = sanitizeFileName(returnCursor.getString(nameIndex)) 189 | returnCursor.close() 190 | val output: File = if (newDirName != "") { 191 | val dir = File(cacheDir, newDirName) 192 | if (!dir.exists()) { 193 | dir.mkdir() 194 | } 195 | File(cacheDir, "$newDirName/$name") 196 | } else { 197 | File(cacheDir, "/$name") 198 | } 199 | return try { 200 | val inputStream: InputStream? = contentResolver.openInputStream(uri) 201 | val outputStream = FileOutputStream(output) 202 | var read: Int? 203 | val buffers = ByteArray(Const.BUFFER_SIZE) 204 | while (inputStream?.read(buffers).also { read = it } != -1) { 205 | read?.let { outputStream.write(buffers, 0, it) } 206 | } 207 | inputStream?.close() 208 | outputStream.close() 209 | output.path 210 | } catch (e: IOException) { 211 | Timber.tag(Const.LogTag.FILE_PICKER_EXCEPTION).e(e.message ?: "") 212 | output.delete() 213 | null 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /filepickerlibrary/src/main/res/drawable-hdpi/ic_media.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChochaNaresh/FilePicker/1df651a9c0cc5db2be674608317e7e6dd7035ff0/filepickerlibrary/src/main/res/drawable-hdpi/ic_media.webp -------------------------------------------------------------------------------- /filepickerlibrary/src/main/res/drawable-mdpi/ic_media.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChochaNaresh/FilePicker/1df651a9c0cc5db2be674608317e7e6dd7035ff0/filepickerlibrary/src/main/res/drawable-mdpi/ic_media.webp -------------------------------------------------------------------------------- /filepickerlibrary/src/main/res/drawable-xhdpi/ic_media.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChochaNaresh/FilePicker/1df651a9c0cc5db2be674608317e7e6dd7035ff0/filepickerlibrary/src/main/res/drawable-xhdpi/ic_media.webp -------------------------------------------------------------------------------- /filepickerlibrary/src/main/res/drawable-xxhdpi/ic_media.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChochaNaresh/FilePicker/1df651a9c0cc5db2be674608317e7e6dd7035ff0/filepickerlibrary/src/main/res/drawable-xxhdpi/ic_media.webp -------------------------------------------------------------------------------- /filepickerlibrary/src/main/res/drawable-xxxhdpi/ic_media.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChochaNaresh/FilePicker/1df651a9c0cc5db2be674608317e7e6dd7035ff0/filepickerlibrary/src/main/res/drawable-xxxhdpi/ic_media.webp -------------------------------------------------------------------------------- /filepickerlibrary/src/main/res/drawable/ic_camera.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /filepickerlibrary/src/main/res/drawable/ic_file.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /filepickerlibrary/src/main/res/drawable/ic_video.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /filepickerlibrary/src/main/res/drawable/transparent.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /filepickerlibrary/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | File Picker 3 | 4 | 5 | Permission Denied 6 | 7 | 8 | Please allow Read media permission for selecting image/video. 9 | Please allow %1$s permission for capture image/video. 10 | %1$s permission is not granted. Please allow it from setting. 11 | Permissions is not found in the manifest. 12 | 13 | 14 | 15 | Permission not allowed 16 | 17 | 18 | Choose option 19 | 20 | 21 | Go To Setting 22 | Cancel 23 | OK 24 | 25 | 26 | Document Pick Error 27 | Media Pick Error 28 | %1$s data is set 29 | %1$s capture Error 30 | -------------------------------------------------------------------------------- /filepickerlibrary/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | -------------------------------------------------------------------------------- /filepickerlibrary/src/main/res/xml/file_picker_library_provider.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /filepickerlibrary/src/test/java/com/nareshchocha/filepickerlibrary/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.nareshchocha.filepickerlibrary 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 2 | android.useAndroidX=true 3 | kotlin.code.style=official 4 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.10.1" 3 | mavenPublish = "0.25.3" 4 | kotlin = "2.1.20" 5 | detekt = "1.23.8" 6 | 7 | # build config 8 | compileSdk = "36" 9 | targetSdk = "36" 10 | minSdk = "21" 11 | jdkVersion = "VERSION_17" 12 | 13 | 14 | # compose 15 | activityCompose = "1.10.1" 16 | composeBom = "2025.05.01" 17 | 18 | # Testing 19 | junit = "4.13.2" 20 | junitVersion = "1.2.1" 21 | espressoCore = "3.6.1" 22 | 23 | # core 24 | startupRuntime = "1.2.0" 25 | timber = "5.0.1" 26 | 27 | # testing 28 | truth = "1.1.5" 29 | 30 | [libraries] 31 | # core 32 | androidx-startup-runtime = { module = "androidx.startup:startup-runtime", version.ref = "startupRuntime" } 33 | timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } 34 | 35 | # compose 36 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } 37 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } 38 | androidx-ui = { group = "androidx.compose.ui", name = "ui" } 39 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } 40 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } 41 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" } 42 | androidx-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } 43 | 44 | # testing 45 | junit = { group = "junit", name = "junit", version.ref = "junit" } 46 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } 47 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } 48 | # testing compose 49 | androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } 50 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } 51 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } 52 | # testing 53 | truth = { module = "com.google.truth:truth", version.ref = "truth" } 54 | 55 | [plugins] 56 | android-application = { id = "com.android.application", version.ref = "agp" } 57 | android-library = { id = "com.android.library", version.ref = "agp" } 58 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 59 | # compose plugin 60 | kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 61 | # parcelize 62 | kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } 63 | # code style review 64 | arturbosch-detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } 65 | # maven publish 66 | maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" } 67 | # signing 68 | signing = { id = "signing" } 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChochaNaresh/FilePicker/1df651a9c0cc5db2be674608317e7e6dd7035ff0/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Nov 22 15:17:24 IST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /sample/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.kotlin.android) 4 | alias(libs.plugins.kotlin.compose) 5 | alias(libs.plugins.kotlin.parcelize) 6 | } 7 | 8 | android { 9 | namespace = "com.nareshchocha.filepicker" 10 | compileSdk = libs.versions.compileSdk.get().toInt() 11 | 12 | defaultConfig { 13 | applicationId = "com.nareshchocha.filepicker" 14 | minSdk = libs.versions.minSdk.get().toInt() 15 | targetSdk = libs.versions.targetSdk.get().toInt() 16 | versionCode = 2 17 | versionName = "1.1" 18 | 19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 20 | } 21 | 22 | buildTypes { 23 | release { 24 | isMinifyEnabled = false 25 | proguardFiles( 26 | getDefaultProguardFile("proguard-android-optimize.txt"), 27 | "proguard-rules.pro" 28 | ) 29 | } 30 | } 31 | buildFeatures { 32 | compose = true 33 | aidl = false 34 | buildConfig = false 35 | renderScript = false 36 | shaders = false 37 | } 38 | 39 | compileOptions { 40 | sourceCompatibility = JavaVersion.valueOf(libs.versions.jdkVersion.get()) 41 | targetCompatibility = JavaVersion.valueOf(libs.versions.jdkVersion.get()) 42 | } 43 | 44 | kotlinOptions { 45 | jvmTarget = JavaVersion.valueOf(libs.versions.jdkVersion.get()).toString() 46 | } 47 | 48 | } 49 | 50 | dependencies { 51 | 52 | implementation(libs.androidx.startup.runtime) 53 | // compose 54 | implementation(libs.androidx.activity.compose) 55 | implementation(platform(libs.androidx.compose.bom)) 56 | implementation(libs.androidx.ui) 57 | implementation(libs.androidx.ui.graphics) 58 | implementation(libs.androidx.ui.tooling.preview) 59 | implementation(libs.androidx.material3) 60 | implementation(libs.androidx.material.icons.extended) 61 | 62 | implementation("io.coil-kt.coil3:coil-compose:3.2.0") 63 | // File Picker 64 | implementation(project(":filepickerlibrary")) 65 | 66 | // timber 67 | implementation(libs.timber) 68 | 69 | 70 | // testing 71 | testImplementation(libs.junit) 72 | androidTestImplementation(libs.androidx.junit) 73 | androidTestImplementation(libs.androidx.espresso.core) 74 | testImplementation(libs.truth) 75 | androidTestImplementation(libs.truth) 76 | 77 | 78 | // testing compose 79 | androidTestImplementation(platform(libs.androidx.compose.bom)) 80 | androidTestImplementation(libs.androidx.ui.test.junit4) 81 | debugImplementation(libs.androidx.ui.tooling) 82 | debugImplementation(libs.androidx.ui.test.manifest) 83 | 84 | } 85 | -------------------------------------------------------------------------------- /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.kts. 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/androidTest/java/com/nareshchocha/filepicker/AppUITest.kt: -------------------------------------------------------------------------------- 1 | package com.nareshchocha.filepicker 2 | 3 | import android.content.res.Resources 4 | import android.os.Build 5 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 6 | import androidx.test.ext.junit.rules.ActivityScenarioRule 7 | import androidx.test.filters.LargeTest 8 | import androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner 9 | import androidx.test.platform.app.InstrumentationRegistry 10 | import androidx.test.uiautomator.By 11 | import androidx.test.uiautomator.UiDevice 12 | import com.google.common.truth.Truth 13 | import junit.framework.TestCase 14 | import org.junit.Before 15 | import org.junit.Rule 16 | import org.junit.Test 17 | import org.junit.runner.RunWith 18 | import java.util.regex.Pattern 19 | 20 | @RunWith(AndroidJUnit4ClassRunner::class) 21 | @LargeTest 22 | class AppUITest { 23 | 24 | private lateinit var device: UiDevice 25 | private lateinit var mResources: Resources 26 | 27 | @get:Rule 28 | var instantTaskExecutorRule: InstantTaskExecutorRule = InstantTaskExecutorRule() 29 | 30 | @get:Rule 31 | var activityScenarioRule = ActivityScenarioRule( 32 | MainActivity::class.java, 33 | ) 34 | 35 | @Before 36 | fun startMainActivityFromHomeScreen() { 37 | device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) 38 | mResources = InstrumentationRegistry.getInstrumentation().targetContext.resources 39 | // startAppAndWaitLaunch() 40 | } 41 | 42 | /* @Before 43 | fun tearDown() { 44 | try { 45 | InstrumentationRegistry.getInstrumentation().uiAutomation.grantRuntimePermission( 46 | TestConst.BASIC_SAMPLE_PACKAGE, 47 | Manifest.permission.CAMERA, 48 | ) 49 | InstrumentationRegistry.getInstrumentation().uiAutomation.grantRuntimePermission( 50 | TestConst.BASIC_SAMPLE_PACKAGE, 51 | Manifest.permission.WRITE_EXTERNAL_STORAGE, 52 | ) 53 | } catch (e: Exception) { 54 | e.stackTraceToString() 55 | } 56 | }*/ 57 | 58 | private fun clickPermissionAllow() { 59 | device.clickAllAllowButtons() 60 | device.clickAllAllowOnlyThisTimeButtons() 61 | device.clickAllAllowLocationButtons() 62 | } 63 | 64 | private fun clickPermissionDeny() { 65 | device.clickAllDenyButtons() 66 | val okButton = device.findObject( 67 | By.text("OK").clazz(TestConst.BUTTON_CLASS_NAME), 68 | ) 69 | okButton?.clickAndWait() 70 | device.clickAllDenyButtons(true) 71 | val gotoSettingButton = device.findObject( 72 | By.text("GO TO SETTING").clazz(TestConst.BUTTON_CLASS_NAME), 73 | ) 74 | TestCase.assertNotNull(gotoSettingButton) 75 | TestCase.assertEquals(gotoSettingButton.isEnabled, true) 76 | val cancelButton = device.findObject( 77 | By.text("CANCEL").clazz(TestConst.BUTTON_CLASS_NAME), 78 | ) 79 | TestCase.assertNotNull(cancelButton) 80 | cancelButton.clickAndWait() 81 | } 82 | 83 | private fun checkItemAddedOnList(expectedCount: Int) { 84 | activityScenarioRule.scenario.onActivity { 85 | Truth.assertThat(it.uriList.size == expectedCount).isTrue() 86 | Truth.assertThat(it.mMediaAdapter.itemCount == expectedCount).isTrue() 87 | } 88 | } 89 | 90 | private fun successCameraCapture(isVideo: Boolean = false) { 91 | val shutterButton = 92 | device.findObject( 93 | By.res( 94 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 95 | "com.android.camera2:id/shutter_button" 96 | } else { 97 | "com.android.camera:id/shutter_button" 98 | }, 99 | ) 100 | .text(Pattern.compile("")) 101 | .pkg("com.android.camera2"), 102 | ) 103 | 104 | if (shutterButton.isEnabled) { 105 | shutterButton.clickAndWait() 106 | } 107 | if (isVideo) { 108 | shutterButton.clickAndWait() 109 | } 110 | val doneButton = device.findObject( 111 | By.res( 112 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 113 | "com.android.camera2:id/done_button" 114 | } else { 115 | "com.android.camera:id/btn_done" 116 | }, 117 | ).text(Pattern.compile("")), 118 | ) 119 | if (doneButton != null) { 120 | doneButton.clickAndWait() 121 | } else { 122 | device.pressBack() 123 | } 124 | } 125 | 126 | @Test 127 | fun captureImageButton_successTest() { 128 | val button = device.findObject( 129 | By.text(mResources.getString(R.string.capture_image)) 130 | .clazz(TestConst.BUTTON_CLASS_NAME), 131 | ) 132 | button?.clickAndWait() 133 | clickPermissionAllow() 134 | successCameraCapture() 135 | checkItemAddedOnList(1) 136 | } 137 | 138 | @Test 139 | fun captureImageButton_FailTest() { 140 | val button = device.findObject( 141 | By.text(mResources.getString(R.string.capture_image)) 142 | .clazz(TestConst.BUTTON_CLASS_NAME), 143 | ) 144 | button.clickAndWait() 145 | clickPermissionAllow() 146 | device.pressBack() 147 | checkItemAddedOnList(0) 148 | } 149 | 150 | @Test 151 | fun captureVideoButton_successTest() { 152 | val button = device.findObject( 153 | By.text(mResources.getString(R.string.capture_video)) 154 | .clazz(TestConst.BUTTON_CLASS_NAME), 155 | ) 156 | button?.clickAndWait() 157 | clickPermissionAllow() 158 | successCameraCapture(true) 159 | checkItemAddedOnList(1) 160 | } 161 | 162 | @Test 163 | fun captureVideoButton_FailTest() { 164 | val button = device.findObject( 165 | By.text(mResources.getString(R.string.capture_video)) 166 | .clazz(TestConst.BUTTON_CLASS_NAME), 167 | ) 168 | button?.clickAndWait() 169 | clickPermissionAllow() 170 | device.pressBack() 171 | checkItemAddedOnList(0) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /sample/src/androidTest/java/com/nareshchocha/filepicker/TestConst.kt: -------------------------------------------------------------------------------- 1 | package com.nareshchocha.filepicker 2 | 3 | import androidx.annotation.Keep 4 | 5 | @Keep 6 | object TestConst { 7 | const val BASIC_SAMPLE_PACKAGE = "com.nareshchocha.filepicker" 8 | const val LAUNCH_TIMEOUT = 5000L 9 | const val BUTTON_CLICK_TIMEOUT = 5000L 10 | const val BUTTON_CLASS_NAME = "android.widget.Button" 11 | const val IMAGE_VIEW_CLASS_NAME = "android.widget.ImageView" 12 | const val CHECK_BOX_CLASS_NAME = "android.widget.CheckBox" 13 | } 14 | -------------------------------------------------------------------------------- /sample/src/androidTest/java/com/nareshchocha/filepicker/TestExtentions.kt: -------------------------------------------------------------------------------- 1 | package com.nareshchocha.filepicker 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Build 6 | import androidx.test.core.app.ApplicationProvider 7 | import androidx.test.espresso.matcher.ViewMatchers 8 | import androidx.test.uiautomator.By 9 | import androidx.test.uiautomator.UiDevice 10 | import androidx.test.uiautomator.UiObject2 11 | import androidx.test.uiautomator.Until 12 | import junit.framework.TestCase 13 | import org.hamcrest.Matchers 14 | 15 | private val denyStr = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 16 | "DENY" 17 | } else { 18 | "Deny" 19 | } 20 | private val allowStr = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 21 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 22 | "Allow" 23 | } else { 24 | "ALLOW" 25 | } 26 | } else { 27 | "Allow" 28 | } 29 | 30 | internal fun UiDevice.clickAllAllowOnlyThisTimeButtons() { 31 | val allowButton = findObject( 32 | By.text("While using the app").clazz(TestConst.BUTTON_CLASS_NAME), 33 | ) 34 | allowButton?.clickAndWait() 35 | if (allowButton != null) { 36 | clickAllAllowOnlyThisTimeButtons() 37 | } 38 | } 39 | 40 | internal fun UiDevice.clickAllAllowLocationButtons() { 41 | val allowButton = findObject( 42 | By.text("Allow all the time").clazz(TestConst.BUTTON_CLASS_NAME), 43 | ) 44 | allowButton?.clickAndWait() 45 | if (allowButton != null) { 46 | clickAllAllowLocationButtons() 47 | } 48 | } 49 | 50 | fun UiDevice.clickAllDenyButtons(withDonAsk: Boolean = false) { 51 | val denyButton = findObject( 52 | By.text(denyStr).clazz(TestConst.BUTTON_CLASS_NAME), 53 | ) 54 | if (withDonAsk && denyButton != null) { 55 | val dontButton = findObject( 56 | By.clazz(TestConst.CHECK_BOX_CLASS_NAME), 57 | ) 58 | dontButton.clickAndWait() 59 | val allowButton = findObject( 60 | By.text(allowStr).clazz(TestConst.BUTTON_CLASS_NAME), 61 | ) 62 | TestCase.assertNotNull(allowButton) 63 | TestCase.assertEquals(allowButton.isEnabled, false) 64 | } 65 | 66 | denyButton?.clickAndWait() 67 | if (denyButton != null) { 68 | clickAllDenyButtons(withDonAsk) 69 | } 70 | } 71 | 72 | // Start from the home screen 73 | fun UiDevice.startAppAndWaitLaunch() { 74 | pressHome() 75 | 76 | // Wait for launcher 77 | val launcherPackage: String = launcherPackageName 78 | ViewMatchers.assertThat(launcherPackage, Matchers.notNullValue()) 79 | wait( 80 | Until.hasObject(By.pkg(launcherPackage).depth(0)), 81 | TestConst.LAUNCH_TIMEOUT, 82 | ) 83 | // Launch the app 84 | val context = ApplicationProvider.getApplicationContext() 85 | val intent = context.packageManager.getLaunchIntentForPackage( 86 | TestConst.BASIC_SAMPLE_PACKAGE, 87 | )?.apply { 88 | addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) 89 | } 90 | context.startActivity(intent) 91 | wait( 92 | Until.hasObject(By.pkg(TestConst.BASIC_SAMPLE_PACKAGE).depth(0)), 93 | TestConst.LAUNCH_TIMEOUT, 94 | ) 95 | } 96 | 97 | fun UiDevice.clickAllAllowButtons() { 98 | val allowButton = findObject( 99 | By.text(allowStr).clazz(TestConst.BUTTON_CLASS_NAME), 100 | ) 101 | allowButton?.clickAndWait() 102 | if (allowButton != null) { 103 | clickAllAllowButtons() 104 | } 105 | } 106 | 107 | fun UiObject2.clickAndWait() { 108 | this.clickAndWait( 109 | Until.newWindow(), 110 | TestConst.BUTTON_CLICK_TIMEOUT, 111 | ) 112 | } 113 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | 12 | 13 | 14 | 17 | 20 | 21 | 31 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 47 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /sample/src/main/java/com/nareshchocha/filepicker/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.nareshchocha.filepicker 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import android.os.Bundle 6 | import androidx.activity.ComponentActivity 7 | import androidx.activity.compose.rememberLauncherForActivityResult 8 | import androidx.activity.compose.setContent 9 | import androidx.activity.result.contract.ActivityResultContracts 10 | import androidx.compose.foundation.Image 11 | import androidx.compose.foundation.layout.Column 12 | import androidx.compose.foundation.layout.Spacer 13 | import androidx.compose.foundation.layout.fillMaxSize 14 | import androidx.compose.foundation.layout.fillMaxWidth 15 | import androidx.compose.foundation.layout.height 16 | import androidx.compose.foundation.layout.padding 17 | import androidx.compose.foundation.layout.size 18 | import androidx.compose.foundation.lazy.LazyRow 19 | import androidx.compose.foundation.lazy.items 20 | import androidx.compose.material3.Button 21 | import androidx.compose.material3.ExperimentalMaterial3Api 22 | import androidx.compose.material3.Scaffold 23 | import androidx.compose.material3.Text 24 | import androidx.compose.material3.TopAppBar 25 | import androidx.compose.runtime.mutableStateListOf 26 | import androidx.compose.runtime.remember 27 | import androidx.compose.ui.Modifier 28 | import androidx.compose.ui.unit.dp 29 | import coil3.compose.rememberAsyncImagePainter 30 | import com.nareshchocha.filepickerlibrary.models.DocumentFilePickerConfig 31 | import com.nareshchocha.filepickerlibrary.models.ImageCaptureConfig 32 | import com.nareshchocha.filepickerlibrary.models.Orientation 33 | import com.nareshchocha.filepickerlibrary.models.PickMediaConfig 34 | import com.nareshchocha.filepickerlibrary.models.PickMediaType 35 | import com.nareshchocha.filepickerlibrary.models.PopUpConfig 36 | import com.nareshchocha.filepickerlibrary.models.PopUpType 37 | import com.nareshchocha.filepickerlibrary.ui.FilePicker 38 | import timber.log.Timber 39 | 40 | class MainActivity : ComponentActivity() { 41 | @OptIn(ExperimentalMaterial3Api::class) 42 | override fun onCreate(savedInstanceState: Bundle?) { 43 | super.onCreate(savedInstanceState) 44 | setContent { 45 | val uriList = remember { mutableStateListOf() } 46 | 47 | val captureImageResultLauncher = 48 | rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> 49 | if (result.resultCode == RESULT_OK) { 50 | uriList.clear() 51 | if (result.data?.data != null) { 52 | result.data?.data?.let { uriList.add(it) } 53 | } else { 54 | val listData = result.data?.getClipDataUris() 55 | listData?.let { uriList.addAll(it) } 56 | } 57 | Timber.tag("FILE_RESULT").v(result.toString()) 58 | Timber.tag("FILE_RESULT").v(result.data?.extras?.toString()) 59 | } else { 60 | Timber.tag("FILE_PICKER_ERROR").v("capture Error") 61 | } 62 | } 63 | 64 | Scaffold( 65 | topBar = { 66 | TopAppBar(title = { Text("File Picker Sample") }) 67 | } 68 | ) { padding -> 69 | Column( 70 | modifier = Modifier 71 | .padding(padding) 72 | .padding(16.dp) 73 | .fillMaxSize() 74 | ) { 75 | LazyRow( 76 | modifier = Modifier 77 | .fillMaxWidth() 78 | .height(100.dp) 79 | ) { 80 | items(uriList) { uri -> 81 | Image( 82 | painter = rememberAsyncImagePainter(uri), 83 | contentDescription = null, 84 | modifier = Modifier 85 | .size(80.dp) 86 | .padding(4.dp) 87 | ) 88 | } 89 | } 90 | Spacer(modifier = Modifier.height(16.dp)) 91 | Button( 92 | onClick = { 93 | captureImageResultLauncher.launch( 94 | FilePicker.Builder(this@MainActivity) 95 | .imageCaptureBuild(ImageCaptureConfig(isUseRearCamera = false)), 96 | ) 97 | }, 98 | modifier = Modifier.fillMaxWidth() 99 | ) { Text("Capture Image") } 100 | 101 | Spacer(modifier = Modifier.height(8.dp)) 102 | Button( 103 | onClick = { 104 | captureImageResultLauncher.launch( 105 | FilePicker.Builder(this@MainActivity) 106 | .videoCaptureBuild(), 107 | ) 108 | }, 109 | modifier = Modifier.fillMaxWidth() 110 | ) { Text("Capture Video") } 111 | 112 | Spacer(modifier = Modifier.height(8.dp)) 113 | Button( 114 | onClick = { 115 | captureImageResultLauncher.launch( 116 | FilePicker.Builder(this@MainActivity) 117 | .pickMediaBuild( 118 | PickMediaConfig( 119 | mPickMediaType = PickMediaType.ImageOnly, 120 | allowMultiple = true, 121 | ), 122 | ), 123 | ) 124 | }, 125 | modifier = Modifier.fillMaxWidth() 126 | ) { Text("Pick Images") } 127 | 128 | Spacer(modifier = Modifier.height(8.dp)) 129 | Button( 130 | onClick = { 131 | captureImageResultLauncher.launch( 132 | FilePicker.Builder(this@MainActivity) 133 | .pickMediaBuild( 134 | PickMediaConfig( 135 | mPickMediaType = PickMediaType.VideoOnly, 136 | allowMultiple = true, 137 | maxFiles = 3, 138 | ), 139 | ), 140 | ) 141 | }, 142 | modifier = Modifier.fillMaxWidth() 143 | ) { Text("Pick Videos") } 144 | 145 | Spacer(modifier = Modifier.height(8.dp)) 146 | Button( 147 | onClick = { 148 | captureImageResultLauncher.launch( 149 | FilePicker.Builder(this@MainActivity) 150 | .pickMediaBuild( 151 | PickMediaConfig( 152 | mPickMediaType = PickMediaType.ImageAndVideo, 153 | allowMultiple = true, 154 | ), 155 | ), 156 | ) 157 | }, 158 | modifier = Modifier.fillMaxWidth() 159 | ) { Text("Pick Images & Videos") } 160 | 161 | Spacer(modifier = Modifier.height(8.dp)) 162 | Button( 163 | onClick = { 164 | captureImageResultLauncher.launch( 165 | FilePicker.Builder(this@MainActivity) 166 | .pickDocumentFileBuild( 167 | DocumentFilePickerConfig( 168 | allowMultiple = true, 169 | mMimeTypes = listOf("application/pdf"), 170 | ), 171 | ), 172 | ) 173 | }, 174 | modifier = Modifier.fillMaxWidth() 175 | ) { Text("Pick PDF") } 176 | 177 | Spacer(modifier = Modifier.height(8.dp)) 178 | Button( 179 | onClick = { 180 | captureImageResultLauncher.launch( 181 | FilePicker.Builder(this@MainActivity) 182 | .setPopUpConfig( 183 | PopUpConfig( 184 | mPopUpType = PopUpType.BOTTOM_SHEET, 185 | mOrientation = Orientation.VERTICAL, 186 | chooserTitle = "Choose Profile", 187 | ), 188 | ) 189 | .addPickDocumentFile() 190 | .addImageCapture() 191 | .addVideoCapture() 192 | .addPickMedia() 193 | .build(), 194 | ) 195 | }, 196 | modifier = Modifier.fillMaxWidth() 197 | ) { Text("All Options") } 198 | } 199 | } 200 | } 201 | } 202 | } 203 | 204 | // Helper extension for getting clip data URIs 205 | fun Intent.getClipDataUris(): ArrayList { 206 | val resultSet = LinkedHashSet() 207 | data?.let { data -> resultSet.add(data) } 208 | val clipData = clipData 209 | if (clipData == null && resultSet.isEmpty()) { 210 | return ArrayList() 211 | } else if (clipData != null) { 212 | for (i in 0 until clipData.itemCount) { 213 | val uri = clipData.getItemAt(i).uri 214 | if (uri != null) { 215 | resultSet.add(uri) 216 | } 217 | } 218 | } 219 | return ArrayList(resultSet) 220 | } -------------------------------------------------------------------------------- /sample/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChochaNaresh/FilePicker/1df651a9c0cc5db2be674608317e7e6dd7035ff0/sample/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChochaNaresh/FilePicker/1df651a9c0cc5db2be674608317e7e6dd7035ff0/sample/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChochaNaresh/FilePicker/1df651a9c0cc5db2be674608317e7e6dd7035ff0/sample/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChochaNaresh/FilePicker/1df651a9c0cc5db2be674608317e7e6dd7035ff0/sample/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChochaNaresh/FilePicker/1df651a9c0cc5db2be674608317e7e6dd7035ff0/sample/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChochaNaresh/FilePicker/1df651a9c0cc5db2be674608317e7e6dd7035ff0/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChochaNaresh/FilePicker/1df651a9c0cc5db2be674608317e7e6dd7035ff0/sample/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChochaNaresh/FilePicker/1df651a9c0cc5db2be674608317e7e6dd7035ff0/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChochaNaresh/FilePicker/1df651a9c0cc5db2be674608317e7e6dd7035ff0/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChochaNaresh/FilePicker/1df651a9c0cc5db2be674608317e7e6dd7035ff0/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | FilePicker 3 | Capture Image 4 | Capture Video 5 | Pick Images 6 | Pick Video 7 | Pick Image Or Video 8 | Pick PDF 9 | All Option 10 | -------------------------------------------------------------------------------- /sample/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 |