├── .gitignore
├── LICENSE.md
├── README.md
├── build.gradle
├── build_instructions.md
├── consumer-rules.pro
├── doc_assets
├── .gitkeep
├── FX_classification.png
├── FileX_attributes.tgn
└── FileX_methods.tgn
├── filex_code_samples_app
├── .gitignore
├── app
│ ├── .gitignore
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ └── balti
│ │ │ └── filex
│ │ │ └── codesamples
│ │ │ └── MainActivity.kt
│ │ └── res
│ │ ├── layout
│ │ └── activity_main.xml
│ │ ├── values-night
│ │ └── themes.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── proguard-rules.pro
└── src
└── main
├── AndroidManifest.xml
├── java
├── balti
│ └── filex
│ │ ├── Copy.kt
│ │ ├── FileX.kt
│ │ ├── FileXInit.kt
│ │ ├── FileXTreeWalk.kt
│ │ ├── Tools.kt
│ │ ├── activity
│ │ ├── ActivityFunctionDelegate.kt
│ │ ├── ActivityInterfaces.kt
│ │ ├── SysFilePickerActivity.kt
│ │ └── TraditionalFileRequest.kt
│ │ ├── exceptions
│ │ ├── DirectoryHierarchyBroken.kt
│ │ ├── ImproperFileXType.kt
│ │ ├── KotlinCopiedExceptions.kt
│ │ └── RootNotInitializedException.kt
│ │ ├── filex11
│ │ ├── FileX11.kt
│ │ ├── FileXServer.kt
│ │ ├── operators
│ │ │ ├── Create.kt
│ │ │ ├── Delete.kt
│ │ │ ├── Filter.kt
│ │ │ ├── Info.kt
│ │ │ ├── Modify.kt
│ │ │ └── Operations.kt
│ │ ├── publicInterfaces
│ │ │ ├── FileXFilter.kt
│ │ │ └── FileXNameFilter.kt
│ │ └── utils
│ │ │ ├── Constants.kt
│ │ │ ├── FileX11DeleteOnExit.kt
│ │ │ ├── RootUri.kt
│ │ │ └── Tools.kt
│ │ └── filexTraditional
│ │ ├── FileXT.kt
│ │ └── operators
│ │ ├── Filter.kt
│ │ └── Modify.kt
└── rough.md
└── res
└── values
└── styles.xml
/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | ### Android template
3 | # Built application files
4 | *.apk
5 | *.ap_
6 | *.aab
7 |
8 | # Files for the ART/Dalvik VM
9 | *.dex
10 |
11 | # Java class files
12 | *.class
13 |
14 | # Generated files
15 | bin/
16 | gen/
17 | release/
18 |
19 | # Gradle files
20 | .gradle/
21 | !.gradle/wrapper/gradle-wrapper.properties
22 | # Local configuration file (sdk path, etc)
23 | local.properties
24 |
25 | # Proguard folder generated by Eclipse
26 | proguard/
27 |
28 | # Log Files
29 | *.log
30 |
31 | # Android Studio Navigation editor temp files
32 | .navigation/
33 |
34 | # Android Studio captures folder
35 | captures/
36 |
37 | # IntelliJ
38 | *.iml
39 | .idea/workspace.xml
40 | .idea/tasks.xml
41 | .idea/gradle.xml
42 | .idea/assetWizardSettings.xml
43 | .idea/dictionaries
44 | .idea/libraries
45 | # Android Studio 3 in .gitignore file.
46 | .idea/caches
47 | .idea/modules.xml
48 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you
49 | .idea/navEditor.xml
50 |
51 | # Keystore files
52 | # Uncomment the following lines if you do not want to check your keystore files in.
53 | #*.jks
54 | #*.keystore
55 |
56 | # External native build folder generated in Android Studio 2.2 and later
57 | .externalNativeBuild
58 |
59 | # Google Services (e.g. APIs or Firebase)
60 | # google-services.json
61 |
62 | # Freeline
63 | freeline.py
64 | freeline/
65 | freeline_project_description.json
66 |
67 | # fastlane
68 | fastlane/report.xml
69 | fastlane/Preview.html
70 | fastlane/screenshots
71 | fastlane/test_output
72 | fastlane/readme.md
73 |
74 | # Version control
75 | vcs.xml
76 |
77 | # lint
78 | lint/intermediates/
79 | lint/generated/
80 | lint/outputs/
81 | lint/tmp/
82 | # lint/reports/
83 |
84 | /.idea/
85 | /gradlew
86 | /gradlew.bat
87 | /gradle/
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
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 2021 SayantanRC
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 |
203 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 | //apply plugin: 'maven'
5 |
6 | android {
7 | compileSdkVersion 33
8 |
9 | defaultConfig {
10 | minSdkVersion 21
11 | targetSdkVersion 32
12 | versionCode 7
13 | versionName "alpha-7"
14 | }
15 | }
16 |
17 | dependencies {
18 | implementation fileTree(dir: "libs", include: ["*.jar"])
19 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
20 | implementation 'androidx.core:core-ktx:1.6.0' // Do not update to 1.7.0 if not targeting API 31
21 | implementation 'androidx.appcompat:appcompat:1.3.1'
22 |
23 | }
24 |
25 | repositories {
26 | google()
27 | }
28 |
29 | buildscript {
30 | ext.kotlin_version = "1.6.10"
31 | repositories {
32 | google()
33 | mavenCentral()
34 | }
35 | dependencies {
36 | classpath 'com.android.tools.build:gradle:7.1.0'
37 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
38 | }
39 | }
40 | allprojects {
41 | repositories {
42 | google()
43 | mavenCentral()
44 | }
45 | }
--------------------------------------------------------------------------------
/build_instructions.md:
--------------------------------------------------------------------------------
1 |
2 | # Build instructions (in Linux terminal):
3 | 1. Make sure you have `git` installed on your system. Type `git --version` to check installed version. If not installed, refer to your distribution's package manager to check how to install it.
4 | 2. Change to your preferred directory. Here we are assuming your home directory on Linux.
5 | ```
6 | cd ~
7 | ```
8 | 3. Clone this repository. Then change directory into the repository.
9 | ```
10 | git clone https://github.com/SayantanRC/FileX.git
11 | cd FileX
12 | ```
13 | 4. You can now directly use this directory in your projects.
14 | - To add it as an external library, open your project in Android Studio.
15 | - Open `settings.gradle` file.
16 | - Add the below lines:
17 | ```
18 | include ':FileX'
19 | project(':FileX').projectDir=new File('/home/[USERNAME]/FileX')
20 | ```
21 | where `[USERNAME]` is your Linux username without square brackets.
22 | - In your app level `build.gradle` file, add the following in dependencies:
23 | ```
24 | implementation project(path: ':FileX')
25 | ```
26 | - Then Gradle sync.
27 | - Advantage of the is that you can again cd to the `FileX` cloned repository (`cd ~/FileX`) and pull new changes/commits anytime you wish (`git pull`) without waiting for releases on jitpack or anywhere.
28 | 5. However, if you with to build AAR, use the following commands. Here we are assuming your `android-studio` directory is under home and you are using the Java runtime provided by it.
29 | ```
30 | cd ~/FileX
31 | export JAVA_HOME="$HOME/android-studio/jre/"
32 | ./gradlew assembleRelease -xtest -xlint
33 | ```
34 | The compiled AAR is located under the FileX directory -> build/outputs/aar/FileX-release.aar
35 |
--------------------------------------------------------------------------------
/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SayantanRC/FileX/985b4b0d1f4b2a4652e459bd0682268f251a98d5/consumer-rules.pro
--------------------------------------------------------------------------------
/doc_assets/.gitkeep:
--------------------------------------------------------------------------------
1 | Tables: https://www.tablesgenerator.com/markdown_tables#
2 | Jitpack badge: https://shields.io/category/version
3 |
--------------------------------------------------------------------------------
/doc_assets/FX_classification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SayantanRC/FileX/985b4b0d1f4b2a4652e459bd0682268f251a98d5/doc_assets/FX_classification.png
--------------------------------------------------------------------------------
/doc_assets/FileX_attributes.tgn:
--------------------------------------------------------------------------------
1 | {"rows_views":[[{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}}],[{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}}],[{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}}],[{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}}],[{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}}],[{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}}],[{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}}],[{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}}],[{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}}],[{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}}],[{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}}],[{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}}],[{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}}],[{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}}],[{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}}],[{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}}],[{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}}],[{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}}],[{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}}],[{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}}],[{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}}],[{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}}],[{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}},{"style":{"borders":"lrtb","font_style":{},"text_color":"","bg_color":"","halign":"left","valign":"top","padding":{"top":10,"right":5,"bottom":10,"left":5},"border_color":""}}]],"model":{"rows":[[{"value":"Attribute name","cspan":1,"rspan":1,"markup":[1,14]},{"value":"Return type\n(`?` - null return possible)","cspan":1,"rspan":1,"markup":[1,13,2,3,1,24]},{"value":"Exclusively for","cspan":1,"rspan":1,"markup":[1,15]},{"value":"Description","cspan":1,"rspan":1,"markup":[1,11]}],[{"value":"uri","cspan":1,"rspan":1,"markup":[1,3]},{"value":"String?","cspan":1,"rspan":1,"markup":[1,7]},{"value":"FileX11\n(`isTraditional`\n=false)","cspan":1,"rspan":1,"markup":[1,9,2,15,1,8]},{"value":"Returns Uri of the document.\nIf used on `FileX11`, returns the tree uri. \nIf used on `FileXT`, returns `Uri.fromFile()`","cspan":1,"rspan":1,"markup":[1,40,2,9,1,37,2,8,1,10,2,16]}],[{"value":"file","cspan":1,"rspan":1,"markup":[1,4]},{"value":"File?","cspan":1,"rspan":1,"markup":[1,5]},{"value":"FileXT\n(`isTraditional`\n=true)","cspan":1,"rspan":1,"markup":[1,8,2,15,1,7]},{"value":"Returns raw Java File.\nMaybe useful for `FileXT`. But usually not of much use for `FileX11` as the returned File object cannot be read from or written to.","cspan":1,"rspan":1,"markup":[1,40,2,8,1,34,2,9,1,63]}],[{"value":"path","cspan":1,"rspan":1,"markup":[1,4]},{"value":"String","cspan":1,"rspan":1,"markup":[1,6]},{"value":"-","cspan":1,"rspan":1,"markup":[1,1]},{"value":"Path of the document. Formatted with leading slash (`/`) and no trailing slash.","cspan":1,"rspan":1,"markup":[1,52,2,3,1,24]}],[{"value":"canonicalPath","cspan":1,"rspan":1,"markup":[1,13]},{"value":"String","cspan":1,"rspan":1,"markup":[1,6]},{"value":"-","cspan":1,"rspan":1,"markup":[1,1]},{"value":"Canonical path of the object.\nFor `FileX11` returns complete path for any physical storage location (including SD cards) only from Android 11+. On lower versions, returns complete path for any location inside the Internal shared storage.","cspan":1,"rspan":1,"markup":[1,34,2,9,1,88,0,3,1,11,0,4,1,95]}],[{"value":"absolutePath","cspan":1,"rspan":1,"markup":[1,12]},{"value":"String","cspan":1,"rspan":1,"markup":[1,6]},{"value":"-","cspan":1,"rspan":1,"markup":[1,1]},{"value":"Absolute path of the object.\nFor `FileX11` it is same as `canonicalPath`","cspan":1,"rspan":1,"markup":[1,33,2,9,1,15,2,15]}],[{"value":"isDirectory","cspan":1,"rspan":1,"markup":[1,11]},{"value":"Boolean","cspan":1,"rspan":1,"markup":[1,7]},{"value":"-","cspan":1,"rspan":1,"markup":[1,1]},{"value":"Returns if the document referred to by the FileX object is directory or not. Returns false if document does not exist already.","cspan":1,"rspan":1,"markup":[1,126]}],[{"value":"isFile","cspan":1,"rspan":1,"markup":[1,6]},{"value":"Boolean","cspan":1,"rspan":1,"markup":[1,7]},{"value":"-","cspan":1,"rspan":1,"markup":[1,1]},{"value":"Returns if the document is a file or not (like text, jpeg etc). Returns false if document does not exist.","cspan":1,"rspan":1,"markup":[1,105]}],[{"value":"name","cspan":1,"rspan":1,"markup":[1,4]},{"value":"String","cspan":1,"rspan":1,"markup":[1,6]},{"value":"-","cspan":1,"rspan":1,"markup":[1,1]},{"value":"Name of the document.","cspan":1,"rspan":1,"markup":[1,21]}],[{"value":"parent","cspan":1,"rspan":1,"markup":[1,6]},{"value":"String?","cspan":1,"rspan":1,"markup":[1,7]},{"value":"-","cspan":1,"rspan":1,"markup":[1,1]},{"value":"Path of the parent directory. This is not `canonicalPath` of the parent. Null if no parent.","cspan":1,"rspan":1,"markup":[1,42,2,15,1,34]}],[{"value":"parentFile","cspan":1,"rspan":1,"markup":[1,10]},{"value":"FileX?","cspan":1,"rspan":1,"markup":[1,6]},{"value":"-","cspan":1,"rspan":1,"markup":[1,1]},{"value":"A FileX object pointing to the parent directory. Null if no parent.","cspan":1,"rspan":1,"markup":[1,67]}],[{"value":"parentCanonical","cspan":1,"rspan":1,"markup":[1,15]},{"value":"String","cspan":1,"rspan":1,"markup":[1,6]},{"value":"-","cspan":1,"rspan":1,"markup":[1,1]},{"value":"`canonicalPath` of the parent directory.","cspan":1,"rspan":1,"markup":[2,15,1,25]}],[{"value":"freeSpace","cspan":1,"rspan":1,"markup":[1,9]},{"value":"Long","cspan":1,"rspan":1,"markup":[1,4]},{"value":"-","cspan":1,"rspan":1,"markup":[1,1]},{"value":"Number of bytes of free space available in the storage location.","cspan":1,"rspan":1,"markup":[1,64]}],[{"value":"usableSpace","cspan":1,"rspan":1,"markup":[1,11]},{"value":"Long","cspan":1,"rspan":1,"markup":[1,4]},{"value":"-","cspan":1,"rspan":1,"markup":[1,1]},{"value":"Number of bytes of usable space to write data. This usually takes care of permissions and other restrictions and more accurate than `freeSpace`","cspan":1,"rspan":1,"markup":[1,132,2,11]}],[{"value":"totalSpace","cspan":1,"rspan":1,"markup":[1,10]},{"value":"Long","cspan":1,"rspan":1,"markup":[1,4]},{"value":"-","cspan":1,"rspan":1,"markup":[1,1]},{"value":"Number of bytes representing total storage of the medium.","cspan":1,"rspan":1,"markup":[1,57]}],[{"value":"isHidden","cspan":1,"rspan":1,"markup":[1,8]},{"value":"Boolean","cspan":1,"rspan":1,"markup":[1,7]},{"value":"-","cspan":1,"rspan":1,"markup":[1,1]},{"value":"Checks if the document is hidden.\nFor `FileX11` checks if the name begins with a `.`","cspan":1,"rspan":1,"markup":[1,38,2,9,1,34,2,3]}],[{"value":"extension","cspan":1,"rspan":1,"markup":[1,9]},{"value":"String","cspan":1,"rspan":1,"markup":[1,6]},{"value":"-","cspan":1,"rspan":1,"markup":[1,1]},{"value":"Extension of the document","cspan":1,"rspan":1,"markup":[1,25]}],[{"value":"nameWithoutExtension","cspan":1,"rspan":1,"markup":[1,20]},{"value":"String","cspan":1,"rspan":1,"markup":[1,6]},{"value":"-","cspan":1,"rspan":1,"markup":[1,1]},{"value":"The name of the document without the extension part.","cspan":1,"rspan":1,"markup":[1,52]}],[{"value":"storagePath","cspan":1,"rspan":1,"markup":[1,11]},{"value":"String?","cspan":1,"rspan":1,"markup":[1,7]},{"value":"FileX11\n(`isTraditional`\n=false)","cspan":1,"rspan":1,"markup":[1,9,2,15,1,8]},{"value":"Returns the path of the document from the root of the storage.\nReturns null for `FileXT`\n\nExample 1: A document with user selected root = `[Internal storage]/dir1/dir2` and having a path `my/test_doc.txt`.\nstoragePath = `/dir1/dir2/my/test_doc.txt`\n\nExample 2: A document with user selected root = `[SD card]/all_documents` and having a path `/thesis/doc.pdf`.\nstoragePath = `/all_documents/thesis/doc.pdf`","cspan":1,"rspan":1,"markup":[1,63,0,3,1,17,2,8,0,6,1,48,2,30,1,19,2,17,1,16,2,28,0,2,1,48,2,25,1,19,2,17,1,16,2,31]}],[{"value":"volumePath","cspan":1,"rspan":1,"markup":[1,10]},{"value":"String?","cspan":1,"rspan":1,"markup":[1,7]},{"value":"FileX11\n(`isTraditional`\n=false)","cspan":1,"rspan":1,"markup":[1,9,2,15,1,8]},{"value":"Returns the canonical path of the storage medium. Useful to find the mount point of SD cards and USB-OTG drives. This path, in most cases, is not readable or writable unless the user picks selects it from the system file picker.\nReturns null for `FileXT`\n\nExample 1: A document with user selected root = `[Internal storage]/dir1/dir2` and having a path `my/test_doc.txt`.\nvolumePath = `/storage/emulated/0`\n\nExample 2: A document with user selected root = `[SD card]/all_documents` and having a path `/thesis/doc.pdf`.\nvolumePath = `/storage/B840-4A40`\n(the location name is based on the UUID of the storage medium)","cspan":1,"rspan":1,"markup":[1,229,0,3,1,17,2,8,0,6,1,48,2,30,1,19,2,17,1,15,2,21,0,2,1,48,2,25,1,19,2,17,1,15,2,20,1,63]}],[{"value":"rootPath","cspan":1,"rspan":1,"markup":[1,8]},{"value":"String?","cspan":1,"rspan":1,"markup":[1,7]},{"value":"FileX11\n(`isTraditional`\n=false)","cspan":1,"rspan":1,"markup":[1,9,2,15,1,8]},{"value":"Returns the canonical path upto the root selected by the user from the system file picker.\nReturns null for `FileXT`\n\nExample 1: In the above cases of the first example, rootPath = `/storage/emulated/0/dir1/dir2`\nExample 2: In the above cases of the second example, rootPath = `/storage/B840-4A40/all_documents`","cspan":1,"rspan":1,"markup":[1,91,0,3,1,17,2,8,0,6,1,63,2,31,1,65,2,34]}],[{"value":"parentUri","cspan":1,"rspan":1,"markup":[1,9]},{"value":"Uri?","cspan":1,"rspan":1,"markup":[1,4]},{"value":"FileX11\n(`isTraditional`\n=false)","cspan":1,"rspan":1,"markup":[1,9,2,15,1,8]},{"value":"Returns the tree uri of the parent directory if present, else null.\nReturns null for `FileXT`","cspan":1,"rspan":1,"markup":[1,68,0,3,1,17,2,8,0,4]}],[{"value":"isEmpty","cspan":1,"rspan":1,"markup":[1,7]},{"value":"Boolean","cspan":1,"rspan":1,"markup":[1,7]},{"value":"-","cspan":1,"rspan":1,"markup":[1,1]},{"value":"Applicable on directories. Returns true if the directory is empty.","cspan":1,"rspan":1,"markup":[1,66]}]]},"theme":null,"fixed_layout":false,"markup":{"instances":[{},{"style":{}},{"style":{"color":"#905","backgroundColor":"#ddd"}},null,null]},"options":{}}
--------------------------------------------------------------------------------
/filex_code_samples_app/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### Android template
3 | # Built application files
4 | *.apk
5 | *.aar
6 | *.ap_
7 | *.aab
8 |
9 | # Files for the ART/Dalvik VM
10 | *.dex
11 |
12 | # Java class files
13 | *.class
14 |
15 | # Generated files
16 | bin/
17 | gen/
18 | out/
19 | # Uncomment the following line in case you need and you don't have the release build type files in your app
20 | # release/
21 |
22 | # Gradle files
23 | .gradle/
24 | build/
25 |
26 | # Local configuration file (sdk path, etc)
27 | local.properties
28 |
29 | # Proguard folder generated by Eclipse
30 | proguard/
31 |
32 | # Log Files
33 | *.log
34 |
35 | # Android Studio Navigation editor temp files
36 | .navigation/
37 |
38 | # Android Studio captures folder
39 | captures/
40 |
41 | # IntelliJ
42 | *.iml
43 | .idea/workspace.xml
44 | .idea/tasks.xml
45 | .idea/gradle.xml
46 | .idea/assetWizardSettings.xml
47 | .idea/dictionaries
48 | .idea/libraries
49 | # Android Studio 3 in .gitignore file.
50 | .idea/caches
51 | .idea/modules.xml
52 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you
53 | .idea/navEditor.xml
54 |
55 | # Keystore files
56 | # Uncomment the following lines if you do not want to check your keystore files in.
57 | #*.jks
58 | #*.keystore
59 |
60 | # External native build folder generated in Android Studio 2.2 and later
61 | .externalNativeBuild
62 | .cxx/
63 |
64 | # Google Services (e.g. APIs or Firebase)
65 | # google-services.json
66 |
67 | # Freeline
68 | freeline.py
69 | freeline/
70 | freeline_project_description.json
71 |
72 | # fastlane
73 | fastlane/report.xml
74 | fastlane/Preview.html
75 | fastlane/screenshots
76 | fastlane/test_output
77 | fastlane/readme.md
78 |
79 | # Version control
80 | vcs.xml
81 |
82 | # lint
83 | lint/intermediates/
84 | lint/generated/
85 | lint/outputs/
86 | lint/tmp/
87 | # lint/reports/
88 |
89 |
--------------------------------------------------------------------------------
/filex_code_samples_app/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/filex_code_samples_app/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'kotlin-android'
4 | id 'kotlin-android-extensions'
5 | }
6 |
7 | android {
8 | compileSdkVersion 30
9 | buildToolsVersion "30.0.3"
10 |
11 | defaultConfig {
12 | applicationId "balti.filex.codesamples"
13 | minSdkVersion 21
14 | targetSdkVersion 30
15 | versionCode 1
16 | versionName "1.0"
17 |
18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
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_1_8
29 | targetCompatibility JavaVersion.VERSION_1_8
30 | }
31 | kotlinOptions {
32 | jvmTarget = '1.8'
33 | }
34 | }
35 |
36 | dependencies {
37 |
38 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
39 | implementation 'androidx.core:core-ktx:1.6.0' // Do not update to 1.7.0 if not targeting API 31
40 | implementation 'androidx.appcompat:appcompat:1.3.1'
41 | implementation 'com.google.android.material:material:1.4.0'
42 |
43 | // include the FileX library
44 | implementation project(path: ':FileX')
45 | }
--------------------------------------------------------------------------------
/filex_code_samples_app/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
--------------------------------------------------------------------------------
/filex_code_samples_app/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/filex_code_samples_app/app/src/main/java/balti/filex/codesamples/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package balti.filex.codesamples
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.Activity
5 | import androidx.appcompat.app.AppCompatActivity
6 | import android.os.Bundle
7 | import balti.filex.FileX
8 | import balti.filex.FileXInit
9 | import kotlinx.android.synthetic.main.activity_main.*
10 |
11 | class MainActivity : AppCompatActivity() {
12 |
13 | @SuppressLint("SetTextI18n")
14 | override fun onCreate(savedInstanceState: Bundle?) {
15 | super.onCreate(savedInstanceState)
16 | setContentView(R.layout.activity_main)
17 |
18 | // Initialize FileX
19 | FileXInit(this, false)
20 |
21 | request_permission.setOnClickListener {
22 |
23 | // Get root location if permission granted.
24 | fun getRootLocation(): String {
25 | val root = FileX.new("/")
26 | return root.canonicalPath
27 | }
28 |
29 | // Check FileX permission
30 | if (!FileXInit.isUserPermissionGranted()) {
31 |
32 | // Not granted. Request for access.
33 | FileXInit.requestUserPermission { resultCode, data ->
34 | // for permission granted
35 | if (resultCode == Activity.RESULT_OK) {
36 | request_result.text = "${getString(R.string.granted)}.\n"
37 | request_result.append("Root location: ${getRootLocation()}")
38 | }
39 | else {
40 | // On system file picker, if the user keeps pressing back and exits out of it, then this block works.
41 | request_result.text = "${getString(R.string.access_denied)}\n"
42 | }
43 | }
44 | }
45 | else {
46 |
47 | // Already granted
48 | request_result.text = "${getString(R.string.already_granted)}\n"
49 | request_result.append("Root location: ${getRootLocation()}")
50 | }
51 | }
52 | }
53 | }
--------------------------------------------------------------------------------
/filex_code_samples_app/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
17 |
18 |
23 |
24 |
29 |
30 |
--------------------------------------------------------------------------------
/filex_code_samples_app/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/filex_code_samples_app/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/filex_code_samples_app/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | FileX code samples
3 |
4 |
5 | Request Permission
6 | Request result
7 |
8 |
9 | Granted.
10 | Access denied.
11 | Already granted.
12 |
--------------------------------------------------------------------------------
/filex_code_samples_app/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/filex_code_samples_app/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | ext.kotlin_version = "1.4.32"
4 | repositories {
5 | google()
6 | jcenter()
7 | }
8 | dependencies {
9 | classpath "com.android.tools.build:gradle:4.1.3"
10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 |
12 | // NOTE: Do not place your application dependencies here; they belong
13 | // in the individual module build.gradle files
14 | }
15 | }
16 |
17 | allprojects {
18 | repositories {
19 | google()
20 | jcenter()
21 | }
22 | }
23 |
24 | task clean(type: Delete) {
25 | delete rootProject.buildDir
26 | }
--------------------------------------------------------------------------------
/filex_code_samples_app/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/filex_code_samples_app/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SayantanRC/FileX/985b4b0d1f4b2a4652e459bd0682268f251a98d5/filex_code_samples_app/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/filex_code_samples_app/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Apr 23 14:10:22 IST 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
7 |
--------------------------------------------------------------------------------
/filex_code_samples_app/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/filex_code_samples_app/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/filex_code_samples_app/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 | rootProject.name = "FileX code samples"
3 |
4 | include ':FileX'
5 | project(':FileX').projectDir=new File('../../FileX')
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | android.useAndroidX=true
2 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SayantanRC/FileX/985b4b0d1f4b2a4652e459bd0682268f251a98d5/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Jul 31 19:29:29 IST 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/src/main/java/balti/filex/Copy.kt:
--------------------------------------------------------------------------------
1 | package balti.filex
2 |
3 | import balti.filex.exceptions.*
4 | import java.io.IOException
5 |
6 | internal class Copy(private val f: FileX) {
7 |
8 | /**
9 | * Logic completely copied from [kotlin.io.copyTo]
10 | *
11 | * This function is used to copy a single file / document.
12 | * If run on a directory, then only a blank directory is created at the [target].
13 | *
14 | * @param target Location where the current file / document is to be copied.
15 | * @param overwrite If `true`, if a file exists at the [target] location,
16 | * then that file / document is deleted and the current file is copied in the target location.
17 | * Default value is `false`.
18 | * @param bufferSize Int specifying the buffer size to be used while copying. Default value is [DEFAULT_BUFFER_SIZE].
19 | *
20 | * @return the [target] file.
21 | *
22 | * @throws NoSuchFileXException If the source file doesn't exist.
23 | * @throws FileXAlreadyExistsException If the destination file already exists and [overwrite] argument is set to `false`.
24 | * @throws FileXSystemException If the source is a directory, this function only creates and empty directory at [target].
25 | * This exception is thrown if creating the empty directory fails.
26 | * @throws NullPointerException If the input stream (from source) or output stream (to target) is null.
27 | * @throws IOException If any errors occur while copying.
28 | */
29 | fun copyTo(target: FileX, overwrite: Boolean = false, bufferSize: Int = DEFAULT_BUFFER_SIZE): FileX = f.run {
30 |
31 | refreshFile()
32 | target.refreshFile()
33 |
34 | if (!exists()) {
35 | throw NoSuchFileXException(file = this, reason = "The source file doesn't exist.")
36 | }
37 |
38 | if (target.exists()){
39 | if (!overwrite)
40 | throw FileXAlreadyExistsException(file = this, other = target, "The destination file already exists.")
41 | else if (!target.delete())
42 | throw FileXAlreadyExistsException(file = this, other = target, "Tried to overwrite the destination, but failed to delete it.")
43 | }
44 |
45 | if (isDirectory) {
46 | if (!target.mkdirs())
47 | throw FileXSystemException(this, target, "Failed to create target directory.")
48 | } else {
49 |
50 | target.createNewFile(makeDirectories = true)
51 |
52 | val inputStream = f.inputStream()
53 | val outputStream = target.outputStream()
54 |
55 | if (inputStream == null) throw NullPointerException("Input stream is null")
56 | if (outputStream == null) throw NullPointerException("Output stream is null")
57 |
58 | inputStream.use { input ->
59 | outputStream.use { output ->
60 | input.copyTo(output, bufferSize)
61 | }
62 | }
63 | }
64 |
65 | return target
66 | }
67 |
68 | /**
69 | * Logic completely copied from [kotlin.io.copyRecursively]
70 | *
71 | * This function is used to recursively copy a directory with files and subdirectories inside.
72 | *
73 | * @param target Location where the current directory is to be copied.
74 | * @param overwrite If `true`, then if conflicting files and directories exists inside [target] location,
75 | * then they are deleted and the source file / subdirectories are copied.
76 | * Default value is `false`.
77 | * @param onError If any errors occur during the copying, then further actions will depend on the result of the call
78 | * to `onError(File, IOException)` function, that will be called with arguments,
79 | * specifying the file that caused the error and the exception itself.
80 | * By default this function rethrows exceptions.
81 | * This function is also expected to return one of [OnErrorAction].
82 | * @param deleteAfterCopy This is an internal flag and not available outside the library.
83 | * If set to `true`, the source file is deleted after copying to target location. This acts as moving the file.
84 | *
85 | * Exceptions that can be passed to the `onError` function:
86 | *
87 | * - [NoSuchFileXException] - if there was an attempt to copy a non-existent file
88 | * - [FileXAlreadyExistsException] - if there is a conflict
89 | * - [AccessDeniedException] - if there was an attempt to open a directory that didn't succeed.
90 | * - [IOException] - if some problems occur when copying.
91 | *
92 | * @throws FileXTerminateException if [onError] returns [OnErrorAction.TERMINATE]
93 | * @return `false` if the copying was terminated, `true` otherwise.
94 | */
95 | fun copyRecursively(
96 | target: FileX,
97 | overwrite: Boolean = false,
98 | onError: (FileX, Exception) -> OnErrorAction = { _, exception -> throw exception },
99 | deleteAfterCopy: Boolean = false // internal flag used to move files.
100 | ): Boolean = f.run {
101 |
102 | this.refreshFile()
103 | target.refreshFile()
104 |
105 | if (!exists()) {
106 | return onError(this, NoSuchFileXException(file = this, reason = "The source file doesn't exist.")) !=
107 | OnErrorAction.TERMINATE
108 | }
109 | try {
110 | // We cannot break for loop from inside a lambda, so we have to use an exception here
111 | for (src in walkTopDown().onFail { f, e -> if (onError(f, e) == OnErrorAction.TERMINATE) throw FileXTerminateException(f) }) {
112 | if (!src.exists()) {
113 | if (onError(src, NoSuchFileXException(file = src, reason = "The source file doesn't exist.")) ==
114 | OnErrorAction.TERMINATE)
115 | return false
116 | } else {
117 |
118 | // own logic: different from kotlin.io.copyRecursively()
119 | val relPath = src.canonicalPath.substring(this.canonicalPath.length).let {
120 | // remove leading '/'
121 | if (it.startsWith('/')) it.substring(1)
122 | else it
123 | }
124 | val targetPath = target.path.let {
125 | // add trailing '/'
126 | if (it.endsWith('/')) it else "$it/"
127 | }
128 | val dstFile = FileX.new("${targetPath}$relPath")
129 | // *****
130 |
131 | if (dstFile.exists() && !(src.isDirectory && dstFile.isDirectory)) {
132 | val stillExists = if (!overwrite) true else {
133 | if (dstFile.isDirectory)
134 | !dstFile.deleteRecursively()
135 | else
136 | !dstFile.delete()
137 | }
138 |
139 | if (stillExists) {
140 | if (onError(dstFile, FileXAlreadyExistsException(file = src,
141 | other = dstFile,
142 | reason = "The destination file already exists.")) == OnErrorAction.TERMINATE)
143 | return false
144 |
145 | continue
146 | }
147 | }
148 |
149 | if (src.isDirectory) {
150 | dstFile.mkdirs()
151 | } else {
152 | if (src.copyTo(dstFile, overwrite).length() != src.length()) {
153 | if (onError(src, IOException("Source file wasn't copied completely, length of destination file differs.")) == OnErrorAction.TERMINATE)
154 | return false
155 | }
156 |
157 | // deleteAfterCopy is an internal flag used to move files.
158 | else if (deleteAfterCopy) src.delete()
159 | }
160 | }
161 | }
162 | return true
163 | } catch (e: FileXSystemException) {
164 | return false
165 | }
166 | }
167 |
168 | }
--------------------------------------------------------------------------------
/src/main/java/balti/filex/FileXInit.kt:
--------------------------------------------------------------------------------
1 | package balti.filex
2 |
3 | import android.Manifest
4 | import android.annotation.SuppressLint
5 | import android.app.Activity
6 | import android.content.Context
7 | import android.content.Context.MODE_PRIVATE
8 | import android.content.Intent
9 | import android.content.pm.PackageManager
10 | import android.os.Build
11 | import android.widget.Toast
12 | import androidx.core.content.ContextCompat
13 | import balti.filex.activity.ActivityFunctionDelegate
14 | import balti.filex.activity.TraditionalFileRequest
15 | import balti.filex.filex11.utils.RootUri
16 | import balti.filex.filex11.utils.Tools
17 | import balti.filex.filexTraditional.FileXT
18 | import balti.filex.filex11.FileX11
19 |
20 | /**
21 | * This class is to be initialised before performing ANY [FileX] related operations.
22 | * It can be done at the beginning of MainActivity of the app.
23 | *
24 | * @param context Context of the activity / service / anywhere else from where it is being called from.
25 | * In any case, in `init` function, [getApplicationContext()][android.content.Context.getApplicationContext] is called on the supplied context.
26 | * @param isTraditional This is a global boolean flag for all [FileX] objects.
27 | * If `true`, by default, all new [FileX] objects are of traditional type (i.e [FileXT], which is a wrapper around [java.io.File] class),
28 | * else if `false` then Storage Access Framework (SAF) way is used (i.e. [FileX11], which uses
29 | * [DocumentContract][android.provider.DocumentsContract] and content uris to perform operations).
30 | * - This value can later be changed by the function [setTraditional].
31 | */
32 | class FileXInit(context: Context, isTraditional: Boolean) {
33 |
34 | /**
35 | * Variables inside companion object is available throughout the library.
36 | */
37 | companion object{
38 |
39 | /**
40 | * The context object shared everywhere inside this library.
41 | * Initialised inside the `init` function.
42 | */
43 | @SuppressLint("StaticFieldLeak")
44 | internal lateinit var fContext: Context
45 | private set
46 |
47 | /**
48 | * The global boolean `isTraditional` flag to denote new [FileX] objects will be traditional type ([FileXT])
49 | * or Storage Access Framework type ([FileX11]).
50 | *
51 | * In [FileX.new], if no value is passed for the argument `isTraditional`, then it defaults to the value set by this parameter.
52 | */
53 | internal var globalIsTraditional: Boolean = false
54 |
55 | /**
56 | * An instance of [ContentResolver][android.content.ContentResolver] to be used throughout the library.
57 | */
58 | internal val fCResolver by lazy { fContext.contentResolver }
59 |
60 | /**
61 | * A tag used in [Log.d][android.util.Log.d] for debugging purposes.
62 | */
63 | internal val DEBUG_TAG = "FILEX_TAG"
64 |
65 | /**
66 | * File name for shared preference of the library. Used in [sharedPreferences].
67 | */
68 | internal val PREF_NAME = "filex"
69 |
70 | /**
71 | * A function to execute a block of code and ignore the errors (if any) that occurs in the block.
72 | * Also displays a [Toast][android.widget.Toast] message if an error occurs, if [showErrorToast] is set to true.
73 | *
74 | * @param f A function block to be executed.
75 | */
76 | internal fun tryIt(f: () -> Unit){
77 | try { f() } catch (e: Exception){
78 | if (showErrorToast) {
79 | try {
80 | Toast.makeText(fContext, e.message.toString(), Toast.LENGTH_SHORT).show()
81 | } catch (_: Exception) { }
82 | }
83 | }
84 | }
85 |
86 | /**
87 | * Set to false if no [Toast][android.widget.Toast] message is to be shown in [tryIt] block, if any error occurs.
88 | * Can be set as:
89 | * > `FileXInit.showErrorToast = false`
90 | */
91 | var showErrorToast: Boolean = true
92 |
93 | /**
94 | * Returns the global value of `isTraditional` flag. Cannot be changed.
95 | * To change please use [setTraditional] method.
96 | */
97 | val isTraditional: Boolean
98 | get() = globalIsTraditional
99 |
100 | /**
101 | * Function to set value of global `isTraditional` flag.
102 | * @param isTraditional If `true`, all future FileX objects will be by default traditional type [FileXT], else SAF type [FileX11].
103 | */
104 | fun setTraditional(isTraditional: Boolean){
105 | globalIsTraditional = isTraditional
106 | }
107 |
108 | /**
109 | * A [SharedPreference][android.content.SharedPreferences] instance used to store information
110 | * like the global root uri (selected by the user from system file picker). Used when [isTraditional] = `false`,
111 | * i.e. SAF way is being used ([FileX11]).
112 | */
113 | internal val sharedPreferences by lazy { fContext.getSharedPreferences(PREF_NAME, MODE_PRIVATE) }
114 |
115 | /**
116 | * This value denotes if a new [FileX] object will be "refreshed" on creation (i.e. the [FileX.refreshFile] will be called.)
117 | * This only works on [FileX11] (SAF way).
118 | * This can be changed by:
119 | * > `FileXInit.refreshFileOnCreation = false`
120 | */
121 | var refreshFileOnCreation: Boolean = true
122 |
123 | /**
124 | * Map of all storage volumes and their actual canonical paths in the file system.
125 | * This includes SD cards and USB-OTG drives.
126 | *
127 | * This is only useful for Android M (6.0) and above. Please see [Tools.getStorageVolumes].
128 | * For Android L (5.x), this variable is not that helpful. Please see [Tools.deduceVolumePathForLollipop].
129 | */
130 | val storageVolumes = HashMap(0)
131 |
132 | /**
133 | * Checks if permission to read-write to storage is available.
134 | * - For traditional type ([isTraditional] = `true`), checks if the permissions
135 | * [Manifest.permission.READ_EXTERNAL_STORAGE] and [Manifest.permission.WRITE_EXTERNAL_STORAGE] are granted.
136 | * - For SAF type ([isTraditional] = `false`), checks if a global root uri is previously set (using [sharedPreferences]),
137 | * i.e if the user has previously selected a root directory. Also checks if that directory exists and is not deleted.
138 | */
139 | fun isUserPermissionGranted(): Boolean{
140 | return if (!globalIsTraditional) RootUri.getGlobalRootUri().let {
141 | it != null && Tools.checkUriExists(it, true)
142 | // The last condition checks of the root exists.
143 | // Say, the user selects a root directory as [USB-OTG]/PDFs,
144 | // but in a later point, the "PDFs" directory is deleted, or the USB-OTG drive is ejected.
145 | // In this case this method will return `false`.
146 | }
147 | else {
148 | ContextCompat.checkSelfPermission(fContext, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED &&
149 | ContextCompat.checkSelfPermission(fContext, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
150 | }
151 | }
152 |
153 | /**
154 | * Request for "permission" to read-write to a location.
155 | * - Please note: This method uses a transparent auxiliary activities to call appropriate android APIs,
156 | * which can be only called from an [Activity]. As such in your current activity's `onPause()` method may be called.
157 | * - For traditional way ([isTraditional] = `true`): Uses [requestTraditionalPermission] function.
158 | * - For SAF way ([isTraditional] = `false`): Uses the [RootUri.resetGlobalRootUri] to initiate the system file picker UI
159 | * for the user to select a root location.
160 | *
161 | * @param reRequest Useful only for SAF way. If `true` then even if a global root is previously set,
162 | * again the system file picker will be started to select a new root location; signifying "requesting the permission again".
163 | * @param onResult Optional callback function called once permission is granted or denied.
164 | * This function will always be called even if permission is denied.
165 | * - `resultCode`: If success, it is `Activity.RESULT_OK` else usually `Activity.RESULT_CANCELED`.
166 | * - `data`: Intent with some information.
167 | * - For FileXT
168 | * `data.getStringArrayExtra("permissions")` = Array is requested permissions. Equal to array consisting `Manifest.permission.READ_EXTERNAL_STORAGE`, `Manifest.permission.WRITE_EXTERNAL_STORAGE`
169 | * `data.getStringArrayExtra("grantResults")` = Array is granted permissions. If granted, should be equal to array of `PackageManager.PERMISSION_GRANTED`, `PackageManager.PERMISSION_GRANTED`
170 | * - For FileX11
171 | * `data.data` = Uri of the selected root directory.
172 | */
173 | fun requestUserPermission(reRequest: Boolean = false, onResult: ((resultCode: Int, data: Intent?) -> Unit)? = null) {
174 | if (!globalIsTraditional) {
175 | val globalRoot = RootUri.getGlobalRootUri()
176 | if (globalRoot == null || !Tools.checkUriExists(globalRoot, true) || reRequest) {
177 | RootUri.resetGlobalRootUri() { resultCode, data ->
178 | onResult?.invoke(resultCode, data)
179 | }
180 | }
181 | else onResult?.invoke(Activity.RESULT_OK, null)
182 | }
183 | else {
184 | requestTraditionalPermission(onResult)
185 | }
186 | }
187 |
188 | /**
189 | * Request for traditional File read-write access. Note that from Android 11+,
190 | * this only grants READ access i.e. [Manifest.permission.READ_EXTERNAL_STORAGE].
191 | * [Manifest.permission.WRITE_EXTERNAL_STORAGE] is only granted in Android 10 and below.
192 | *
193 | * This function need not be explicitly called. The [requestUserPermission] method automatically
194 | * takes care of the appropriate method to call based on the value of [isTraditional].
195 | */
196 | fun requestTraditionalPermission(onResult: ((resultCode: Int, data: Intent?) -> Unit)? = null){
197 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
198 | ActivityFunctionDelegate({}, { _, resultCode, data ->
199 | onResult?.invoke(resultCode, data)
200 | }, TraditionalFileRequest::class.java)
201 | }
202 | else {
203 | onResult?.invoke(Activity.RESULT_OK, null)
204 | }
205 | }
206 |
207 | /**
208 | * Similar to [requestUserPermission], without the `reRequest` argument.
209 | * The value of `reRequest` = `false`.
210 | *
211 | * @param onResult Optional callback function called once permission is granted or denied.
212 | * This function will always be called even if permission is denied.
213 | * - `resultCode`: If success, it is `Activity.RESULT_OK` else usually `Activity.RESULT_CANCELED`.
214 | * - `data`: Intent with some information.
215 | * - For FileXT
216 | * `data.getStringArrayExtra("permissions")` = Array is requested permissions. Equal to array consisting `Manifest.permission.READ_EXTERNAL_STORAGE`, `Manifest.permission.WRITE_EXTERNAL_STORAGE`
217 | * `data.getStringArrayExtra("grantResults")` = Array is granted permissions. If granted, should be equal to array of `PackageManager.PERMISSION_GRANTED`, `PackageManager.PERMISSION_GRANTED`
218 | * - For FileX11
219 | * `data.data` = Uri of the selected root directory.
220 | */
221 | fun requestUserPermission(onResult: ((resultCode: Int, data: Intent?) -> Unit)? = null) =
222 | Companion.requestUserPermission(false, onResult)
223 |
224 | /**
225 | * Reads all paths to all the available storage volumes (including SD cards and USB-OTG).
226 | * Please see [storageVolumes] and [Tools.getStorageVolumes].
227 | */
228 | fun refreshStorageVolumes() {
229 | storageVolumes.clear()
230 | Tools.getStorageVolumes().run {
231 | this.keys.forEach {
232 | storageVolumes[it] = this[it]
233 | }
234 | }
235 | }
236 | }
237 |
238 | /**
239 | * Init method.
240 | * Used to initialise the context and global `isTraditional` flag.
241 | * Also reads all the available storage volumes.
242 | */
243 | init {
244 | fContext = context.applicationContext
245 | globalIsTraditional = isTraditional
246 | refreshStorageVolumes()
247 | }
248 | }
--------------------------------------------------------------------------------
/src/main/java/balti/filex/FileXTreeWalk.kt:
--------------------------------------------------------------------------------
1 | package balti.filex
2 |
3 | import balti.filex.exceptions.FileXAccessDeniedException
4 | import java.io.IOException
5 | import java.util.*
6 |
7 | /**
8 | * This is copied from [kotlin._Assertions].
9 | */
10 | @PublishedApi
11 | internal object _Assertions {
12 | @JvmField
13 | @PublishedApi
14 | internal val ENABLED: Boolean = javaClass.desiredAssertionStatus()
15 | }
16 |
17 | /**
18 | * This function is copied from [kotlin.io.FileTreeWalk].
19 | * > Please note that the Assertions used are from [_Assertions] and not [kotlin._Assertions].
20 | * > However the walk directions are same from [kotlin.io.FileWalkDirection].
21 | *
22 | * This class is intended to implement different file traversal methods.
23 | * It allows to iterate through all files inside a given directory.
24 | *
25 | * Use [FileX.walk], [FileX.walkTopDown] or [FileX.walkBottomUp] extension functions to instantiate a `FileTreeWalk` instance.
26 |
27 | * If the file path given is just a file, walker iterates only it.
28 | * If the file path given does not exist, walker iterates nothing, i.e. it's equivalent to an empty sequence.
29 | */
30 | public class FileXTreeWalk private constructor(
31 | private val start: FileX,
32 | private val direction: FileWalkDirection = FileWalkDirection.TOP_DOWN,
33 | private val onEnter: ((FileX) -> Boolean)?,
34 | private val onLeave: ((FileX) -> Unit)?,
35 | private val onFail: ((f: FileX, e: IOException) -> Unit)?,
36 | private val maxDepth: Int = Int.MAX_VALUE
37 | ) : Sequence {
38 |
39 | internal constructor(start: FileX, direction: FileWalkDirection = FileWalkDirection.TOP_DOWN) : this(start, direction, null, null, null)
40 |
41 |
42 | /** Returns an iterator walking through files. */
43 | override fun iterator(): Iterator = FileTreeWalkIterator()
44 |
45 | /** Abstract class that encapsulates file visiting in some order, beginning from a given [root] */
46 | private abstract class WalkState(val root: FileX) {
47 | /** Call of this function proceeds to a next file for visiting and returns it */
48 | public abstract fun step(): FileX?
49 | }
50 |
51 | /** Abstract class that encapsulates directory visiting in some order, beginning from a given [rootDir] */
52 | private abstract class DirectoryState(rootDir: FileX) : WalkState(rootDir) {
53 | init {
54 | if (_Assertions.ENABLED)
55 | if (BuildConfig.DEBUG && !rootDir.isDirectory) {
56 | error("rootDir must be verified to be directory beforehand.")
57 | }
58 | }
59 | }
60 |
61 | private inner class FileTreeWalkIterator : AbstractIterator() {
62 |
63 | // Stack of directory states, beginning from the start directory
64 | private val state = ArrayDeque()
65 |
66 | init {
67 | when {
68 | start.isDirectory -> state.push(directoryState(start))
69 | start.isFile -> state.push(SingleFileState(start))
70 | else -> done()
71 | }
72 | }
73 |
74 | override fun computeNext() {
75 | val nextFile = gotoNext()
76 | if (nextFile != null)
77 | setNext(nextFile)
78 | else
79 | done()
80 | }
81 |
82 |
83 | private fun directoryState(root: FileX): DirectoryState {
84 | return when (direction) {
85 | FileWalkDirection.TOP_DOWN -> TopDownDirectoryState(root)
86 | FileWalkDirection.BOTTOM_UP -> BottomUpDirectoryState(root)
87 | }
88 | }
89 |
90 | private tailrec fun gotoNext(): FileX? {
91 | // Take next file from the top of the stack or return if there's nothing left
92 | val topState = state.peek() ?: return null
93 | val file = topState.step()
94 | if (file == null) {
95 | // There is nothing more on the top of the stack, go back
96 | state.pop()
97 | return gotoNext()
98 | } else {
99 | // Check that file/directory matches the filter
100 | if (file == topState.root || !file.isDirectory || state.size >= maxDepth) {
101 | // Proceed to a root directory or a simple file
102 | return file
103 | } else {
104 | // Proceed to a sub-directory
105 | state.push(directoryState(file))
106 | return gotoNext()
107 | }
108 | }
109 | }
110 |
111 | /** Visiting in bottom-up order */
112 | private inner class BottomUpDirectoryState(rootDir: FileX) : DirectoryState(rootDir) {
113 |
114 | private var rootVisited = false
115 |
116 | private var fileList: Array? = null
117 |
118 | private var fileIndex = 0
119 |
120 | private var failed = false
121 |
122 | /** First all children, then root directory */
123 | override fun step(): FileX? {
124 | if (!failed && fileList == null) {
125 | if (onEnter?.invoke(root) == false) {
126 | return null
127 | }
128 |
129 | fileList = root.listFiles()
130 | if (fileList == null) {
131 | onFail?.invoke(root, FileXAccessDeniedException(file = root, reason = "Cannot list files in a directory"))
132 | failed = true
133 | }
134 | }
135 | if (fileList != null && fileIndex < fileList!!.size) {
136 | // First visit all files
137 | return fileList!![fileIndex++]
138 | } else if (!rootVisited) {
139 | // Then visit root
140 | rootVisited = true
141 | return root
142 | } else {
143 | // That's all
144 | onLeave?.invoke(root)
145 | return null
146 | }
147 | }
148 | }
149 |
150 | /** Visiting in top-down order */
151 | private inner class TopDownDirectoryState(rootDir: FileX) : DirectoryState(rootDir) {
152 |
153 | private var rootVisited = false
154 |
155 | private var fileList: Array? = null
156 |
157 | private var fileIndex = 0
158 |
159 | /** First root directory, then all children */
160 | override fun step(): FileX? {
161 | if (!rootVisited) {
162 | // First visit root
163 | if (onEnter?.invoke(root) == false) {
164 | return null
165 | }
166 |
167 | rootVisited = true
168 | return root
169 | } else if (fileList == null || fileIndex < fileList!!.size) {
170 | if (fileList == null) {
171 | // Then read an array of files, if any
172 | fileList = root.listFiles()
173 | if (fileList == null) {
174 | onFail?.invoke(root, FileXAccessDeniedException(file = root, reason = "Cannot list files in a directory"))
175 | }
176 | if (fileList == null || fileList!!.size == 0) {
177 | onLeave?.invoke(root)
178 | return null
179 | }
180 | }
181 | // Then visit all files
182 | return fileList!![fileIndex++]
183 | } else {
184 | // That's all
185 | onLeave?.invoke(root)
186 | return null
187 | }
188 | }
189 | }
190 |
191 | private inner class SingleFileState(rootFile: FileX) : WalkState(rootFile) {
192 | private var visited: Boolean = false
193 |
194 | init {
195 | if (_Assertions.ENABLED)
196 | if (BuildConfig.DEBUG && !rootFile.isFile) {
197 | error("rootFile must be verified to be file beforehand.")
198 | }
199 | }
200 |
201 | override fun step(): FileX? {
202 | if (visited) return null
203 | visited = true
204 | return root
205 | }
206 | }
207 |
208 | }
209 |
210 | /**
211 | * Sets a predicate [function], that is called on any entered directory before its files are visited
212 | * and before it is visited itself.
213 | *
214 | * If the [function] returns `false` the directory is not entered and neither it nor its files are visited.
215 | */
216 | public fun onEnter(function: (FileX) -> Boolean): FileXTreeWalk {
217 | return FileXTreeWalk(start, direction, onEnter = function, onLeave = onLeave, onFail = onFail, maxDepth = maxDepth)
218 | }
219 |
220 | /**
221 | * Sets a callback [function], that is called on any left directory after its files are visited and after it is visited itself.
222 | */
223 | public fun onLeave(function: (FileX) -> Unit): FileXTreeWalk {
224 | return FileXTreeWalk(start, direction, onEnter = onEnter, onLeave = function, onFail = onFail, maxDepth = maxDepth)
225 | }
226 |
227 | /**
228 | * Set a callback [function], that is called on a directory when it's impossible to get its file list.
229 | *
230 | * [onEnter] and [onLeave] callback functions are called even in this case.
231 | */
232 | public fun onFail(function: (FileX, IOException) -> Unit): FileXTreeWalk {
233 | return FileXTreeWalk(start, direction, onEnter = onEnter, onLeave = onLeave, onFail = function, maxDepth = maxDepth)
234 | }
235 |
236 | /**
237 | * Sets the maximum [depth] of a directory tree to traverse. By default there is no limit.
238 | *
239 | * The value must be positive and [Int.MAX_VALUE] is used to specify an unlimited depth.
240 | *
241 | * With a value of 1, walker visits only the origin directory and all its immediate children,
242 | * with a value of 2 also grandchildren, etc.
243 | */
244 | public fun maxDepth(depth: Int): FileXTreeWalk {
245 | if (depth <= 0)
246 | throw IllegalArgumentException("depth must be positive, but was $depth.")
247 | return FileXTreeWalk(start, direction, onEnter, onLeave, onFail, depth)
248 | }
249 | }
250 |
251 | /**
252 | * Gets a sequence for visiting this directory and all its content.
253 | *
254 | * @param direction walk direction, top-down (by default) or bottom-up.
255 | */
256 | public fun FileX.walk(direction: FileWalkDirection = FileWalkDirection.TOP_DOWN): FileXTreeWalk =
257 | FileXTreeWalk(this, direction)
258 |
259 | /**
260 | * Gets a sequence for visiting this directory and all its content in top-down order.
261 | * Depth-first search is used and directories are visited before all their files.
262 | */
263 | public fun FileX.walkTopDown(): FileXTreeWalk = walk(FileWalkDirection.TOP_DOWN)
264 |
265 | /**
266 | * Gets a sequence for visiting this directory and all its content in bottom-up order.
267 | * Depth-first search is used and directories are visited after all their files.
268 | */
269 | public fun FileX.walkBottomUp(): FileXTreeWalk = walk(FileWalkDirection.BOTTOM_UP)
270 |
--------------------------------------------------------------------------------
/src/main/java/balti/filex/Tools.kt:
--------------------------------------------------------------------------------
1 | package balti.filex
2 |
3 | /**
4 | * Inspired from [kotlin.Pair].
5 | * Represents a set of four values.
6 | *
7 | * There is no meaning attached to values in this class, it can be used for any purpose.
8 | * Quad exhibits value semantics, i.e. two quads are equal if both components are equal.
9 | *
10 | * @param A type of the first value.
11 | * @param B type of the second value.
12 | * @param C type of the third value.
13 | * @param D type of the fourth value.
14 | *
15 | * @property first First value.
16 | * @property second Second value.
17 | * @property third Third value.
18 | * @property fourth Fourth value.
19 | *
20 | * @constructor Creates a new instance of Quad.
21 | */
22 | public data class Quad(
23 | public val first: A,
24 | public val second: B,
25 | public val third: C,
26 | public val fourth: D
27 | )
28 |
29 | /**
30 | * Some useful functions, mainly used to format file paths.
31 | */
32 | internal object Tools {
33 |
34 | /**
35 | * Adds a slash ('/') at front of the file path if not already present.
36 | * Removes trailing slash ('/') of present at the end.
37 | */
38 | @Suppress("NAME_SHADOWING")
39 | internal fun removeTrailingSlashOrColonAddFrontSlash(path: String): String {
40 | path.trim().let { path ->
41 | if (path.isBlank()) return ""
42 | val noFrontColon = if (path.startsWith(":")) {
43 | if (path.length > 1) path.substring(1)
44 | else ""
45 | } else path
46 | val withFrontSlash = noFrontColon.let { if (!it.startsWith("/")) "/$it" else it }
47 | return removeRearSlash(withFrontSlash)
48 | }
49 | }
50 |
51 | /**
52 | * Removes slash ('/') at end of the file path if present.
53 | */
54 | @Suppress("NAME_SHADOWING")
55 | internal fun removeRearSlash(path: String): String {
56 | path.trim().let { path ->
57 | if (path.isBlank() || path == "/") return "/"
58 | return if (path.last() == '/') {
59 | if (path.length > 1) path.substring(0, path.length - 1)
60 | else "/"
61 | } else path
62 | }
63 | }
64 |
65 | /**
66 | * Removes multiple slashes ('/') if present in the path.
67 | * Whenever a new [FileX] object is created, the path is passed through this function.
68 | *
69 | * Examples:
70 | * - "//aaa////bbb/ccc//ddd/" will be converted to "/aaa/bbb/ccc/ddd/"
71 | * - "a/b/bbb//c///dd/dde///" will be converted to "a/b/bbb/c/dd/dde/"
72 | */
73 | @Suppress("NAME_SHADOWING")
74 | internal fun removeDuplicateSlashes(path: String): String {
75 | try {
76 | path.trim().let {
77 | if (it.length < 2) return path
78 | // add a space at the end for cases where duplicate is at the end.
79 | "$it "
80 | }.let { path ->
81 |
82 | var lastConsideredPtr: Char = path[0]
83 | var ptr: Char = path[1]
84 |
85 | fun qualifyForRemoval(startPtr: Char, endPtr: Char): Boolean {
86 | // this function can be modified to remove and duplicate character, not just '/'
87 | // return startPtr == endPtr
88 | return startPtr == '/' && endPtr == '/'
89 | }
90 |
91 | val modifiedString = StringBuffer("")
92 |
93 | for (i in 1 until path.length) {
94 | ptr = path[i]
95 | val behindPtr = path[i-1]
96 | // behindPtr is one place behind ptr.
97 | // Normally, lastConsideredPtr = behindPtr and added to modifiedString.
98 | // Add lastConsideredPtr char to modifiedString if ptr and behindPtr are not duplicate.
99 | // If duplicate, freeze lastConsideredPtr in its place, do not add anything to modifiedString.
100 | // Once duplication is over, again move lastConsideredPtr at behindPtr and
101 | // add lastConsideredPtr char to modifiedString. This will add only one instance of
102 | // all the adjacent duplicate characters.
103 | if (!qualifyForRemoval(ptr, behindPtr)) {
104 | lastConsideredPtr = behindPtr
105 | modifiedString.append(lastConsideredPtr)
106 | }
107 | }
108 | return modifiedString.toString()
109 | }
110 | }
111 | catch (e: Exception){
112 | e.printStackTrace()
113 | return path
114 | }
115 | }
116 | }
--------------------------------------------------------------------------------
/src/main/java/balti/filex/activity/ActivityFunctionDelegate.kt:
--------------------------------------------------------------------------------
1 | package balti.filex.activity
2 |
3 | import android.app.Activity
4 | import android.content.Intent
5 | import balti.filex.FileXInit.Companion.fContext
6 |
7 | /**
8 | * Some jobs require to be executed with an activity context. This class facilitates such jobs.
9 | * This is a class which launches an activity, then passes a function to launch inside the activity.
10 | * It works on Interface based callback methods.
11 | * This class works as a middle-man because functions cannot be directly sent to Activities by Intent extras.
12 | *
13 | * - `trigger()`: It is a block of code (as a function) which is to be run on an activity context
14 | * - `onResult()`: Another block of code (as a function), which is to be run after executing the `trigger` function.
15 | *
16 | * How the flow of control works:
17 | * 1. A different caller class (say for example, in [resetGlobalRootUri()][balti.filex.filex11.utils.RootUri.resetGlobalRootUri])
18 | * instantiates this class with a [trigger] function, an [onResult] function and a [launchingActivity].
19 | * Both these two functions will be executed inside the [launchingActivity].
20 | * 2. Immediately upon instantiating an object of this class, inside `init` the [trigger] and [onResult] functions are stored in the
21 | * companion object's [triggerFunction] and [onResultFunction] respectively.
22 | * 3. Immediately after, the Activity specified by [launchingActivity] is started. This activity implements [ToActivity] interface.
23 | * 4. When the Activity starts, in `onCreate()` it calls this class's companion object's [onActivityInit] method.
24 | * 5. The [onActivityInit] function now calls the Activity's [toActivity(trigger, onResult)][ToActivity.toActivity] method.
25 | * This is an overridden method from the [ToActivity] interface.
26 | * This `toActivity` method delivers the actual [triggerFunction] (=[trigger]) and [onResultFunction] (=[onResult]) to the activity.
27 | * 6. Finally, the [launchingActivity] executes the `trigger` function and then executes the `onResult` code block with the result from `trigger.`
28 | *
29 | * @param trigger Function sent to activity to execute after activity start.
30 | * @param onResult Function sent to activity to execute after executing [trigger].
31 | * This function runs on the result from executing [trigger] function.
32 | * @param launchingActivity Activity class to launch to execute [trigger].
33 | * This activity must implement the [ToActivity] interface. Default is [SysFilePickerActivity].
34 | *
35 | * @see ToActivity
36 | */
37 | class ActivityFunctionDelegate(
38 | private val trigger: (context: Activity) -> Unit,
39 | private val onResult: (context: Activity, resultCode: Int, data: Intent?) -> Unit,
40 | private val launchingActivity: Class<*> = SysFilePickerActivity::class.java
41 | ){
42 |
43 | /**
44 | * This is used to hold the `trigger` and `onResult` functions, and they are sent to the [launchingActivity] after
45 | * it calls [onActivityInit] function. This the activity usually does on `onCreate()`,
46 | * basically after the activity is fully "ready" to execute the `trigger` function.
47 | *
48 | * The [launchingActivity] implements [ToActivity], hence overrides [ToActivity.toActivity].
49 | * When the activity calls the [ActivityFunctionDelegate.onActivityInit] function, it receives the `trigger` and `onResult`
50 | * function in its overridden `toActivity()` function.
51 | * The activity then runs the `trigger` function and the results of it are sent to `onResult`.
52 | */
53 | companion object {
54 |
55 | /**
56 | * If the `trigger` function requires calling the Activity's [Activity.startActivityForResult] function,
57 | * then it is called with this job code. It is also validated inside [Activity.onActivityResult].
58 | *
59 | * This is not done by actually analyzing the trigger function.
60 | * The logic is embedded in the different activities set in [launchingActivity].
61 | *
62 | * For example:
63 | * - [SysFilePickerActivity] is mainly written to launch System file picker UI (an activity) for selecting a root location for SAF storage.
64 | * Hence it is associated with [Activity.startActivityForResult], and uses this parameter.
65 | * - But [TraditionalFileRequest] is created to ask for file permissions in the old way, prior to how it was done in Android 9.
66 | * This is not related to any activity. Hence this parameter is useless.
67 | */
68 | private var optionalJobcode: Int = 111
69 |
70 | /**
71 | * Stores the [trigger] function.
72 | */
73 | private lateinit var triggerFunction: (context: Activity) -> Unit
74 |
75 | /**
76 | * Stores the [onResult] function.
77 | */
78 | private lateinit var onResultFunction: (context: Activity, resultCode: Int, data: Intent?) -> Unit
79 |
80 | /**
81 | * Function called by the [launchingActivity], to receive the [triggerFunction] and [onResultFunction].
82 | * @param activity The running activity (which is an instance of [ToActivity]) to which
83 | * the [triggerFunction] and [onResultFunction] is to be passed.
84 | */
85 | fun onActivityInit(activity: ToActivity) {
86 | activity.toActivity(triggerFunction, onResultFunction, optionalJobcode)
87 | }
88 | }
89 |
90 | /**
91 | * An new constructor created to specially for SAF uses. Used for setting the root location.
92 | * For SAF uses, mainly an [Intent] is launched to start the system file picker activity.
93 | * Hence this constructor is focused to receive an intent and launch in the [launchingActivity] with the provided [jobCode].
94 | *
95 | * @param jobCode Integer code, functions as `requestCode` for [Activity.startActivityForResult].
96 | * @param intent Intent (to launch system file picker ui) to be launched from the activity using [Activity.startActivityForResult].
97 | * This intent can have different flags and extras depending on the type of SAF based work to be performed.
98 | * @param onResult Function to be executed after receiving the result in the activity's [Activity.onActivityResult] method.
99 | */
100 | constructor(jobCode: Int, intent: Intent, onResult: (context: Activity, resultCode: Int, data: Intent?) -> Unit):
101 | this({ it.startActivityForResult(intent, jobCode) }, onResult) {
102 | // Store the provided jobCode.
103 | optionalJobcode = jobCode
104 | }
105 |
106 | init {
107 | // store the trigger and onResult functions.
108 | triggerFunction = trigger
109 | onResultFunction = onResult
110 |
111 | // launch the required activity.
112 | fContext.run {
113 | startActivity(Intent(this, launchingActivity).apply {
114 | flags = Intent.FLAG_ACTIVITY_NEW_TASK
115 | })
116 | }
117 | }
118 | }
--------------------------------------------------------------------------------
/src/main/java/balti/filex/activity/ActivityInterfaces.kt:
--------------------------------------------------------------------------------
1 | package balti.filex.activity
2 |
3 | import android.app.Activity
4 | import android.content.Intent
5 |
6 | /**
7 | * Interface to pass `trigger` and `onResult` methods to an activity class.
8 | * This interface acts like a middle man, as functions cannot be passed as intent extras to an activity.
9 | *
10 | * Please see [ActivityFunctionDelegate] to understand how this interface is used.
11 | */
12 | interface ToActivity {
13 |
14 | /**
15 | * This function is used to pass the `trigger` and `onResult` methods as function arguments to an activity.
16 | * The activity class implements this interface. When it calls [ActivityFunctionDelegate.onActivityInit] function,
17 | * the `trigger` and `onResult` methods are sent via this function.
18 | *
19 | * @param trigger The function (a block of code) to be executed on an activity context.
20 | * @param onResult The function to be executed with results of executing [trigger] function.
21 | * @param optionalJobCode Request code for cases where [Activity.startActivityForResult] needs to be called as trigger function.
22 | */
23 | fun toActivity(
24 | trigger: (context: Activity) -> Unit,
25 | onResult: (context: Activity, resultCode: Int, data: Intent?) -> Unit,
26 | optionalJobCode: Int = 111
27 | )
28 | }
--------------------------------------------------------------------------------
/src/main/java/balti/filex/activity/SysFilePickerActivity.kt:
--------------------------------------------------------------------------------
1 | package balti.filex.activity
2 |
3 | import android.app.Activity
4 | import android.content.Intent
5 | import android.os.Bundle
6 |
7 | /**
8 | * Class created for Storage Access Framework (SAF) usage.
9 | *
10 | * Depending on `trigger` function, is used for various purposes like
11 | * opening the system file picker activity to set a root storage location.
12 | */
13 | class SysFilePickerActivity: Activity(), ToActivity {
14 |
15 | /**
16 | * Stores the block of code to be executed after executing the `trigger` function.
17 | * This is also the `onResult` function.
18 | *
19 | * This class is made assuming that the `trigger` function will start the system file picker UI which is an activity.
20 | * Hence the results from the user's interaction will be received in [onActivityResult].
21 | * Thus this function is run on the results received inside `onActivityResult`.
22 | */
23 | private lateinit var onResultFunction: (context: Activity, resultCode: Int, data: Intent?) -> Unit
24 |
25 | /**
26 | * Integer request code with which the system file picker activity is called.
27 | * The `trigger` function is expected to contain some form of [Activity.startActivityForResult] with this code.
28 | */
29 | private var jobCode: Int = 111
30 |
31 | override fun onCreate(savedInstanceState: Bundle?) {
32 | super.onCreate(savedInstanceState)
33 | /**
34 | * This activity is called by [ActivityFunctionDelegate] class, which at this point,
35 | * already contains `trigger` and `onResult` functions.
36 | *
37 | * On calling this function, the [ActivityFunctionDelegate] passes those
38 | * `trigger` and `onResult` functions to this activity class, via the [toActivity] method.
39 | */
40 | ActivityFunctionDelegate.onActivityInit(this)
41 | }
42 |
43 | /**
44 | * Receive results from system file picker activity and pass
45 | * it to [onResultFunction], if the request code is equal to the [jobCode].
46 | */
47 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
48 | super.onActivityResult(requestCode, resultCode, data)
49 | if (requestCode == jobCode) {
50 | onResultFunction(this, resultCode, data)
51 | // close the activity after running `onResult`
52 | finish()
53 | }
54 | }
55 |
56 | /**
57 | * Overridden function of the [ToActivity] interface.
58 | *
59 | * - Receives the [trigger] function (which is expected to start system file picker activity).
60 | * - Also receives [onResult] function which is stored in [onResultFunction].
61 | * - [optionalJobCode] is stored in [jobCode].
62 | *
63 | * Finally, the [trigger] function is executed.
64 | */
65 | override fun toActivity(
66 | trigger: (context: Activity) -> Unit,
67 | onResult: (context: Activity, resultCode: Int, data: Intent?) -> Unit,
68 | optionalJobCode: Int
69 | ) {
70 | jobCode = optionalJobCode
71 | onResultFunction = onResult
72 |
73 | // execute the trigger function with the current activity context.
74 | trigger(this)
75 | }
76 | }
--------------------------------------------------------------------------------
/src/main/java/balti/filex/activity/TraditionalFileRequest.kt:
--------------------------------------------------------------------------------
1 | package balti.filex.activity
2 |
3 | import android.Manifest
4 | import android.app.Activity
5 | import android.content.Intent
6 | import android.content.pm.PackageManager
7 | import android.os.Bundle
8 | import androidx.core.app.ActivityCompat
9 |
10 | class TraditionalFileRequest: Activity(), ToActivity {
11 |
12 | private lateinit var onResultFunction: (context: Activity, resultCode: Int, data: Intent?) -> Unit
13 | private val REQUEST_CODE = 11112
14 |
15 | val EXTRA_PERMISSIONS = "permissions"
16 | val EXTRA_RESULTS = "grantResults"
17 |
18 | override fun onCreate(savedInstanceState: Bundle?) {
19 | super.onCreate(savedInstanceState)
20 | ActivityFunctionDelegate.onActivityInit(this)
21 | }
22 |
23 | override fun onRequestPermissionsResult(
24 | requestCode: Int,
25 | permissions: Array,
26 | grantResults: IntArray
27 | ) {
28 | super.onRequestPermissionsResult(requestCode, permissions, grantResults)
29 | val resultCode: Int =
30 | if (grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults[1] == PackageManager.PERMISSION_GRANTED)
31 | RESULT_OK
32 | else RESULT_CANCELED
33 | onResultFunction(this, resultCode, Intent().apply {
34 | putExtra(EXTRA_PERMISSIONS, permissions)
35 | putExtra(EXTRA_RESULTS, grantResults)
36 | })
37 | finish()
38 | }
39 |
40 | override fun toActivity(
41 | trigger: (context: Activity) -> Unit,
42 | onResult: (context: Activity, resultCode: Int, data: Intent?) -> Unit,
43 | optionalJobCode: Int
44 | ) {
45 | onResultFunction = onResult
46 | ActivityCompat.requestPermissions(this,
47 | arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_CODE)
48 | }
49 | }
--------------------------------------------------------------------------------
/src/main/java/balti/filex/exceptions/DirectoryHierarchyBroken.kt:
--------------------------------------------------------------------------------
1 | package balti.filex.exceptions
2 |
3 | import java.io.IOException
4 |
5 | class DirectoryHierarchyBroken(message: String?): IOException(message) {
6 | }
--------------------------------------------------------------------------------
/src/main/java/balti/filex/exceptions/ImproperFileXType.kt:
--------------------------------------------------------------------------------
1 | package balti.filex.exceptions
2 |
3 | class ImproperFileXType(message: String): Exception(message) {
4 | }
--------------------------------------------------------------------------------
/src/main/java/balti/filex/exceptions/KotlinCopiedExceptions.kt:
--------------------------------------------------------------------------------
1 | package balti.filex.exceptions
2 |
3 | import balti.filex.FileX
4 |
5 | // All are copied from [kotlin.io.Exceptions]
6 |
7 | /**
8 | * A base exception class for file system exceptions.
9 | * @property fileX the file on which the failed operation was performed.
10 | * @property otherFileX the second file involved in the operation, if any (for example, the target of a copy or move)
11 | * @property reasonString the description of the error
12 | */
13 |
14 | public open class FileXSystemException(
15 | val fileX: FileX,
16 | val otherFileX: FileX? = null,
17 | val reasonString: String? = null
18 | ): kotlin.io.FileSystemException(fileX.file, otherFileX?.file, reasonString)
19 |
20 | /**
21 | * An exception class which is used when some file to create or copy to already exists.
22 | */
23 | public class FileXAlreadyExistsException(
24 | file: FileX,
25 | other: FileX? = null,
26 | reason: String? = null
27 | ) : FileXSystemException(file, other, reason)
28 |
29 | /**
30 | * An exception class which is used when we have not enough access for some operation.
31 | */
32 | public class FileXAccessDeniedException(
33 | file: FileX,
34 | other: FileX? = null,
35 | reason: String? = null
36 | ) : FileXSystemException(file, other, reason)
37 |
38 | /**
39 | * An exception class which is used when file to copy does not exist.
40 | */
41 | public class NoSuchFileXException(
42 | file: FileX,
43 | other: FileX? = null,
44 | reason: String? = null
45 | ) : FileXSystemException(file, other, reason)
46 |
47 | /** Private exception class, used to terminate recursive copying. */
48 | public class FileXTerminateException(file: FileX) : FileXSystemException(file) {}
--------------------------------------------------------------------------------
/src/main/java/balti/filex/exceptions/RootNotInitializedException.kt:
--------------------------------------------------------------------------------
1 | package balti.filex.exceptions
2 |
3 | import java.io.IOException
4 |
5 | class RootNotInitializedException(message: String?): IOException(message) {
6 | }
--------------------------------------------------------------------------------
/src/main/java/balti/filex/filex11/FileX11.kt:
--------------------------------------------------------------------------------
1 | package balti.filex.filex11
2 |
3 | import android.app.Activity
4 | import android.content.Intent
5 | import android.net.Uri
6 | import android.os.Handler
7 | import android.os.Looper
8 | import android.provider.DocumentsContract
9 | import androidx.lifecycle.Lifecycle
10 | import androidx.lifecycle.LifecycleOwner
11 | import androidx.lifecycle.LifecycleRegistry
12 | import balti.filex.FileX
13 | import balti.filex.FileXInit.Companion.fCResolver
14 | import balti.filex.FileXInit.Companion.refreshFileOnCreation
15 | import balti.filex.Quad
16 | import balti.filex.Tools.removeTrailingSlashOrColonAddFrontSlash
17 | import balti.filex.activity.ActivityFunctionDelegate
18 | import balti.filex.exceptions.RootNotInitializedException
19 | import balti.filex.filex11.operators.*
20 | import balti.filex.filex11.publicInterfaces.FileXFilter
21 | import balti.filex.filex11.publicInterfaces.FileXNameFilter
22 | import balti.filex.filex11.utils.RootUri.getGlobalRootUri
23 | import balti.filex.filex11.utils.Tools.buildTreeDocumentUriFromId
24 | import balti.filex.filex11.utils.Tools.checkUriExists
25 | import balti.filex.filex11.utils.Tools.convertToDocumentUri
26 | import java.io.File
27 | import java.io.InputStream
28 | import java.io.OutputStream
29 |
30 |
31 | internal class FileX11(path: String, currentRootUri: Uri? = null): FileX(false), LifecycleOwner {
32 |
33 | internal constructor(uri: Uri, currentRootUri: Uri) : this(
34 | currentRootUri.let {
35 | val docId = DocumentsContract.getDocumentId(uri)
36 | val rootId = DocumentsContract.getTreeDocumentId(it)
37 | if (!docId.startsWith(rootId)) throw IllegalArgumentException("Root uri not parent of given uri")
38 | else if (rootId == docId) ""
39 | else docId.substring(rootId.length)
40 | }
41 | ) { this.uri = uri; rootUri = currentRootUri; }
42 |
43 | fun setLocalRootUri(afterJob: ((resultCode: Int, data: Intent?) -> Unit)? = null) {
44 | val JOB_CODE = 100
45 | val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
46 | ActivityFunctionDelegate(JOB_CODE,
47 | Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
48 | flags = takeFlags
49 | }) { context, resultCode, data ->
50 | if (resultCode == Activity.RESULT_OK && data != null) {
51 | data.data?.let {
52 | context.contentResolver.takePersistableUriPermission(it, takeFlags)
53 | rootUri = it
54 | uri = buildTreeDocumentUriFromId(rootDocumentId)
55 | afterJob?.invoke(resultCode, data)
56 | } ?: afterJob?.invoke(resultCode, data)
57 | }
58 | else afterJob?.invoke(resultCode, data)
59 | }
60 | }
61 |
62 | var rootDocumentId: String = ""
63 | private set
64 |
65 | var rootUri: Uri? = null
66 | private set(value) {
67 | rootDocumentId = if (value != null) DocumentsContract.getTreeDocumentId(value) else ""
68 | field = value
69 | }
70 |
71 | var documentId: String? = null
72 | private set
73 |
74 | override var uri: Uri? = null
75 | private set(value) {
76 | if (value != null) {
77 | documentId =
78 | if (value != rootUri) DocumentsContract.getDocumentId(value)
79 | else DocumentsContract.getTreeDocumentId(value)
80 | field = value
81 | }
82 | }
83 |
84 | var mimeType: String = "*/*"
85 | internal set
86 |
87 | override var path: String = ""
88 | private set
89 |
90 | private lateinit var lifecycleRegistry: LifecycleRegistry
91 |
92 | private fun init(initPath: String? = null, currentRootUri: Uri? = null){
93 | currentRootUri?.let{ root ->
94 | convertToDocumentUri(root)?.let { conv ->
95 | val permissibleUris = fCResolver.persistedUriPermissions.map { it.uri }
96 | if (checkUriExists(conv) && permissibleUris.contains(root)) rootUri = root
97 | }
98 | }
99 | if (rootUri == null) rootUri = getGlobalRootUri().apply {
100 | if (this == null) throw RootNotInitializedException("Global root uri not set")
101 | }
102 | if (initPath != null) {
103 | this.path = removeTrailingSlashOrColonAddFrontSlash(initPath)
104 | if (initPath == "" || initPath == "/") {
105 | uri = buildTreeDocumentUriFromId(rootDocumentId)
106 | }
107 | }
108 | }
109 |
110 | init {
111 | init(path, currentRootUri)
112 | val runnable = Runnable {
113 | lifecycleRegistry = LifecycleRegistry(this)
114 | FileXServer.pathAndUri.observe(this) {
115 | if (it.first == rootUri && it.second == this.path && it.third != null) {
116 | directlySetUriAndPath(it.third, it.fourth)
117 | }
118 | }
119 | lifecycleRegistry.currentState = Lifecycle.State.STARTED
120 | }
121 | if (refreshFileOnCreation) refreshFile()
122 | if (Looper.myLooper() == Looper.getMainLooper()) runnable.run()
123 | else Handler(Looper.getMainLooper()).post(runnable)
124 | }
125 |
126 | /*enum class FileXCodes {
127 | OK, OVERWRITE, SKIP, TERMINATE, MERGE, NEW_IF_EXISTS
128 | }*/
129 |
130 | internal fun directlySetUriAndPath(uri: Uri?, path: String?){
131 | uri?.let { this.uri = it }
132 | path?.let { this.path = removeTrailingSlashOrColonAddFrontSlash(it) }
133 | }
134 |
135 | override fun getLifecycle(): Lifecycle {
136 | return lifecycleRegistry
137 | }
138 |
139 | private val Info = Info(this)
140 |
141 | override val file: File get() = File(Info.canonicalPath)
142 | override fun refreshFile() = refreshFileX11()
143 |
144 | override val canonicalPath: String get() = Info.canonicalPath
145 | override val absolutePath: String get() = Info.absolutePath
146 | override fun exists(): Boolean = Info.exists()
147 | override val isDirectory: Boolean get() = Info.isDirectory
148 | override val isFile: Boolean get() = Info.isFile
149 | override val name: String get() = Info.name
150 | override val parent: String? get() = Info.parent
151 | override val parentFile: FileX? get() = Info.parentFile
152 | override val storagePath: String get() = Info.storagePath
153 | override val volumePath: String get() = Info.volumePath
154 | override val rootPath: String get() = Info.rootPath
155 | override val parentUri: Uri? get() = Info.parentUri
156 | override fun canExecute(): Boolean = false
157 | override val parentCanonical: String get() = Info.parentCanonical
158 | override fun length(): Long = Info.length()
159 | override fun lastModified(): Long = Info.lastModified()
160 | override fun canRead(): Boolean = Info.canRead()
161 | override fun canWrite(): Boolean = Info.canWrite()
162 | override val extension: String get() = Info.extension
163 | override val nameWithoutExtension: String get() = Info.nameWithoutExtension
164 | override val freeSpace: Long get() = Info.freeSpace
165 | override val usableSpace: Long get() = Info.usableSpace
166 | override val totalSpace: Long get() = Info.totalSpace
167 | override val isHidden: Boolean get() = Info.isHidden
168 |
169 | private val Delete = Delete(this)
170 |
171 | override fun delete(): Boolean = Delete.delete()
172 | override fun deleteRecursively(): Boolean = Delete.deleteRecursively()
173 | override fun deleteOnExit() = Delete.deleteOnExit()
174 |
175 | private val Create = Create(this)
176 |
177 | override fun createNewFile(): Boolean = Create.createNewFile()
178 | override fun createNewFile(makeDirectories: Boolean, overwriteIfExists: Boolean, optionalMimeType: String): Boolean =
179 | Create.createNewFile(makeDirectories, overwriteIfExists, optionalMimeType)
180 | override fun mkdir(): Boolean = Create.mkdir()
181 | override fun mkdirs(): Boolean = Create.mkdirs()
182 | override fun createFileUsingPicker(optionalMimeType: String, afterJob: ((resultCode: Int, data: Intent?) -> Unit)?) =
183 | Create.createFileUsingPicker(optionalMimeType, afterJob)
184 |
185 | private val Modify = Modify(this)
186 |
187 | override fun renameTo(dest: FileX): Boolean = Modify.renameTo(dest)
188 | override fun renameTo(newFileName: String): Boolean = Modify.renameTo(newFileName)
189 |
190 | private val Filter = Filter(this)
191 |
192 | override val isEmpty: Boolean get() = Filter.isEmpty
193 | override fun listFiles(): Array? = Filter.listFiles()
194 | override fun listFiles(filter: FileXFilter): Array? = Filter.listFiles(filter)
195 | override fun listFiles(filter: FileXNameFilter): Array? = Filter.listFiles(filter)
196 | override fun list() = Filter.list()
197 | override fun list(filter: FileXFilter): Array? = Filter.list(filter)
198 | override fun list(filter: FileXNameFilter): Array? = Filter.list(filter)
199 | override fun listEverythingInQuad(): List>? = Filter.listEverythingInQuad()
200 | override fun listEverything(): Quad, List, List, List>? = Filter.listEverything()
201 |
202 | private val Operations = Operations(this)
203 |
204 | override fun inputStream(): InputStream? = Operations.inputStream()
205 | override fun outputStream(mode: String): OutputStream? = Operations.outputStream(mode)
206 | }
207 |
--------------------------------------------------------------------------------
/src/main/java/balti/filex/filex11/FileXServer.kt:
--------------------------------------------------------------------------------
1 | package balti.filex.filex11
2 |
3 | import android.net.Uri
4 | import android.os.Handler
5 | import android.os.Looper
6 | import androidx.lifecycle.MutableLiveData
7 | import balti.filex.Quad
8 |
9 | internal class FileXServer {
10 | companion object{
11 | internal val pathAndUri = MutableLiveData>().apply {
12 | value = Quad(Uri.EMPTY, "", null, null)
13 | }
14 | internal fun setPathAndUri(rootUri: Uri, path: String, uri: Uri?, newPath: String? = null){
15 | val runnable = Runnable {
16 | pathAndUri.value = Quad(rootUri, path, uri, newPath)
17 | }
18 | if (Looper.myLooper() == Looper.getMainLooper()) runnable.run()
19 | else Handler(Looper.getMainLooper()).post(runnable)
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/src/main/java/balti/filex/filex11/operators/Create.kt:
--------------------------------------------------------------------------------
1 | package balti.filex.filex11.operators
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import android.provider.DocumentsContract
6 | import balti.filex.filex11.FileX11
7 | import balti.filex.FileXInit.Companion.fCResolver
8 | import balti.filex.FileXInit.Companion.tryIt
9 | import balti.filex.filex11.FileXServer
10 | import balti.filex.activity.ActivityFunctionDelegate
11 | import balti.filex.exceptions.DirectoryHierarchyBroken
12 | import balti.filex.exceptions.RootNotInitializedException
13 | import balti.filex.filex11.utils.Tools.buildTreeDocumentUriFromId
14 | import balti.filex.filex11.utils.Tools.checkUriExists
15 | import balti.filex.filex11.utils.Tools.getChildrenUri
16 |
17 | internal class Create(private val f: FileX11) {
18 |
19 | fun createFileUsingPicker(optionalMimeType: String = "*/*", afterJob: ((resultCode: Int, data: Intent?) -> Unit)? = null): Unit = f.run {
20 | if (rootUri == null) throw RootNotInitializedException("root not initialised")
21 | val JOB_CODE = 200
22 | ActivityFunctionDelegate(JOB_CODE, Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
23 | addCategory(Intent.CATEGORY_OPENABLE)
24 | type = optionalMimeType
25 | mimeType = optionalMimeType
26 | putExtra(Intent.EXTRA_TITLE, name)
27 | }) { _, resultCode, data ->
28 | directlySetUriAndPath(data?.data, null)
29 | FileXServer.setPathAndUri(rootUri!!, path, data?.data)
30 | afterJob?.invoke(resultCode, data)
31 | }
32 | }
33 |
34 | fun createNewFile(makeDirectories: Boolean = false, overwriteIfExists: Boolean = false, optionalMimeType: String = "*/*"): Boolean = f.run {
35 | if (!makeDirectories && uri != null) {
36 | return uri?.let {
37 | if (!exists()) createBlankDoc(parentUri ?: rootUri!!, name, optionalMimeType)
38 | else if (overwriteIfExists) {
39 | tryIt { DocumentsContract.deleteDocument(cResolver, it) }
40 | createBlankDoc(parentUri ?: rootUri!!, name, optionalMimeType)
41 | } else false
42 | } ?: false
43 | } else return traverse({ dir, nextDocId, childrenUri ->
44 | if (nextDocId == "") {
45 | if (makeDirectories) getChildrenUri(DocumentsContract.createDocument(cResolver, childrenUri, DocumentsContract.Document.MIME_TYPE_DIR, dir)!!)
46 | else throw DirectoryHierarchyBroken("No such file or directory")
47 | } else getChildrenUri(nextDocId)
48 | }, { fileName, nextDocId, childrenUri ->
49 | return@traverse if (nextDocId != "") {
50 | val existingUri = buildTreeDocumentUriFromId(nextDocId)
51 | if (overwriteIfExists) {
52 | DocumentsContract.deleteDocument(cResolver, existingUri)
53 | createBlankDoc(childrenUri, fileName)
54 | } else {
55 | directlySetUriAndPath(existingUri, null)
56 | FileXServer.setPathAndUri(rootUri!!, path, existingUri)
57 | false
58 | }
59 | } else {
60 | createBlankDoc(childrenUri, fileName)
61 | }
62 | })
63 | }
64 |
65 | fun createNewFile() = createNewFile(makeDirectories = false, overwriteIfExists = false)
66 |
67 | fun mkdirs(): Boolean = f.run {
68 | traverse({ dir, nextDocId, childrenUri ->
69 | if (nextDocId == "") {
70 | getChildrenUri(DocumentsContract.createDocument(cResolver, childrenUri, DocumentsContract.Document.MIME_TYPE_DIR, dir)!!)
71 | } else getChildrenUri(nextDocId)
72 | }, { fileName, nextDocId, childrenUri ->
73 | if (nextDocId == "") createBlankDoc(childrenUri, fileName, DocumentsContract.Document.MIME_TYPE_DIR)
74 | else false
75 | })
76 | }
77 |
78 | fun mkdir(): Boolean = f.run {
79 | traverse({ _, nextDocId, _ ->
80 | if (nextDocId == "") null else getChildrenUri(nextDocId)
81 | }, { fileName, nextDocId, childrenUri ->
82 | if (nextDocId == "") createBlankDoc(childrenUri, fileName, DocumentsContract.Document.MIME_TYPE_DIR)
83 | else false
84 | })
85 | }
86 |
87 | //
88 | //
89 | // private methods
90 | // *****************************************
91 |
92 | private val cResolver = fCResolver
93 |
94 | private fun FileX11.createBlankDoc(parentUri: Uri, fileName: String, optionalMimeType: String = "*/*"): Boolean {
95 | if (!parentUri.toString().endsWith("/children") && !checkUriExists(parentUri)) throw DirectoryHierarchyBroken("Complete parent uri not present: $parentUri")
96 | return DocumentsContract.createDocument(cResolver, parentUri, optionalMimeType, fileName).let {
97 | if (it != null) {
98 | directlySetUriAndPath(it, null)
99 | FileXServer.setPathAndUri(rootUri!!, path, it)
100 | true
101 | } else false
102 | }
103 | }
104 |
105 | private fun FileX11.traverse(
106 | directoryFunc: (dirName: String, nextDocId: String, childrenUri: Uri) -> Uri?,
107 | fileFunction: (fileName: String, nextDocId: String, childrenUri: Uri) -> Boolean): Boolean {
108 |
109 | val dirs = if (path.length > 1) path.substring(1).split("/") else ArrayList(0)
110 | val projection = arrayOf(DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_DOCUMENT_ID)
111 | var childrenUri = getChildrenUri(rootUri!!)
112 | for (i in dirs.indices) {
113 | val dir = dirs[i]
114 | var nextDocId = ""
115 | cResolver.query(childrenUri, projection, null, null, null)?.run {
116 | while (moveToNext()) {
117 | if (getString(0) == dir) {
118 | nextDocId = getString(1)
119 | break
120 | }
121 | }
122 | close()
123 | }
124 | if (i < dirs.indices.last) directoryFunc(dir, nextDocId, childrenUri).let { if (it != null) childrenUri = it else return false }
125 | else return fileFunction(dirs.last(), nextDocId, childrenUri)
126 | }
127 | return false
128 | }
129 | }
--------------------------------------------------------------------------------
/src/main/java/balti/filex/filex11/operators/Delete.kt:
--------------------------------------------------------------------------------
1 | package balti.filex.filex11.operators
2 |
3 | import android.provider.DocumentsContract
4 | import balti.filex.filex11.FileX11
5 | import balti.filex.FileXInit.Companion.fCResolver
6 | import balti.filex.filex11.utils.FileX11DeleteOnExit
7 |
8 | internal class Delete(private val f: FileX11) {
9 | fun delete(): Boolean = f.run {
10 | return uri?.let {
11 | if (isFile || isEmpty) DocumentsContract.deleteDocument(fCResolver, it)
12 | else false
13 | } ?: false
14 | }
15 |
16 | fun deleteRecursively(): Boolean = f.run {
17 | return uri?.let {
18 | DocumentsContract.deleteDocument(fCResolver, it)
19 | } ?: false
20 | }
21 |
22 | fun deleteOnExit() {
23 | FileX11DeleteOnExit.add(f)
24 | }
25 | }
--------------------------------------------------------------------------------
/src/main/java/balti/filex/filex11/operators/Filter.kt:
--------------------------------------------------------------------------------
1 | package balti.filex.filex11.operators
2 |
3 | import android.provider.DocumentsContract
4 | import balti.filex.FileX
5 | import balti.filex.FileXInit.Companion.fCResolver
6 | import balti.filex.Quad
7 | import balti.filex.filex11.FileX11
8 | import balti.filex.filex11.publicInterfaces.FileXFilter
9 | import balti.filex.filex11.publicInterfaces.FileXNameFilter
10 | import balti.filex.filex11.utils.Tools.getChildrenUri
11 |
12 | internal class Filter(private val f: FileX11) {
13 |
14 | val isEmpty: Boolean
15 | get() = f.run{
16 | if (!this.isDirectory || documentId == null) return false
17 | val childrenUri = getChildrenUri(documentId!!)
18 | val projection = arrayOf(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
19 | var isEmpty = false
20 | try {
21 | fCResolver.query(childrenUri, projection, null, null, null)?.run {
22 | isEmpty = this.count <= 0
23 | close()
24 | }
25 | } catch (e: Exception) {
26 | e.printStackTrace()
27 | }
28 | return isEmpty
29 | }
30 |
31 | fun listFiles(filter: FileXFilter? = null): Array? = f.run {
32 | if (!this.isDirectory || documentId == null) return null
33 | val qualifyingList = ArrayList(0)
34 | val childrenUri = getChildrenUri(documentId!!)
35 | val projection = arrayOf(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
36 | try {
37 | fCResolver.query(childrenUri, projection, null, null, null)?.run {
38 | while (moveToNext()) {
39 | val f = FileX11(path + "/" + getString(0), rootUri)
40 | if (filter == null || filter.accept(f))
41 | qualifyingList.add(f)
42 | }
43 | close()
44 | }
45 | } catch (e: Exception) {
46 | e.printStackTrace()
47 | }
48 | return qualifyingList.toTypedArray()
49 | }
50 |
51 | fun listFiles(filter: FileXNameFilter): Array? = f.run {
52 | if (!this.isDirectory || documentId == null) return null
53 | val qualifyingList = ArrayList(0)
54 | val childrenUri = getChildrenUri(documentId!!)
55 | val projection = arrayOf(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
56 | try {
57 | fCResolver.query(childrenUri, projection, null, null, null)?.run {
58 | while (moveToNext()) {
59 | getString(0).let { name ->
60 | if (filter.accept(f, name)) {
61 | val f = FileX11("$path/$name", rootUri)
62 | qualifyingList.add(f)
63 | }
64 | }
65 | }
66 | close()
67 | }
68 | } catch (e: Exception) {
69 | e.printStackTrace()
70 | }
71 | return qualifyingList.toTypedArray()
72 | }
73 |
74 | fun listFiles(): Array? = listFiles(null)
75 |
76 | fun list(filter: FileXFilter? = null): Array? = f.run {
77 | if (!this.isDirectory || documentId == null) return null
78 | val qualifyingList = ArrayList(0)
79 | val childrenUri = getChildrenUri(documentId!!)
80 | val projection = arrayOf(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
81 | try {
82 | fCResolver.query(childrenUri, projection, null, null, null)?.run {
83 | while (moveToNext()) {
84 | getString(0).let {
85 | val f = FileX11("$path/$it", rootUri)
86 | if (filter == null || filter.accept(f))
87 | qualifyingList.add(it)
88 | }
89 | }
90 | close()
91 | }
92 | } catch (e: Exception) {
93 | e.printStackTrace()
94 | }
95 | return qualifyingList.toTypedArray()
96 | }
97 |
98 | fun list(filter: FileXNameFilter): Array? = f.run {
99 | if (!this.isDirectory || documentId == null) return null
100 | val qualifyingList = ArrayList(0)
101 | val childrenUri = getChildrenUri(documentId!!)
102 | val projection = arrayOf(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
103 | try {
104 | fCResolver.query(childrenUri, projection, null, null, null)?.run {
105 | while (moveToNext()) {
106 | getString(0).let { name ->
107 | if (filter.accept(f, name)) {
108 | qualifyingList.add(name)
109 | }
110 | }
111 | }
112 | close()
113 | }
114 | } catch (e: Exception) {
115 | e.printStackTrace()
116 | }
117 | return qualifyingList.toTypedArray()
118 | }
119 |
120 | fun list(): Array? = list(null)
121 |
122 | fun listEverythingInQuad(): List>? = f.run {
123 | val results = ArrayList>(0)
124 |
125 | if (!this.isDirectory || documentId == null) return null
126 | val childrenUri = getChildrenUri(documentId!!)
127 |
128 | val projection = arrayOf(
129 | DocumentsContract.Document.COLUMN_DISPLAY_NAME,
130 | DocumentsContract.Document.COLUMN_MIME_TYPE,
131 | DocumentsContract.Document.COLUMN_SIZE,
132 | DocumentsContract.Document.COLUMN_LAST_MODIFIED
133 | )
134 | try {
135 | fCResolver.query(childrenUri, projection, null, null, null)?.run {
136 | while (moveToNext()) {
137 | val name = getString(0)
138 | val isDirectory = getString(1) == DocumentsContract.Document.MIME_TYPE_DIR
139 | val size = try { getString(2).toLong() } catch (_: Exception) { 0L }
140 | val lastModified = try { getString(3).toLong() } catch (_: Exception) { 0L }
141 | val entry = Quad(name, isDirectory, size, lastModified)
142 | results.add(entry)
143 | }
144 | close()
145 | }
146 | return results
147 | } catch (e: Exception) {
148 | e.printStackTrace()
149 | return null
150 | }
151 | }
152 |
153 | fun listEverything(): Quad, List, List, List>? = f.run {
154 | val resultNames = ArrayList(0)
155 | val resultDirectory = ArrayList(0)
156 | val resultSize = ArrayList(0)
157 | val resultLastModified = ArrayList(0)
158 |
159 | if (!this.isDirectory || documentId == null) return null
160 | val childrenUri = getChildrenUri(documentId!!)
161 |
162 | val projection = arrayOf(
163 | DocumentsContract.Document.COLUMN_DISPLAY_NAME,
164 | DocumentsContract.Document.COLUMN_MIME_TYPE,
165 | DocumentsContract.Document.COLUMN_SIZE,
166 | DocumentsContract.Document.COLUMN_LAST_MODIFIED
167 | )
168 | try {
169 | fCResolver.query(childrenUri, projection, null, null, null)?.run {
170 | while (moveToNext()) {
171 | val name = getString(0)
172 | val isDirectory = getString(1) == DocumentsContract.Document.MIME_TYPE_DIR
173 | val size = try { getString(2).toLong() } catch (_: Exception) { 0L }
174 | val lastModified = try { getString(3).toLong() } catch (_: Exception) { 0L }
175 |
176 | resultNames.add(name)
177 | resultDirectory.add(isDirectory)
178 | resultSize.add(size)
179 | resultLastModified.add(lastModified)
180 | }
181 | close()
182 | }
183 | return Quad(resultNames, resultDirectory, resultSize, resultLastModified)
184 | } catch (e: Exception) {
185 | e.printStackTrace()
186 | return null
187 | }
188 |
189 | }
190 |
191 | }
--------------------------------------------------------------------------------
/src/main/java/balti/filex/filex11/operators/Info.kt:
--------------------------------------------------------------------------------
1 | package balti.filex.filex11.operators
2 |
3 | import android.net.Uri
4 | import android.os.Build
5 | import android.provider.DocumentsContract
6 | import android.system.Os
7 | import balti.filex.FileXInit
8 | import balti.filex.FileXInit.Companion.fCResolver
9 | import balti.filex.FileXInit.Companion.storageVolumes
10 | import balti.filex.FileXInit.Companion.tryIt
11 | import balti.filex.Tools.removeRearSlash
12 | import balti.filex.filex11.FileX11
13 | import balti.filex.filex11.utils.Tools
14 | import balti.filex.filex11.utils.Tools.convertToDocumentUri
15 | import balti.filex.filex11.utils.Tools.deduceVolumePathForLollipop
16 | import balti.filex.filex11.utils.Tools.getStringQuery
17 |
18 | internal class Info(private val f: FileX11) {
19 |
20 | val canonicalPath: String get() = f.run { "${volumePath}${storagePath}" }
21 | val absolutePath: String get() = canonicalPath
22 | val rootPath: String get() = canonicalPath.let { it.substring(0, it.indexOf(f.path)) }
23 |
24 | val storagePath: String
25 | get() = f.run {
26 | rootDocumentId.split(":").let {
27 | if (it.size > 1 && it[1].isNotBlank()) "/${removeRearSlash(it[1])}$path" else path
28 | }
29 | }
30 |
31 | val volumePath: String
32 | get() = f.run {
33 | FileXInit.refreshStorageVolumes()
34 |
35 | // some file providers (Example: the Termux app) provide direct file path as the rootDocumentId
36 | if (!rootDocumentId.contains(":") && rootDocumentId.startsWith("/")) {
37 | rootDocumentId
38 | }
39 |
40 | // all other normal usage
41 | else rootDocumentId.split(":").let {
42 | //Log.d(FileXInit.DEBUG_TAG, "rootDocId: $rootDocumentId split: $it")
43 | var volPathFound: String = if (it.isNotEmpty()) {
44 | val uuid = it[0]
45 | storageVolumes[uuid] ?: ""
46 | } else ""
47 |
48 | // For Android L
49 | if (volPathFound.isBlank() && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
50 | volPathFound = this.deduceVolumePathForLollipop()
51 | }
52 |
53 | volPathFound
54 | }
55 | }
56 |
57 | fun exists(): Boolean = f.run{
58 |
59 | fun checkUriExists() = uri?.let { Tools.checkUriExists(it) } ?: false
60 |
61 | return when {
62 | uri == null -> {
63 | refreshFileX11()
64 | checkUriExists()
65 | }
66 | checkUriExists() -> true
67 | else -> {
68 | refreshFileX11()
69 | checkUriExists()
70 | }
71 | }
72 | }
73 |
74 | val isDirectory: Boolean
75 | get() =
76 | exists() && try {
77 | f.getStringQuery(DocumentsContract.Document.COLUMN_MIME_TYPE) == DocumentsContract.Document.MIME_TYPE_DIR
78 | } catch (_: Exception) {
79 | false
80 | }
81 |
82 | val isFile: Boolean
83 | get() =
84 | exists() && try {
85 | f.getStringQuery(DocumentsContract.Document.COLUMN_MIME_TYPE) != DocumentsContract.Document.MIME_TYPE_DIR
86 | } catch (_: Exception) {
87 | false
88 | }
89 |
90 | val name: String
91 | get() = f.run {
92 | return when {
93 | path.isNotBlank() -> path.split("/").let { it[it.size - 1] }
94 | else -> try {
95 | getStringQuery(DocumentsContract.Document.COLUMN_DISPLAY_NAME) ?: ""
96 | } catch (_: Exception) {
97 | ""
98 | }
99 | }
100 | }
101 |
102 | val parent: String?
103 | get() = f.run {
104 | return when {
105 | path == "/" -> null
106 | path.indexOf('/') != path.lastIndexOf('/') -> path.substring(0, path.lastIndexOf("/"))
107 | else -> "/"
108 | }
109 | }
110 |
111 | val parentFile: FileX11? get() = parent?.let { FileX11(it, currentRootUri = f.rootUri) }
112 |
113 | val parentUri: Uri? get() = if (parent != "/") parentFile?.uri else f.rootUri?.let { convertToDocumentUri(it) }
114 |
115 | val parentCanonical: String get() = canonicalPath.let { if (it.isNotBlank()) it.substring(0, it.lastIndexOf("/")) else "/" }
116 |
117 | fun length(): Long = try {
118 | f.getStringQuery(DocumentsContract.Document.COLUMN_SIZE)!!.toLong()
119 | } catch (_: Exception) {
120 | 0L
121 | }
122 |
123 | fun lastModified(): Long = try {
124 | f.getStringQuery(DocumentsContract.Document.COLUMN_LAST_MODIFIED)!!.toLong()
125 | } catch (_: Exception) {
126 | 0L
127 | }
128 |
129 | fun canRead(): Boolean {
130 | fCResolver.persistedUriPermissions.forEach {
131 | if (f.uri.toString().startsWith(it.uri.toString())) return it.isReadPermission
132 | }
133 | return false
134 | }
135 |
136 | fun canWrite(): Boolean {
137 | fCResolver.persistedUriPermissions.forEach {
138 | if (f.uri.toString().startsWith(it.uri.toString())) return it.isWritePermission
139 | }
140 | return false
141 | }
142 |
143 | val freeSpace: Long get() = getSpace(Space.FREE)
144 | val usableSpace: Long get() = getSpace(Space.AVAILABLE)
145 | val totalSpace: Long get() = getSpace(Space.TOTAL)
146 |
147 | val isHidden: Boolean get() = name.startsWith(".")
148 |
149 | val extension: String get() = name.substringAfterLast('.', "")
150 | val nameWithoutExtension: String get() = name.substringBeforeLast(".")
151 |
152 | //
153 | //
154 | // private methods
155 | // *****************************************
156 |
157 | private enum class Space {
158 | FREE, AVAILABLE, TOTAL
159 | }
160 |
161 | private fun getSpace(spaceType: Space): Long = f.run {
162 | if (rootUri == null) return 0L
163 | return try {
164 | val pfd = fCResolver.openFileDescriptor(DocumentsContract.buildDocumentUriUsingTree(rootUri!!, rootDocumentId), "r")
165 | val stats = Os.fstatvfs(pfd?.fileDescriptor)
166 | (when (spaceType) {
167 | Space.FREE -> stats.f_bfree
168 | Space.AVAILABLE -> stats.f_bavail
169 | Space.TOTAL -> stats.f_blocks
170 | } * stats.f_bsize).apply { tryIt { pfd?.close() } }
171 | } catch (e: Exception) {
172 | e.printStackTrace()
173 | 0L
174 | }
175 | }
176 | }
--------------------------------------------------------------------------------
/src/main/java/balti/filex/filex11/operators/Modify.kt:
--------------------------------------------------------------------------------
1 | package balti.filex.filex11.operators
2 |
3 | import android.os.Build
4 | import android.provider.DocumentsContract
5 | import balti.filex.FileX
6 | import balti.filex.FileXInit.Companion.fCResolver
7 | import balti.filex.FileXInit.Companion.tryIt
8 | import balti.filex.filex11.FileX11
9 | import balti.filex.filex11.FileXServer
10 | import balti.filex.filexTraditional.FileXT
11 |
12 | internal class Modify(private val f: FileX11) {
13 |
14 | fun renameTo(dest: FileX): Boolean {
15 | return when (dest) {
16 | is FileX11 -> renameTo(dest)
17 | is FileXT -> renameTo(dest)
18 | else -> false
19 | }
20 | }
21 |
22 | private fun renameTo(dest: FileX11): Boolean = f.run {
23 | if (dest.exists()) {
24 | if (dest.isFile) return false
25 | else if (!dest.isEmpty) return false
26 | }
27 | val parentFile = dest.parentFile
28 | parentFile?.mkdirs()
29 | return if (parentFile?.uri == null) false
30 | else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
31 | DocumentsContract.moveDocument(fCResolver, uri!!, parentUri!!, parentFile.uri!!).let { movedUri ->
32 | if (movedUri != null) {
33 | tryIt { DocumentsContract.renameDocument(fCResolver, movedUri, dest.name) }
34 | dest.refreshFile()
35 | if (dest.exists()) FileXServer.setPathAndUri(rootUri!!, path, dest.uri, dest.path)
36 | else FileXServer.setPathAndUri(rootUri!!, path, movedUri, dest.parent + "/" + name)
37 | true
38 | } else false
39 | }
40 | }
41 |
42 | // for Android M and below, copy and then delete.
43 | else {
44 | if (balti.filex.Copy(this).copyRecursively(dest, deleteAfterCopy = true)) {
45 | if (deleteRecursively()) return@run true
46 | }
47 | return@run false
48 | }
49 | }
50 |
51 | private fun renameTo(dest: FileXT): Boolean = f.run {
52 | return if (dest.canonicalPath.startsWith(rootPath)) {
53 | val relativePath = dest.canonicalPath.substring(rootPath.length)
54 | renameTo(FileX11(relativePath, rootUri))
55 | } else FileXT(canonicalPath).renameTo(dest)
56 | }
57 |
58 | fun renameTo(newFileName: String): Boolean = f.run{
59 | tryIt { DocumentsContract.renameDocument(fCResolver, uri!!, newFileName) }
60 | val newFilePath = "$parent/$newFileName"
61 | return FileX11(newFilePath, rootUri).let {
62 | if (exists()) {
63 | directlySetUriAndPath(it.uri, newFilePath)
64 | FileXServer.setPathAndUri(rootUri!!, path, it.uri, newFilePath)
65 | true
66 | } else false
67 | }
68 | }
69 | }
--------------------------------------------------------------------------------
/src/main/java/balti/filex/filex11/operators/Operations.kt:
--------------------------------------------------------------------------------
1 | package balti.filex.filex11.operators
2 |
3 | import android.provider.DocumentsContract
4 | import balti.filex.FileXInit.Companion.fCResolver
5 | import balti.filex.filex11.FileX11
6 | import balti.filex.filex11.utils.Tools
7 | import balti.filex.filex11.utils.Tools.buildTreeDocumentUriFromId
8 | import balti.filex.filex11.utils.Tools.getChildrenUri
9 | import java.io.InputStream
10 | import java.io.OutputStream
11 |
12 | internal fun FileX11.refreshFileX11(){
13 | val dirs = if (path.length > 1) path.substring(1).split("/") else ArrayList(0)
14 | val projection = arrayOf(DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_DOCUMENT_ID)
15 | var childrenUri = getChildrenUri(rootUri!!)
16 | for (i in dirs.indices) {
17 | val dir = dirs[i]
18 | var nextDocId = ""
19 | try {
20 | fCResolver.query(childrenUri, projection, null, null, null)?.run {
21 | while (moveToNext()) {
22 | if (getString(0) == dir) {
23 | nextDocId = getString(1)
24 | break
25 | }
26 | }
27 | close()
28 | }
29 | }
30 | catch (_: Exception){
31 | break
32 | }
33 | if (i < dirs.indices.last) childrenUri = getChildrenUri(nextDocId)
34 | else if (nextDocId != "") {
35 | val toSetUri = buildTreeDocumentUriFromId(nextDocId)
36 | directlySetUriAndPath(toSetUri, null)
37 | balti.filex.filex11.FileXServer.setPathAndUri(rootUri!!, path, toSetUri)
38 | }
39 | }
40 | }
41 |
42 | internal class Operations(private val f: FileX11) {
43 |
44 | fun inputStream(): InputStream? = f.run {
45 | refreshFile()
46 | uri?.let { fCResolver.openInputStream(it) }
47 | }
48 |
49 | fun outputStream(mode: String): OutputStream? = f.run {
50 | refreshFile()
51 | uri?.let { fCResolver.openOutputStream(it, mode) }
52 | }
53 | }
--------------------------------------------------------------------------------
/src/main/java/balti/filex/filex11/publicInterfaces/FileXFilter.kt:
--------------------------------------------------------------------------------
1 | package balti.filex.filex11.publicInterfaces
2 |
3 | import balti.filex.FileX
4 | import balti.filex.filex11.FileX11
5 | import balti.filex.filexTraditional.FileXT
6 |
7 | /**
8 | * This interface is made to replicate logic of [java.io.FileFilter].
9 | * It is used to provide logic to filter files in all FileX listing methods. This logic is provided in the [accept] method.
10 | *
11 | * This interface provides a unified way to deal with both [FileXT] and [FileX11] types.
12 | */
13 | interface FileXFilter {
14 |
15 | /**
16 | * Function to be overridden to define the logic to filter files in file lists.
17 | *
18 | * @param file The FileX object, pointinig to a file / directory to be evaluated with the filtering logic inside the method.
19 | * An alternate interface [FileXNameFilter] is also present, which focuses on the string name of the files, rather than FileX objects.
20 | *
21 | * @return `true` if the file qualifies to be included in the filtered list of files, `false` otherwise.
22 | */
23 | fun accept(file: FileX): Boolean
24 | }
--------------------------------------------------------------------------------
/src/main/java/balti/filex/filex11/publicInterfaces/FileXNameFilter.kt:
--------------------------------------------------------------------------------
1 | package balti.filex.filex11.publicInterfaces
2 |
3 | import balti.filex.FileX
4 | import balti.filex.filexTraditional.FileXT
5 | import balti.filex.filex11.FileX11
6 |
7 | /**
8 | * This interface is made to replicate logic of [java.io.FilenameFilter].
9 | * It is used to provide logic to filter files in all FileX listing methods. This logic is provided in the [accept] method.
10 | *
11 | * This interface provides a unified way to deal with both [FileXT] and [FileX11] types.
12 | */
13 | interface FileXNameFilter {
14 |
15 | /**
16 | * Function to be overridden to define the logic to filter files in file lists.
17 | *
18 | * @param dir Directory in which the file to be evaluated for the logic was found in.
19 | * @param name Name of the file to be evaluated with the filtering logic.
20 | * If actual FileX object is required for the logic, please check out [FileXFilter], rather than creating a FileX instance for every new file.
21 | *
22 | * @return `true` if the file qualifies to be included in the filtered list of files, `false` otherwise.
23 | */
24 | fun accept(dir: FileX, name: String): Boolean
25 | }
--------------------------------------------------------------------------------
/src/main/java/balti/filex/filex11/utils/Constants.kt:
--------------------------------------------------------------------------------
1 | package balti.filex.filex11.utils
2 |
3 | object Constants {
4 | internal val PREF_GLOBAL_ROOT_URI = "PREF_GLOBAL_ROOT_URI"
5 | internal val MNT_MEDIA_RW = "/mnt/media_rw"
6 | internal val STOARGE_RAW_PATH = "/storage"
7 | }
--------------------------------------------------------------------------------
/src/main/java/balti/filex/filex11/utils/FileX11DeleteOnExit.kt:
--------------------------------------------------------------------------------
1 | package balti.filex.filex11.utils
2 |
3 | import balti.filex.FileX
4 | import java.util.*
5 |
6 | /**
7 | * This class was created to simulate file deletion like [java.io.File.deleteOnExit].
8 | * This was simply copied from `java.io.DeleteOnExitHook` and converted to kotlin by Android Studio's "Convert file to kotlin" feature.
9 | *
10 | * (the class `java.io.DeleteOnExitHook` cannot be linked for some reason, please check via [deleteOnExit()][java.io.File.deleteOnExit])
11 | *
12 | * - Theoretically:
13 | *
14 | * This class holds a set of filenames to be deleted on VM exit through a shutdown hook.
15 | * A set is used both to prevent double-insertion of the same file as well as offer
16 | * quick removal.
17 | */
18 | internal object FileX11DeleteOnExit {
19 | private var files: LinkedHashSet? = LinkedHashSet()
20 | @Synchronized
21 | fun add(file: FileX?) {
22 | checkNotNull(files) {
23 | // DeleteOnExitHook is running. Too late to add a file
24 | "Shutdown in progress"
25 | }
26 | files!!.add(file)
27 | }
28 |
29 | fun runHooks() {
30 | var theFiles: LinkedHashSet?
31 | synchronized(FileX11DeleteOnExit::class.java) {
32 | theFiles = files
33 | files = null
34 | }
35 | val toBeDeleted = ArrayList(theFiles)
36 |
37 | // reverse the list to maintain previous jdk deletion order.
38 | // Last in first deleted.
39 | toBeDeleted.reverse()
40 | for (file in toBeDeleted) {
41 | file?.delete();
42 | }
43 | }
44 |
45 | init {
46 | // BEGIN Android-changed: Use Runtime.addShutdownHook() rather than SharedSecrets.
47 | Runtime.getRuntime().addShutdownHook(object : Thread() {
48 | override fun run() {
49 | runHooks()
50 | }
51 | })
52 | // END Android-changed: Use Runtime.addShutdownHook() rather than SharedSecrets.
53 | }
54 | }
--------------------------------------------------------------------------------
/src/main/java/balti/filex/filex11/utils/RootUri.kt:
--------------------------------------------------------------------------------
1 | package balti.filex.filex11.utils
2 |
3 | import android.app.Activity
4 | import android.content.Intent
5 | import android.net.Uri
6 | import balti.filex.FileXInit.Companion.fContext
7 | import balti.filex.FileXInit.Companion.sharedPreferences
8 | import balti.filex.activity.ActivityFunctionDelegate
9 |
10 | internal object RootUri {
11 |
12 | fun setGlobalRootUri(afterJob: ((resultCode: Int, data: Intent?) -> Unit)? = null) {
13 | val uri = getGlobalRootUri()
14 | if (uri == null) resetGlobalRootUri(afterJob)
15 | else afterJob?.invoke(Activity.RESULT_OK, Intent().setData(uri))
16 | }
17 |
18 | fun resetGlobalRootUri(afterJob: ((resultCode: Int, data: Intent?) -> Unit)? = null) {
19 | val takeFlags =
20 | Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
21 | ActivityFunctionDelegate(10,
22 | Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
23 | flags = takeFlags
24 | }) { context, resultCode, data ->
25 | if (resultCode == Activity.RESULT_OK && data != null) {
26 | data.data?.let {
27 | context.contentResolver.takePersistableUriPermission(it, takeFlags)
28 | sharedPreferences.edit().run {
29 | putString(Constants.PREF_GLOBAL_ROOT_URI, it.toString())
30 | commit()
31 | }
32 | afterJob?.invoke(resultCode, data)
33 | } ?: afterJob?.invoke(resultCode, data)
34 | }
35 | else afterJob?.invoke(resultCode, data)
36 | }
37 | }
38 |
39 | fun getGlobalRootUri(): Uri? {
40 | val gr = sharedPreferences.getString(Constants.PREF_GLOBAL_ROOT_URI, "")
41 | if (gr == "") return null
42 | else {
43 | try {
44 | val uri = Uri.parse(gr)
45 | fContext.contentResolver.persistedUriPermissions.forEach {
46 | if (it.uri == uri && it.isReadPermission && it.isWritePermission) return uri
47 | }
48 | } catch (_: Exception) {
49 | }
50 | return null
51 | }
52 | }
53 | }
--------------------------------------------------------------------------------
/src/main/java/balti/filex/filex11/utils/Tools.kt:
--------------------------------------------------------------------------------
1 | package balti.filex.filex11.utils
2 |
3 | import android.annotation.TargetApi
4 | import android.content.ContentResolver
5 | import android.content.Context
6 | import android.net.Uri
7 | import android.os.Build
8 | import android.os.Environment
9 | import android.os.storage.StorageManager
10 | import android.os.storage.StorageVolume
11 | import android.provider.DocumentsContract
12 | import balti.filex.FileXInit
13 | import balti.filex.FileXInit.Companion.fContext
14 | import balti.filex.filex11.FileX11
15 | import balti.filex.filex11.utils.Constants.MNT_MEDIA_RW
16 | import balti.filex.filex11.utils.Constants.STOARGE_RAW_PATH
17 | import java.io.File
18 | import java.text.SimpleDateFormat
19 | import java.util.*
20 | import kotlin.collections.HashMap
21 |
22 | object Tools {
23 |
24 | private val PRIMARY_VOLUME_NAME = "primary"
25 | private val predefinedVolNames = HashMap().apply {
26 | this["downloads"] = "Download"
27 | this["home"] = "Documents"
28 | }
29 |
30 | internal fun getStorageVolumes(): HashMap {
31 | val allVolumes = HashMap(0)
32 | val mStorageManager: StorageManager = fContext.getSystemService(Context.STORAGE_SERVICE) as StorageManager
33 |
34 | // Android 11 and above
35 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
36 | try {
37 | val storageVolumes: List = mStorageManager.storageVolumes
38 | storageVolumes.forEach { storageVolume ->
39 | storageVolume.directory?.let {
40 | if (storageVolume.isPrimary) allVolumes[PRIMARY_VOLUME_NAME] = it.path
41 | val uuid: String? = storageVolume.uuid
42 | if (uuid != null) allVolumes[uuid] = it.path
43 | }
44 | }
45 | } catch (ex: Exception) {
46 | ex.printStackTrace()
47 | }
48 | }
49 | // lower android versions
50 | else {
51 |
52 | // next two lines common for all lower android versions
53 | Environment.getExternalStorageDirectory()?.absolutePath?.let { allVolumes[PRIMARY_VOLUME_NAME] = it }
54 | Environment.getExternalStorageDirectory()?.absolutePath?.let { storagePath ->
55 | predefinedVolNames.forEach { vol ->
56 | allVolumes[vol.key] = "$storagePath/${vol.value}"
57 | }
58 | }
59 |
60 | val SELF_NAME = "self"
61 | val EMULATED_NAME = "emulated"
62 |
63 | // for Android N and above, useful to get SD-CARD paths
64 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
65 | fun getIfExists(uuid: String): String {
66 | val expectedParents = arrayListOf(
67 | STOARGE_RAW_PATH,
68 | // Fill here with other known accessible paths, if available
69 | )
70 | expectedParents.forEach {
71 | if (File(it, uuid).exists()) return it
72 | }
73 | return ""
74 | }
75 | mStorageManager.storageVolumes.forEach { storageVolume ->
76 | storageVolume.uuid?.let { uuid ->
77 | getIfExists(uuid).let {
78 | if (it.isNotBlank()) allVolumes[uuid] = "$it/$uuid"
79 | else allVolumes[uuid] = "$MNT_MEDIA_RW/$uuid"
80 | }
81 | }
82 | }
83 | }
84 | // For Android M
85 | else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.M) {
86 | val storageDir = File(STOARGE_RAW_PATH)
87 | storageDir.list()?.run {
88 | forEach {
89 | if (it != SELF_NAME && it != EMULATED_NAME) {
90 | // check for USB devices.
91 | // Observation: USB OTG drives are mounted with executable flag off, but SDCARD is with executable on.
92 | // They are available under /storage/..., they are neither readable nor writable.
93 | // Oddly this location of the USB OTG drive (under /storage/...) is also not accessible with
94 | // any root explorer or root based processes. It always displays empty.
95 | // However USB OTG is also mounted at /mnt/media_rw, with same name.
96 | // This location is also not readable/writable, but is accessible to any root based file explorer.
97 | // Hence it is at-least somewhat usable than the location under /storage/...
98 | //if (it.toUpperCase(Locale.ROOT) == it && !it.contains('-')): older logic found incorrect.
99 | if (!File("$STOARGE_RAW_PATH/$it").canExecute())
100 | allVolumes[it] = "$MNT_MEDIA_RW/$it"
101 |
102 | // for SD-CARD
103 | else allVolumes[it] = "$STOARGE_RAW_PATH/$it"
104 | }
105 | }
106 | }
107 | }
108 | // No reliable way to find volumes for Android L
109 | // please check deduceVolumePathForLollipop() below which is called from Info.volumePath
110 |
111 | }
112 |
113 | return allVolumes
114 | }
115 |
116 | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
117 | internal fun FileX11.deduceVolumePathForLollipop(): String {
118 | val rawStorageDir = File(STOARGE_RAW_PATH)
119 |
120 | fun generateRandomFileName(): String {
121 | val currentTime = SimpleDateFormat("yyyyMMddHHmmssSSS", Locale.ROOT).format(Calendar.getInstance().time)
122 | val randomNumber = (10000000..99999999).random()
123 | return "$currentTime$randomNumber"
124 | }
125 |
126 | val randomFileName = generateRandomFileName()
127 |
128 | // create a random file in the root uri
129 | val testFile = FileX11(path = "/.$randomFileName", currentRootUri = rootUri)
130 | testFile.createNewFile()
131 | val testFileStoragePath = testFile.storagePath
132 | val testFileLastModified = testFile.lastModified()
133 |
134 | val filteredRawStorages = rawStorageDir.listFiles()?.filter { it.canWrite() } ?: listOf()
135 |
136 | // check for the above created file in all possible locations and return when found
137 | for (f in filteredRawStorages) {
138 | File(f, testFileStoragePath).run {
139 | if (exists() && testFileLastModified == lastModified()) {
140 | // found. delete and return.
141 | delete()
142 | return f.canonicalPath
143 | }
144 | }
145 | }
146 |
147 | // not found. delete and return empty string.
148 | testFile.delete()
149 | return ""
150 | }
151 |
152 | internal fun FileX11.buildTreeDocumentUriFromId(documentId: String): Uri{
153 | return Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(rootUri!!.authority)
154 | .appendPath("tree").appendPath(rootDocumentId)
155 | .appendPath("document").appendPath(documentId)
156 | .build()
157 | }
158 |
159 | internal fun FileX11.getStringQuery(field: String, documentUri: Uri = this.uri?: Uri.EMPTY): String? {
160 | if (uri == Uri.EMPTY) return null
161 | return documentUri.let { uri ->
162 | val cursor = FileXInit.fCResolver.query(uri, arrayOf(field), null, null, null)
163 | cursor?.moveToFirst()
164 | cursor?.getString(0).apply {
165 | cursor?.close()
166 | }
167 | }
168 | }
169 |
170 | internal fun FileX11.getStringQuery(field: String, documentId: String): String? =
171 | getStringQuery(field, buildTreeDocumentUriFromId(documentId))
172 |
173 | internal fun convertToDocumentUri(uri: Uri): Uri? {
174 | return if (DocumentsContract.isDocumentUri(fContext, uri)) uri
175 | else try {
176 | DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri))
177 | }
178 | catch (_: Exception) { null }
179 | }
180 |
181 | internal fun checkUriExists(uri: Uri, checkIfDirectory: Boolean = false): Boolean{
182 | var result = false
183 | val evalUri = convertToDocumentUri(uri) ?: return false
184 | val projection = if (checkIfDirectory) arrayOf(DocumentsContract.Document.COLUMN_MIME_TYPE) else null
185 | try {
186 | val c = FileXInit.fCResolver.query(evalUri, projection, null, null, null, null)
187 | if (c != null && c.count > 0 && c.moveToFirst()) result = c.columnCount != 0
188 | if (result && checkIfDirectory) result = c?.getString(0) == DocumentsContract.Document.MIME_TYPE_DIR
189 | c?.close()
190 | }
191 | catch (e: Exception){
192 | e.printStackTrace()
193 | }
194 | return result
195 | }
196 |
197 | internal fun FileX11.getChildrenUri(docId: Uri): Uri {
198 | return DocumentsContract.buildChildDocumentsUriUsingTree(
199 | rootUri,
200 | if (docId == rootUri) DocumentsContract.getTreeDocumentId(rootUri)
201 | else DocumentsContract.getDocumentId(docId)
202 | )
203 | }
204 | internal fun FileX11.getChildrenUri(docId: String): Uri {
205 | return DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, docId)
206 | }
207 | }
--------------------------------------------------------------------------------
/src/main/java/balti/filex/filexTraditional/FileXT.kt:
--------------------------------------------------------------------------------
1 | package balti.filex.filexTraditional
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import balti.filex.FileX
6 | import balti.filex.Quad
7 | import balti.filex.Tools.removeTrailingSlashOrColonAddFrontSlash
8 | import balti.filex.exceptions.DirectoryHierarchyBroken
9 | import balti.filex.exceptions.ImproperFileXType
10 | import balti.filex.filex11.publicInterfaces.FileXFilter
11 | import balti.filex.filex11.publicInterfaces.FileXNameFilter
12 | import balti.filex.filexTraditional.operators.Filter
13 | import balti.filex.filexTraditional.operators.Modify
14 | import java.io.File
15 | import java.io.FileOutputStream
16 | import java.io.InputStream
17 | import java.io.OutputStream
18 |
19 | internal class FileXT(path: String): FileX(false) {
20 |
21 | internal constructor(file: File): this(file.canonicalPath)
22 |
23 | override var path: String = ""
24 | internal set
25 |
26 | override lateinit var file: File
27 |
28 | init {
29 | this.path = path
30 | refreshFile()
31 | }
32 |
33 | override fun refreshFile() {
34 | this.path = removeTrailingSlashOrColonAddFrontSlash(path)
35 | file = File(this.path)
36 | }
37 |
38 | override val uri: Uri? get() = Uri.fromFile(file)
39 | override val canonicalPath: String = file.canonicalPath
40 | override val absolutePath: String = file.absolutePath
41 | override fun exists(): Boolean = file.exists()
42 | override val isDirectory: Boolean = file.isDirectory
43 | override val isFile: Boolean = file.isFile
44 | override val name: String = file.name
45 | override val parent: String? = file.parent
46 | override val parentFile: FileX? = file.parentFile?.let { FileXT(it.canonicalPath)}
47 | override val storagePath: String? = null
48 | override val volumePath: String? = null
49 | override val parentCanonical: String = canonicalPath.let { if (it.isNotBlank()) it.substring(0, it.lastIndexOf("/")) else "/" }
50 | override fun length(): Long = file.length()
51 | override fun lastModified(): Long = file.lastModified()
52 | override fun canRead(): Boolean = file.canRead()
53 | override fun canWrite(): Boolean = file.canWrite()
54 | override val freeSpace: Long = file.freeSpace
55 | override val usableSpace: Long = file.usableSpace
56 | override val totalSpace: Long = file.totalSpace
57 | override val isHidden: Boolean = file.isHidden
58 | override val extension: String = file.extension
59 | override val nameWithoutExtension: String = file.nameWithoutExtension
60 | override val parentUri: Uri? = null
61 | override fun canExecute(): Boolean = file.canExecute()
62 | override val rootPath: String? = null
63 |
64 | override fun delete(): Boolean = file.delete()
65 | override fun deleteOnExit() = file.deleteOnExit()
66 | override fun deleteRecursively(): Boolean = file.deleteRecursively()
67 |
68 | override fun createNewFile(): Boolean {
69 | if (parentFile?.exists() != true) throw DirectoryHierarchyBroken("No such file or directory")
70 | return file.createNewFile()
71 | }
72 | override fun createNewFile(makeDirectories: Boolean, overwriteIfExists: Boolean, optionalMimeType: String): Boolean {
73 | if (makeDirectories){
74 | parentFile.let { it?.mkdirs() }
75 | }
76 | else {
77 | if (parentFile?.exists() != true) throw DirectoryHierarchyBroken("No such file or directory")
78 | }
79 | if (overwriteIfExists) file.delete()
80 | return file.createNewFile()
81 | }
82 |
83 | override fun mkdir(): Boolean = file.mkdir()
84 | override fun mkdirs(): Boolean = file.mkdirs()
85 | override fun createFileUsingPicker(optionalMimeType: String, afterJob: ((resultCode: Int, data: Intent?) -> Unit)?) {
86 | throw ImproperFileXType("Not applicable on traditional FileX")
87 | }
88 |
89 | private val Modify = Modify(this)
90 | override fun renameTo(dest: FileX): Boolean = Modify.renameTo(dest)
91 | override fun renameTo(newFileName: String): Boolean = Modify.renameTo(newFileName)
92 |
93 | private val Filter = Filter(this)
94 |
95 | override val isEmpty: Boolean get () = Filter.isEmpty
96 | override fun listFiles(): Array? = Filter.listFiles()
97 | override fun listFiles(filter: FileXFilter): Array? = Filter.listFiles(filter)
98 | override fun listFiles(filter: FileXNameFilter): Array? = Filter.listFiles(filter)
99 | override fun list() = Filter.list()
100 | override fun list(filter: FileXFilter): Array? = Filter.list(filter)
101 | override fun list(filter: FileXNameFilter): Array? = Filter.list(filter)
102 | override fun listEverythingInQuad(): List>? = Filter.listEverythingInQuad()
103 | override fun listEverything(): Quad, List, List, List>? = Filter.listEverything()
104 |
105 | override fun inputStream(): InputStream = file.inputStream()
106 | override fun outputStream(mode: String): OutputStream = FileOutputStream(file, mode == "wa")
107 | }
--------------------------------------------------------------------------------
/src/main/java/balti/filex/filexTraditional/operators/Filter.kt:
--------------------------------------------------------------------------------
1 | package balti.filex.filexTraditional.operators
2 |
3 | import balti.filex.FileX
4 | import balti.filex.Quad
5 | import balti.filex.filex11.publicInterfaces.FileXFilter
6 | import balti.filex.filex11.publicInterfaces.FileXNameFilter
7 | import balti.filex.filexTraditional.FileXT
8 | import java.io.File
9 |
10 | internal class Filter(private val f: FileXT) {
11 |
12 | val isEmpty: Boolean
13 | get() = f.run {
14 | if (isDirectory) {
15 | file.list()?.isEmpty() ?: false
16 | } else false
17 | }
18 |
19 | fun listFiles(filter: FileXFilter): Array? = convertToFileXArray(f.file.listFiles { file ->
20 | filter.accept(FileXT(file))
21 | })
22 |
23 | fun listFiles(filter: FileXNameFilter): Array? = convertToFileXArray(f.file.listFiles {
24 | dir, name -> filter.accept(FileXT(dir), name)
25 | })
26 |
27 | fun listFiles() = convertToFileXArray(f.file.listFiles())
28 |
29 | fun list(filter: FileXFilter): Array? = f.run {
30 | file.listFiles()?.filter { filter.accept(FileXT(it)) }?.map { it.name }?.toTypedArray()
31 | }
32 | fun list(filter: FileXNameFilter): Array? = convertToStringArray(f.file.listFiles{
33 | dir, name -> filter.accept(FileXT(dir), name)
34 | })
35 |
36 | fun list(): Array? = f.file.list()
37 |
38 | fun listEverythingInQuad(): List>? = f.run {
39 | val results = ArrayList>(0)
40 |
41 | f.file.listFiles()?.forEach {
42 | val name = it.name
43 | val isDirectory = it.isDirectory
44 | val size = it.length()
45 | val lastModified = it.lastModified()
46 | val entry = Quad(name, isDirectory, size, lastModified)
47 | results.add(entry)
48 | } ?: return null
49 |
50 | return results
51 | }
52 |
53 | fun listEverything(): Quad, List, List, List>? = f.run {
54 | val resultNames = ArrayList(0)
55 | val resultDirectory = ArrayList(0)
56 | val resultSize = ArrayList(0)
57 | val resultLastModified = ArrayList(0)
58 |
59 | f.file.listFiles()?.forEach {
60 | val name = it.name
61 | val isDirectory = it.isDirectory
62 | val size = it.length()
63 | val lastModified = it.lastModified()
64 |
65 | resultNames.add(name)
66 | resultDirectory.add(isDirectory)
67 | resultSize.add(size)
68 | resultLastModified.add(lastModified)
69 | } ?: return null
70 |
71 | return Quad(resultNames, resultDirectory, resultSize, resultLastModified)
72 | }
73 |
74 | private fun convertToFileXArray(files: Array?): Array? = files?.map { FileXT(it) }?.toTypedArray()
75 | private fun convertToStringArray(files: Array?): Array? = files?.map { it.name }?.toTypedArray()
76 | }
--------------------------------------------------------------------------------
/src/main/java/balti/filex/filexTraditional/operators/Modify.kt:
--------------------------------------------------------------------------------
1 | package balti.filex.filexTraditional.operators
2 |
3 | import balti.filex.FileX
4 | import balti.filex.filex11.FileX11
5 | import balti.filex.filexTraditional.FileXT
6 | import java.io.File
7 |
8 | internal class Modify(private val f: FileXT) {
9 | fun renameTo(dest: FileX): Boolean{
10 | return when (dest) {
11 | is FileXT -> renameTo(dest)
12 | is FileX11 -> renameTo(dest)
13 | else -> false
14 | }
15 | }
16 |
17 | private fun renameTo(dest: FileXT): Boolean = f.file.renameTo(dest.file)
18 | private fun renameTo(dest: FileX11): Boolean = f.run {
19 | val fx11Path: String? = if (dest.exists()) null else dest.canonicalPath
20 | return fx11Path?.let { file.renameTo(File(it)) } ?: false
21 | }
22 |
23 | fun renameTo(newFileName: String): Boolean = f.run{
24 | parentFile?.let {
25 | val newFile = File(it.file, newFileName)
26 | val res = file.renameTo(newFile)
27 | if (res) {
28 | file = newFile
29 | path = try {
30 | path.let { it.substring(0, it.lastIndexOf('/')) } + "/" + newFileName
31 | } catch (_ : Exception){
32 | file.path
33 | }
34 | }
35 | res
36 | }?: false
37 | }
38 | }
--------------------------------------------------------------------------------
/src/main/java/rough.md:
--------------------------------------------------------------------------------
1 | ### NOTE TO MYSELF:
2 | - To regenerate tables from `doc_assets`: https://www.tablesgenerator.com/markdown_tables#
3 | - Generate new jitpack badge: https://shields.io/category/version
4 | - Regenerate jitpack badge: https://shields.io/jitpack/v/github/SayantanRC/FileX
5 | - Markdown preview: https://jbt.github.io/markdown-editor/
6 |
7 | ### VERSION UPDATE
8 | 1. `README.md` : Update the jitpack release name in the `Getting started` section.
9 | 2. `README.md` : Check if the jitpack badge is updated. If not, regenerate the badge.
10 | 3. Change `versionCode` and `versionName` in build.gradle.
--------------------------------------------------------------------------------
/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
--------------------------------------------------------------------------------