├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── ir │ │ └── one_developer │ │ └── filepickerlibrary │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── ir │ │ │ └── one_developer │ │ │ └── filepickerlibrary │ │ │ ├── FileAdapter.kt │ │ │ └── MainActivity.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_chevron_right.xml │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ └── file_layout.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-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── ir │ └── one_developer │ └── filepickerlibrary │ └── ExampleUnitTest.kt ├── build.gradle ├── file-picker ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── ir │ │ └── one_developer │ │ └── file_picker │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── file_picker │ │ │ ├── Const.kt │ │ │ ├── FilePicker.kt │ │ │ ├── adapter │ │ │ ├── FilePickerAdapter.kt │ │ │ ├── ItemAdapter.kt │ │ │ └── ItemVH.kt │ │ │ ├── data │ │ │ ├── model │ │ │ │ └── Media.kt │ │ │ └── repository │ │ │ │ ├── FilesPagingSource.kt │ │ │ │ └── FilesRepository.kt │ │ │ ├── extension │ │ │ ├── ActivityExt.kt │ │ │ ├── CollectionExt.kt │ │ │ ├── ContextExt.kt │ │ │ ├── FileExt.kt │ │ │ ├── FragmentExt.kt │ │ │ ├── NumberExt.kt │ │ │ └── StringExt.kt │ │ │ ├── listener │ │ │ ├── OnItemClickListener.kt │ │ │ └── OnSubmitClickListener.kt │ │ │ └── view │ │ │ └── SquareCardView.kt │ └── res │ │ ├── drawable │ │ ├── ic_audiotrack.xml │ │ ├── ic_check.xml │ │ ├── ic_image.xml │ │ └── ic_play.xml │ │ ├── layout │ │ ├── file_picker.xml │ │ └── item_layout.xml │ │ └── values │ │ ├── dimens.xml │ │ └── themes.xml │ └── test │ └── java │ └── ir │ └── one_developer │ └── file_picker │ └── ExampleUnitTest.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── screenshots ├── audio.png ├── image-2col-full.jpg ├── image-3col.jpg ├── image.png ├── video-2col-full.jpg └── video.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | -------------------------------------------------------------------------------- /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 2022 Majid Arabi 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 | ![Latest release](https://img.shields.io/github/v/release/majidarabi/androidfilepicker?color=%2308c94d) 2 | ![Code size in bytes](https://img.shields.io/github/languages/code-size/majidarabi/androidfilepicker) 3 | [![Stars](https://img.shields.io/github/stars/MajidArabi/AndroidFilePicker)](https://github.com/MajidArabi/AndroidFilePicker/stargazers) 4 | [![Downloads](https://jitpack.io/v/MajidArabi/AndroidFilePicker/month.svg)](https://jitpack.io/#MajidArabi/AndroidFilePicker) 5 | [![API](https://img.shields.io/badge/API-21%2B-blue.svg?style=flat)](https://android-arsenal.com/api?level=21) 6 | [![ktlint](https://img.shields.io/badge/code%20style-%E2%9D%A4-FF4081.svg)](https://ktlint.github.io/) 7 | [![License](https://img.shields.io/badge/license-Apache%202-4EB1BA.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) 8 | 9 | 10 | ## Simple, Powerful and Beautiful Android Image/Video/Audio Picker 😎 11 | **Features** 😍 12 | - No need check storage permission 😉 13 | - Single and multiple selection 14 | - Supported RTL and LTR list direction (default=LTR) 15 | - Supported image, video or audio (default=image) 16 | - Supported custom title 17 | - Supported custom colors 18 | - Supported custom overlay alpha (default=0.5F) 19 | - Supported dynamic span count (default=2) 20 | - Limit max item selection (default=1) 21 | - Set Cancellable (default=true) 22 | - Show file directory 23 | - Show file size 24 | 25 | ## Donate 26 | 27 | **BTC** 28 | 29 | 1KFvFt3ZvGbxSZpz28XUiqzgWYmdvbaHvo 30 | 31 | ## Screenshots 32 | 33 | | Image | Video | Audio 34 | |--|--|--| 35 | | | | 36 | 37 | ## Download 38 | 39 | Step 1. Add it in your root build.gradle at the end of repositories: 40 | 41 | allprojects { 42 | repositories { 43 | ... 44 | maven { url 'https://jitpack.io' } 45 | } 46 | } 47 | 48 | Step 2. Add the dependency 49 | 50 | ***Latest Version*** : ![Latest release](https://img.shields.io/github/v/release/majidarabi/androidfilepicker?color=%2308c94d) 51 | 52 | dependencies { 53 | implementation("com.github.MajidArabi:AndroidFilePicker:$LATEST_VERSION") 54 | } 55 | 56 | ## Usage 57 | 58 | **Kotlin** 59 | 60 | showFilePicker( 61 | limitItemSelection = 5, 62 | listDirection = ListDirection.RTL, 63 | accentColor = ContextCompat.getColor(this@MainActivity, R.color.purple_700), 64 | titleTextColor = ContextCompat.getColor(this@MainActivity, R.color.purple_700), 65 | onSubmitClickListener = object : OnSubmitClickListener { 66 | override fun onClick(files: List) { 67 | // Do something here with selected files 68 | } 69 | }, 70 | onItemClickListener = object : OnItemClickListener { 71 | override fun onClick(media: Media, position: Int, adapter: FilePickerAdapter) { 72 | if (!media.file.isDirectory) { 73 | adapter.setSelected(position) 74 | } 75 | } 76 | } 77 | ) 78 | 79 | **Java** 80 | 81 | new FilePicker.Builder(this) 82 | .setLimitItemSelection(3) 83 | .setAccentColor(Color.CYAN) 84 | .setCancellable(false) 85 | .setOnSubmitClickListener(files -> { 86 | // Do something here with selected files 87 | }) 88 | .setOnItemClickListener((media, pos, adapter) -> { 89 | if (!media.getFile().isDirectory()) { 90 | adapter.setSelected(pos); 91 | } 92 | }) 93 | .buildAndShow(); 94 | 95 | ## Author 96 | 97 | **Majid Arabi** 98 | - [Github](https://github.com/MajidArabi) 99 | - [Linkedin](https://www.linkedin.com/in/majid-arabi-673855129/) 100 | - [Telegram](https://t.me/one_developer) 101 | - [Site](http://one-developer.ir) 102 | - [Email](mailto:majidarabi73@gmail.com) 103 | 104 | ## Thanks for your support ! 105 | **Stargazers** 106 | 107 | [![Stargazers repo roster for @MajidArabi/AndroidFilePicker](https://reporoster.com/stars/dark/MajidArabi/AndroidFilePicker)](https://github.com/MajidArabi/AndroidFilePicker/stargazers) 108 | 109 | **Forkers** 110 | 111 | [![Forkers repo roster for @MajidArabi/AndroidFilePicker](https://reporoster.com/forks/dark/MajidArabi/AndroidFilePicker)](https://github.com/MajidArabi/AndroidFilePicker/network/members) 112 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | compileSdk 33 8 | 9 | defaultConfig { 10 | applicationId "ir.one_developer.filepickerlibrary" 11 | minSdk 21 12 | targetSdk 33 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | kotlinOptions { 30 | jvmTarget = '1.8' 31 | } 32 | buildFeatures { 33 | viewBinding true 34 | } 35 | } 36 | 37 | dependencies { 38 | implementation project(':file-picker') 39 | implementation 'androidx.core:core-ktx:1.7.0' 40 | implementation 'androidx.appcompat:appcompat:1.4.1' 41 | implementation 'androidx.recyclerview:recyclerview:1.2.1' 42 | implementation 'com.google.android.material:material:1.5.0' 43 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/ir/one_developer/filepickerlibrary/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package ir.one_developer.filepickerlibrary 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("ir.one_developer.filepickerlibrary", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/ir/one_developer/filepickerlibrary/FileAdapter.kt: -------------------------------------------------------------------------------- 1 | package ir.one_developer.filepickerlibrary 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.core.net.toUri 6 | import androidx.recyclerview.widget.DiffUtil 7 | import androidx.recyclerview.widget.ListAdapter 8 | import androidx.recyclerview.widget.RecyclerView 9 | import com.github.file_picker.data.model.Media 10 | import ir.one_developer.filepickerlibrary.databinding.FileLayoutBinding 11 | import java.io.File 12 | 13 | class FileAdapter : ListAdapter(Comparator) { 14 | 15 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = 16 | VH(FileLayoutBinding.inflate(LayoutInflater.from(parent.context), parent, false)) 17 | 18 | override fun onBindViewHolder(holder: VH, position: Int) { 19 | holder.bind(getItem(position).file) 20 | } 21 | 22 | inner class VH( 23 | private val binding: FileLayoutBinding 24 | ) : RecyclerView.ViewHolder(binding.root) { 25 | fun bind(file: File) { 26 | binding.ivImagePreview.setImageURI(file.toUri()) 27 | } 28 | } 29 | 30 | companion object { 31 | private val Comparator = object : DiffUtil.ItemCallback() { 32 | override fun areItemsTheSame(oldItem: Media, newItem: Media): Boolean { 33 | return oldItem.id == newItem.id 34 | } 35 | 36 | override fun areContentsTheSame(oldItem: Media, newItem: Media): Boolean { 37 | return oldItem == newItem 38 | } 39 | } 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /app/src/main/java/ir/one_developer/filepickerlibrary/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package ir.one_developer.filepickerlibrary 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.core.content.ContextCompat 6 | import com.github.file_picker.FileType 7 | import com.github.file_picker.adapter.FilePickerAdapter 8 | import com.github.file_picker.extension.showFilePicker 9 | import com.github.file_picker.listener.OnItemClickListener 10 | import com.github.file_picker.listener.OnSubmitClickListener 11 | import com.github.file_picker.data.model.Media 12 | import ir.one_developer.filepickerlibrary.databinding.ActivityMainBinding 13 | 14 | class MainActivity : AppCompatActivity() { 15 | 16 | private lateinit var adapter: FileAdapter 17 | private val selectedFiles = arrayListOf() 18 | private lateinit var binding: ActivityMainBinding 19 | 20 | private var fileType: FileType = FileType.IMAGE 21 | private var accentColor: Int = R.color.purple_500 22 | private var spanCount: Int = 2 23 | private var limit: Int = 2 24 | 25 | override fun onCreate(savedInstanceState: Bundle?) { 26 | super.onCreate(savedInstanceState) 27 | binding = ActivityMainBinding.inflate(layoutInflater) 28 | setContentView(binding.root) 29 | setupViews() 30 | } 31 | 32 | private fun setupViews() = binding.apply { 33 | adapter = FileAdapter() 34 | rvFiles.adapter = adapter 35 | btnOpenFiles.setOnClickListener { 36 | showFiles() 37 | } 38 | radioGroups.setOnCheckedChangeListener { _, i -> 39 | fileType = when (i) { 40 | 1 -> { 41 | spanCount = 2 42 | limit = 7 43 | accentColor = R.color.purple_500 44 | FileType.IMAGE 45 | } 46 | 2 -> { 47 | spanCount = 2 48 | limit = 3 49 | accentColor = R.color.pink_500 50 | FileType.VIDEO 51 | } 52 | 3 -> { 53 | spanCount = 3 54 | limit = 1 55 | accentColor = R.color.blue_500 56 | FileType.AUDIO 57 | } 58 | else -> FileType.IMAGE 59 | } 60 | } 61 | } 62 | 63 | private fun showFiles(): Unit = showFilePicker( 64 | fileType = fileType, 65 | limitItemSelection = limit, 66 | gridSpanCount = spanCount, 67 | selectedFiles = selectedFiles, 68 | overlayAlpha = 0.75f, 69 | accentColor = ContextCompat.getColor(this@MainActivity, accentColor), 70 | titleTextColor = ContextCompat.getColor(this@MainActivity, accentColor), 71 | onSubmitClickListener = object : OnSubmitClickListener { 72 | override fun onClick(files: List) { 73 | adapter.submitList(files) 74 | updateSelectedFiles(files) 75 | } 76 | }, 77 | onItemClickListener = object : OnItemClickListener { 78 | override fun onClick(media: Media, position: Int, adapter: FilePickerAdapter) { 79 | if (!media.file.isDirectory) { 80 | adapter.setSelected(position) 81 | } 82 | } 83 | } 84 | ) 85 | 86 | private fun updateSelectedFiles(files: List) { 87 | selectedFiles.clear() 88 | selectedFiles.addAll(files) 89 | } 90 | 91 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_chevron_right.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/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 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 16 | 17 | 22 | 23 | 28 | 29 | 33 | 34 | 38 | 39 | 43 | 44 | 45 | 46 | 47 | 48 | 61 | 62 | 76 | 77 | -------------------------------------------------------------------------------- /app/src/main/res/layout/file_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MajidArabi/AndroidFilePicker/2479edfaaf87d437b12987885a9a7bcf03224c38/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MajidArabi/AndroidFilePicker/2479edfaaf87d437b12987885a9a7bcf03224c38/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MajidArabi/AndroidFilePicker/2479edfaaf87d437b12987885a9a7bcf03224c38/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MajidArabi/AndroidFilePicker/2479edfaaf87d437b12987885a9a7bcf03224c38/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MajidArabi/AndroidFilePicker/2479edfaaf87d437b12987885a9a7bcf03224c38/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MajidArabi/AndroidFilePicker/2479edfaaf87d437b12987885a9a7bcf03224c38/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MajidArabi/AndroidFilePicker/2479edfaaf87d437b12987885a9a7bcf03224c38/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MajidArabi/AndroidFilePicker/2479edfaaf87d437b12987885a9a7bcf03224c38/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MajidArabi/AndroidFilePicker/2479edfaaf87d437b12987885a9a7bcf03224c38/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MajidArabi/AndroidFilePicker/2479edfaaf87d437b12987885a9a7bcf03224c38/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | #E91E63 11 | #2196F3 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | FilePickerLibrary 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/test/java/ir/one_developer/filepickerlibrary/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package ir.one_developer.filepickerlibrary 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | plugins { 3 | id 'com.android.application' version '7.1.2' apply false 4 | id 'com.android.library' version '7.1.2' apply false 5 | id 'org.jetbrains.kotlin.android' version '1.6.10' apply false 6 | } 7 | 8 | task clean(type: Delete) { 9 | delete rootProject.buildDir 10 | } -------------------------------------------------------------------------------- /file-picker/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /file-picker/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'maven-publish' 5 | id 'kotlin-parcelize' 6 | } 7 | 8 | android { 9 | compileSdk 33 10 | 11 | defaultConfig { 12 | minSdk 21 13 | targetSdk 33 14 | vectorDrawables.useSupportLibrary = true 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | consumerProguardFiles "consumer-rules.pro" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | buildFeatures { 26 | viewBinding true 27 | } 28 | compileOptions { 29 | sourceCompatibility JavaVersion.VERSION_1_8 30 | targetCompatibility JavaVersion.VERSION_1_8 31 | } 32 | kotlinOptions { 33 | jvmTarget = '1.8' 34 | } 35 | } 36 | 37 | dependencies { 38 | implementation 'androidx.core:core-ktx:1.8.0' 39 | implementation 'androidx.appcompat:appcompat:1.4.1' 40 | implementation 'androidx.recyclerview:recyclerview:1.2.1' 41 | implementation 'com.google.android.material:material:1.6.1' 42 | 43 | def activity_version = "1.5.1" 44 | implementation "androidx.activity:activity-ktx:$activity_version" 45 | 46 | def fragment_version = "1.5.2" 47 | implementation "androidx.fragment:fragment-ktx:$fragment_version" 48 | 49 | def coroutine_version = "1.6.1" 50 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version" 51 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version" 52 | 53 | def glide_version = "4.13.0" 54 | implementation "com.github.bumptech.glide:glide:$glide_version" 55 | annotationProcessor "com.github.bumptech.glide:compiler:$glide_version" 56 | 57 | def paging_version = "3.1.1" 58 | implementation "androidx.paging:paging-runtime:$paging_version" 59 | 60 | def lifecycle_version = "2.5.1" 61 | implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' 62 | implementation "androidx.lifecycle:lifecycle-runtime:$lifecycle_version" 63 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" 64 | } 65 | 66 | afterEvaluate { 67 | publishing { 68 | publications { 69 | release(MavenPublication) { 70 | from components.release 71 | groupId = 'com.github.majidarabi' 72 | artifactId = 'file-picker' 73 | version = '0.2.2' 74 | } 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /file-picker/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MajidArabi/AndroidFilePicker/2479edfaaf87d437b12987885a9a7bcf03224c38/file-picker/consumer-rules.pro -------------------------------------------------------------------------------- /file-picker/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -keep public class * implements com.bumptech.glide.module.GlideModule 24 | -keep class * extends com.bumptech.glide.module.AppGlideModule { 25 | (...); 26 | } 27 | -keep public enum com.bumptech.glide.load.ImageHeaderParser$** { 28 | **[] $VALUES; 29 | public *; 30 | } 31 | -keep class com.bumptech.glide.load.data.ParcelFileDescriptorRewinder$InternalRewinder { 32 | *** rewind(); 33 | } -------------------------------------------------------------------------------- /file-picker/src/androidTest/java/ir/one_developer/file_picker/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package ir.one_developer.file_picker 2 | 3 | /** 4 | * Instrumented test, which will execute on an Android device. 5 | * 6 | * See [testing documentation](http://d.android.com/tools/testing). 7 | */ 8 | //@RunWith(AndroidJUnit4::class) 9 | class ExampleInstrumentedTest { 10 | // @Test 11 | // fun useAppContext() { 12 | // // Context of the app under test. 13 | // val appContext = InstrumentationRegistry.getInstrumentation().targetContext 14 | // assertEquals("ir.one_developer.file_picker.test", appContext.packageName) 15 | // } 16 | } -------------------------------------------------------------------------------- /file-picker/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /file-picker/src/main/java/com/github/file_picker/Const.kt: -------------------------------------------------------------------------------- 1 | package com.github.file_picker 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | @Parcelize 7 | enum class ListDirection : Parcelable { 8 | LTR, 9 | RTL 10 | } 11 | 12 | @Parcelize 13 | enum class FileType : Parcelable { 14 | VIDEO, 15 | IMAGE, 16 | AUDIO, 17 | } 18 | 19 | const val PAGE_SIZE = 10 -------------------------------------------------------------------------------- /file-picker/src/main/java/com/github/file_picker/FilePicker.kt: -------------------------------------------------------------------------------- 1 | package com.github.file_picker 2 | 3 | import android.Manifest 4 | import android.content.DialogInterface 5 | import android.content.res.ColorStateList 6 | import android.graphics.Color 7 | import android.os.Build 8 | import android.os.Bundle 9 | import android.view.LayoutInflater 10 | import android.view.View 11 | import android.view.ViewGroup 12 | import androidx.activity.result.contract.ActivityResultContracts 13 | import androidx.annotation.ColorInt 14 | import androidx.annotation.FloatRange 15 | import androidx.appcompat.app.AppCompatActivity 16 | import androidx.core.view.isVisible 17 | import androidx.fragment.app.FragmentManager 18 | import androidx.paging.LoadState 19 | import androidx.recyclerview.widget.GridLayoutManager 20 | import androidx.recyclerview.widget.RecyclerView 21 | import com.github.file_picker.adapter.ItemAdapter 22 | import com.github.file_picker.data.model.Media 23 | import com.github.file_picker.data.repository.FilesRepository 24 | import com.github.file_picker.extension.hasPermission 25 | import com.github.file_picker.listener.OnItemClickListener 26 | import com.github.file_picker.listener.OnSubmitClickListener 27 | import com.google.android.material.bottomsheet.BottomSheetBehavior 28 | import com.google.android.material.bottomsheet.BottomSheetDialog 29 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment 30 | import ir.one_developer.file_picker.R 31 | import ir.one_developer.file_picker.databinding.FilePickerBinding 32 | import kotlinx.coroutines.CoroutineScope 33 | import kotlinx.coroutines.Dispatchers 34 | import kotlinx.coroutines.launch 35 | import kotlin.properties.Delegates 36 | 37 | /** 38 | * 39 | * A fragment that shows a list of items as a modal bottom sheet. 40 | * 41 | * You can show this modal bottom sheet from your activity like this: 42 | *
 43 |  *    FilePicker.Builder([Context]).buildAndShow()
 44 |  * 
45 | */ 46 | class FilePicker private constructor( 47 | builder: Builder 48 | ) : BottomSheetDialogFragment() { 49 | 50 | // This property is only valid between onCreateView and onDestroyView. 51 | private val binding get() = _binding!! 52 | private var _binding: FilePickerBinding? = null 53 | 54 | private var itemsAdapter: ItemAdapter? = null 55 | private lateinit var repository: FilesRepository 56 | 57 | private var title: String 58 | private var titleTextColor by Delegates.notNull() 59 | private var submitText: String 60 | private var submitTextColor by Delegates.notNull() 61 | private var selectedFiles: List 62 | private var fileType: FileType 63 | private var listDirection: ListDirection 64 | private var limitCount by Delegates.notNull() 65 | private var accentColor by Delegates.notNull() 66 | private var gridSpanCount by Delegates.notNull() 67 | private var cancellable by Delegates.notNull() 68 | private var overlayAlpha by Delegates.notNull() 69 | 70 | private var onItemClickListener: OnItemClickListener? 71 | private var onSubmitClickListener: OnSubmitClickListener? 72 | 73 | init { 74 | this.title = builder.title 75 | this.titleTextColor = builder.titleTextColor 76 | this.submitText = builder.submitText 77 | this.submitTextColor = builder.submitTextColor 78 | this.selectedFiles = builder.selectedFiles 79 | this.fileType = builder.fileType 80 | this.listDirection = builder.listDirection 81 | this.cancellable = builder.cancellable 82 | this.gridSpanCount = builder.gridSpanCount 83 | this.limitCount = builder.limitCount 84 | this.accentColor = builder.accentColor 85 | this.overlayAlpha = builder.overlayAlpha 86 | this.onItemClickListener = builder.onItemClickListener 87 | this.onSubmitClickListener = builder.onSubmitClickListener 88 | } 89 | 90 | private var requestPermission = 91 | registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -> 92 | if (isGranted) loadFiles() 93 | else dismissAllowingStateLoss() 94 | } 95 | 96 | class Builder( 97 | private val appCompatActivity: AppCompatActivity 98 | ) { 99 | var title: String = DEFAULT_TITLE 100 | private set 101 | var titleTextColor: Int = DEFAULT_TITLE_TEXT_COLOR 102 | private set 103 | var submitText: String = DEFAULT_SUBMIT_TEXT 104 | private set 105 | var submitTextColor: Int = DEFAULT_SUBMIT_TEXT_COLOR 106 | private set 107 | var accentColor: Int = DEFAULT_ACCENT_COLOR 108 | private set 109 | var selectedFiles: List = arrayListOf() 110 | private set 111 | var limitCount: Int = DEFAULT_LIMIT_COUNT 112 | private set 113 | var fileType: FileType = DEFAULT_FILE_TYPE 114 | private set 115 | var gridSpanCount: Int = DEFAULT_SPAN_COUNT 116 | private set 117 | var cancellable: Boolean = DEFAULT_CANCELABLE 118 | private set 119 | var listDirection: ListDirection = DEFAULT_LIST_DIRECTION 120 | private set 121 | var onItemClickListener: OnItemClickListener? = null 122 | private set 123 | var onSubmitClickListener: OnSubmitClickListener? = null 124 | private set 125 | var overlayAlpha: Float = DEFAULT_OVERLAY_ALPHA 126 | private set 127 | 128 | /** 129 | * Set title 130 | * 131 | * @param title 132 | * @return 133 | */ 134 | fun setTitle(title: String) = apply { this.title = title } 135 | 136 | /** 137 | * Set title text color 138 | * 139 | * @param color 140 | * @return 141 | */ 142 | fun setTitleTextColor(color: Int) = apply { this.titleTextColor = color } 143 | 144 | /** 145 | * Set submit text 146 | * 147 | * @param text 148 | * @return 149 | */ 150 | fun setSubmitText(text: String) = apply { this.submitText = text } 151 | 152 | /** 153 | * Set submit text color 154 | * 155 | * @param color 156 | * @return 157 | */ 158 | fun setSubmitTextColor(color: Int) = apply { this.submitTextColor = color } 159 | 160 | /** 161 | * Set accent color 162 | * 163 | * @param color 164 | * @return 165 | */ 166 | fun setAccentColor(@ColorInt color: Int) = apply { this.accentColor = color } 167 | 168 | /** 169 | * Set selected files, for show as selected style in list 170 | * 171 | * @param selectedFiles 172 | * @return 173 | */ 174 | fun setSelectedFiles(selectedFiles: List) = 175 | apply { this.selectedFiles = selectedFiles } 176 | 177 | /** 178 | * Set limit item selection 179 | * 180 | * @param limit the limit item can select 181 | * @return 182 | */ 183 | fun setLimitItemSelection(limit: Int) = apply { this.limitCount = limit } 184 | 185 | /** 186 | * Set file type 187 | * 188 | * @param fileType the [FileType] 189 | * @return 190 | */ 191 | fun setFileType(fileType: FileType) = apply { this.fileType = fileType } 192 | 193 | /** 194 | * Set grid span count 195 | * 196 | * @param spanCount the list span count 197 | * @return 198 | */ 199 | fun setGridSpanCount(spanCount: Int) = apply { this.gridSpanCount = spanCount } 200 | 201 | /** 202 | * Set cancellable 203 | * 204 | * @param cancellable 205 | * @return 206 | */ 207 | fun setCancellable(cancellable: Boolean) = apply { this.cancellable = cancellable } 208 | 209 | /** 210 | * Set list direction 211 | * 212 | * @param listDirection [ListDirection] 213 | * @return 214 | */ 215 | fun setListDirection(listDirection: ListDirection) = 216 | apply { this.listDirection = listDirection } 217 | 218 | /** 219 | * Set on submit click listener 220 | * 221 | * @param onSubmitClickListener 222 | * @return 223 | */ 224 | fun setOnSubmitClickListener( 225 | onSubmitClickListener: OnSubmitClickListener? 226 | ) = apply { this.onSubmitClickListener = onSubmitClickListener } 227 | 228 | /** 229 | * Set on item click listener 230 | * 231 | * @param onItemClickListener 232 | * @return 233 | */ 234 | fun setOnItemClickListener( 235 | onItemClickListener: OnItemClickListener? 236 | ) = apply { this.onItemClickListener = onItemClickListener } 237 | 238 | /** 239 | * Set overlay alpha 240 | * 241 | * @param alpha 242 | */ 243 | fun setOverlayAlpha( 244 | alpha: Float 245 | ) = apply { this.overlayAlpha = alpha } 246 | 247 | /** 248 | * Build file picker instance 249 | */ 250 | fun build() = FilePicker(this) 251 | 252 | /** 253 | * Build file picker and show it 254 | */ 255 | fun buildAndShow() = build().show( 256 | appCompatActivity.supportFragmentManager, 257 | "file.picker" 258 | ) 259 | } 260 | 261 | override fun onCreate(savedInstanceState: Bundle?) { 262 | super.onCreate(savedInstanceState) 263 | setStyle(STYLE_NORMAL, R.style.BottomSheetDialog) 264 | } 265 | 266 | override fun onCreateView( 267 | inflater: LayoutInflater, 268 | container: ViewGroup?, 269 | savedInstanceState: Bundle? 270 | ): View { 271 | _binding = FilePickerBinding.inflate(inflater, container, false) 272 | return binding.root 273 | } 274 | 275 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 276 | repository = FilesRepository(requireActivity().application) 277 | setCancellableDialog(cancellable) 278 | setupViews() 279 | } 280 | 281 | override fun onStart() { 282 | super.onStart() 283 | val readStoragePermission = getRequiredPermissionByType() 284 | if (!hasPermission(readStoragePermission)) { 285 | requestPermission(readStoragePermission) 286 | return 287 | } 288 | loadFiles() 289 | } 290 | 291 | /** 292 | * Get required permission by file type 293 | * 294 | * @return the permission 295 | */ 296 | private fun getRequiredPermissionByType() : String { 297 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 298 | return when(fileType) { 299 | FileType.AUDIO -> Manifest.permission.READ_MEDIA_AUDIO 300 | FileType.VIDEO -> Manifest.permission.READ_MEDIA_VIDEO 301 | FileType.IMAGE -> Manifest.permission.READ_MEDIA_IMAGES 302 | } 303 | } 304 | return Manifest.permission.READ_EXTERNAL_STORAGE 305 | } 306 | 307 | override fun show(manager: FragmentManager, tag: String?) { 308 | if (isShown) return 309 | super.show(manager, tag) 310 | isShown = true 311 | } 312 | 313 | override fun onDismiss(dialog: DialogInterface) { 314 | isShown = false 315 | super.onDismiss(dialog) 316 | } 317 | 318 | override fun onDestroyView() { 319 | super.onDestroyView() 320 | _binding = null 321 | requestPermission.unregister() 322 | } 323 | 324 | /** 325 | * Request permission 326 | * 327 | * @param permission 328 | */ 329 | private fun requestPermission(permission: String) = requestPermission.launch(permission) 330 | 331 | /** 332 | * Set cancellable dialog 333 | * 334 | * @param cancellable 335 | */ 336 | private fun setCancellableDialog(cancellable: Boolean) { 337 | dialog?.setCancelable(cancellable) 338 | dialog?.setCanceledOnTouchOutside(cancellable) 339 | } 340 | 341 | /** 342 | * Set fixed submit button 343 | * 344 | */ 345 | private fun setFixedSubmitButton() { 346 | val behavior: BottomSheetBehavior<*> = (dialog as BottomSheetDialog).behavior 347 | behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { 348 | override fun onStateChanged(bottomSheet: View, newState: Int) {} 349 | override fun onSlide(bottomSheet: View, slideOffset: Float) { 350 | val button = binding.buttonContainer 351 | button.y = 352 | ((bottomSheet.parent as View).height - bottomSheet.top - button.height).toFloat() 353 | } 354 | }.apply { 355 | val root = binding.root 356 | root.post { onSlide(root.parent as View, 0f) } 357 | }) 358 | } 359 | 360 | /** 361 | * Setup views 362 | */ 363 | private fun setupViews() = binding.apply { 364 | changeSubmitButtonState() 365 | setupRecyclerView(rvFiles) 366 | setFixedSubmitButton() 367 | updateSelectedCount() 368 | 369 | cardLine.setCardBackgroundColor(ColorStateList.valueOf(accentColor)) 370 | progress.indeterminateTintList = ColorStateList.valueOf(accentColor) 371 | tvTitle.apply { 372 | text = title 373 | setTextColor(titleTextColor) 374 | } 375 | btnSubmit.apply { 376 | text = submitText 377 | setOnClickListener { 378 | submitList() 379 | dismissAllowingStateLoss() 380 | } 381 | } 382 | } 383 | 384 | /** 385 | * Show selected count 386 | * 387 | */ 388 | private fun updateSelectedCount() { 389 | val selectedCount = getSelectedItems()?.size ?: 0 390 | binding.tvTitle.text = "$title ($selectedCount/$limitCount)" 391 | } 392 | 393 | /** 394 | * Setup recycler view 395 | * 396 | * @param recyclerView 397 | */ 398 | private fun setupRecyclerView(recyclerView: RecyclerView) { 399 | itemsAdapter = ItemAdapter( 400 | accentColor = accentColor, 401 | overlayAlpha = overlayAlpha, 402 | limitSelectionCount = limitCount, 403 | listener = { itemPosition -> 404 | setupOnItemClickListener(itemPosition) 405 | updateSelectedCount() 406 | changeSubmitButtonState() 407 | } 408 | ) 409 | recyclerView.apply { 410 | layoutDirection = when (listDirection) { 411 | ListDirection.LTR -> RecyclerView.LAYOUT_DIRECTION_LTR 412 | ListDirection.RTL -> RecyclerView.LAYOUT_DIRECTION_RTL 413 | } 414 | layoutManager = GridLayoutManager(requireContext(), gridSpanCount) 415 | adapter = itemsAdapter 416 | } 417 | } 418 | 419 | /** 420 | * Setup on item click listener 421 | * 422 | * @param position 423 | */ 424 | private fun setupOnItemClickListener(position: Int) { 425 | if (onItemClickListener == null) return 426 | if (itemsAdapter == null) return 427 | val media = itemsAdapter?.snapshot()?.items?.get(position) ?: return 428 | onItemClickListener?.onClick(media, position, itemsAdapter!!) 429 | } 430 | 431 | /** 432 | * change submit button state 433 | * if has selected item change to enable otherwise disable it 434 | */ 435 | private fun changeSubmitButtonState() = binding.btnSubmit.apply { 436 | isEnabled = hasSelectedItem() 437 | if (isEnabled) { 438 | setTextColor(submitTextColor) 439 | setBackgroundColor(accentColor) 440 | return@apply 441 | } 442 | setTextColor(Color.GRAY) 443 | setBackgroundColor(Color.LTGRAY) 444 | } 445 | 446 | /** 447 | * Load files 448 | */ 449 | private fun loadFiles() = CoroutineScope(Dispatchers.IO).launch { 450 | itemsAdapter?.addLoadStateListener { state -> 451 | binding.progress.isVisible = state.source.refresh is LoadState.Loading 452 | if (state.source.refresh is LoadState.NotLoading) { 453 | selectedFiles.forEach { media -> 454 | itemsAdapter?.snapshot()?.items?.find { it.id == media.id }?.let { 455 | it.isSelected = media.isSelected 456 | it.order = media.order 457 | } 458 | } 459 | updateSelectedCount() 460 | setFixedSubmitButton() 461 | changeSubmitButtonState() 462 | } 463 | } 464 | repository.getFiles(fileType = fileType).collect { pagingData -> 465 | itemsAdapter?.submitData(pagingData) 466 | } 467 | } 468 | 469 | /** 470 | * Submit list 471 | */ 472 | private fun submitList() = getSelectedItems()?.let { 473 | onSubmitClickListener?.onClick(it) 474 | } 475 | 476 | /** 477 | * Get selected items 478 | * 479 | * @return 480 | */ 481 | private fun getSelectedItems(): List? = 482 | itemsAdapter?.snapshot()?.items?.filter { it.isSelected }?.sortedBy { it.order } 483 | 484 | /** 485 | * Has selected item 486 | * 487 | * @return 488 | */ 489 | private fun hasSelectedItem(): Boolean = !getSelectedItems().isNullOrEmpty() 490 | 491 | companion object { 492 | // Defaults 493 | const val DEFAULT_SPAN_COUNT = 2 494 | const val DEFAULT_LIMIT_COUNT = 1 495 | const val DEFAULT_CANCELABLE = true 496 | val DEFAULT_FILE_TYPE = FileType.IMAGE 497 | val DEFAULT_LIST_DIRECTION = ListDirection.LTR 498 | 499 | const val DEFAULT_ACCENT_COLOR = Color.BLACK 500 | const val DEFAULT_TITLE = "Choose File" 501 | const val DEFAULT_TITLE_TEXT_COLOR = DEFAULT_ACCENT_COLOR 502 | 503 | @FloatRange(from = 0.0, to = 1.0) 504 | const val DEFAULT_OVERLAY_ALPHA = 0.5F 505 | 506 | const val DEFAULT_SUBMIT_TEXT = "Submit" 507 | const val DEFAULT_SUBMIT_TEXT_COLOR = Color.WHITE 508 | 509 | private var isShown: Boolean = false 510 | } 511 | 512 | } -------------------------------------------------------------------------------- /file-picker/src/main/java/com/github/file_picker/adapter/FilePickerAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.github.file_picker.adapter 2 | 3 | interface FilePickerAdapter { 4 | fun setSelected(position: Int) 5 | } -------------------------------------------------------------------------------- /file-picker/src/main/java/com/github/file_picker/adapter/ItemAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.github.file_picker.adapter 2 | 3 | import android.view.LayoutInflater 4 | import android.view.ViewGroup 5 | import androidx.paging.PagingDataAdapter 6 | import androidx.recyclerview.widget.DiffUtil 7 | import androidx.recyclerview.widget.ListAdapter 8 | import com.github.file_picker.FilePicker 9 | import com.github.file_picker.extension.isValidPosition 10 | import com.github.file_picker.data.model.Media 11 | import ir.one_developer.file_picker.databinding.ItemLayoutBinding 12 | import java.util.Locale.filter 13 | 14 | internal class ItemAdapter( 15 | private var accentColor: Int = FilePicker.DEFAULT_ACCENT_COLOR, 16 | private var overlayAlpha: Float = FilePicker.DEFAULT_OVERLAY_ALPHA, 17 | private var limitSelectionCount: Int = FilePicker.DEFAULT_LIMIT_COUNT, 18 | private var listener: ((Int) -> Unit)? = null 19 | ) : PagingDataAdapter(COMPARATOR), FilePickerAdapter { 20 | 21 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ItemVH( 22 | listener = listener, 23 | accentColor = accentColor, 24 | overlayAlpha =overlayAlpha, 25 | limitSelectionCount = limitSelectionCount, 26 | binding = ItemLayoutBinding.inflate( 27 | LayoutInflater.from(parent.context), 28 | parent, 29 | false 30 | ) 31 | ) 32 | 33 | override fun onBindViewHolder(holder: ItemVH, position: Int) { 34 | getItem(position)?.let { holder.bind(it) } 35 | } 36 | 37 | /** 38 | * Set selected 39 | * 40 | * @param position the selected item position 41 | */ 42 | override fun setSelected(position: Int) { 43 | if (limitSelectionCount > 1) { 44 | val item = getItem(position) ?: return 45 | val selectedItems = snapshot().items.filter { it.isSelected && it.id != item.id } 46 | val selectedItemCount = selectedItems.size 47 | 48 | if (item.isSelected) { 49 | item.isSelected = false 50 | notifyItemChanged(position) 51 | selectedItems.forEach { media -> 52 | if (media.order > item.order) { 53 | media.order-- 54 | notifyItemChanged(snapshot().items.indexOf(media)) 55 | } 56 | } 57 | return 58 | } 59 | 60 | if (selectedItemCount < limitSelectionCount) { 61 | item.isSelected = true 62 | item.order = selectedItemCount + 1 63 | notifyItemChanged(position) 64 | } 65 | return 66 | } 67 | 68 | if (!snapshot().items.isValidPosition(lastSelectedPosition)) { 69 | lastSelectedPosition = position 70 | } 71 | getItem(lastSelectedPosition)?.isSelected = false 72 | notifyItemChanged(lastSelectedPosition) 73 | lastSelectedPosition = position 74 | getItem(lastSelectedPosition)?.isSelected = true 75 | notifyItemChanged(lastSelectedPosition) 76 | } 77 | 78 | companion object { 79 | private var lastSelectedPosition = -1 80 | 81 | private val COMPARATOR = object : DiffUtil.ItemCallback() { 82 | override fun areItemsTheSame( 83 | oldItem: Media, 84 | newItem: Media 85 | ) = oldItem.id == newItem.id 86 | 87 | override fun areContentsTheSame( 88 | oldItem: Media, 89 | newItem: Media 90 | ) = oldItem == newItem 91 | } 92 | } 93 | 94 | } -------------------------------------------------------------------------------- /file-picker/src/main/java/com/github/file_picker/adapter/ItemVH.kt: -------------------------------------------------------------------------------- 1 | package com.github.file_picker.adapter 2 | 3 | import android.graphics.drawable.Drawable 4 | import androidx.core.view.isVisible 5 | import androidx.recyclerview.widget.RecyclerView 6 | import com.bumptech.glide.Glide 7 | import com.bumptech.glide.load.DataSource 8 | import com.bumptech.glide.load.engine.DiskCacheStrategy 9 | import com.bumptech.glide.load.engine.GlideException 10 | import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions 11 | import com.bumptech.glide.request.RequestListener 12 | import com.bumptech.glide.request.target.Target 13 | import com.github.file_picker.FileType 14 | import com.github.file_picker.data.model.Media 15 | import com.github.file_picker.extension.getMusicCoverArt 16 | import com.github.file_picker.extension.lastPathTitle 17 | import com.github.file_picker.extension.size 18 | import ir.one_developer.file_picker.R 19 | import ir.one_developer.file_picker.databinding.ItemLayoutBinding 20 | 21 | internal class ItemVH( 22 | private val listener: ((Int) -> Unit)?, 23 | private val binding: ItemLayoutBinding, 24 | private val accentColor: Int, 25 | private val overlayAlpha: Float, 26 | private val limitSelectionCount: Int 27 | ) : RecyclerView.ViewHolder(binding.root) { 28 | 29 | init { 30 | binding.apply { 31 | frameChecked.setBackgroundColor(accentColor) 32 | frameChecked.alpha = overlayAlpha 33 | card.setOnClickListener { 34 | listener?.invoke(bindingAdapterPosition) 35 | } 36 | } 37 | } 38 | 39 | fun bind(item: Media) = binding.apply { 40 | cardErrorState.isVisible = false 41 | frameChecked.isVisible = item.isSelected 42 | cardOrder.isVisible = item.isSelected && limitSelectionCount > 1 43 | ivChecked.isVisible = item.isSelected && limitSelectionCount == 1 44 | 45 | tvOrder.text = "${item.order}" 46 | tvFileSize.text = item.file.size() 47 | 48 | val previewImage: Any? = when (item.type) { 49 | FileType.AUDIO -> { 50 | tvPath.text = item.file.name 51 | ivMediaIcon.setImageResource(R.drawable.ic_audiotrack) 52 | item.file.getMusicCoverArt() 53 | } 54 | FileType.IMAGE -> { 55 | tvPath.text = item.file.lastPathTitle() 56 | ivMediaIcon.setImageResource(R.drawable.ic_image) 57 | item.file 58 | } 59 | FileType.VIDEO -> { 60 | tvPath.text = item.file.lastPathTitle() 61 | ivMediaIcon.setImageResource(R.drawable.ic_play) 62 | item.file 63 | } 64 | } 65 | 66 | Glide.with(ivImage) 67 | .load(previewImage) 68 | .diskCacheStrategy(DiskCacheStrategy.RESOURCE) 69 | .transition(DrawableTransitionOptions.withCrossFade()) 70 | .listener(object : RequestListener { 71 | override fun onLoadFailed( 72 | e: GlideException?, 73 | model: Any?, 74 | target: Target?, 75 | isFirstResource: Boolean 76 | ): Boolean { 77 | setErrorState(item.type) 78 | return false 79 | } 80 | 81 | override fun onResourceReady( 82 | resource: Drawable?, 83 | model: Any?, 84 | target: Target?, 85 | dataSource: DataSource?, 86 | isFirstResource: Boolean 87 | ): Boolean = false 88 | }) 89 | .into(ivImage) 90 | } 91 | 92 | private fun setErrorState(type: FileType) = binding.apply { 93 | cardErrorState.isVisible = true 94 | when (type) { 95 | FileType.VIDEO -> Glide.with(ivErrorIcon).load(R.drawable.ic_play).into(ivErrorIcon) 96 | FileType.IMAGE -> Glide.with(ivErrorIcon).load(R.drawable.ic_image).into(ivErrorIcon) 97 | FileType.AUDIO -> Glide.with(ivErrorIcon).load(R.drawable.ic_audiotrack) 98 | .into(ivErrorIcon) 99 | } 100 | } 101 | 102 | } -------------------------------------------------------------------------------- /file-picker/src/main/java/com/github/file_picker/data/model/Media.kt: -------------------------------------------------------------------------------- 1 | package com.github.file_picker.data.model 2 | 3 | import android.os.Parcelable 4 | import com.github.file_picker.FileType 5 | import kotlinx.parcelize.Parcelize 6 | import java.io.File 7 | 8 | @Parcelize 9 | data class Media( 10 | val file: File, 11 | val type: FileType, 12 | var order: Int = 0, 13 | var isSelected: Boolean = false, 14 | val id: Int = file.path.hashCode() 15 | ) : Parcelable 16 | -------------------------------------------------------------------------------- /file-picker/src/main/java/com/github/file_picker/data/repository/FilesPagingSource.kt: -------------------------------------------------------------------------------- 1 | package com.github.file_picker.data.repository 2 | 3 | import android.app.Application 4 | import androidx.paging.PagingSource 5 | import androidx.paging.PagingState 6 | import com.github.file_picker.FileType 7 | import com.github.file_picker.data.model.Media 8 | import com.github.file_picker.extension.getStorageFiles 9 | 10 | internal class FilesPagingSource( 11 | private val application: Application, 12 | private val fileType: FileType, 13 | ) : PagingSource() { 14 | 15 | // the initial load size for the first page may be different from the requested size 16 | private var initialLoadSize: Int = 0 17 | 18 | override fun getRefreshKey(state: PagingState): Int? = 19 | state.anchorPosition?.let { anchorPosition -> 20 | val anchorPage = state.closestPageToPosition(anchorPosition) 21 | anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1) 22 | } 23 | 24 | override suspend fun load(params: LoadParams): LoadResult { 25 | return try { 26 | // Start refresh at page 1 if undefined. 27 | val nextPageNumber = params.key ?: 1 28 | 29 | if (params.key == null) initialLoadSize = params.loadSize 30 | 31 | // work out the offset into the database to retrieve records from the page number, 32 | // allow for a different load size for the first page 33 | val offsetCalc = { 34 | if (nextPageNumber == 2) 35 | initialLoadSize 36 | else 37 | ((nextPageNumber - 1) * params.loadSize) + (initialLoadSize - params.loadSize) 38 | } 39 | 40 | val offset = offsetCalc.invoke() 41 | 42 | val files = application.getStorageFiles( 43 | fileType = fileType, 44 | limit = params.loadSize, 45 | offset = offset 46 | ) 47 | val count = files.size 48 | 49 | LoadResult.Page( 50 | data = files, 51 | prevKey = null, 52 | nextKey = if (count < params.loadSize) null else nextPageNumber + 1 53 | ) 54 | } catch (e: Exception) { 55 | LoadResult.Error(e) 56 | } 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /file-picker/src/main/java/com/github/file_picker/data/repository/FilesRepository.kt: -------------------------------------------------------------------------------- 1 | package com.github.file_picker.data.repository 2 | 3 | import android.app.Application 4 | import androidx.paging.Pager 5 | import androidx.paging.PagingConfig 6 | import com.github.file_picker.FileType 7 | import com.github.file_picker.PAGE_SIZE 8 | 9 | class FilesRepository( 10 | private val application: Application 11 | ) { 12 | 13 | fun getFiles( 14 | fileType: FileType = FileType.IMAGE, 15 | ) = Pager( 16 | PagingConfig( 17 | pageSize = PAGE_SIZE, 18 | enablePlaceholders = true, 19 | initialLoadSize = PAGE_SIZE, 20 | ) 21 | ) { 22 | FilesPagingSource( 23 | application = application, 24 | fileType = fileType 25 | ) 26 | }.flow 27 | 28 | } 29 | -------------------------------------------------------------------------------- /file-picker/src/main/java/com/github/file_picker/extension/ActivityExt.kt: -------------------------------------------------------------------------------- 1 | package com.github.file_picker.extension 2 | 3 | import androidx.annotation.FloatRange 4 | import androidx.appcompat.app.AppCompatActivity 5 | import com.github.file_picker.FilePicker 6 | import com.github.file_picker.FileType 7 | import com.github.file_picker.ListDirection 8 | import com.github.file_picker.listener.OnItemClickListener 9 | import com.github.file_picker.listener.OnSubmitClickListener 10 | import com.github.file_picker.data.model.Media 11 | 12 | /** 13 | * Show file picker 14 | * 15 | * @param title 16 | * @param titleTextColor 17 | * @param submitText 18 | * @param submitTextColor 19 | * @param accentColor 20 | * @param fileType 21 | * @param listDirection 22 | * @param cancellable 23 | * @param gridSpanCount 24 | * @param limitItemSelection 25 | * @param selectedFiles 26 | * @param onSubmitClickListener 27 | * @param onItemClickListener 28 | */ 29 | fun AppCompatActivity.showFilePicker( 30 | title: String = FilePicker.DEFAULT_TITLE, 31 | titleTextColor: Int = FilePicker.DEFAULT_TITLE_TEXT_COLOR, 32 | submitText: String = FilePicker.DEFAULT_SUBMIT_TEXT, 33 | submitTextColor: Int = FilePicker.DEFAULT_SUBMIT_TEXT_COLOR, 34 | accentColor: Int = FilePicker.DEFAULT_ACCENT_COLOR, 35 | fileType: FileType = FilePicker.DEFAULT_FILE_TYPE, 36 | listDirection: ListDirection = FilePicker.DEFAULT_LIST_DIRECTION, 37 | cancellable: Boolean = FilePicker.DEFAULT_CANCELABLE, 38 | gridSpanCount: Int = FilePicker.DEFAULT_SPAN_COUNT, 39 | limitItemSelection: Int = FilePicker.DEFAULT_LIMIT_COUNT, 40 | @FloatRange(from = 0.0, to = 1.0) 41 | overlayAlpha: Float = FilePicker.DEFAULT_OVERLAY_ALPHA, 42 | selectedFiles: ArrayList = arrayListOf(), 43 | onSubmitClickListener: OnSubmitClickListener? = null, 44 | onItemClickListener: OnItemClickListener? = null, 45 | ) = FilePicker.Builder(this) 46 | .setTitle(title) 47 | .setTitleTextColor(titleTextColor) 48 | .setSubmitText(submitText) 49 | .setSubmitTextColor(submitTextColor) 50 | .setAccentColor(accentColor) 51 | .setFileType(fileType) 52 | .setListDirection(listDirection) 53 | .setCancellable(cancellable) 54 | .setGridSpanCount(gridSpanCount) 55 | .setLimitItemSelection(limitItemSelection) 56 | .setSelectedFiles(selectedFiles) 57 | .setOverlayAlpha(overlayAlpha) 58 | .setOnSubmitClickListener(onSubmitClickListener) 59 | .setOnItemClickListener(onItemClickListener) 60 | .buildAndShow() -------------------------------------------------------------------------------- /file-picker/src/main/java/com/github/file_picker/extension/CollectionExt.kt: -------------------------------------------------------------------------------- 1 | package com.github.file_picker.extension 2 | 3 | /** 4 | * Is valid position in list 5 | * 6 | * @param position 7 | * @return 8 | */ 9 | internal fun List<*>.isValidPosition(position: Int): Boolean { 10 | return if (isNotEmpty()) position in 0 until size else position >= 0 11 | } -------------------------------------------------------------------------------- /file-picker/src/main/java/com/github/file_picker/extension/ContextExt.kt: -------------------------------------------------------------------------------- 1 | package com.github.file_picker.extension 2 | 3 | import android.content.ContentResolver 4 | import android.content.Context 5 | import android.content.pm.PackageManager 6 | import android.os.Build 7 | import android.os.Bundle 8 | import android.provider.MediaStore 9 | import androidx.core.app.ActivityCompat 10 | import com.github.file_picker.FileType 11 | import com.github.file_picker.PAGE_SIZE 12 | import com.github.file_picker.data.model.Media 13 | import java.io.File 14 | 15 | /** 16 | * check has runtime permission 17 | * 18 | * @param permission permission name ex: Manifest.permission.READ_EXTERNAL_STORAGE 19 | * @return if has permission return true otherwise false 20 | */ 21 | internal fun Context.hasPermission( 22 | permission: String 23 | ): Boolean = ActivityCompat.checkSelfPermission( 24 | this, 25 | permission 26 | ) == PackageManager.PERMISSION_GRANTED 27 | 28 | /** 29 | * Get storage files path 30 | * 31 | * @return list of file path, ex: /storage/0/emulated/download/image.jpg 32 | */ 33 | internal fun Context.getStorageFiles( 34 | fileType: FileType = FileType.IMAGE, 35 | limit: Int = PAGE_SIZE, 36 | offset: Int = 0 37 | ): List { 38 | 39 | val resolver = applicationContext.contentResolver 40 | 41 | val media = when (fileType) { 42 | FileType.VIDEO -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI 43 | FileType.IMAGE -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI 44 | FileType.AUDIO -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 45 | } 46 | 47 | val projection = arrayOf(MediaStore.Files.FileColumns.DATA, MediaStore.Files.FileColumns._ID) 48 | val modified = MediaStore.Files.FileColumns.DATE_MODIFIED 49 | 50 | val cursor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 51 | val sortArgs = arrayOf(modified) 52 | val bundle = Bundle().apply { 53 | putInt(ContentResolver.QUERY_ARG_LIMIT, limit) 54 | putInt(ContentResolver.QUERY_ARG_OFFSET, offset) 55 | putStringArray(ContentResolver.QUERY_ARG_SORT_COLUMNS, sortArgs) 56 | putInt( 57 | ContentResolver.QUERY_ARG_SORT_DIRECTION, 58 | ContentResolver.QUERY_SORT_DIRECTION_DESCENDING 59 | ) 60 | } 61 | resolver.query( 62 | media, 63 | projection, 64 | bundle, 65 | null 66 | ) 67 | } else resolver.query( 68 | media, 69 | projection, 70 | null, 71 | null, 72 | "$modified DESC LIMIT $limit OFFSET $offset", 73 | ) 74 | 75 | //Total number of images 76 | val count = cursor?.count ?: return emptyList() 77 | 78 | //Create an array to store path to all the images 79 | val files = arrayListOf() 80 | 81 | for (i in 0 until count) { 82 | cursor.moveToPosition(i) 83 | val dataColumnIndex = cursor.getColumnIndex(MediaStore.Images.Media.DATA) 84 | //Store the path of the image 85 | val file = File(cursor.getString(dataColumnIndex)) 86 | if (file.size > 0.0) { 87 | files.add(Media(file = file, type = fileType)) 88 | } 89 | } 90 | 91 | // The cursor should be freed up after use with close() 92 | cursor.close() 93 | return files 94 | } 95 | -------------------------------------------------------------------------------- /file-picker/src/main/java/com/github/file_picker/extension/FileExt.kt: -------------------------------------------------------------------------------- 1 | package com.github.file_picker.extension 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.BitmapFactory 5 | import android.media.MediaMetadataRetriever 6 | import java.io.File 7 | 8 | internal val File.size get() = if (!exists()) 0.0 else length().toDouble() 9 | internal val File.sizeInKb get() = size / 1024 10 | internal val File.sizeInMb get() = sizeInKb / 1024 11 | internal val File.sizeInGb get() = sizeInMb / 1024 12 | internal val File.sizeInTb get() = sizeInGb / 1024 13 | 14 | /** 15 | * Format file size 16 | * 17 | * @return string ex: 2.35 MB 18 | */ 19 | internal fun File.size(): String = when { 20 | sizeInGb > 1024 -> "${sizeInTb.roundTo()} TB" 21 | sizeInMb > 1024 -> "${sizeInGb.roundTo()} GB" 22 | sizeInKb > 1024 -> "${sizeInMb.roundTo()} MB" 23 | size > 1024 -> "${sizeInKb.roundTo()} KB" 24 | else -> "${size.roundTo()} Bytes" 25 | } 26 | 27 | /** 28 | * Path name 29 | * 30 | * @return 31 | */ 32 | internal fun File.lastPathTitle(): CharSequence { 33 | val paths = path.split("/") 34 | val titleIndex = paths.lastIndex - 1 35 | if (titleIndex >= 0) return paths[titleIndex] 36 | return "" 37 | } 38 | 39 | /** 40 | * Get music cover art 41 | * 42 | * @return 43 | */ 44 | internal fun File.getMusicCoverArt(): Bitmap? = try { 45 | val mData = MediaMetadataRetriever() 46 | mData.setDataSource(path) 47 | val art = mData.embeddedPicture 48 | BitmapFactory.decodeByteArray(art, 0, art?.size ?: 0) 49 | } catch (e: Exception) { 50 | null 51 | } -------------------------------------------------------------------------------- /file-picker/src/main/java/com/github/file_picker/extension/FragmentExt.kt: -------------------------------------------------------------------------------- 1 | package com.github.file_picker.extension 2 | 3 | import androidx.annotation.FloatRange 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.fragment.app.Fragment 6 | import com.github.file_picker.FilePicker 7 | import com.github.file_picker.FileType 8 | import com.github.file_picker.ListDirection 9 | import com.github.file_picker.data.model.Media 10 | import com.github.file_picker.listener.OnItemClickListener 11 | import com.github.file_picker.listener.OnSubmitClickListener 12 | 13 | /** 14 | * check has runtime permission 15 | * 16 | * @param permission permission name ex: Manifest.permission.READ_EXTERNAL_STORAGE 17 | * @return if has permission return true otherwise false 18 | */ 19 | internal fun Fragment.hasPermission( 20 | permission: String 21 | ): Boolean = requireContext().hasPermission(permission) 22 | 23 | /** 24 | * Get storage files path 25 | * 26 | * @return list of file path, ex: /storage/0/emulated/download/image.jpg 27 | */ 28 | internal fun Fragment.getStorageFiles( 29 | fileType: FileType = FileType.IMAGE 30 | ) = requireContext().getStorageFiles(fileType = fileType) 31 | 32 | /** 33 | * Show file picker 34 | * 35 | * @param title 36 | * @param titleTextColor 37 | * @param submitText 38 | * @param submitTextColor 39 | * @param accentColor 40 | * @param fileType 41 | * @param listDirection 42 | * @param cancellable 43 | * @param gridSpanCount 44 | * @param limitItemSelection 45 | * @param selectedFiles 46 | * @param onSubmitClickListener 47 | * @param onItemClickListener 48 | */ 49 | fun Fragment.showFilePicker( 50 | title: String = FilePicker.DEFAULT_TITLE, 51 | titleTextColor: Int = FilePicker.DEFAULT_TITLE_TEXT_COLOR, 52 | submitText: String = FilePicker.DEFAULT_SUBMIT_TEXT, 53 | submitTextColor: Int = FilePicker.DEFAULT_SUBMIT_TEXT_COLOR, 54 | accentColor: Int = FilePicker.DEFAULT_ACCENT_COLOR, 55 | fileType: FileType = FilePicker.DEFAULT_FILE_TYPE, 56 | listDirection: ListDirection = FilePicker.DEFAULT_LIST_DIRECTION, 57 | cancellable: Boolean = FilePicker.DEFAULT_CANCELABLE, 58 | gridSpanCount: Int = FilePicker.DEFAULT_SPAN_COUNT, 59 | limitItemSelection: Int = FilePicker.DEFAULT_LIMIT_COUNT, 60 | selectedFiles: ArrayList = arrayListOf(), 61 | @FloatRange(from = 0.0, to = 1.0) 62 | overlayAlpha: Float = FilePicker.DEFAULT_OVERLAY_ALPHA, 63 | onSubmitClickListener: OnSubmitClickListener? = null, 64 | onItemClickListener: OnItemClickListener? = null, 65 | ) { 66 | if (requireActivity() !is AppCompatActivity) { 67 | throw IllegalAccessException("Fragment host must be extend AppCompatActivity") 68 | } 69 | (requireActivity() as AppCompatActivity).showFilePicker( 70 | title = title, 71 | titleTextColor = titleTextColor, 72 | submitText = submitText, 73 | submitTextColor = submitTextColor, 74 | accentColor = accentColor, 75 | fileType = fileType, 76 | listDirection = listDirection, 77 | cancellable = cancellable, 78 | gridSpanCount = gridSpanCount, 79 | limitItemSelection = limitItemSelection, 80 | selectedFiles = selectedFiles, 81 | overlayAlpha = overlayAlpha, 82 | onSubmitClickListener = onSubmitClickListener, 83 | onItemClickListener = onItemClickListener, 84 | ) 85 | } -------------------------------------------------------------------------------- /file-picker/src/main/java/com/github/file_picker/extension/NumberExt.kt: -------------------------------------------------------------------------------- 1 | package com.github.file_picker.extension 2 | 3 | import java.util.* 4 | 5 | /** 6 | * Round to 7 | * 8 | * @param numFractionDigits 9 | */ 10 | internal fun Number.roundTo( 11 | numFractionDigits: Int = 2 12 | ) = "%.${numFractionDigits}f".format(this, Locale.ENGLISH) -------------------------------------------------------------------------------- /file-picker/src/main/java/com/github/file_picker/extension/StringExt.kt: -------------------------------------------------------------------------------- 1 | package com.github.file_picker.extension 2 | 3 | /** 4 | * check path is video 5 | * ex : https://example.com/video.mp4 6 | * @return boolean 7 | */ 8 | internal fun String?.isVideo(): Boolean { 9 | 10 | if (this == null) return false 11 | 12 | val formats = listOf( 13 | ".mp4", 14 | ".m4b", 15 | ".m4v", 16 | ".m4a", 17 | ".f4a", 18 | ".f4b", 19 | ".mov", 20 | ".3gp", 21 | ".3gp2", 22 | ".3g2", 23 | ".3gpp", 24 | ".3gpp2", 25 | ".wmv", 26 | ".wma", 27 | ".FLV", 28 | ".AVI" 29 | ) 30 | 31 | val searched = formats.find { 32 | this.endsWith(it, ignoreCase = true) 33 | } 34 | 35 | return !searched.isNullOrBlank() 36 | } -------------------------------------------------------------------------------- /file-picker/src/main/java/com/github/file_picker/listener/OnItemClickListener.kt: -------------------------------------------------------------------------------- 1 | package com.github.file_picker.listener 2 | 3 | import com.github.file_picker.adapter.FilePickerAdapter 4 | import com.github.file_picker.data.model.Media 5 | 6 | interface OnItemClickListener { 7 | fun onClick(media: Media, position: Int, adapter: FilePickerAdapter) 8 | } -------------------------------------------------------------------------------- /file-picker/src/main/java/com/github/file_picker/listener/OnSubmitClickListener.kt: -------------------------------------------------------------------------------- 1 | package com.github.file_picker.listener 2 | 3 | import com.github.file_picker.data.model.Media 4 | 5 | interface OnSubmitClickListener { 6 | fun onClick(files: List) 7 | } -------------------------------------------------------------------------------- /file-picker/src/main/java/com/github/file_picker/view/SquareCardView.kt: -------------------------------------------------------------------------------- 1 | package com.github.file_picker.view 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import com.google.android.material.card.MaterialCardView 6 | 7 | internal class SquareCardView : MaterialCardView { 8 | 9 | constructor(context: Context?) : super(context) 10 | 11 | constructor(context: Context?, attributes: AttributeSet) : super( 12 | context, 13 | attributes 14 | ) 15 | 16 | constructor(context: Context?, attributes: AttributeSet, defStyleAttr: Int) : super( 17 | context, 18 | attributes, 19 | defStyleAttr 20 | ) 21 | 22 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 23 | super.onMeasure(widthMeasureSpec, widthMeasureSpec) 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /file-picker/src/main/res/drawable/ic_audiotrack.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /file-picker/src/main/res/drawable/ic_check.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /file-picker/src/main/res/drawable/ic_image.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /file-picker/src/main/res/drawable/ic_play.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /file-picker/src/main/res/layout/file_picker.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 20 | 21 | 30 | 31 | 35 | 36 | 51 | 52 | 60 | 61 | 62 | 63 | 71 | 72 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /file-picker/src/main/res/layout/item_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 19 | 27 | 28 | 29 | 39 | 40 | 47 | 48 | 49 | 50 | 51 | 57 | 58 | 59 | 68 | 69 | 81 | 82 | 83 | 84 | 85 | 95 | 96 | 97 | 104 | 105 | 117 | 118 | 119 | 120 | 121 | 126 | 127 | 134 | 135 | 147 | 148 | 149 | 150 | 158 | 159 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /file-picker/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 8dp 5 | -------------------------------------------------------------------------------- /file-picker/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 11 | 12 | 19 | 20 | -------------------------------------------------------------------------------- /file-picker/src/test/java/ir/one_developer/file_picker/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package ir.one_developer.file_picker 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MajidArabi/AndroidFilePicker/2479edfaaf87d437b12987885a9a7bcf03224c38/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Feb 24 18:26:20 IRST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 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 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk11 3 | before_install: 4 | - ./scripts/prepareJitpackEnvironment.sh -------------------------------------------------------------------------------- /screenshots/audio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MajidArabi/AndroidFilePicker/2479edfaaf87d437b12987885a9a7bcf03224c38/screenshots/audio.png -------------------------------------------------------------------------------- /screenshots/image-2col-full.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MajidArabi/AndroidFilePicker/2479edfaaf87d437b12987885a9a7bcf03224c38/screenshots/image-2col-full.jpg -------------------------------------------------------------------------------- /screenshots/image-3col.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MajidArabi/AndroidFilePicker/2479edfaaf87d437b12987885a9a7bcf03224c38/screenshots/image-3col.jpg -------------------------------------------------------------------------------- /screenshots/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MajidArabi/AndroidFilePicker/2479edfaaf87d437b12987885a9a7bcf03224c38/screenshots/image.png -------------------------------------------------------------------------------- /screenshots/video-2col-full.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MajidArabi/AndroidFilePicker/2479edfaaf87d437b12987885a9a7bcf03224c38/screenshots/video-2col-full.jpg -------------------------------------------------------------------------------- /screenshots/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MajidArabi/AndroidFilePicker/2479edfaaf87d437b12987885a9a7bcf03224c38/screenshots/video.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | gradlePluginPortal() 12 | maven { url 'https://jitpack.io' } 13 | google() 14 | mavenCentral() 15 | } 16 | } 17 | rootProject.name = "FilePickerLibrary" 18 | include ':app' 19 | include ':file-picker' 20 | --------------------------------------------------------------------------------