├── .gitignore
├── .idea
    ├── codeStyles
    │   ├── Project.xml
    │   └── codeStyleConfig.xml
    ├── compiler.xml
    ├── encodings.xml
    ├── gradle.xml
    ├── jarRepositories.xml
    ├── kotlinc.xml
    ├── misc.xml
    └── vcs.xml
├── LICENSE
├── README.md
├── app
    ├── .gitignore
    ├── build.gradle
    ├── proguard-rules.pro
    └── src
    │   ├── androidTest
    │       └── java
    │       │   └── com
    │       │       └── android
    │       │           └── attachproject
    │       │               └── ExampleInstrumentedTest.kt
    │   ├── main
    │       ├── AndroidManifest.xml
    │       ├── java
    │       │   └── com
    │       │   │   └── android
    │       │   │       └── attachproject
    │       │   │           ├── AttachmentAdapter.kt
    │       │   │           ├── Main2Activity.java
    │       │   │           └── MainActivity.kt
    │       └── res
    │       │   ├── drawable-v24
    │       │       └── ic_launcher_foreground.xml
    │       │   ├── drawable
    │       │       ├── bg_circle.xml
    │       │       ├── ic_add.xml
    │       │       ├── ic_close.xml
    │       │       ├── ic_launcher_background.xml
    │       │       ├── ic_pdf.xml
    │       │       └── ic_plus_empty_image.xml
    │       │   ├── layout
    │       │       ├── activity_main.xml
    │       │       ├── activity_main2.xml
    │       │       ├── content_main.xml
    │       │       ├── content_main2.xml
    │       │       └── row_attachment.xml
    │       │   ├── mipmap-anydpi-v26
    │       │       ├── ic_launcher.xml
    │       │       └── ic_launcher_round.xml
    │       │   ├── mipmap-hdpi
    │       │       ├── ic_launcher.png
    │       │       └── ic_launcher_round.png
    │       │   ├── mipmap-mdpi
    │       │       ├── ic_launcher.png
    │       │       └── ic_launcher_round.png
    │       │   ├── mipmap-xhdpi
    │       │       ├── ic_launcher.png
    │       │       └── ic_launcher_round.png
    │       │   ├── mipmap-xxhdpi
    │       │       ├── ic_launcher.png
    │       │       └── ic_launcher_round.png
    │       │   ├── mipmap-xxxhdpi
    │       │       ├── ic_launcher.png
    │       │       └── ic_launcher_round.png
    │       │   └── values
    │       │       ├── colors.xml
    │       │       ├── dimens.xml
    │       │       ├── strings.xml
    │       │       └── styles.xml
    │   └── test
    │       └── java
    │           └── com
    │               └── android
    │                   └── attachproject
    │                       └── ExampleUnitTest.kt
├── attachmentmanager
    ├── .gitignore
    ├── build.gradle
    ├── proguard-rules.pro
    └── src
    │   ├── androidTest
    │       └── java
    │       │   └── com
    │       │       └── mirza
    │       │           └── attachmentmanager
    │       │               └── ExampleInstrumentedTest.java
    │   ├── main
    │       ├── AndroidManifest.xml
    │       ├── java
    │       │   └── com
    │       │   │   └── mirza
    │       │   │       └── attachmentmanager
    │       │   │           ├── fragments
    │       │   │               ├── AttachmentBottomSheet.kt
    │       │   │               └── AttachmentFragment.kt
    │       │   │           ├── managers
    │       │   │               ├── AttachmentManager.kt
    │       │   │               ├── ConstantManager.kt
    │       │   │               └── PermissionManager.kt
    │       │   │           ├── models
    │       │   │               ├── AttachmentDetail.kt
    │       │   │               └── Tuple.kt
    │       │   │           └── utils
    │       │   │               ├── AttachmentUtil.kt
    │       │   │               ├── FileUtil.kt
    │       │   │               └── ImageUtils.kt
    │       └── res
    │       │   ├── drawable
    │       │       ├── ic_am_attach_file.xml
    │       │       ├── ic_am_camera.xml
    │       │       ├── ic_am_close.xml
    │       │       └── ic_am_photo.xml
    │       │   ├── layout
    │       │       ├── fragment_attachment.xml
    │       │       ├── layout_attachment_dialog.xml
    │       │       └── layout_attachment_sheet.xml
    │       │   ├── values-ar
    │       │       └── strings.xml
    │       │   ├── values
    │       │       ├── colors.xml
    │       │       ├── dimens.xml
    │       │       └── strings.xml
    │       │   └── xml
    │       │       └── file_provider.xml
    │   └── test
    │       └── java
    │           └── com
    │               └── mirza
    │                   └── attachmentmanager
    │                       └── ExampleUnitTest.java
├── build.gradle
├── gradle.properties
├── gradle
    └── wrapper
    │   ├── gradle-wrapper.jar
    │   └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
 1 | *.iml
 2 | .gradle
 3 | /local.properties
 4 | /.idea/caches
 5 | /.idea/libraries
 6 | /.idea/modules.xml
 7 | /.idea/workspace.xml
 8 | /.idea/navEditor.xml
 9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | 
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
  1 | 
  2 |   
  3 |     
  4 |        
  5 |      
  6 |     
  7 |       
  8 |         
  9 |           
 10 |             
 11 |               
 12 |                 
 13 |                   xmlns:android 
 14 |                    
 15 |                   ^$ 
 16 |                  
 17 |                
 18 |              
 19 |            
 20 |           
 21 |             
 22 |               
 23 |                 
 24 |                   xmlns:.* 
 25 |                    
 26 |                   ^$ 
 27 |                  
 28 |                
 29 |               BY_NAME 
 30 |              
 31 |            
 32 |           
 33 |             
 34 |               
 35 |                 
 36 |                   .*:id 
 37 |                    
 38 |                   http://schemas.android.com/apk/res/android 
 39 |                  
 40 |                
 41 |              
 42 |            
 43 |           
 44 |             
 45 |               
 46 |                 
 47 |                   .*:name 
 48 |                    
 49 |                   http://schemas.android.com/apk/res/android 
 50 |                  
 51 |                
 52 |              
 53 |            
 54 |           
 55 |             
 56 |               
 57 |                 
 58 |                   name 
 59 |                    
 60 |                   ^$ 
 61 |                  
 62 |                
 63 |              
 64 |            
 65 |           
 66 |             
 67 |               
 68 |                 
 69 |                   style 
 70 |                    
 71 |                   ^$ 
 72 |                  
 73 |                
 74 |              
 75 |            
 76 |           
 77 |             
 78 |               
 79 |                 
 80 |                   .* 
 81 |                    
 82 |                   ^$ 
 83 |                  
 84 |                
 85 |               BY_NAME 
 86 |              
 87 |            
 88 |           
 89 |             
 90 |               
 91 |                 
 92 |                   .* 
 93 |                    
 94 |                   http://schemas.android.com/apk/res/android 
 95 |                  
 96 |                
 97 |               ANDROID_ATTRIBUTE_ORDER 
 98 |              
 99 |            
100 |           
101 |             
102 |               
103 |                 
104 |                   .* 
105 |                    
106 |                   .* 
107 |                  
108 |                
109 |               BY_NAME 
110 |              
111 |            
112 |          
113 |        
114 |      
115 |     
116 |        
117 |      
118 |    
119 |  
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 | 
2 |   
3 |      
4 |    
5 |  
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 | 
2 | 
3 |   
4 |      
5 |    
6 |  
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 | 
2 | 
3 |    
4 |  
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |    
 4 |   
 5 |     
 6 |       
 7 |          
 8 |          
 9 |          
10 |          
11 |         
12 |           
13 |              
14 |              
15 |              
16 |            
17 |          
18 |        
19 |      
20 |    
21 |  
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |   
 4 |     
 5 |        
 6 |        
 7 |        
 8 |      
 9 |     
10 |        
11 |        
12 |        
13 |      
14 |     
15 |        
16 |        
17 |        
18 |      
19 |     
20 |        
21 |        
22 |        
23 |      
24 |     
25 |        
26 |        
27 |        
28 |      
29 |    
30 |  
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 | 
2 | 
3 |   
4 |      
5 |    
6 |  
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
 1 | 
 2 |   
 3 |     
 4 |        
 5 |      
 6 |    
 7 |   
 8 |     
 9 |       
10 |          
11 |          
12 |          
13 |          
14 |          
15 |          
16 |          
17 |          
18 |        
19 |      
20 |    
21 |   
22 |      
23 |    
24 |   
25 |      
26 |    
27 |  
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 | 
2 | 
3 |   
4 |      
5 |    
6 |  
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
  1 |                                  Apache License
  2 |                            Version 2.0, January 2004
  3 |                         http://www.apache.org/licenses/
  4 | 
  5 |    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
  6 | 
  7 |    1. Definitions.
  8 | 
  9 |       "License" shall mean the terms and conditions for use, reproduction,
 10 |       and distribution as defined by Sections 1 through 9 of this document.
 11 | 
 12 |       "Licensor" shall mean the copyright owner or entity authorized by
 13 |       the copyright owner that is granting the License.
 14 | 
 15 |       "Legal Entity" shall mean the union of the acting entity and all
 16 |       other entities that control, are controlled by, or are under common
 17 |       control with that entity. For the purposes of this definition,
 18 |       "control" means (i) the power, direct or indirect, to cause the
 19 |       direction or management of such entity, whether by contract or
 20 |       otherwise, or (ii) ownership of fifty percent (50%) or more of the
 21 |       outstanding shares, or (iii) beneficial ownership of such entity.
 22 | 
 23 |       "You" (or "Your") shall mean an individual or Legal Entity
 24 |       exercising permissions granted by this License.
 25 | 
 26 |       "Source" form shall mean the preferred form for making modifications,
 27 |       including but not limited to software source code, documentation
 28 |       source, and configuration files.
 29 | 
 30 |       "Object" form shall mean any form resulting from mechanical
 31 |       transformation or translation of a Source form, including but
 32 |       not limited to compiled object code, generated documentation,
 33 |       and conversions to other media types.
 34 | 
 35 |       "Work" shall mean the work of authorship, whether in Source or
 36 |       Object form, made available under the License, as indicated by a
 37 |       copyright notice that is included in or attached to the work
 38 |       (an example is provided in the Appendix below).
 39 | 
 40 |       "Derivative Works" shall mean any work, whether in Source or Object
 41 |       form, that is based on (or derived from) the Work and for which the
 42 |       editorial revisions, annotations, elaborations, or other modifications
 43 |       represent, as a whole, an original work of authorship. For the purposes
 44 |       of this License, Derivative Works shall not include works that remain
 45 |       separable from, or merely link (or bind by name) to the interfaces of,
 46 |       the Work and Derivative Works thereof.
 47 | 
 48 |       "Contribution" shall mean any work of authorship, including
 49 |       the original version of the Work and any modifications or additions
 50 |       to that Work or Derivative Works thereof, that is intentionally
 51 |       submitted to Licensor for inclusion in the Work by the copyright owner
 52 |       or by an individual or Legal Entity authorized to submit on behalf of
 53 |       the copyright owner. For the purposes of this definition, "submitted"
 54 |       means any form of electronic, verbal, or written communication sent
 55 |       to the Licensor or its representatives, including but not limited to
 56 |       communication on electronic mailing lists, source code control systems,
 57 |       and issue tracking systems that are managed by, or on behalf of, the
 58 |       Licensor for the purpose of discussing and improving the Work, but
 59 |       excluding communication that is conspicuously marked or otherwise
 60 |       designated in writing by the copyright owner as "Not a Contribution."
 61 | 
 62 |       "Contributor" shall mean Licensor and any individual or Legal Entity
 63 |       on behalf of whom a Contribution has been received by Licensor and
 64 |       subsequently incorporated within the Work.
 65 | 
 66 |    2. Grant of Copyright License. Subject to the terms and conditions of
 67 |       this License, each Contributor hereby grants to You a perpetual,
 68 |       worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 69 |       copyright license to reproduce, prepare Derivative Works of,
 70 |       publicly display, publicly perform, sublicense, and distribute the
 71 |       Work and such Derivative Works in Source or Object form.
 72 | 
 73 |    3. Grant of Patent License. Subject to the terms and conditions of
 74 |       this License, each Contributor hereby grants to You a perpetual,
 75 |       worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 76 |       (except as stated in this section) patent license to make, have made,
 77 |       use, offer to sell, sell, import, and otherwise transfer the Work,
 78 |       where such license applies only to those patent claims licensable
 79 |       by such Contributor that are necessarily infringed by their
 80 |       Contribution(s) alone or by combination of their Contribution(s)
 81 |       with the Work to which such Contribution(s) was submitted. If You
 82 |       institute patent litigation against any entity (including a
 83 |       cross-claim or counterclaim in a lawsuit) alleging that the Work
 84 |       or a Contribution incorporated within the Work constitutes direct
 85 |       or contributory patent infringement, then any patent licenses
 86 |       granted to You under this License for that Work shall terminate
 87 |       as of the date such litigation is filed.
 88 | 
 89 |    4. Redistribution. You may reproduce and distribute copies of the
 90 |       Work or Derivative Works thereof in any medium, with or without
 91 |       modifications, and in Source or Object form, provided that You
 92 |       meet the following conditions:
 93 | 
 94 |       (a) You must give any other recipients of the Work or
 95 |           Derivative Works a copy of this License; and
 96 | 
 97 |       (b) You must cause any modified files to carry prominent notices
 98 |           stating that You changed the files; and
 99 | 
100 |       (c) You must retain, in the Source form of any Derivative Works
101 |           that You distribute, all copyright, patent, trademark, and
102 |           attribution notices from the Source form of the Work,
103 |           excluding those notices that do not pertain to any part of
104 |           the Derivative Works; and
105 | 
106 |       (d) If the Work includes a "NOTICE" text file as part of its
107 |           distribution, then any Derivative Works that You distribute must
108 |           include a readable copy of the attribution notices contained
109 |           within such NOTICE file, excluding those notices that do not
110 |           pertain to any part of the Derivative Works, in at least one
111 |           of the following places: within a NOTICE text file distributed
112 |           as part of the Derivative Works; within the Source form or
113 |           documentation, if provided along with the Derivative Works; or,
114 |           within a display generated by the Derivative Works, if and
115 |           wherever such third-party notices normally appear. The contents
116 |           of the NOTICE file are for informational purposes only and
117 |           do not modify the License. You may add Your own attribution
118 |           notices within Derivative Works that You distribute, alongside
119 |           or as an addendum to the NOTICE text from the Work, provided
120 |           that such additional attribution notices cannot be construed
121 |           as modifying the License.
122 | 
123 |       You may add Your own copyright statement to Your modifications and
124 |       may provide additional or different license terms and conditions
125 |       for use, reproduction, or distribution of Your modifications, or
126 |       for any such Derivative Works as a whole, provided Your use,
127 |       reproduction, and distribution of the Work otherwise complies with
128 |       the conditions stated in this License.
129 | 
130 |    5. Submission of Contributions. Unless You explicitly state otherwise,
131 |       any Contribution intentionally submitted for inclusion in the Work
132 |       by You to the Licensor shall be under the terms and conditions of
133 |       this License, without any additional terms or conditions.
134 |       Notwithstanding the above, nothing herein shall supersede or modify
135 |       the terms of any separate license agreement you may have executed
136 |       with Licensor regarding such Contributions.
137 | 
138 |    6. Trademarks. This License does not grant permission to use the trade
139 |       names, trademarks, service marks, or product names of the Licensor,
140 |       except as required for reasonable and customary use in describing the
141 |       origin of the Work and reproducing the content of the NOTICE file.
142 | 
143 |    7. Disclaimer of Warranty. Unless required by applicable law or
144 |       agreed to in writing, Licensor provides the Work (and each
145 |       Contributor provides its Contributions) on an "AS IS" BASIS,
146 |       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 |       implied, including, without limitation, any warranties or conditions
148 |       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 |       PARTICULAR PURPOSE. You are solely responsible for determining the
150 |       appropriateness of using or redistributing the Work and assume any
151 |       risks associated with Your exercise of permissions under this License.
152 | 
153 |    8. Limitation of Liability. In no event and under no legal theory,
154 |       whether in tort (including negligence), contract, or otherwise,
155 |       unless required by applicable law (such as deliberate and grossly
156 |       negligent acts) or agreed to in writing, shall any Contributor be
157 |       liable to You for damages, including any direct, indirect, special,
158 |       incidental, or consequential damages of any character arising as a
159 |       result of this License or out of the use or inability to use the
160 |       Work (including but not limited to damages for loss of goodwill,
161 |       work stoppage, computer failure or malfunction, or any and all
162 |       other commercial damages or losses), even if such Contributor
163 |       has been advised of the possibility of such damages.
164 | 
165 |    9. Accepting Warranty or Additional Liability. While redistributing
166 |       the Work or Derivative Works thereof, You may choose to offer,
167 |       and charge a fee for, acceptance of support, warranty, indemnity,
168 |       or other liability obligations and/or rights consistent with this
169 |       License. However, in accepting such obligations, You may act only
170 |       on Your own behalf and on Your sole responsibility, not on behalf
171 |       of any other Contributor, and only if You agree to indemnify,
172 |       defend, and hold each Contributor harmless for any liability
173 |       incurred by, or claims asserted against, such Contributor by reason
174 |       of your accepting any such warranty or additional liability.
175 | 
176 |    END OF TERMS AND CONDITIONS
177 | 
178 |    APPENDIX: How to apply the Apache License to your work.
179 | 
180 |       To apply the Apache License to your work, attach the following
181 |       boilerplate notice, with the fields enclosed by brackets "[]"
182 |       replaced with your own identifying information. (Don't include
183 |       the brackets!)  The text should be enclosed in the appropriate
184 |       comment syntax for the file format. We also recommend that a
185 |       file or class name and description of purpose be included on the
186 |       same "printed page" as the copyright notice for easier
187 |       identification within third-party archives.
188 | 
189 |    Copyright [yyyy] [name of copyright owner]
190 | 
191 |    Licensed under the Apache License, Version 2.0 (the "License");
192 |    you may not use this file except in compliance with the License.
193 |    You may obtain a copy of the License at
194 | 
195 |        http://www.apache.org/licenses/LICENSE-2.0
196 | 
197 |    Unless required by applicable law or agreed to in writing, software
198 |    distributed under the License is distributed on an "AS IS" BASIS,
199 |    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 |    See the License for the specific language governing permissions and
201 |    limitations under the License.
202 | 
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
  1 | # AttachmentManager
  2 | 
  3 | [](https://jitpack.io/#Zaid-Mirza/AttachmentManager)
  4 | [](https://android-arsenal.com/api?level=23)
  5 | 
  6 |  
  7 | 
 14 |  
 15 |                                                                          
 16 | You can use this lightweight library to implement the attachment feature (taking pictures using the camera, picking up files/images from gallery or file system, or google drive). The library helps you to simplify all the processes related to picking files without worrying about system permissions
 17 | 
 18 | ### Language Support
 19 | 
 20 | * English
 21 | * Arabic
 22 | 
 23 | ### Warning!
 24 | 
 25 | 1. This library is build using **AndroidX**.So, I recommend you to migrate your project to **AndroidX** otherwise it may cause problem using both androidx and support libs togather.
 26 | 
 27 | 2. You might face error ``` Invoke-customs are only supported starting with android 0 --min-api 26  ``` 
 28 | .To solve this add below lines in app level **build.gradle** file.
 29 | 
 30 | ```groovy
 31 | compileOptions {
 32 |         sourceCompatibility JavaVersion.VERSION_17
 33 |         targetCompatibility JavaVersion.VERSION_17
 34 |     }
 35 | ```
 36 | 
 37 | 
 38 | 
 39 | # Prerequisite
 40 | 
 41 | 1. Add permissions and provider in **AndroidManifest.xml**
 42 | 
 43 | ```xml
 44 |        
 45 |      
 47 |      
 48 | 
 49 |      
 50 |      
 51 | ```
 52 | ```xml
 53 |  
 59 |              
 63 |    
 64 | ```
 65 | 
 66 | 2. Create **file_provider.xml** in res/xml
 67 | ```xml
 68 | 
 69 | 
 70 |      
 73 |      
 76 |  
 77 | ```
 78 | 
 79 | 3. If you are targeting Android 11+, you need to add following queries in **AndroidManifest.xml**
 80 | ```xml
 81 | 
 82 |         
 83 |              
 84 |             
 85 |              
 86 |          
 87 |         
 88 |              
 89 |             
 90 |              
 91 |          
 92 |      
 93 |   
 94 |   ```
 95 | 
 96 | 4. Update  project level **build.gradle** file.
 97 | ```groovy
 98 | allprojects {
 99 |    repositories {
100 |       	jcenter()
101 |        	maven { url "https://jitpack.io" }  //Make sure to add this in your project
102 |    }
103 | }
104 | ```
105 | 
106 | ```groovy
107 |    implementation 'com.github.Zaid-Mirza:AttachmentManager:3.0.0'
108 | ```
109 | 
110 | # Usage
111 | 
112 | 
113 | 1. Initiate AttachmentManager object using builder pattern
114 | 
115 |   **Kotlin**
116 | 
117 | ```kotlin
118 | private var attachmentManager: AttachmentManager? = null
119 | var gallery = arrayOf("image/png",
120 |             "image/jpg",
121 |             "image/jpeg")
122 |     var files = arrayOf("application/msword",
123 |             "application/vnd.openxmlformats-officedocument.wordprocessingml.document",  // .ppt & .pptx
124 |             "application/pdf")
125 | 
126 | override fun onCreate(savedInstanceState: Bundle?) {
127 |         super.onCreate(savedInstanceState)
128 |         setContentView(R.layout.activity_main)
129 |         
130 |       attachmentManager = AttachmentManager.AttachmentBuilder(this) // must pass Context
131 |             .fragment(null) // pass fragment reference if you are in fragment
132 |             .setUiTitle("Choose File") // title of dialog or bottom sheet
133 |             .allowMultiple(false) // set true if you want make multiple selection, default is false
134 |             .asBottomSheet(true) // set true if you need to show selection as bottom sheet, default is as Dialog
135 |             .setOptionsTextColor(android.R.color.holo_green_light) // change text color
136 |             .setImagesColor(R.color.colorAccent) // change icon color
137 |             .hide(HideOption.DOCUMENT) // You can hide any option do you want
138 |             .setMaxPhotoSize(200000) // Set max  photo size in bytes
139 |             .galleryMimeTypes(gallery) // mime types for gallery
140 |             .filesMimeTypes(files) // mime types for files
141 |             .build(); // Hide any of the three options
142 |        
143 |     }
144 |     
145 | ```
146 | **Java**
147 | ```java
148 |     private AttachmentManager attachmentManager = null;
149 |     String[] gallery = {"image/png",
150 |             "image/jpg",
151 |             "image/jpeg"};
152 |     String[] files  = { "application/msword",
153 |             "application/vnd.openxmlformats-officedocument.wordprocessingml.document", // .ppt & .pptx
154 |             "application/pdf"};
155 | 
156 |     @Override
157 |     protected void onCreate(Bundle savedInstanceState) {
158 |         super.onCreate(savedInstanceState);
159 |         setContentView(R.layout.activity_main);
160 |        attachmentManager = new AttachmentManager.AttachmentBuilder(this) // must pass Context
161 |                 .fragment(null) // pass fragment reference if you are in fragment
162 |                 .setUiTitle("Choose File") // title of dialog or bottom sheet
163 |                 .allowMultiple(false) // set true if you want make multiple selection, default is false
164 |                 .asBottomSheet(true) // set true if you need to show selection as bottom sheet, default is as Dialog
165 |                 .setOptionsTextColor(android.R.color.holo_green_light) // change text color
166 |                 .setImagesColor(R.color.colorAccent) // change icon color
167 |                 .hide(HideOption.DOCUMENT) // You can hide any option do you want
168 |                 .setMaxPhotoSize(200000) // Set max  photo size in bytes
169 |                 .galleryMimeTypes(gallery) // mime types for gallery
170 |                 .filesMimeTypes(files) // mime types for files
171 |                 .build(); // Hide any of the three options
172 |     }
173 | ```
174 | 
175 | 3. Declare registerForActivityResult
176 | 
177 | **Kotlin**
178 | ```kotlin
179 |  private var mLauncher = registerForActivityResult(StartActivityForResult()) { result ->
180 | 
181 |     val list =  attachmentManager?.manipulateAttachments(this,result.resultCode,result.data)
182 |     Toast.makeText(this, list?.size.toString(), Toast.LENGTH_LONG).show()
183 | }
184 | ````
185 | **Java**
186 | ```java
187 |  ActivityResultLauncher mLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
188 | 
189 |         ArrayList list = attachmentManager.manipulateAttachments(this,result.getResultCode(),result.getData());
190 | 
191 |         });
192 | ```
193 | 4. Call **openSelection()** method to show selection UI and pass ActivityResultLauncher
194 | 
195 | **Kotlin**
196 | ```kotlin
197 |  attachmentManager?.openSelection(mLauncher)
198 | ````
199 | **Java**
200 | ```java
201 | attachmentManager.openSelection(mLauncher);
202 | ```
203 | 
204 | 5. Override onRequestPermissionsResult (Optional)
205 | 
206 | **Kotlin**
207 | ```kotlin
208 | override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
209 |         attachmentManager?.handlePermissionResponse(requestCode, permissions, grantResults)
210 |     }
211 | 
212 | ```
213 | **Java**
214 | ```java
215 |     @Override
216 |     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
217 |         super.onRequestPermissionsResult(requestCode, permissions, grantResults);
218 |         attachmentManager.handlePermissionResponse(requestCode,permissions,grantResults);
219 |     }
220 | ```
221 | ### Other Usage
222 | 
223 | 1. You can open gallery,camera or file system directly without showing selection UI to user
224 | 
225 | ***Kotlin***
226 | ```kotlin
227 |  attachmentManager?.startCamera(mLauncher)
228 |  // OR
229 |  attachmentManager?.openGallery(mLauncher)
230 |  // OR
231 |  attachmentManager?.openFilSystem(mLauncher)
232 | ```
233 | 
234 | **Java**
235 | ```java
236 |  attachmentManager.startCamera(mLauncher);
237 |  // OR
238 |  attachmentManager.openGallery(mLauncher);
239 |  // OR
240 |  attachmentManager.openFilSystem(mLauncher);
241 | ```
242 | 
243 | ## Note
244 | 
245 | Any kind of improvements and suggestions are welcomed. Also, if you are using this library in your project then please do provide me your app url. I will list your app here.
246 | 
247 | 
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | 
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
 1 | plugins {
 2 |     id 'com.android.application'
 3 |     id 'org.jetbrains.kotlin.android'
 4 |     id 'kotlin-kapt'
 5 | }
 6 | 
 7 | android {
 8 |     compileSdk 34
 9 |     namespace("com.android.attachproject")
10 |     defaultConfig {
11 |         applicationId "com.android.attachproject"
12 |         minSdkVersion 26
13 |         targetSdkVersion 34
14 |         versionCode 1
15 |         versionName "1.0"
16 |         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17 |     }
18 |     buildTypes {
19 |         release {
20 |             minifyEnabled false
21 |             proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
22 |         }
23 | 
24 |     }
25 |     buildFeatures {
26 |         viewBinding true
27 |     }
28 |     compileOptions {
29 |         sourceCompatibility JavaVersion.VERSION_17
30 |         targetCompatibility JavaVersion.VERSION_17
31 |     }
32 | 
33 | }
34 | 
35 | dependencies {
36 |     implementation fileTree(dir: 'libs', include: ['*.jar'])
37 |     implementation project(':attachmentmanager')
38 |     implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
39 |     implementation 'androidx.appcompat:appcompat:1.7.0'
40 |     implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
41 |     implementation 'com.google.android.material:material:1.12.0'
42 |     implementation 'com.google.android.gms:play-services-location:21.3.0'
43 |     testImplementation 'junit:junit:4.13.2'
44 |     implementation 'commons-io:commons-io:2.6'
45 |     implementation 'com.github.bumptech.glide:glide:4.13.0'
46 |     kapt 'com.github.bumptech.glide:compiler:4.13.0'
47 |     androidTestImplementation 'androidx.test:runner:1.6.2'
48 |     androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
49 | }
50 | 
--------------------------------------------------------------------------------
/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
22 | 
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/android/attachproject/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
 1 | package com.android.attachproject
 2 | 
 3 | import androidx.test.InstrumentationRegistry
 4 | import androidx.test.runner.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.getTargetContext()
22 |         assertEquals("com.android.attachproject", appContext.packageName)
23 |     }
24 | }
25 | 
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 5 | 
 6 |      
 9 | 
10 |      
11 |      
12 |      
13 |      
15 |      
16 | 
17 |      
18 |      
19 |      
20 |      
21 |      
22 | 
23 |     
31 | 
32 |          
36 |         
42 | 
43 |          
44 |         
48 |             
49 |                  
50 | 
51 |                  
52 |              
53 | 
54 |          
55 | 
56 |         
62 |              
66 |          
67 |      
68 | 
69 | 
70 |     
71 |         
72 |              
73 |             
74 |              
75 |          
76 |         
77 |              
78 |             
79 |              
80 |          
81 |      
82 | 
83 | 
84 |  
--------------------------------------------------------------------------------
/app/src/main/java/com/android/attachproject/AttachmentAdapter.kt:
--------------------------------------------------------------------------------
 1 | package com.android.attachproject
 2 | 
 3 | import android.view.LayoutInflater
 4 | import android.view.View
 5 | import android.view.ViewGroup
 6 | import androidx.recyclerview.widget.RecyclerView
 7 | import com.android.attachproject.databinding.RowAttachmentBinding
 8 | import com.bumptech.glide.Glide
 9 | import com.bumptech.glide.load.DecodeFormat
10 | import com.bumptech.glide.request.RequestOptions
11 | import com.google.android.material.shape.CornerFamily
12 | import com.mirza.attachmentmanager.models.AttachmentDetail
13 | 
14 | 
15 | 
16 | class AttachmentAdapter(private var list: ArrayList) : RecyclerView.Adapter() {
17 | 
18 |     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
19 |         return ViewHolder(
20 |             RowAttachmentBinding.inflate(LayoutInflater.from(parent.context), parent, false)
21 | 
22 |         )
23 |     }
24 | 
25 |     override fun getItemCount(): Int {
26 |         return list.size
27 |     }
28 | 
29 |     override fun onBindViewHolder(holder: ViewHolder, position: Int) {
30 |         holder.bind(list, this)
31 |     }
32 | 
33 |     class ViewHolder(val binding: RowAttachmentBinding) : RecyclerView.ViewHolder(binding.root) {
34 |         fun bind(list: ArrayList, attachmentAdapter: AttachmentAdapter) = with(itemView) {
35 |             val attachmentDetail = list[adapterPosition]
36 |             binding.deleteAttachmentImageView.tag = adapterPosition
37 |            // val bitmap = attachmentDetail.uri?.let { ImageUtils.getCapturedImage(it) }
38 | 
39 |             binding.attachmentImageView.shapeAppearanceModel = binding.attachmentImageView.shapeAppearanceModel
40 |                     .toBuilder()
41 |                     .setAllCorners(CornerFamily.ROUNDED, resources.getDimension(com.mirza.attachmentmanager.R.dimen.small_margin))
42 |                     .build()
43 | 
44 |             val requestOptions = RequestOptions()
45 |             requestOptions.format(DecodeFormat.PREFER_ARGB_8888)
46 |             requestOptions.disallowHardwareConfig()
47 | 
48 |             if(attachmentDetail.mimeType?.contains("pdf") == true) {
49 |                 Glide.with(itemView).applyDefaultRequestOptions(requestOptions)
50 |                     .load(R.drawable.ic_pdf).into(binding.attachmentImageView)
51 |             }else{
52 |                 if (attachmentDetail.uri != null)
53 |                     Glide.with(itemView).applyDefaultRequestOptions(requestOptions)
54 |                         .load(attachmentDetail.uri).into(binding.attachmentImageView)
55 |                 else {
56 |                     Glide.with(itemView).applyDefaultRequestOptions(requestOptions)
57 |                         .load(R.drawable.ic_add).into(binding.attachmentImageView)
58 |                 }
59 |             }
60 | 
61 | 
62 |             binding.deleteAttachmentImageView.setOnClickListener {
63 |                 removeAndUpdate(list, attachmentAdapter)
64 | 
65 |             }
66 |         }
67 | 
68 |         private fun removeAndUpdate(
69 |             list: ArrayList,
70 |             attachmentAdapter: AttachmentAdapter
71 |         ) {
72 |             list.removeAt(adapterPosition)
73 |             attachmentAdapter.notifyDataSetChanged()
74 |         }
75 | 
76 |     }
77 | 
78 | 
79 |     fun updateData(arrayList: ArrayList) {
80 |         list = arrayList
81 |         notifyDataSetChanged()
82 |     }
83 | 
84 |     fun getItems(): ArrayList {
85 |         return list
86 |     }
87 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/android/attachproject/Main2Activity.java:
--------------------------------------------------------------------------------
 1 | package com.android.attachproject;
 2 | 
 3 | import android.content.Intent;
 4 | import android.net.Uri;
 5 | import android.os.Bundle;
 6 | 
 7 | import com.google.android.material.floatingactionbutton.FloatingActionButton;
 8 | import com.mirza.attachmentmanager.managers.AttachmentManager;
 9 | import com.mirza.attachmentmanager.managers.HideOption;
10 | import com.mirza.attachmentmanager.models.AttachmentDetail;
11 | import com.mirza.attachmentmanager.utils.FileUtil;
12 | 
13 | import androidx.activity.result.ActivityResult;
14 | import androidx.activity.result.ActivityResultCallback;
15 | import androidx.activity.result.ActivityResultLauncher;
16 | import androidx.activity.result.contract.ActivityResultContracts;
17 | import androidx.annotation.NonNull;
18 | import androidx.annotation.Nullable;
19 | import androidx.appcompat.app.AppCompatActivity;
20 | import androidx.appcompat.widget.Toolbar;
21 | 
22 | import android.util.Log;
23 | import android.widget.Toast;
24 | 
25 | import java.util.ArrayList;
26 | 
27 | public class Main2Activity extends AppCompatActivity {
28 | 
29 | 
30 |     private AttachmentManager attachmentManager = null;
31 |     String[] gallery = {"image/*"};
32 |     String[] files  = { "application/msword",
33 |             "application/vnd.openxmlformats-officedocument.wordprocessingml.document", // .ppt & .pptx
34 |             "application/pdf"};
35 | 
36 |     ActivityResultLauncher mLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
37 | 
38 |         ArrayList list = attachmentManager.manipulateAttachments(this,result.getResultCode(),result.getData());
39 | 
40 |     });
41 | 
42 | 
43 |     @Override
44 |     protected void onCreate(Bundle savedInstanceState) {
45 |         super.onCreate(savedInstanceState);
46 |         setContentView(R.layout.activity_main2);
47 |         Toolbar toolbar = findViewById(R.id.toolbar);
48 |         setSupportActionBar(toolbar);
49 | 
50 |         attachmentManager = new AttachmentManager.AttachmentBuilder(this) // must pass Context
51 |                 .fragment(null) // pass fragment reference if you are in fragment
52 |                 .setUiTitle("Choose File") // title of dialog or bottom sheet
53 |                 .allowMultiple(false) // set true if you want make multiple selection, default is false
54 |                 .asBottomSheet(true) // set true if you need to show selection as bottom sheet, default is as Dialog
55 |                 .setOptionsTextColor(android.R.color.holo_green_light) // change text color
56 |                 .setImagesColor(R.color.colorAccent) // change icon color
57 |                  .hide(HideOption.DOCUMENT)// You can hide any option do you want
58 |                 .setMaxPhotoSize(200000) // Set max  photo size in bytes
59 |                 .galleryMimeTypes(gallery) // mime types for gallery
60 |                 .filesMimeTypes(files) // mime types for files
61 |                 .build(); // Hide any of the three options
62 | 
63 |         Toast.makeText(this, "", Toast.LENGTH_LONG).show();
64 |         FloatingActionButton fab = findViewById(R.id.fab);
65 |         fab.setOnClickListener(view -> attachmentManager.openSelection(mLauncher));
66 | 
67 |     }
68 | 
69 | 
70 |     @Override
71 |     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
72 |         super.onRequestPermissionsResult(requestCode, permissions, grantResults);
73 |         attachmentManager.handlePermissionResponse(requestCode, permissions, grantResults,mLauncher);
74 |     }
75 | 
76 | }
77 | 
--------------------------------------------------------------------------------
/app/src/main/java/com/android/attachproject/MainActivity.kt:
--------------------------------------------------------------------------------
  1 | package com.android.attachproject
  2 | 
  3 | 
  4 | import android.os.Bundle
  5 | import android.widget.Toast
  6 | import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
  7 | import androidx.appcompat.app.AppCompatActivity;
  8 | import androidx.recyclerview.widget.GridLayoutManager
  9 | import androidx.recyclerview.widget.RecyclerView
 10 | import com.android.attachproject.databinding.ActivityMainBinding
 11 | import com.mirza.attachmentmanager.managers.AttachmentManager
 12 | import com.mirza.attachmentmanager.managers.HideOption
 13 | import com.mirza.attachmentmanager.models.AttachmentDetail
 14 | import com.mirza.attachmentmanager.utils.FileUtil
 15 | import java.util.ArrayList
 16 | 
 17 | class MainActivity : AppCompatActivity() {
 18 | 
 19 |     private lateinit var binding: ActivityMainBinding
 20 |     private var attachmentManager: AttachmentManager? = null
 21 |     private var attachmentAdapter: AttachmentAdapter? = null
 22 |     private var allAttachments : ArrayList?= arrayListOf()
 23 |     private var mLauncher = registerForActivityResult(StartActivityForResult()) { result ->
 24 | 
 25 |         allAttachments = attachmentAdapter?.getItems()
 26 | 
 27 | 
 28 |         attachmentManager?.manipulateAttachments(this,result.resultCode,result.data)?.let {
 29 | 
 30 | 
 31 |             if(it.size > 0 && it[0].mimeType?.contains("pdf",ignoreCase = true) == true) {
 32 |                 if (it.size > 0 && it[0].size!! <= 2000000) {
 33 |                     allAttachments?.addAll(it)
 34 |                 } else {
 35 |                     Toast.makeText(
 36 |                         this,
 37 |                         "File size can't be more than 2MB",
 38 |                         Toast.LENGTH_LONG
 39 |                     ).show()
 40 |                 }
 41 |             }else if (it.size > 0){
 42 |                 allAttachments?.addAll(it)
 43 |             }else{
 44 | 
 45 |             }
 46 |             it.forEach {
 47 |                  val path = FileUtil.getPath(it.uri!!,this)
 48 |                 val ss = ""
 49 |             }
 50 | 
 51 |         }
 52 |         attachmentAdapter?.updateData(allAttachments!!)
 53 |     }
 54 |     var gallery = arrayOf(
 55 |         "image/png",
 56 |         "image/jpg",
 57 |         "image/jpeg"
 58 |     )
 59 |     var files = arrayOf(
 60 |         "application/msword",
 61 |         "application/vnd.openxmlformats-officedocument.wordprocessingml.document",  // .ppt & .pptx
 62 |         "application/pdf"
 63 |     )
 64 | 
 65 | 
 66 | 
 67 |     override fun onCreate(savedInstanceState: Bundle?) {
 68 |         super.onCreate(savedInstanceState)
 69 |         binding = ActivityMainBinding.inflate(layoutInflater)
 70 |         setContentView(binding.root)
 71 |         setSupportActionBar(binding.toolbar)
 72 | 
 73 | 
 74 | 
 75 |         attachmentManager = AttachmentManager.AttachmentBuilder(this) // must pass Context
 76 |             .fragment(null) // pass fragment reference if you are in fragment
 77 |             .setUiTitle("Choose File") // title of dialog or bottom sheet
 78 |             .allowMultiple(false) // set true if you want make multiple selection, default is false
 79 |             .asBottomSheet(true) // set true if you need to show selection as bottom sheet, default is as Dialog
 80 |             .setOptionsTextColor(android.R.color.holo_green_light) // change text color
 81 |             .setImagesColor(R.color.colorAccent) // change icon color
 82 |              // You can hide any option do you want
 83 |             .setMaxPhotoSize(200000) // Set max  photo size in bytes
 84 |             .galleryMimeTypes(gallery) // mime types for gallery
 85 |             .filesMimeTypes(files) // mime types for files
 86 |             .build(); // Hide any of the three options
 87 |         binding.fab.setOnClickListener {
 88 | 
 89 |             attachmentManager?.openSelection(mLauncher)
 90 |         }
 91 | 
 92 |         binding.contentLayout.attachmentRecyclerView.layoutManager =
 93 |             GridLayoutManager(this, 1, RecyclerView.HORIZONTAL, false)
 94 |         attachmentAdapter = AttachmentAdapter(allAttachments!!)
 95 |         binding.contentLayout.attachmentRecyclerView.adapter = attachmentAdapter
 96 |         binding.contentLayout.addAttachmentImageView.setOnClickListener {
 97 |             attachmentManager?.openSelection(mLauncher)
 98 |         }
 99 |     }
100 | 
101 | 
102 | 
103 |     override fun onRequestPermissionsResult(
104 |         requestCode: Int,
105 |         permissions: Array,
106 |         grantResults: IntArray
107 |     ) {
108 |         super.onRequestPermissionsResult(requestCode, permissions, grantResults)
109 |         attachmentManager?.handlePermissionResponse(
110 |             requestCode,
111 |             permissions,
112 |             grantResults,
113 |             mLauncher
114 |         )
115 |     }
116 | 
117 | }
118 | 
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
 1 | 
 7 |     
12 |         
13 |             
19 |                  
22 |                  
25 |              
26 |          
27 |      
28 |      
34 |  
35 | 
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_circle.xml:
--------------------------------------------------------------------------------
1 | 
2 | 
4 |      
7 |  
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_add.xml:
--------------------------------------------------------------------------------
1 | 
4 |      
5 |  
6 | 
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_close.xml:
--------------------------------------------------------------------------------
 1 | 
 6 |      
 9 |  
10 | 
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 8 |      
10 |      
12 |      
14 |      
16 |      
18 |      
20 |      
22 |      
24 |      
26 |      
28 |      
30 |      
32 |      
34 |      
36 |      
38 |      
40 |      
42 |      
44 |      
46 |      
48 |      
50 |      
52 |      
54 |      
56 |      
58 |      
60 |      
62 |      
64 |      
66 |      
68 |      
70 |      
72 |      
74 |  
75 | 
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_pdf.xml:
--------------------------------------------------------------------------------
 1 | 
 7 |      
10 |      
13 |      
16 |      
19 |      
22 |      
25 |      
28 |  
29 | 
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_plus_empty_image.xml:
--------------------------------------------------------------------------------
 1 | 
 7 |      
13 |      
20 |      
23 |      
26 |  
27 | 
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 8 | 
 9 |     
13 | 
14 |          
20 | 
21 |      
22 | 
23 |      
24 | 
25 |      
32 | 
33 |  
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main2.xml:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 8 | 
 9 |     
13 | 
14 |          
20 | 
21 |      
22 | 
23 |      
24 | 
25 |      
32 | 
33 |  
--------------------------------------------------------------------------------
/app/src/main/res/layout/content_main.xml:
--------------------------------------------------------------------------------
 1 | 
 2 | 
10 | 
11 |     
18 | 
19 |          
25 | 
26 |          
31 | 
32 |         
38 | 
39 |              
46 | 
47 |              
52 |          
53 |      
54 | 
55 |  
--------------------------------------------------------------------------------
/app/src/main/res/layout/content_main2.xml:
--------------------------------------------------------------------------------
 1 | 
 2 | 
10 | 
11 |  
--------------------------------------------------------------------------------
/app/src/main/res/layout/row_attachment.xml:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 8 | 
 9 | 
10 |      
22 | 
23 | 
24 |      
35 |  
--------------------------------------------------------------------------------
/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.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zaid-Mirza/AttachmentManager/826dab50cc1c2d95087bfe3e897b01527b0300b0/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zaid-Mirza/AttachmentManager/826dab50cc1c2d95087bfe3e897b01527b0300b0/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zaid-Mirza/AttachmentManager/826dab50cc1c2d95087bfe3e897b01527b0300b0/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zaid-Mirza/AttachmentManager/826dab50cc1c2d95087bfe3e897b01527b0300b0/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zaid-Mirza/AttachmentManager/826dab50cc1c2d95087bfe3e897b01527b0300b0/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zaid-Mirza/AttachmentManager/826dab50cc1c2d95087bfe3e897b01527b0300b0/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zaid-Mirza/AttachmentManager/826dab50cc1c2d95087bfe3e897b01527b0300b0/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zaid-Mirza/AttachmentManager/826dab50cc1c2d95087bfe3e897b01527b0300b0/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zaid-Mirza/AttachmentManager/826dab50cc1c2d95087bfe3e897b01527b0300b0/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zaid-Mirza/AttachmentManager/826dab50cc1c2d95087bfe3e897b01527b0300b0/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 | 
2 | 
3 |     #008577 
4 |     #00574B 
5 |     #D81B60 
6 |     @android:color/white 
7 |     #e60000 
8 |  
9 | 
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 | 
2 |     16dp 
3 |     100dp 
4 |     1dp 
5 |     22dp 
6 |     2dp 
7 |  
8 | 
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 | 
2 |     AttachProject 
3 |     Main2Activity 
4 |     Attachments 
5 |     Close 
6 |     Add Attachments 
7 | 
8 |  
9 | 
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |     
 4 |     
10 | 
11 |     
15 | 
16 |     
17 | 
18 |     
19 | 
20 |  
21 | 
--------------------------------------------------------------------------------
/app/src/test/java/com/android/attachproject/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
 1 | package com.android.attachproject
 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 | }
18 | 
--------------------------------------------------------------------------------
/attachmentmanager/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | 
--------------------------------------------------------------------------------
/attachmentmanager/build.gradle:
--------------------------------------------------------------------------------
 1 | plugins {
 2 |     id "com.android.library"
 3 |     id 'org.jetbrains.kotlin.android'
 4 |     id 'kotlin-kapt'
 5 | }
 6 | 
 7 | android {
 8 |     compileSdk 34
 9 | 
10 | namespace("com.mirza.attachmentmanager")
11 |     defaultConfig {
12 |         minSdkVersion 26
13 |         targetSdkVersion 34
14 |         versionCode 1
15 |         versionName "1.0"
16 | 
17 |         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
18 | 
19 |     }
20 | 
21 |     buildTypes {
22 |         release {
23 |             minifyEnabled false
24 |             proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
25 |         }
26 |     }
27 |     compileOptions {
28 |         sourceCompatibility JavaVersion.VERSION_17
29 |         targetCompatibility JavaVersion.VERSION_17
30 |     }
31 |     kotlinOptions {
32 |         jvmTarget = JavaVersion.VERSION_17.toString()
33 |     }
34 | }
35 | 
36 | dependencies {
37 |     implementation fileTree(dir: 'libs', include: ['*.jar'])
38 | 
39 |     implementation 'androidx.appcompat:appcompat:1.6.1'
40 |     implementation 'androidx.legacy:legacy-support-v4:1.0.0'
41 |     implementation 'id.zelory:compressor:3.0.1'
42 |     testImplementation 'junit:junit:4.13.2'
43 |     implementation "androidx.activity:activity-ktx:1.9.0-beta01"
44 |     implementation 'androidx.fragment:fragment-ktx:1.7.0-beta01'
45 |     androidTestImplementation 'androidx.test:runner:1.5.2'
46 |     androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
47 |     implementation "androidx.core:core-ktx:1.12.0"
48 |     implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
49 |     implementation 'com.google.android.material:material:1.11.0'
50 | }
51 | repositories {
52 |     mavenCentral()
53 | }
54 | 
--------------------------------------------------------------------------------
/attachmentmanager/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 | 
--------------------------------------------------------------------------------
/attachmentmanager/src/androidTest/java/com/mirza/attachmentmanager/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
 1 | package com.mirza.attachmentmanager;
 2 | 
 3 | import android.content.Context;
 4 | import androidx.test.InstrumentationRegistry;
 5 | import androidx.test.runner.AndroidJUnit4;
 6 | 
 7 | import org.junit.Test;
 8 | import org.junit.runner.RunWith;
 9 | 
10 | import static org.junit.Assert.*;
11 | 
12 | /**
13 |  * Instrumented test, which will execute on an Android device.
14 |  *
15 |  * @see Testing documentation 
16 |  */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 |     @Test
20 |     public void useAppContext() {
21 |         // Context of the app under test.
22 |         Context appContext = InstrumentationRegistry.getTargetContext();
23 | 
24 |         assertEquals("com.mirza.attachmentmanager.test", appContext.getPackageName());
25 |     }
26 | }
27 | 
--------------------------------------------------------------------------------
/attachmentmanager/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |  
3 | 
--------------------------------------------------------------------------------
/attachmentmanager/src/main/java/com/mirza/attachmentmanager/fragments/AttachmentBottomSheet.kt:
--------------------------------------------------------------------------------
  1 | package com.mirza.attachmentmanager.fragments
  2 | 
  3 | import android.graphics.PorterDuff
  4 | import android.os.Bundle
  5 | import android.view.LayoutInflater
  6 | import android.view.View
  7 | import android.view.View.GONE
  8 | import android.view.ViewGroup
  9 | import android.widget.ImageView
 10 | import android.widget.LinearLayout
 11 | import android.widget.TextView
 12 | import androidx.core.content.ContextCompat
 13 | 
 14 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment
 15 | import com.mirza.attachmentmanager.R
 16 | import com.mirza.attachmentmanager.managers.AttachmentManager
 17 | import com.mirza.attachmentmanager.managers.HideOption
 18 | import android.graphics.PorterDuffColorFilter
 19 | 
 20 | import android.graphics.drawable.Drawable
 21 | import com.google.android.material.button.MaterialButton
 22 | 
 23 | 
 24 | class AttachmentBottomSheet(
 25 |     var title: String? = null,
 26 |     private val optionTextColor: Int? = null,
 27 |     private val imagesColor: Int? = null,
 28 |     private val hideOption: HideOption?,
 29 |     val listener: (DialogAction) -> Unit
 30 | ) : BottomSheetDialogFragment() {
 31 | 
 32 | 
 33 |     override fun onCreate(savedInstanceState: Bundle?) {
 34 |         super.onCreate(savedInstanceState)
 35 |         retainInstance = true
 36 |     }
 37 | 
 38 |     override fun onCreateView(
 39 |         inflater: LayoutInflater,
 40 |         container: ViewGroup?,
 41 |         savedInstanceState: Bundle?
 42 |     ): View? {
 43 | 
 44 | 
 45 |         val view = inflater.inflate(R.layout.layout_attachment_sheet, container, false)
 46 | 
 47 |         val titleTextView = view.findViewById(R.id.title_textView)
 48 |         val imageTextView = view.findViewById(R.id.gallery_textView)
 49 |         val cameraTextView = view.findViewById(R.id.camera_textView)
 50 |         val fileTextView = view.findViewById(R.id.file_textView)
 51 |         val cancelMaterialButton = view.findViewById(R.id.cancel_materialButton)
 52 | 
 53 |         setTextViewDrawableColor(imageTextView, imagesColor, optionTextColor)
 54 |         setTextViewDrawableColor(cameraTextView, imagesColor, optionTextColor)
 55 |         setTextViewDrawableColor(fileTextView, imagesColor, optionTextColor)
 56 | 
 57 |         when (hideOption) {
 58 |             HideOption.GALLERY -> {
 59 |                 imageTextView.visibility = GONE
 60 |             }
 61 |             HideOption.CAMERA -> {
 62 |                 cameraTextView.visibility = GONE
 63 |             }
 64 |             HideOption.DOCUMENT -> {
 65 |                 fileTextView.visibility = GONE
 66 |             }
 67 |             else->{}
 68 |         }
 69 |         title?.let {
 70 |             titleTextView.text = it
 71 |         }
 72 | 
 73 | 
 74 |         imageTextView.setOnClickListener {
 75 |             listener(DialogAction.GALLERY)
 76 |             dismiss()
 77 |         }
 78 |         cameraTextView.setOnClickListener {
 79 |             listener(DialogAction.CAMERA)
 80 |             dismiss()
 81 |         }
 82 |         fileTextView.setOnClickListener {
 83 |             listener(DialogAction.FILE)
 84 |             dismiss()
 85 |         }
 86 |         cancelMaterialButton.setOnClickListener { dismiss() }
 87 |         return view
 88 |     }
 89 | 
 90 | 
 91 |     private fun setTextViewDrawableColor(textView: TextView, imageColor: Int?, textColor: Int?) {
 92 |         imageColor?.let {
 93 |             for (drawable in textView.compoundDrawablesRelative) {
 94 |                 if (drawable != null) {
 95 |                     drawable.colorFilter = PorterDuffColorFilter(
 96 |                         ContextCompat.getColor(textView.context, imageColor),
 97 |                         PorterDuff.Mode.SRC_IN
 98 |                     )
 99 |                 }
100 |             }
101 |         }
102 | 
103 |         textColor?.let {
104 |             textView.setTextColor(ContextCompat.getColor(context!!, textColor))
105 |         }
106 | 
107 |     }
108 | 
109 | 
110 | }
--------------------------------------------------------------------------------
/attachmentmanager/src/main/java/com/mirza/attachmentmanager/fragments/AttachmentFragment.kt:
--------------------------------------------------------------------------------
  1 | package com.mirza.attachmentmanager.fragments
  2 | 
  3 | 
  4 | import android.app.Dialog
  5 | import android.graphics.Color
  6 | import android.graphics.PorterDuff
  7 | import android.graphics.drawable.ColorDrawable
  8 | import android.os.Bundle
  9 | import android.view.LayoutInflater
 10 | import android.view.View
 11 | import android.view.ViewGroup
 12 | import android.widget.ImageView
 13 | import android.widget.LinearLayout
 14 | import android.widget.TextView
 15 | import androidx.appcompat.app.AlertDialog
 16 | import androidx.appcompat.widget.DialogTitle
 17 | import androidx.core.content.ContextCompat
 18 | import androidx.fragment.app.DialogFragment
 19 | import com.mirza.attachmentmanager.R
 20 | import com.mirza.attachmentmanager.managers.AttachmentManager
 21 | import com.mirza.attachmentmanager.managers.HideOption
 22 | 
 23 | 
 24 | enum class DialogAction {
 25 |     GALLERY, CAMERA, FILE
 26 | }
 27 | 
 28 | class AttachmentFragment(val title: String? = null, private val optionTextColor: Int? = null, private val imagesColor: Int? = null, private val hideOption: HideOption?, val listener: (DialogAction) -> Unit) : DialogFragment() {
 29 | 
 30 | 
 31 |     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
 32 |                               savedInstanceState: Bundle?): View? {
 33 |         // Inflate the layout for this fragment
 34 |         retainInstance = true
 35 |         return inflater.inflate(R.layout.fragment_attachment, container, false)
 36 |     }
 37 | 
 38 |     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
 39 | 
 40 |         val inflater = LayoutInflater.from(activity)
 41 |         val view = inflater.inflate(R.layout.layout_attachment_dialog, null)
 42 | 
 43 |         arguments?.let {
 44 | 
 45 |         }
 46 | 
 47 |         val imageLinearLayout = view.findViewById(R.id.image_linearLayout)
 48 |         val cameraLinearLayout = view.findViewById(R.id.camera_linearLayout)
 49 |         val fileLinearLayout = view.findViewById(R.id.file_linearLayout)
 50 |         val cancelImageView = view.findViewById(R.id.cancel_imageView)
 51 |         val titleTextView = view.findViewById(R.id.title_textView)
 52 |         applySettings(view)
 53 | 
 54 |         when (hideOption) {
 55 |             HideOption.GALLERY -> {
 56 |                 imageLinearLayout.visibility = View.GONE
 57 |             }
 58 |             HideOption.CAMERA -> {
 59 |                 cameraLinearLayout.visibility = View.GONE
 60 |             }
 61 |             HideOption.DOCUMENT -> {
 62 |                 fileLinearLayout.visibility = View.GONE
 63 |             }
 64 |             else->{}
 65 |         }
 66 |         title?.let {
 67 |             titleTextView.text = it
 68 |         }
 69 |         cancelImageView.setOnClickListener { dismiss() }
 70 | 
 71 |         imageLinearLayout.setOnClickListener {
 72 |             listener(DialogAction.GALLERY)
 73 |             dismiss()
 74 |         }
 75 |         cameraLinearLayout.setOnClickListener {
 76 |             listener(DialogAction.CAMERA)
 77 |             dismiss()
 78 |         }
 79 |         fileLinearLayout.setOnClickListener {
 80 |             listener(DialogAction.FILE)
 81 |             dismiss()
 82 |         }
 83 | 
 84 |         val dialog = AlertDialog.Builder(activity!!)
 85 |                 .setView(view).create()
 86 |         dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
 87 |         return dialog
 88 |     }
 89 | 
 90 |     private fun applySettings(view: View) {
 91 |         val galleryImageView = view.findViewById(R.id.gallary_imageView)
 92 |         val cameraImageView = view.findViewById(R.id.camera_imageView)
 93 |         val fileImageView = view.findViewById(R.id.file_imageView)
 94 | 
 95 |         val galleryTextView = view.findViewById(R.id.gallery_textView)
 96 |         val cameraTextView = view.findViewById(R.id.camera_textView)
 97 |         val fileTextView = view.findViewById(R.id.file_textView)
 98 | 
 99 |         imagesColor?.let {
100 |             galleryImageView.setColorFilter(ContextCompat.getColor(context!!, imagesColor), PorterDuff.Mode.SRC_IN)
101 |             cameraImageView.setColorFilter(ContextCompat.getColor(context!!, imagesColor), PorterDuff.Mode.SRC_IN)
102 |             fileImageView.setColorFilter(ContextCompat.getColor(context!!, imagesColor), PorterDuff.Mode.SRC_IN)
103 |         }
104 |         optionTextColor?.let {
105 |             galleryTextView.setTextColor(ContextCompat.getColor(context!!, optionTextColor))
106 |             cameraTextView.setTextColor(ContextCompat.getColor(context!!, optionTextColor))
107 |             fileTextView.setTextColor(ContextCompat.getColor(context!!, optionTextColor))
108 | 
109 | 
110 |         }
111 | 
112 | 
113 |     }
114 | 
115 | 
116 | }
117 | 
--------------------------------------------------------------------------------
/attachmentmanager/src/main/java/com/mirza/attachmentmanager/managers/AttachmentManager.kt:
--------------------------------------------------------------------------------
  1 | package com.mirza.attachmentmanager.managers
  2 | 
  3 | import android.app.Activity.RESULT_OK
  4 | import android.content.Context
  5 | import android.content.Intent
  6 | import android.content.pm.PackageManager
  7 | import android.net.Uri
  8 | import android.os.Build
  9 | import android.os.Build.VERSION.SDK_INT
 10 | import android.os.Environment
 11 | import androidx.activity.result.ActivityResultLauncher
 12 | import androidx.appcompat.app.AppCompatActivity
 13 | import androidx.fragment.app.Fragment
 14 | import com.mirza.attachmentmanager.R
 15 | import com.mirza.attachmentmanager.fragments.AttachmentBottomSheet
 16 | import com.mirza.attachmentmanager.fragments.AttachmentFragment
 17 | import com.mirza.attachmentmanager.fragments.DialogAction
 18 | import com.mirza.attachmentmanager.models.AttachmentDetail
 19 | import com.mirza.attachmentmanager.utils.AttachmentUtil
 20 | import com.mirza.attachmentmanager.utils.FileUtil
 21 | import java.io.File
 22 | import java.lang.ref.WeakReference
 23 | 
 24 | 
 25 | enum class HideOption {
 26 |     GALLERY, CAMERA, DOCUMENT
 27 | }
 28 | 
 29 | var selectedRequestCode = AttachmentUtil.CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE
 30 | 
 31 | class AttachmentManager private constructor(builder: AttachmentBuilder) {
 32 | 
 33 | 
 34 |     companion object {
 35 |         val instance = this
 36 |     }
 37 | 
 38 |     private var title: String? = ""
 39 |     private var activity: WeakReference? = null
 40 |     private var fragment: WeakReference? = null
 41 |     private var selection: DialogAction? = null
 42 |     private var isMultiple = false
 43 |     private var cameraFile: File? = null
 44 |     private var isBottomSheet = false
 45 |     private var imagesColor: Int? = null
 46 |     private var optionsTextColor: Int? = null
 47 |     private var hideOptions: HideOption? = null
 48 |     private var maxPhotoSize: Long? = null
 49 |     private var galleryMimeTypes: Array? = null
 50 |     private var filesMimeTypes: Array? = null
 51 | 
 52 |     init {
 53 |         activity = builder.activity
 54 |         fragment = builder.fragment
 55 |         title = builder.title
 56 |         isMultiple = builder.isMultiple
 57 |         isBottomSheet = builder.isBottomSheet
 58 |         imagesColor = builder.imagesColor
 59 |         optionsTextColor = builder.optionsTextColor
 60 |         hideOptions = builder.hideOption
 61 |         maxPhotoSize = builder.maxPhotoSize
 62 |         galleryMimeTypes = builder.galleryMimeTypes
 63 |         filesMimeTypes = builder.filesMimeTypes
 64 |     }
 65 | 
 66 |     /**
 67 |      * Call this method to open attachment selection
 68 |      */
 69 |     fun openSelection(launcher: ActivityResultLauncher) {
 70 |         activity?.get()?.let {
 71 | 
 72 |             if (isBottomSheet) {
 73 |                 val attachmentFragmentSheet = AttachmentBottomSheet(
 74 |                     title,
 75 |                     optionsTextColor,
 76 |                     imagesColor,
 77 |                     hideOptions
 78 |                 ) { action ->
 79 |                     handleSelectionResponse(action, launcher)
 80 |                 }
 81 |                 attachmentFragmentSheet.show(it.supportFragmentManager, "DIALOG_SELECTION")
 82 |             } else {
 83 |                 val attachmentFragmentDialog = AttachmentFragment(
 84 |                     title,
 85 |                     optionsTextColor,
 86 |                     imagesColor,
 87 |                     hideOptions
 88 |                 ) { action ->
 89 |                     handleSelectionResponse(action, launcher)
 90 |                 }
 91 |                 attachmentFragmentDialog.show(it.supportFragmentManager, "DIALOG_SELECTION")
 92 |             }
 93 | 
 94 |         }
 95 |     }
 96 | 
 97 |     /**
 98 |      * @param action contains selection value selected by user using dialog or bottom sheet
 99 |      */
100 | 
101 |     private fun handleSelectionResponse(
102 |         action: DialogAction,
103 |         launcher: ActivityResultLauncher
104 |     ) {
105 |         selection = action
106 |         when (action) {
107 |             DialogAction.GALLERY -> {
108 |                 openGallery(launcher)
109 |             }
110 | 
111 |             DialogAction.CAMERA -> {
112 |                 startCamera(launcher)
113 |             }
114 | 
115 |             DialogAction.FILE -> {
116 |                 openFileSystem(launcher)
117 |             }
118 |         }
119 |     }
120 | 
121 |     /**
122 |      * @param activity container Activity and could be null if if user is interacting with AttachmentManager from fragment
123 |      * @param fragment will hold the reference to fragment if user is interacting with AttachmentManager from fragment
124 |      * @param permissionCode used in case of permission grant
125 |      */
126 | 
127 |     private fun openCamera(
128 |         activity: AppCompatActivity?,
129 |         fragment: Fragment?,
130 |         permissionCode: Int,
131 |         launcher: ActivityResultLauncher
132 |     ) {
133 |         if (PermissionManager.checkForPermissions(
134 |                 activity,
135 |                 fragment,
136 |                 PermissionManager.cameraPermissionList,
137 |                 permissionCode
138 |             )
139 |         ) {
140 |             val tuple = AttachmentUtil.onCamera(activity!!)
141 |             cameraFile = tuple.photoFile
142 |             AttachmentUtil.openCamera(tuple, activity, fragment, launcher)
143 |             selectedRequestCode = AttachmentUtil.CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE
144 |         }
145 |     }
146 | 
147 |     /**
148 |      * @param activity container Activity and could be null if if user is interacting with AttachmentManager from fragment
149 |      * @param fragment will hold the reference to fragment if user is interacting with AttachmentManager from fragment
150 |      * @param permissionCode used in case of permission grant
151 |      */
152 | 
153 |     private fun openGallery(
154 |         activity: AppCompatActivity?,
155 |         fragment: Fragment?,
156 |         permissionCode: Int, launcher: ActivityResultLauncher
157 |     ) {
158 |         if (PermissionManager.checkForPermissions(
159 |                 activity,
160 |                 fragment,
161 |                 PermissionManager.storagePermissionList,
162 |                 permissionCode
163 |             )
164 |         ) {
165 | 
166 |             val intent =
167 |                 AttachmentUtil.onPhoto(activity!!, isMultiple, galleryMimeTypes = galleryMimeTypes)
168 |             AttachmentUtil.openGallery(intent, activity, fragment, launcher)
169 |             selectedRequestCode = AttachmentUtil.PICK_PHOTO_CODE
170 |         }
171 |     }
172 | 
173 |     /**
174 |      * @param activity container Activity and could be null if if user is interacting with AttachmentManager from fragment
175 |      * @param fragment will hold the reference to fragment if user is interacting with AttachmentManager from fragment.
176 |      * @param permissionCode used in case of permission grant
177 |      */
178 |     private fun openFileSystem(
179 |         activity: AppCompatActivity?,
180 |         fragment: Fragment?,
181 |         permissionCode: Int, launcher: ActivityResultLauncher
182 |     ) {
183 | 
184 | 
185 |         selectedRequestCode = AttachmentUtil.FILE_CODE
186 |         val intent = AttachmentUtil.onFile(activity, fragment, isMultiple, filesMimeTypes, launcher)
187 | 
188 |     }
189 | 
190 |     /**
191 |      * Use below three methods to interact with AttachmentManager directly without any dialog or bottom sheet
192 |      */
193 |     fun startCamera(launcher: ActivityResultLauncher) {
194 |         openCamera(
195 |             activity?.get(),
196 |             fragment?.get(),
197 |             AttachmentUtil.CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE, launcher
198 |         )
199 |     }
200 | 
201 |     fun openGallery(launcher: ActivityResultLauncher) {
202 |         openGallery(activity?.get(), fragment?.get(), AttachmentUtil.PICK_PHOTO_CODE, launcher)
203 |     }
204 | 
205 |     fun openFileSystem(launcher: ActivityResultLauncher) {
206 |         openFileSystem(activity?.get(), fragment?.get(), AttachmentUtil.FILE_CODE, launcher)
207 |     }
208 | 
209 | 
210 |     /**
211 |      * Use this method from onActivityResult within your activity or fragment
212 |      * @return List of AttachmentDetail objects
213 |      */
214 |     fun manipulateAttachments(
215 |         context: Context,
216 |         resultCode: Int,
217 |         data: Intent?
218 |     ): ArrayList {
219 |         val list = ArrayList()
220 |         if (resultCode == RESULT_OK) {
221 |             when (selectedRequestCode) {
222 |                 AttachmentUtil.FILE_CODE, AttachmentUtil.PICK_PHOTO_CODE -> {
223 |                     if (data != null) {
224 |                         if (isMultiple && data.clipData != null) {
225 | 
226 |                             data.clipData?.let {
227 |                                 // Toast.makeText(context!!, it.itemCount.toString(), Toast.LENGTH_SHORT).show()
228 |                                 for (x in 0 until it.itemCount) {
229 |                                     it.getItemAt(x).uri?.let { uri ->
230 |                                         var uriFromFile = uri
231 |                                         if (selectedRequestCode == AttachmentUtil.PICK_PHOTO_CODE && FileUtil.getMimeType(
232 |                                                 uriFromFile,
233 |                                                 activity?.get()!!
234 |                                             )?.contains("video", true) == false
235 |                                         ) {
236 |                                             checkAndAdjustImageSize(uri, context)?.let {
237 |                                                 uriFromFile = it
238 |                                             }
239 |                                         }
240 |                                         list.add(
241 |                                             prepareAttachment(
242 |                                                 uriFromFile,
243 |                                                 FileUtil.getFileDisplayName(
244 |                                                     uriFromFile,
245 |                                                     activity?.get()!!,
246 |                                                     File(uriFromFile.toString())
247 |                                                 ),
248 |                                                 FileUtil.getMimeType(
249 |                                                     uriFromFile,
250 |                                                     activity?.get()!!
251 |                                                 ),
252 |                                                 FileUtil.getFileSize(uriFromFile, activity?.get()!!)
253 |                                             )
254 |                                         )
255 |                                     }
256 | 
257 |                                 }
258 |                             }
259 | 
260 |                         } else {
261 | 
262 |                             val fileUri = data.data
263 |                             fileUri?.let { uri ->
264 |                                 var uriFromFile = uri
265 |                                 if (selectedRequestCode == AttachmentUtil.PICK_PHOTO_CODE && FileUtil.getMimeType(
266 |                                         uriFromFile,
267 |                                         activity?.get()!!
268 |                                     )?.contains("video", true) == false
269 |                                 ) {
270 |                                     checkAndAdjustImageSize(uri, context)?.let {
271 |                                         uriFromFile = it
272 |                                     }
273 |                                 }
274 | 
275 |                                 list.add(
276 |                                     prepareAttachment(
277 |                                         uriFromFile,
278 |                                         FileUtil.getFileDisplayName(
279 |                                             uriFromFile,
280 |                                             activity?.get()!!,
281 |                                             File(uriFromFile.toString())
282 |                                         ),
283 |                                         FileUtil.getMimeType(uriFromFile, activity?.get()!!),
284 |                                         FileUtil.getFileSize(uriFromFile, activity?.get()!!)
285 |                                     )
286 |                                 )
287 |                             }
288 |                         }
289 |                     }
290 |                 }
291 | 
292 |                 AttachmentUtil.CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE -> {
293 | 
294 |                     cameraFile?.let {
295 | 
296 |                         var fileUri = Uri.fromFile(cameraFile)
297 |                         val displayName = FileUtil.getFileDisplayName(
298 |                             fileUri,
299 |                             activity?.get() as AppCompatActivity,
300 |                             it
301 |                         )
302 |                         // Resize Image
303 |                         checkAndAdjustImageSize(fileUri, context)?.let {
304 |                             fileUri = it
305 |                         }
306 | 
307 | 
308 |                         list.add(
309 |                             prepareAttachment(
310 |                                 fileUri,
311 |                                 displayName,
312 |                                 FileUtil.getMimeType(fileUri, activity?.get()!!),
313 |                                 FileUtil.getFileSize(fileUri, activity?.get()!!)
314 |                             )
315 |                         )
316 |                     }
317 | 
318 | 
319 |                 }
320 |             }
321 |         }
322 | 
323 |         return list
324 |     }
325 | 
326 | 
327 |     private fun checkAndAdjustImageSize(uri: Uri, context: Context): Uri? {
328 |         maxPhotoSize?.let {
329 |             if (FileUtil.getFileSize(uri, activity?.get()!!) > it) {
330 |                 val file = AttachmentUtil.resizeImage(uri, 700, context)
331 |                 return Uri.fromFile(file)
332 |             }
333 |         }
334 | 
335 |         return null
336 | 
337 |     }
338 | 
339 |     /**
340 |      * Use this method from onRequestPermissionsResult within your activity or fragment
341 |      * It will handle permission results for you
342 |      */
343 |     fun handlePermissionResponse(
344 |         requestCode: Int,
345 |         permissions: Array,
346 |         grantResults: IntArray, launcher: ActivityResultLauncher
347 |     ) {
348 |         when (requestCode) {
349 |             AttachmentUtil.CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE -> {
350 |                 if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
351 |                     startCamera(launcher)
352 |                 }
353 |             }
354 | 
355 |             AttachmentUtil.PICK_PHOTO_CODE -> {
356 |                 if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
357 |                     openGallery(launcher)
358 |                 }
359 |             }
360 | 
361 |             AttachmentUtil.FILE_CODE -> {
362 |                 if (SDK_INT >= Build.VERSION_CODES.R) {
363 |                     if (Environment.isExternalStorageManager()) {
364 |                         openFileSystem(launcher)
365 |                     }
366 |                 } else {
367 |                     if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
368 |                         openFileSystem(launcher)
369 |                     }
370 |                 }
371 |             }
372 |         }
373 |     }
374 | 
375 |     private fun prepareAttachment(
376 |         uri: Uri,
377 |         name: String,
378 |         mimeType: String?,
379 |         size: Long
380 |     ): AttachmentDetail {
381 | 
382 |         val attachmentDetail = AttachmentDetail()
383 |         attachmentDetail.uri = uri
384 |         attachmentDetail.name = name
385 |         attachmentDetail.path = uri.path
386 |         attachmentDetail.mimeType = mimeType
387 |         attachmentDetail.size = size
388 |         return attachmentDetail
389 |     }
390 | 
391 |     /**
392 |      * Initiates AttachmentManager object for you
393 |      */
394 |     data class AttachmentBuilder(private var activityContext: AppCompatActivity) {
395 | 
396 | 
397 |         var fragment: WeakReference? = null
398 |         var title: String? = activityContext.getString(R.string.m_choose)
399 |         var activity: WeakReference? = null
400 |         var isMultiple: Boolean = false
401 |         var isBottomSheet: Boolean = false
402 |         var imagesColor: Int? = null
403 |         var optionsTextColor: Int? = null
404 |         var hideOption: HideOption? = null
405 |         var galleryMimeTypes: Array? = null
406 |         var filesMimeTypes: Array? = null
407 | 
408 |         var maxPhotoSize: Long? = null
409 |         fun fragment(fragment: Fragment?) =
410 |             apply { this.fragment = WeakReference(fragment) }
411 | 
412 |         /**
413 |          * @param title of dialog or bottom sheet
414 |          */
415 |         fun setUiTitle(title: String?) = apply { this.title = title }
416 | 
417 |         fun allowMultiple(isMultiple: Boolean) = apply { this.isMultiple = isMultiple }
418 |         fun asBottomSheet(isBottomSheet: Boolean) = apply { this.isBottomSheet = isBottomSheet }
419 |         fun setImagesColor(imagesColor: Int) = apply { this.imagesColor = imagesColor }
420 |         fun setOptionsTextColor(textColor: Int) = apply { this.optionsTextColor = textColor }
421 |         fun hide(option: HideOption?) = apply { this.hideOption = option }
422 |         fun setMaxPhotoSize(maxSize: Long) = apply {
423 |             maxPhotoSize = maxSize
424 |         }
425 | 
426 |         fun galleryMimeTypes(types: Array) = apply {
427 |             galleryMimeTypes = types
428 |         }
429 | 
430 |         fun filesMimeTypes(types: Array) = apply {
431 |             filesMimeTypes = types
432 |         }
433 | 
434 | 
435 |         fun build() = AttachmentManager(this)
436 | 
437 |         init {
438 |             activity = WeakReference(activityContext)
439 |         }
440 |     }
441 | 
442 | 
443 | }
--------------------------------------------------------------------------------
/attachmentmanager/src/main/java/com/mirza/attachmentmanager/managers/ConstantManager.kt:
--------------------------------------------------------------------------------
1 | package com.mirza.attachmentmanager.managers
2 | 
3 | object ConstantManager {
4 | 
5 |     const val TITLE = "title"
6 |     const val OPTION_TEXT_COLOR = "option_text_color"
7 |     const val IMAGES_COLOR = "images_color"
8 | }
--------------------------------------------------------------------------------
/attachmentmanager/src/main/java/com/mirza/attachmentmanager/managers/PermissionManager.kt:
--------------------------------------------------------------------------------
  1 | package com.mirza.attachmentmanager.managers
  2 | 
  3 | import android.Manifest
  4 | import android.content.Context
  5 | import android.content.pm.PackageManager
  6 | import android.os.Build
  7 | import android.os.Build.VERSION.SDK_INT
  8 | import androidx.appcompat.app.AppCompatActivity
  9 | import androidx.core.app.ActivityCompat
 10 | import androidx.core.content.ContextCompat
 11 | import androidx.fragment.app.Fragment
 12 | import java.util.ArrayList
 13 | import androidx.core.app.ActivityCompat.startActivityForResult
 14 | 
 15 | import android.content.Intent
 16 | import android.net.Uri
 17 | import android.os.Environment
 18 | import android.provider.Settings
 19 | import com.mirza.attachmentmanager.utils.AttachmentUtil
 20 | import java.lang.Exception
 21 | 
 22 | 
 23 | object PermissionManager {
 24 | 
 25 | 
 26 | 
 27 | 
 28 |     val storagePermissionList: ArrayList
 29 |         get() {
 30 |             val requiredPermissions = ArrayList()
 31 |             if(SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
 32 |                 requiredPermissions.add(Manifest.permission.READ_MEDIA_IMAGES)
 33 |                 requiredPermissions.add(Manifest.permission.READ_MEDIA_VIDEO)
 34 |             }else{
 35 |                 requiredPermissions.add(Manifest.permission.READ_EXTERNAL_STORAGE)
 36 |             }
 37 | 
 38 |             return requiredPermissions
 39 |         }
 40 | 
 41 |     val cameraPermissionList: ArrayList
 42 |         get() {
 43 |             val requiredPermissions = ArrayList()
 44 |             requiredPermissions.add(Manifest.permission.CAMERA)
 45 | 
 46 |             return requiredPermissions
 47 |         }
 48 | 
 49 | 
 50 |     private fun checkForPermission(permissions: ArrayList, context: Context?): ArrayList {
 51 |         val missingPermission = ArrayList()
 52 |         for (permission in permissions) {
 53 | 
 54 |             if (ContextCompat.checkSelfPermission(context!!, permission) != PackageManager.PERMISSION_GRANTED) {
 55 |                 missingPermission.add(permission)
 56 |             }
 57 |         }
 58 |         return missingPermission
 59 | 
 60 |     }
 61 | 
 62 |     fun checkAndroidVersionAndPermission(appCompatActivity: AppCompatActivity?, fragment: Fragment?, pRequiredPermissions: ArrayList, permissionCode: Int): Boolean {
 63 |         if(SDK_INT >= Build.VERSION_CODES.R) {
 64 |             if(Environment.isExternalStorageManager()){
 65 |                 return true
 66 |             }
 67 |             try {
 68 |                 val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
 69 |                 intent.addCategory("android.intent.category.DEFAULT")
 70 |                 intent.data = Uri.parse(java.lang.String.format("package:%s", if(fragment == null) appCompatActivity?.packageName else fragment.requireActivity().packageName))
 71 |                 if(fragment != null)
 72 |                     fragment.startActivityForResult(intent, AttachmentUtil.STORAGE_CODE)
 73 |                 else{
 74 |                     appCompatActivity?.startActivityForResult(intent, AttachmentUtil.STORAGE_CODE)
 75 |                 }
 76 |             } catch (e: Exception) {
 77 |                 val intent = Intent()
 78 |                 intent.action = Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION
 79 |                 if(fragment != null)
 80 |                     fragment.startActivityForResult(intent, AttachmentUtil.STORAGE_CODE)
 81 |                 else{
 82 |                     appCompatActivity?.startActivityForResult(intent, AttachmentUtil.STORAGE_CODE)
 83 |                 }
 84 |             }
 85 |             return false
 86 |         }else{
 87 |             // below 11
 88 |             return checkForPermissions(appCompatActivity, fragment, pRequiredPermissions, permissionCode)
 89 |         }
 90 |     }
 91 |     fun checkForPermissions(appCompatActivity: AppCompatActivity?, fragment: Fragment?, pRequiredPermissions: ArrayList, permissionCode: Int): Boolean {
 92 |         var requiredPermissions = pRequiredPermissions
 93 | 
 94 |         if (fragment != null) {
 95 |             requiredPermissions = checkForPermission(requiredPermissions, fragment.context)
 96 |         } else {
 97 |             requiredPermissions = checkForPermission(requiredPermissions, appCompatActivity)
 98 |         }
 99 |         if (requiredPermissions.size > 0) {
100 |             askForPermission(requiredPermissions, appCompatActivity, fragment, permissionCode)
101 |         } else {
102 |             return true
103 |         }
104 |         return false
105 | 
106 |     }
107 | 
108 |     private fun askForPermission(requiredPermission: ArrayList, appCompatActivity: AppCompatActivity?, fragment: Fragment?, permissionCode: Int) {
109 | 
110 | 
111 |           if (fragment == null) {
112 |               ActivityCompat.requestPermissions(
113 |                   appCompatActivity!!,
114 |                   requiredPermission.toTypedArray(),
115 |                   permissionCode
116 |               )
117 |           } else {
118 |               fragment.requestPermissions(requiredPermission.toTypedArray(), permissionCode)
119 |           }
120 | 
121 |       }
122 | 
123 | 
124 | 
125 | 
126 | }
--------------------------------------------------------------------------------
/attachmentmanager/src/main/java/com/mirza/attachmentmanager/models/AttachmentDetail.kt:
--------------------------------------------------------------------------------
 1 | package com.mirza.attachmentmanager.models
 2 | 
 3 | import android.net.Uri
 4 | 
 5 | class AttachmentDetail {
 6 | 
 7 |     var uri: Uri? = null
 8 |     var name: String? = null
 9 |     var path: String? = null
10 |     var mimeType: String? = null
11 |     var size: Long? = null
12 | }
--------------------------------------------------------------------------------
/attachmentmanager/src/main/java/com/mirza/attachmentmanager/models/Tuple.kt:
--------------------------------------------------------------------------------
 1 | package com.mirza.attachmentmanager.models
 2 | 
 3 | import android.content.Intent
 4 | import java.io.File
 5 | 
 6 | class Tuple {
 7 | 
 8 |     var photoFile: File? = null
 9 |     var intent: Intent? = null
10 | }
--------------------------------------------------------------------------------
/attachmentmanager/src/main/java/com/mirza/attachmentmanager/utils/AttachmentUtil.kt:
--------------------------------------------------------------------------------
  1 | package com.mirza.attachmentmanager.utils
  2 | 
  3 | import android.content.Context
  4 | import android.content.Intent
  5 | import android.content.pm.PackageManager
  6 | import android.graphics.Bitmap
  7 | import android.graphics.BitmapFactory
  8 | import android.net.Uri
  9 | import android.os.Environment
 10 | import android.provider.MediaStore
 11 | import android.util.Log
 12 | import androidx.activity.result.ActivityResultLauncher
 13 | import androidx.appcompat.app.AppCompatActivity
 14 | import androidx.core.content.FileProvider
 15 | import androidx.fragment.app.Fragment
 16 | import com.mirza.attachmentmanager.R
 17 | import com.mirza.attachmentmanager.models.Tuple
 18 | import java.io.ByteArrayOutputStream
 19 | import java.io.File
 20 | import java.io.FileOutputStream
 21 | import java.lang.reflect.GenericArrayType
 22 | 
 23 | object AttachmentUtil {
 24 |     const val APP_TAG = "AttachmentManager"
 25 |     const val CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE = 1000
 26 |     const val PICK_PHOTO_CODE = 1001
 27 |     const val FILE_CODE = 125
 28 |     const val STORAGE_CODE = 126
 29 |     const val REQUEST_CODE = "REQUEST_CODE"
 30 |     val types = arrayOf(
 31 | 
 32 |             "image/png",
 33 |             "image/jpg",
 34 |             "image/jpeg",
 35 |             "video/mp4",
 36 |             "video/MP2T"
 37 |     )
 38 | 
 39 |     /**
 40 |      * @param fileName name of the image
 41 |      * @return  File for a photo stored on disk given the fileName
 42 |      */
 43 |     private fun getPhotoFileUri(fileName: String, context: Context): File {
 44 |         // Get safe storage directory for photos
 45 |         // Use `getExternalFilesDir` on Context to access package-specific directories.
 46 |         // This way, we don't need to request external read/write runtime permissions.
 47 |         val mediaStorageDir = File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), APP_TAG)
 48 | 
 49 |         // Create the storage directory if it does not exist
 50 |         if (!mediaStorageDir.exists() && !mediaStorageDir.mkdirs()) {
 51 |             Log.d(APP_TAG, "failed to create directory")
 52 |         }
 53 |         return File(mediaStorageDir.path + File.separator + fileName)
 54 |     }
 55 | 
 56 |     fun resizeImage(photoUri: Uri, desiredWidth: Int, context: Context): File {
 57 | 
 58 |         // by this point we have the camera photo on disk
 59 |         val inputStream = context.contentResolver.openInputStream(photoUri)
 60 |         val rawTakenImage = BitmapFactory.decodeStream(inputStream)
 61 | 
 62 |         val resizedBitmap: Bitmap = ImageUtils.scaleToFitWidth(rawTakenImage, desiredWidth)
 63 |         // Configure byte output stream
 64 |         val bytes = ByteArrayOutputStream()
 65 |         // Compress the image further
 66 |         resizedBitmap.compress(Bitmap.CompressFormat.JPEG, 70, bytes)
 67 |         // Create a new file for the resized bitmap (`getPhotoFileUri` defined above)
 68 |         val resizedFile = getPhotoFileUri("IMGR_" + System.currentTimeMillis() + ".jpg", context)
 69 |         resizedFile.createNewFile()
 70 |         val fos = FileOutputStream(resizedFile)
 71 |         // Write the bytes of the bitmap to file
 72 |         fos.write(bytes.toByteArray())
 73 |         fos.close()
 74 |         return resizedFile
 75 |     }
 76 | 
 77 |     fun onCamera(context: Context): Tuple {
 78 |         // create Intent to take a picture and return control to the caller
 79 | 
 80 |         val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
 81 |         intent.putExtra(REQUEST_CODE, CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE)
 82 |         // Create a File reference to access to future access
 83 |         val photoFileName = "IMG_" + System.currentTimeMillis() + ".jpg"
 84 |         val photoFile = getPhotoFileUri(photoFileName, context)
 85 | 
 86 |         val fileProvider = FileProvider.getUriForFile(context, context.packageName + ".attachmentmanager", photoFile)
 87 |         intent.putExtra(MediaStore.EXTRA_OUTPUT, fileProvider)
 88 | 
 89 | 
 90 |         val tuple = Tuple()
 91 |         tuple.intent = intent
 92 |         tuple.photoFile = photoFile
 93 |         return tuple
 94 |     }
 95 | 
 96 |     fun onPhoto(context: Context, isMultiple: Boolean, galleryMimeTypes: Array? = types): Intent {
 97 |         // Create intent for picking a photo from the gallery
 98 | 
 99 |         val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI).putExtra(Intent.EXTRA_ALLOW_MULTIPLE, isMultiple)
100 | 
101 |         intent.type = galleryMimeTypes?.joinToString(separator = ",") ?: types.joinToString("|")
102 |         intent.putExtra(REQUEST_CODE, PICK_PHOTO_CODE)
103 |         intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
104 |         intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
105 |      //   intent.putExtra(Intent.EXTRA_MIME_TYPES, galleryMimeTypes)
106 |         return intent
107 | 
108 |     }
109 | 
110 |     fun onFile(activity: AppCompatActivity?, fragmentContext: Fragment?, isMultiple: Boolean?, pFileMimeTypes: Array?,launcher: ActivityResultLauncher): Intent {
111 |         val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
112 |             action = Intent.ACTION_OPEN_DOCUMENT
113 |             addCategory(Intent.CATEGORY_OPENABLE)
114 |             putExtra(REQUEST_CODE, FILE_CODE)
115 |             putExtra(Intent.EXTRA_ALLOW_MULTIPLE, isMultiple ?: false)
116 |             addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
117 |             addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
118 |         }
119 | 
120 |         val fileMimeTypes = pFileMimeTypes ?: FileUtil.mimeTypes
121 |         intent.type = if (fileMimeTypes.size == 1) fileMimeTypes[0] else "*/*"
122 |         if (fileMimeTypes.isNotEmpty()) {
123 |             intent.putExtra(Intent.EXTRA_MIME_TYPES, fileMimeTypes)
124 |         }
125 | 
126 |         val list = activity?.packageManager?.queryIntentActivities(intent, PackageManager.MATCH_ALL)
127 |         if (list?.size!! > 0) run {
128 |             launcher.launch(intent)
129 |         }
130 |         return intent
131 |     }
132 | 
133 |     fun openCamera(tuple: Tuple, activity: AppCompatActivity?, fragmentContext: Fragment?,launcher: ActivityResultLauncher) {
134 | 
135 |         if (tuple.intent?.resolveActivity(activity?.packageManager!!) != null) {
136 |            launcher.launch(tuple.intent!!)
137 |         }
138 |     }
139 | 
140 |     fun openGallery(intent: Intent, activity: AppCompatActivity?, fragmentContext: Fragment?,launcher: ActivityResultLauncher) {
141 |         if (intent.resolveActivity(activity?.packageManager!!) != null) {
142 |             // Bring up gallery to select a photo
143 |             launcher.launch(intent)
144 |         }
145 |     }
146 | 
147 | 
148 | }
--------------------------------------------------------------------------------
/attachmentmanager/src/main/java/com/mirza/attachmentmanager/utils/FileUtil.kt:
--------------------------------------------------------------------------------
  1 | package com.mirza.attachmentmanager.utils
  2 | 
  3 | //import okhttp3.MediaType
  4 | //import okhttp3.MultipartBody
  5 | //import okhttp3.RequestBody
  6 | import android.annotation.SuppressLint
  7 | import android.app.Application
  8 | import android.content.ContentResolver
  9 | import android.content.ContentUris
 10 | import android.content.Context
 11 | import android.database.Cursor
 12 | import android.graphics.Bitmap
 13 | import android.graphics.BitmapFactory
 14 | import android.net.Uri
 15 | import android.os.Build
 16 | import android.os.Environment
 17 | import android.provider.DocumentsContract
 18 | import android.provider.MediaStore
 19 | import android.provider.OpenableColumns
 20 | import android.util.Log
 21 | import android.webkit.MimeTypeMap
 22 | import androidx.core.net.toUri
 23 | import java.io.*
 24 | 
 25 | 
 26 | object FileUtil {
 27 | 
 28 |     const val DOCUMENTS_DIR = "documents"
 29 | 
 30 |     fun getFileDisplayName(fileUri: Uri, activity: Context, file: File?): String {
 31 |         var displayName: String = "File_" + System.currentTimeMillis()
 32 |         if (fileUri.toString().startsWith("content://")) {
 33 |             var cursor: Cursor? = null
 34 |             try {
 35 |                 cursor = activity.contentResolver.query(fileUri, null, null, null, null)
 36 |                 if (cursor != null && cursor.moveToFirst()) {
 37 |                     displayName =
 38 |                         cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
 39 |                 }
 40 |             } finally {
 41 |                 cursor!!.close()
 42 |             }
 43 |         } else if (fileUri.toString().startsWith("file://") && file != null) {
 44 |             displayName = file.name
 45 |         }
 46 |         return displayName
 47 |     }
 48 | 
 49 |     fun getMimeType(uri: Uri, context: Context): String? {
 50 |         var mimeType: String? = null
 51 |         if (uri.scheme == ContentResolver.SCHEME_CONTENT) {
 52 |             val cr = context.contentResolver
 53 |             mimeType = cr.getType(uri)
 54 |         } else {
 55 |             val fileExtension = MimeTypeMap.getFileExtensionFromUrl(uri.toString())
 56 |             mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
 57 |                 fileExtension.toLowerCase()
 58 |             )
 59 |         }
 60 |         return mimeType
 61 |     }
 62 | 
 63 |     fun generateFileName(name: String?, directory: File): File? {
 64 |         var name1 = name
 65 | 
 66 |         var file = File(directory, name1)
 67 | 
 68 | 
 69 |         if (file.exists()) {
 70 |             var fileName: String = name1!!
 71 |             var extension = ""
 72 |             val dotIndex = name1.lastIndexOf('.')
 73 |             if (dotIndex > 0) {
 74 |                 fileName = name1.substring(0, dotIndex)
 75 |                 extension = name1.substring(dotIndex)
 76 |             }
 77 | 
 78 |             var index = 0
 79 | 
 80 |             while (file.exists()) {
 81 |                 index++
 82 |                 name1 = "$fileName($index)$extension"
 83 |                 file = File(directory, name1)
 84 |             }
 85 |         }
 86 | 
 87 |         try {
 88 |             if (!file.createNewFile()) {
 89 |                 return null
 90 |             }
 91 |         } catch (e: IOException) {
 92 | 
 93 |             return null
 94 |         }
 95 | 
 96 | 
 97 |         return file
 98 |     }
 99 | 
100 |     var mimeTypes = arrayOf(
101 |         "application/msword",
102 |         "application/vnd.openxmlformats-officedocument.wordprocessingml.document", // .ppt & .pptx
103 |         "application/vnd.ms-excel",
104 |         "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
105 |         "application/pdf"
106 |     )
107 | 
108 |     @SuppressLint("NewApi")
109 |     fun getPath(uri1: Uri, context: Context): String? {
110 |         var uri = uri1
111 |         val isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
112 | 
113 |         // DocumentProvider
114 |         if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
115 |             // LocalStorageProvider
116 |             if (isExternalStorageDocument(uri)) {
117 |                 // The path is the id
118 |                 return DocumentsContract.getDocumentId(uri)
119 |             } else if (isExternalStorageDocument(uri)) {
120 |                 val docId = DocumentsContract.getDocumentId(uri)
121 |                 val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
122 |                 val type = split[0]
123 | 
124 |                 if ("primary".equals(type, ignoreCase = true)) {
125 |                     return Environment.getExternalStorageDirectory().toString() + "/" + split[1]
126 |                 } else if ("home".equals(type, ignoreCase = true)) {
127 |                     return Environment.getExternalStorageDirectory()
128 |                         .toString() + "/documents/" + split[1]
129 |                 }
130 |             } else if (isDownloadsDocument(uri)) {
131 | 
132 |                 var id = DocumentsContract.getDocumentId(uri)
133 | 
134 |                 if (id != null && id.startsWith("raw:")) {
135 |                     return id.substring(4)
136 |                 }
137 | 
138 | 
139 |                 val contentUriPrefixesToTry =
140 |                     arrayOf(
141 |                         "content://downloads/public_downloads",
142 |                         "content://downloads/my_downloads",
143 |                         "content://downloads/all_downloads"
144 |                     )
145 | 
146 |                 for (contentUriPrefix in contentUriPrefixesToTry) {
147 |                     Log.e("DASHT", id.toString())
148 |                     var contentUri: Uri? = null
149 |                     if (id != null && !id.startsWith("msf:")) {
150 |                         contentUri = ContentUris.withAppendedId(
151 |                             Uri.parse(contentUriPrefix),
152 |                             java.lang.Long.valueOf(id)
153 |                         )
154 |                     } else {
155 |                         contentUri = uri
156 |                     }
157 | 
158 |                     try {
159 |                         val path = getDataColumn(context, contentUri, null, null)
160 |                         if (path != null) {
161 |                             return path
162 |                         }
163 |                     } catch (e: Exception) {
164 |                     }
165 | 
166 |                 }
167 | 
168 | 
169 |                 return copyFileToAppCache(uri, context)
170 |             } else if (isMediaDocument(uri)) {
171 |                 val docId = DocumentsContract.getDocumentId(uri)
172 |                 val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
173 |                 val type = split[0]
174 | 
175 |                 var contentUri: Uri? = null
176 |                 if ("image" == type) {
177 |                     contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
178 |                 } else if ("video" == type) {
179 |                     contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
180 |                 } else if ("audio" == type) {
181 |                     contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
182 |                 }
183 | 
184 | 
185 |                 val selection = "_id=?"
186 |                 val selectionArgs = arrayOf(split[1])
187 | 
188 |                 return contentUri?.let { getDataColumn(context, it, selection, selectionArgs) }
189 |                     ?: return copyFileToAppCache(uri, context)
190 |             }
191 |             // MediaProvider
192 |             // DownloadsProvider
193 |             // ExternalStorageProvider
194 |             else if ("content".equals(uri.scheme, ignoreCase = true)) {
195 | 
196 |                 // Return the remote address
197 |                 return if (isGooglePhotosUri(uri)) {
198 |                     uri.lastPathSegment
199 |                 } else if (isGoogleocumentUri(uri)) {
200 |                     return getDriveFilePath(uri, context)
201 |                 } else getDataColumn(context, uri, null, null)
202 | 
203 |             } else if ("file".equals(uri.scheme, ignoreCase = true)) {
204 |                 return uri.path
205 |             }
206 |         } else if ("content".equals((uri.scheme))) {
207 | 
208 |             // Return the remote address
209 |             if (isGooglePhotosUri(uri))
210 |                 return uri.getLastPathSegment();
211 | 
212 |             return getDataColumn(context, uri, null, null);
213 |         }
214 |         // File
215 |         else if ("file".equals(uri.scheme, ignoreCase = true)) {
216 |             return uri.getPath();
217 |         }
218 | 
219 |         return null
220 |     }
221 | 
222 |     private fun copyFileToAppCache(uri: Uri, context: Context): String? {
223 |         // path could not be retrieved using ContentResolver, therefore copy file to accessible cache using streams
224 |         val fileName = getFileDisplayName(uri, context, null)
225 |         val cacheDir = getDocumentCacheDir(context)
226 |         val file = generateFileName(fileName, cacheDir)
227 |         var destinationPath: String? = null
228 |         if (file != null) {
229 |             destinationPath = file.getAbsolutePath()
230 |             saveFileFromUri(context, uri, destinationPath)
231 |         }
232 | 
233 |         return destinationPath
234 |     }
235 | 
236 | 
237 |     fun saveBitmapToFile(context: Context, file: File): File? {
238 |         return try {
239 | 
240 |             // BitmapFactory options to downsize the image
241 |             val o: BitmapFactory.Options = BitmapFactory.Options()
242 |             o.inJustDecodeBounds = true
243 |             o.inSampleSize = 6
244 |             // factor of downsizing the image
245 |             var inputStream = context.contentResolver.openInputStream(file.toUri())
246 |             //Bitmap selectedBitmap = null;
247 |             BitmapFactory.decodeStream(inputStream, null, o)
248 |             inputStream?.close()
249 | 
250 |             // The new size we want to scale to
251 |             val REQUIRED_SIZE = 75
252 | 
253 |             // Find the correct scale value. It should be the power of 2.
254 |             var scale = 1
255 |             while (o.outWidth / scale / 2 >= REQUIRED_SIZE &&
256 |                 o.outHeight / scale / 2 >= REQUIRED_SIZE
257 |             ) {
258 |                 scale *= 2
259 |             }
260 |             val o2: BitmapFactory.Options = BitmapFactory.Options()
261 |             o2.inSampleSize = scale
262 |             inputStream = context.contentResolver.openInputStream(file.toUri())
263 |             val selectedBitmap: Bitmap? = BitmapFactory.decodeStream(inputStream, null, o2)
264 |             inputStream?.close()
265 | 
266 |             // here i override the original image file
267 |             file.createNewFile()
268 |             val outputStream = context.contentResolver.openOutputStream(file.toUri())
269 |             selectedBitmap?.compress(Bitmap.CompressFormat.JPEG, 20, outputStream!!)
270 |             outputStream?.close()
271 |             file
272 |         } catch (e: java.lang.Exception) {
273 |             null
274 |         }
275 |     }
276 | 
277 |     private fun getDriveFilePath(uri: Uri, context: Context): String {
278 |         val returnCursor = context.contentResolver.query(uri, null, null, null, null)
279 |         val nameIndex = returnCursor!!.getColumnIndex(OpenableColumns.DISPLAY_NAME)
280 |         val sizeIndex = returnCursor.getColumnIndex(OpenableColumns.SIZE)
281 |         returnCursor.moveToFirst()
282 |         val name = returnCursor.getString(nameIndex)
283 |         val size = java.lang.Long.toString(returnCursor.getLong(sizeIndex))
284 |         val file = File(context.cacheDir, name)
285 |         try {
286 |             val inputStream = context.contentResolver.openInputStream(uri)
287 |             val outputStream = FileOutputStream(file)
288 |             var read = 0
289 |             val maxBufferSize = 1 * 1024 * 1024
290 |             val bytesAvailable = inputStream!!.available()
291 |             val bufferSize = Math.min(bytesAvailable, maxBufferSize)
292 | 
293 |             val buffers = ByteArray(bufferSize)
294 |             while ((inputStream.read(buffers).also { read = it }) != -1) {
295 |                 outputStream.write(buffers, 0, read)
296 |             }
297 |             inputStream.close()
298 |             outputStream.close()
299 |         } catch (e: Exception) {
300 |             //Log.e("Exception", e.message)
301 |         }
302 | 
303 |         return file.path
304 |     }
305 | 
306 |     private fun saveFileFromUri(context: Context, uri: Uri, destinationPath: String) {
307 |         var `is`: InputStream? = null
308 |         var bos: BufferedOutputStream? = null
309 |         try {
310 |             `is` = context.contentResolver.openInputStream(uri)
311 |             bos = BufferedOutputStream(FileOutputStream(destinationPath, false))
312 |             val buf = ByteArray(1024)
313 |             `is`!!.read(buf)
314 |             do {
315 |                 bos.write(buf)
316 |             } while (`is`.read(buf) != -1)
317 |         } catch (e: IOException) {
318 |             e.printStackTrace()
319 |         } finally {
320 |             try {
321 |                 `is`?.close()
322 |                 bos?.close()
323 |             } catch (e: IOException) {
324 |                 e.printStackTrace()
325 |             }
326 | 
327 |         }
328 |     }
329 | 
330 |     private fun getDocumentCacheDir(context: Context): File {
331 |         val dir = File(context.cacheDir, DOCUMENTS_DIR);
332 |         if (!dir.exists()) {
333 |             dir.mkdirs();
334 |         }
335 | 
336 |         return dir;
337 |     }
338 | 
339 |     /**
340 |      * @param uri The Uri to check.
341 |      * @return Whether the Uri authority is ExternalStorageProvider.
342 |      */
343 |     private fun isExternalStorageDocument(uri: Uri): Boolean {
344 |         return "com.android.externalstorage.documents" == uri.authority
345 |     }
346 | 
347 |     /**
348 |      * @param uri The Uri to check.
349 |      * @return Whether the Uri authority is DownloadsProvider.
350 |      */
351 |     private fun isDownloadsDocument(uri: Uri): Boolean {
352 |         return "com.android.providers.downloads.documents" == uri.authority
353 |     }
354 | 
355 |     /**
356 |      * @param uri The Uri to check.
357 |      * @return Whether the Uri authority is MediaProvider.
358 |      */
359 |     private fun isMediaDocument(uri: Uri): Boolean {
360 |         return "com.android.providers.media.documents" == uri.authority
361 |     }
362 | 
363 |     private fun isGooglePhotosUri(uri: Uri): Boolean {
364 |         return "com.google.android.apps.photos.content" == uri.authority
365 |     }
366 | 
367 |     private fun isGoogleocumentUri(uri: Uri): Boolean {
368 |         return "com.google.android.apps.docs.storage" == uri.authority
369 |     }
370 | 
371 | //    fun getMultiParts(attachments: ArrayList, context: Context, paraName: String): ArrayList {
372 | //        val list = ArrayList()
373 | //
374 | //        attachments.forEach { attachment ->
375 | //            attachment.uri?.let { uri ->
376 | //                val file: File? = getFileFromUri(uri, context);
377 | //
378 | //                file?.let {
379 | //                    val mimeType = getMimeType(uri, context)
380 | //                    mimeType?.let {
381 | //                        val requestBody = RequestBody.create(MediaType.parse(mimeType), file)
382 | //                        val part = MultipartBody.Part.createFormData(paraName + System.currentTimeMillis(), file.name, requestBody)
383 | //                        list.add(part)
384 | //                    }
385 | //                }
386 | //
387 | //            }
388 | //
389 | //        }
390 | //
391 | //        return list
392 | //    }
393 | 
394 |     private fun getFileFromUri(uri: Uri, context: Context): File? {
395 |         val path = null// getPath(uri, context)
396 |         var file: File? = null
397 |         if (path == null) {
398 |             file = File((uri.toString()))
399 |         } else {
400 |             // file = File(path)
401 |         }
402 |         return file
403 |     }
404 | 
405 |     private fun getDataColumn(
406 |         context: Context, uri: Uri, selection: String?,
407 |         selectionArgs: Array?
408 |     ): String? {
409 | 
410 |         var cursor: Cursor? = null;
411 |         val column = MediaStore.Files.FileColumns.DATA
412 |         val projection = arrayOf(column)
413 | 
414 |         try {
415 |             cursor = context.contentResolver.query(
416 |                 uri, projection, selection, selectionArgs,
417 |                 null
418 |             );
419 |             if (cursor != null && cursor.moveToFirst()) {
420 | 
421 |                 val columnIndex = cursor.getColumnIndexOrThrow(column);
422 |                 return cursor.getString(columnIndex);
423 |             }
424 |         } catch (e: Exception) {
425 |             val ss = ""
426 |         } finally {
427 |             cursor?.close();
428 |         }
429 |         return null;
430 |     }
431 | 
432 |     fun getFileSize(uri: Uri, context: Context): Long {
433 | 
434 | 
435 | //        val file = getFileFromUri(uri, context)
436 | //        var fileSize: Long? = null
437 | //        // Get length of file in bytes
438 | //        file?.let {
439 | //            fileSize = file.length();
440 | //
441 | //            // Convert the bytes to Kilobytes (1 KB = 1024 Bytes)
442 | ////            fileSize = fileSize?.let {
443 | ////                (it / 1024) / 1024;
444 | ////
445 | ////            }
446 | //        }
447 | 
448 | 
449 |         // return fileSize ?: 0;
450 |         return uri.length(context.contentResolver)
451 |     }
452 | 
453 |     private fun Uri.length(contentResolver: ContentResolver)
454 |             : Long {
455 | 
456 |         val assetFileDescriptor = try {
457 |             contentResolver.openAssetFileDescriptor(this, "r")
458 |         } catch (e: FileNotFoundException) {
459 |             null
460 |         }
461 |         // uses ParcelFileDescriptor#getStatSize underneath if failed
462 |         val length = assetFileDescriptor?.use { it.length } ?: -1L
463 |         if (length != -1L) {
464 |             return length
465 |         }
466 | 
467 |         // if "content://" uri scheme, try contentResolver table
468 |         if (scheme.equals(ContentResolver.SCHEME_CONTENT)) {
469 |             return contentResolver.query(this, arrayOf(OpenableColumns.SIZE), null, null, null)
470 |                 ?.use { cursor ->
471 |                     // maybe shouldn't trust ContentResolver for size: https://stackoverflow.com/questions/48302972/content-resolver-returns-wrong-size
472 |                     val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
473 |                     if (sizeIndex == -1) {
474 |                         return@use -1L
475 |                     }
476 |                     cursor.moveToFirst()
477 |                     return try {
478 |                         cursor.getLong(sizeIndex)
479 |                     } catch (_: Throwable) {
480 |                         -1L
481 |                     }
482 |                 } ?: -1L
483 |         } else {
484 |             return -1L
485 |         }
486 |     }
487 | 
488 | }
489 | 
--------------------------------------------------------------------------------
/attachmentmanager/src/main/java/com/mirza/attachmentmanager/utils/ImageUtils.kt:
--------------------------------------------------------------------------------
 1 | package com.mirza.attachmentmanager.utils
 2 | 
 3 | import android.graphics.Bitmap
 4 | import android.graphics.BitmapFactory
 5 | import android.graphics.Matrix
 6 | import android.media.ExifInterface
 7 | import java.io.IOException
 8 | 
 9 | 
10 | object ImageUtils {
11 | 
12 | 
13 |     // Scale and maintain aspect ratio given a desired width
14 |     // BitmapScaler.scaleToFitWidth(bitmap, 100);
15 |     fun scaleToFitWidth(b: Bitmap, width: Int): Bitmap {
16 |         val factor = width / b.width.toFloat()
17 |         return Bitmap.createScaledBitmap(b, width, (b.height * factor).toInt(), true)
18 |     }
19 | 
20 |     // Scale and maintain aspect ratio given a desired height
21 |     // BitmapScaler.scaleToFitHeight(bitmap, 100);
22 |     fun scaleToFitHeight(b: Bitmap, height: Int): Bitmap {
23 |         val factor = height / b.height.toFloat()
24 |         return Bitmap.createScaledBitmap(b, (b.width * factor).toInt(), height, true)
25 |     } // ...
26 | 
27 | 
28 |     fun rotateBitmapOrientation(photoFilePath: String?): Bitmap? {
29 |         // Create and configure BitmapFactory
30 |         val bounds = BitmapFactory.Options()
31 |         bounds.inJustDecodeBounds = true
32 |         BitmapFactory.decodeFile(photoFilePath, bounds)
33 |         val opts = BitmapFactory.Options()
34 |         val bm = BitmapFactory.decodeFile(photoFilePath, opts)
35 |         // Read EXIF Data
36 |         var exif: ExifInterface? = null
37 |         try {
38 |             exif = ExifInterface(photoFilePath!!)
39 |         } catch (e: IOException) {
40 |             e.printStackTrace()
41 |         }
42 |         val orientString = exif!!.getAttribute(ExifInterface.TAG_ORIENTATION)
43 |         val orientation = orientString?.toInt() ?: ExifInterface.ORIENTATION_NORMAL
44 |         var rotationAngle = 0F
45 |         if (orientation == ExifInterface.ORIENTATION_ROTATE_90) rotationAngle = 90F
46 |         if (orientation == ExifInterface.ORIENTATION_ROTATE_180) rotationAngle = 180F
47 |         if (orientation == ExifInterface.ORIENTATION_ROTATE_270) rotationAngle = 270F
48 |         // Rotate Bitmap
49 |         val matrix = Matrix()
50 |         matrix.setRotate(rotationAngle, bm.width.toFloat() / 2, bm.height.toFloat() / 2)
51 |         // Return result
52 |         return Bitmap.createBitmap(bm, 0, 0, bounds.outWidth, bounds.outHeight, matrix, true)
53 |     }
54 | 
55 | 
56 | 
57 | 
58 | }
--------------------------------------------------------------------------------
/attachmentmanager/src/main/res/drawable/ic_am_attach_file.xml:
--------------------------------------------------------------------------------
 1 | 
 6 |      
 9 |  
10 | 
--------------------------------------------------------------------------------
/attachmentmanager/src/main/res/drawable/ic_am_camera.xml:
--------------------------------------------------------------------------------
 1 | 
 6 |      
 9 |      
12 |  
13 | 
--------------------------------------------------------------------------------
/attachmentmanager/src/main/res/drawable/ic_am_close.xml:
--------------------------------------------------------------------------------
 1 | 
 6 |      
 9 |  
10 | 
--------------------------------------------------------------------------------
/attachmentmanager/src/main/res/drawable/ic_am_photo.xml:
--------------------------------------------------------------------------------
 1 | 
 6 |      
 9 |  
10 | 
--------------------------------------------------------------------------------
/attachmentmanager/src/main/res/layout/fragment_attachment.xml:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 7 | 
 8 |     
 9 |      
13 | 
14 |  
--------------------------------------------------------------------------------
/attachmentmanager/src/main/res/layout/layout_attachment_dialog.xml:
--------------------------------------------------------------------------------
  1 | 
  2 | 
  9 | 
 10 |     
 19 | 
 20 | 
 21 |         
 29 | 
 30 |             
 34 | 
 35 |                  
 42 | 
 43 |                  
 51 |              
 52 | 
 53 |             
 61 | 
 62 |                 
 69 | 
 70 |                      
 77 | 
 78 |                      
 87 |                  
 88 | 
 89 |                 
 96 | 
 97 |                      
103 | 
104 |                      
113 |                  
114 | 
115 |                 
122 | 
123 |                      
129 | 
130 |                      
139 |                  
140 | 
141 | 
142 |              
143 |          
144 | 
145 |      
146 |  
--------------------------------------------------------------------------------
/attachmentmanager/src/main/res/layout/layout_attachment_sheet.xml:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 9 | 
10 |      
20 | 
21 | 
22 |      
37 | 
38 |      
53 | 
54 |      
69 | 
70 |      
78 | 
79 | 
80 |  
--------------------------------------------------------------------------------
/attachmentmanager/src/main/res/values-ar/strings.xml:
--------------------------------------------------------------------------------
 1 | 
 2 |     AttachmentManager 
 3 |     حدد الملف 
 4 | 
 5 |     
 6 |     Hello blank fragment 
 7 |     تحميل… 
 8 |     اختر 
 9 |     الكاميرا 
10 |     ملف 
11 |     صورة 
12 |     Close Dialog 
13 |     المعرض 
14 |     Cancel 
15 |  
16 | 
--------------------------------------------------------------------------------
/attachmentmanager/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 | 
2 | 
3 |     #FFFFFF 
4 |  
--------------------------------------------------------------------------------
/attachmentmanager/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 | 
2 | 
3 |     5dp 
4 |     5dp 
5 |     10dp 
6 |     10dp 
7 |  
--------------------------------------------------------------------------------
/attachmentmanager/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
 1 | 
 2 |     AttachmentManager 
 3 |     Select File 
 4 | 
 5 |     
 6 |     Hello blank fragment 
 7 |     Loading 
 8 |     Choose 
 9 |     Camera 
10 |     Document 
11 |     Image 
12 |     Close Dialog 
13 |     Gallery 
14 |     Cancel 
15 |  
16 | 
--------------------------------------------------------------------------------
/attachmentmanager/src/main/res/xml/file_provider.xml:
--------------------------------------------------------------------------------
1 | 
2 | 
3 |      
6 |      
9 |  
--------------------------------------------------------------------------------
/attachmentmanager/src/test/java/com/mirza/attachmentmanager/ExampleUnitTest.java:
--------------------------------------------------------------------------------
 1 | package com.mirza.attachmentmanager;
 2 | 
 3 | import org.junit.Test;
 4 | 
 5 | import static org.junit.Assert.*;
 6 | 
 7 | /**
 8 |  * Example local unit test, which will execute on the development machine (host).
 9 |  *
10 |  * @see Testing documentation 
11 |  */
12 | public class ExampleUnitTest {
13 |     @Test
14 |     public void 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 | 
 3 | buildscript {
 4 | 
 5 |     ext.kotlin_version = '1.9.0'
 6 |     repositories {
 7 |         mavenCentral()
 8 |         google()
 9 | 
10 |         
11 |     }
12 |     dependencies {
13 |         classpath 'com.android.tools.build:gradle:8.1.4'
14 |         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
15 |         // NOTE: Do not place your application dependencies here; they belong
16 |         // in the individual module build.gradle files
17 |     }
18 | }
19 | 
20 | allprojects {
21 |     repositories {
22 |         mavenCentral()
23 |         google()
24 | 
25 |         
26 |     }
27 | }
28 | 
29 | tasks.register('clean', Delete) {
30 |     delete rootProject.buildDir
31 | }
32 | 
--------------------------------------------------------------------------------
/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=-Xmx1536m
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 | # Kotlin code style for this project: "official" or "obsolete":
15 | android.useAndroidX=true
16 | android.enableJetifier=true
17 | 
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Zaid-Mirza/AttachmentManager/826dab50cc1c2d95087bfe3e897b01527b0300b0/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | 
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
  1 | #!/bin/sh
  2 | 
  3 | #
  4 | # Copyright © 2015-2021 the original 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 POSIX generated by Gradle.
 22 | #
 23 | #   Important for running:
 24 | #
 25 | #   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
 26 | #       noncompliant, but you have some other compliant shell such as ksh or
 27 | #       bash, then to run this script, type that shell name before the whole
 28 | #       command line, like:
 29 | #
 30 | #           ksh Gradle
 31 | #
 32 | #       Busybox and similar reduced shells will NOT work, because this script
 33 | #       requires all of these POSIX shell features:
 34 | #         * functions;
 35 | #         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
 36 | #           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
 37 | #         * compound commands having a testable exit status, especially «case»;
 38 | #         * various built-in commands including «command», «set», and «ulimit».
 39 | #
 40 | #   Important for patching:
 41 | #
 42 | #   (2) This script targets any POSIX shell, so it avoids extensions provided
 43 | #       by Bash, Ksh, etc; in particular arrays are avoided.
 44 | #
 45 | #       The "traditional" practice of packing multiple parameters into a
 46 | #       space-separated string is a well documented source of bugs and security
 47 | #       problems, so this is (mostly) avoided, by progressively accumulating
 48 | #       options in "$@", and eventually passing that to Java.
 49 | #
 50 | #       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
 51 | #       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
 52 | #       see the in-line comments for details.
 53 | #
 54 | #       There are tweaks for specific operating systems such as AIX, CygWin,
 55 | #       Darwin, MinGW, and NonStop.
 56 | #
 57 | #   (3) This script is generated from the Groovy template
 58 | #       https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
 59 | #       within the Gradle project.
 60 | #
 61 | #       You can find Gradle at https://github.com/gradle/gradle/.
 62 | #
 63 | ##############################################################################
 64 | 
 65 | # Attempt to set APP_HOME
 66 | 
 67 | # Resolve links: $0 may be a link
 68 | app_path=$0
 69 | 
 70 | # Need this for daisy-chained symlinks.
 71 | while
 72 |     APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
 73 |     [ -h "$app_path" ]
 74 | do
 75 |     ls=$( ls -ld "$app_path" )
 76 |     link=${ls#*' -> '}
 77 |     case $link in             #(
 78 |       /*)   app_path=$link ;; #(
 79 |       *)    app_path=$APP_HOME$link ;;
 80 |     esac
 81 | done
 82 | 
 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
 84 | 
 85 | APP_NAME="Gradle"
 86 | APP_BASE_NAME=${0##*/}
 87 | 
 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
 90 | 
 91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
 92 | MAX_FD=maximum
 93 | 
 94 | warn () {
 95 |     echo "$*"
 96 | } >&2
 97 | 
 98 | die () {
 99 |     echo
100 |     echo "$*"
101 |     echo
102 |     exit 1
103 | } >&2
104 | 
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in                #(
111 |   CYGWIN* )         cygwin=true  ;; #(
112 |   Darwin* )         darwin=true  ;; #(
113 |   MSYS* | MINGW* )  msys=true    ;; #(
114 |   NONSTOP* )        nonstop=true ;;
115 | esac
116 | 
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 | 
119 | 
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 |     if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 |         # IBM's JDK on AIX uses strange locations for the executables
124 |         JAVACMD=$JAVA_HOME/jre/sh/java
125 |     else
126 |         JAVACMD=$JAVA_HOME/bin/java
127 |     fi
128 |     if [ ! -x "$JAVACMD" ] ; then
129 |         die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 | 
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 |     fi
134 | else
135 |     JAVACMD=java
136 |     which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 | 
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 | 
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 |     case $MAX_FD in #(
145 |       max*)
146 |         MAX_FD=$( ulimit -H -n ) ||
147 |             warn "Could not query maximum file descriptor limit"
148 |     esac
149 |     case $MAX_FD in  #(
150 |       '' | soft) :;; #(
151 |       *)
152 |         ulimit -n "$MAX_FD" ||
153 |             warn "Could not set maximum file descriptor limit to $MAX_FD"
154 |     esac
155 | fi
156 | 
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | #   * args from the command line
159 | #   * the main class name
160 | #   * -classpath
161 | #   * -D...appname settings
162 | #   * --module-path (only if needed)
163 | #   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 | 
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 |     APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 |     CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 | 
170 |     JAVACMD=$( cygpath --unix "$JAVACMD" )
171 | 
172 |     # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 |     for arg do
174 |         if
175 |             case $arg in                                #(
176 |               -*)   false ;;                            # don't mess with options #(
177 |               /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
178 |                     [ -e "$t" ] ;;                      #(
179 |               *)    false ;;
180 |             esac
181 |         then
182 |             arg=$( cygpath --path --ignore --mixed "$arg" )
183 |         fi
184 |         # Roll the args list around exactly as many times as the number of
185 |         # args, so each arg winds up back in the position where it started, but
186 |         # possibly modified.
187 |         #
188 |         # NB: a `for` loop captures its iteration list before it begins, so
189 |         # changing the positional parameters here affects neither the number of
190 |         # iterations, nor the values presented in `arg`.
191 |         shift                   # remove old arg
192 |         set -- "$@" "$arg"      # push replacement arg
193 |     done
194 | fi
195 | 
196 | # Collect all arguments for the java command;
197 | #   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | #     shell script including quotes and variable substitutions, so put them in
199 | #     double quotes to make sure that they get re-expanded; and
200 | #   * put everything else in single quotes, so that it's not re-expanded.
201 | 
202 | set -- \
203 |         "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 |         -classpath "$CLASSPATH" \
205 |         org.gradle.wrapper.GradleWrapperMain \
206 |         "$@"
207 | 
208 | # Use "xargs" to parse quoted args.
209 | #
210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211 | #
212 | # In Bash we could simply go:
213 | #
214 | #   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215 | #   set -- "${ARGS[@]}" "$@"
216 | #
217 | # but POSIX shell has neither arrays nor command substitution, so instead we
218 | # post-process each arg (as a line of input to sed) to backslash-escape any
219 | # character that might be a shell metacharacter, then use eval to reverse
220 | # that process (while maintaining the separation between arguments), and wrap
221 | # the whole thing up as a single "set" statement.
222 | #
223 | # This will of course break if any of these variables contains a newline or
224 | # an unmatched quote.
225 | #
226 | 
227 | eval "set -- $(
228 |         printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 |         xargs -n1 |
230 |         sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 |         tr '\n' ' '
232 |     )" '"$@"'
233 | 
234 | exec "$JAVACMD" "$@"
235 | 
--------------------------------------------------------------------------------
/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 | 
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':attachmentmanager'
2 | 
--------------------------------------------------------------------------------