├── .github └── workflows │ └── android.yml ├── .gitignore ├── .idea ├── .gitignore ├── .name ├── compiler.xml └── misc.xml ├── LICENSE ├── README.md ├── android13dataobbDemo.apk ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── android │ │ └── test │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ ├── android │ │ │ └── test │ │ │ │ ├── AppSelectDialogFragment.kt │ │ │ │ ├── AppSelectRecyclerAdapter.java │ │ │ │ ├── DocumentVM.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── MyGlideModule.kt │ │ │ │ └── TestApp.java │ │ │ └── github │ │ │ └── k1rakishou │ │ │ └── fsaf_test_app │ │ │ ├── TestBaseDirectory.kt │ │ │ ├── extensions │ │ │ └── Extensions.kt │ │ │ └── tests │ │ │ ├── BadPathSymbolResolutionTest.kt │ │ │ ├── BaseTest.kt │ │ │ ├── CopyTest.kt │ │ │ ├── CreateFilesTest.kt │ │ │ ├── DeleteTest.kt │ │ │ ├── FindTests.kt │ │ │ ├── SimpleTest.kt │ │ │ ├── SnapshotTest.kt │ │ │ ├── TestException.kt │ │ │ └── TestSuite.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable-xxxhdpi │ │ └── file_icon_apk.png │ │ ├── drawable │ │ ├── ic_check_circle_green_24dp.xml │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── dialog_fragment_app.xml │ │ └── item_select_app.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── android │ └── test │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── source ├── com.folderv.file.webp ├── coolapk-badge.png └── google-play-badge.png /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: set up JDK 11 17 | uses: actions/setup-java@v3 18 | with: 19 | java-version: '11' 20 | distribution: 'temurin' 21 | cache: gradle 22 | 23 | - name: Grant execute permission for gradlew 24 | run: chmod +x gradlew 25 | - name: Build with Gradle 26 | run: ./gradlew assembleDebug 27 | - name: Upload APK 28 | uses: actions/upload-artifact@v3 29 | if: ${{ !github.head_ref }} 30 | with: 31 | name: apk-debug 32 | path: app/build/outputs/apk/debug/app-debug.apk 33 | - name: Build aab with Gradle 34 | run: ./gradlew bundleDebug 35 | - name: Upload aab 36 | uses: actions/upload-artifact@v3 37 | if: ${{ !github.head_ref }} 38 | with: 39 | name: apk-debug 40 | path: app/build/outputs/bundle/debug/app-debug.aab -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.aar 4 | *.ap_ 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | # Uncomment the following line in case you need and you don't have the release build type files in your app 18 | # release/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # IntelliJ 40 | *.iml 41 | .idea/workspace.xml 42 | .idea/tasks.xml 43 | .idea/gradle.xml 44 | .idea/assetWizardSettings.xml 45 | .idea/dictionaries 46 | .idea/libraries 47 | # Android Studio 3 in .gitignore file. 48 | .idea/caches 49 | .idea/modules.xml 50 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 51 | .idea/navEditor.xml 52 | 53 | # Keystore files 54 | # Uncomment the following lines if you do not want to check your keystore files in. 55 | #*.jks 56 | #*.keystore 57 | 58 | # External native build folder generated in Android Studio 2.2 and later 59 | .externalNativeBuild 60 | .cxx/ 61 | 62 | # Google Services (e.g. APIs or Firebase) 63 | # google-services.json 64 | 65 | # Freeline 66 | freeline.py 67 | freeline/ 68 | freeline_project_description.json 69 | 70 | # fastlane 71 | fastlane/report.xml 72 | fastlane/Preview.html 73 | fastlane/screenshots 74 | fastlane/test_output 75 | fastlane/readme.md 76 | 77 | # Version control 78 | vcs.xml 79 | 80 | # lint 81 | lint/intermediates/ 82 | lint/generated/ 83 | lint/outputs/ 84 | lint/tmp/ 85 | # lint/reports/ 86 | 87 | 88 | # Android Studio 89 | .gradle 90 | /local.properties 91 | /.idea/caches 92 | /.idea/libraries 93 | /.idea/modules.xml 94 | /.idea/workspace.xml 95 | /.idea/navEditor.xml 96 | /.idea/assetWizardSettings.xml 97 | .DS_Store 98 | /build 99 | /captures 100 | .cxx 101 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Android13AccessDataObbWithoutRoot -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Deprecated 3 | 4 | Read this new method to access /Android/data on Android 14 with shizuku: 5 | 6 | http://folderv.com/2023/11/24/access-Android-data-and-Android-obb-on-Android-14/ 7 | 8 | 9 | 10 | 11 | ## `Android 13+ (Tiramisu API 33+)` Read and write /Android/data or /Android/obb not need root 12 | 13 | 14 | 15 | 16 | - `Android 13` read and write `/Android/data` or `/Android/obb` ***without root*** 17 | 18 | [![Android CI](https://github.com/folderv/androidDataWithoutRootAPI33/actions/workflows/android.yml/badge.svg)](https://github.com/folderv/androidDataWithoutRootAPI33/actions/workflows/android.yml) 19 | 20 | 21 | ### Video: 22 | [![Watch the video](https://img.youtube.com/vi/-4H0K70WhDg/maxresdefault.jpg)](https://youtu.be/-4H0K70WhDg) 23 | 24 | 25 | ### My App FV File Manager 26 | 27 | May be this is the first APP that can visit other app’s exteral data file 😀. 28 | 29 | 30 | [](https://www.coolapk.com/apk/com.folderv.file) 31 | [](https://play.google.com/store/apps/details?id=com.folderv.file) 32 | 33 | [](http://folderv.com/2022/08/16/Access-Android-data-on-Android-13/) 34 | 35 | Want to [Download](http://folderv.com/2022/08/16/Access-Android-data-on-Android-13/) ? 36 | 37 | 38 | ## Demo apk 39 | 40 | [Demo](https://github.com/folderv/androidDataWithoutRootAPI33/raw/main/android13dataobbDemo.apk) 41 | 42 | 43 | ## 中文 44 | 45 | Android 13+ 仍然可以访问 /Android/data 和 /Android/obb 目录 46 | 47 | 48 | Android 13 应用不能直接授权访问/Android/data 和 /Android/obb,需要针对每个应用单独授权访问。 49 | 50 | 该方法首先在Android 13 beta版本上发现并验证,Android 13正式版发布之后没有改变。所以 __FV 文件管理是首个支持Android 13 系统免root访问 /Android/data/ 和 /Android/obb/ 目录的工具__。 51 | 52 | -------------------------------------------------------------------------------- /android13dataobbDemo.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folderv/androidDataWithoutRootAPI33/230a2c445e1590b4340109da25ed976e80b3fbdb/android13dataobbDemo.apk -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | id 'kotlin-kapt' 5 | 6 | } 7 | 8 | android { 9 | compileSdk 33 10 | 11 | defaultConfig { 12 | applicationId "com.android.api33dataobb" 13 | minSdk 21 14 | targetSdk 33 15 | versionCode 1 16 | versionName "1.0" 17 | multiDexEnabled true 18 | 19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 20 | } 21 | 22 | lint { 23 | checkReleaseBuilds false 24 | // Or, if you prefer, you can continue to check for errors in release builds, 25 | // but continue the build even when errors are found: 26 | abortOnError false 27 | } 28 | buildTypes { 29 | release { 30 | minifyEnabled false 31 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 32 | } 33 | } 34 | compileOptions { 35 | sourceCompatibility JavaVersion.VERSION_1_8 36 | targetCompatibility JavaVersion.VERSION_1_8 37 | } 38 | kotlinOptions { 39 | jvmTarget = '1.8' 40 | } 41 | } 42 | 43 | dependencies { 44 | 45 | implementation 'androidx.core:core-ktx:1.9.0' 46 | implementation 'androidx.appcompat:appcompat:1.6.1' 47 | implementation 'com.google.android.material:material:1.8.0' 48 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 49 | 50 | // https://github.com/K1rakishou/Fuck-Storage-Access-Framework 51 | implementation 'com.github.K1rakishou:Fuck-Storage-Access-Framework:v1.1.3' 52 | 53 | implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.7' 54 | implementation 'com.simplecityapps:recyclerview-fastscroll:2.0.1' 55 | implementation 'com.github.bumptech.glide:glide:4.15.1' 56 | //annotationProcessor "com.github.bumptech.glide:compiler:4.15.1" 57 | kapt 'com.github.bumptech.glide:compiler:4.15.1' 58 | 59 | def appiconloaderVersion = "1.5.0"//1.4.0 60 | // For using with Glide. 61 | implementation "me.zhanghai.android.appiconloader:appiconloader-glide:$appiconloaderVersion" 62 | // For using with Coil. 63 | //implementation "me.zhanghai.android.appiconloader:appiconloader-coil:$appiconloaderVersion" 64 | // For using AppIconLoader directly. 65 | implementation "me.zhanghai.android.appiconloader:appiconloader:$appiconloaderVersion" 66 | // For using Launcher3 iconloaderlib directly. 67 | implementation "me.zhanghai.android.appiconloader:appiconloader-iconloaderlib:$appiconloaderVersion" 68 | 69 | implementation 'androidx.multidex:multidex:2.0.1' 70 | implementation 'androidx.recyclerview:recyclerview:1.3.0' 71 | 72 | def spider_man = "v1.1.9" 73 | debugImplementation "com.github.simplepeng.SpiderMan:spiderman:${spider_man}" 74 | releaseImplementation "com.github.simplepeng.SpiderMan:spiderman-no-op:${spider_man}" 75 | 76 | testImplementation 'junit:junit:4.13.2' 77 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 78 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 79 | } 80 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/android/test/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.android.test 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.android.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/test/AppSelectDialogFragment.kt: -------------------------------------------------------------------------------- 1 | package com.android.test 2 | 3 | import android.app.Dialog 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.os.Bundle 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.widget.ImageView 10 | import androidx.appcompat.app.AppCompatDialogFragment 11 | import androidx.recyclerview.widget.RecyclerView 12 | import com.bumptech.glide.Glide 13 | import com.chad.library.adapter.base.BaseQuickAdapter 14 | import com.chad.library.adapter.base.viewholder.BaseViewHolder 15 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 16 | import java.io.File 17 | 18 | class AppSelectDialogFragment : AppCompatDialogFragment() { 19 | 20 | var isAndroidData = true 21 | 22 | companion object { 23 | const val TAG = "AppSelectDialogFragment" 24 | 25 | const val ANDROID_DATA = "androidData" 26 | 27 | const val DOCID_ANDROID_DATA = "primary:Android/data" 28 | const val DOCID_ANDROID_OBB = "primary:Android/obb" 29 | 30 | fun newInstance(androidData: Boolean): AppSelectDialogFragment { 31 | val asd = AppSelectDialogFragment() 32 | val bundle = Bundle().apply { 33 | putBoolean(ANDROID_DATA, androidData) 34 | } 35 | asd.arguments = bundle 36 | return asd 37 | } 38 | } 39 | 40 | private var adapter: AppAdapter? = null 41 | var onSelectedListener: OnSelectedListener? = null 42 | 43 | interface OnSelectedListener { 44 | fun onSelected(appItem: AppItem?) 45 | } 46 | 47 | override fun onCreate(savedInstanceState: Bundle?) { 48 | super.onCreate(savedInstanceState) 49 | val act = activity 50 | if (act != null) { 51 | adapter = AppAdapter(arrayListOf()) 52 | } 53 | arguments?.let { 54 | isAndroidData = it.getBoolean(ANDROID_DATA) 55 | } 56 | } 57 | 58 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 59 | val act = activity 60 | if (act != null) { 61 | val inflater = LayoutInflater.from(act) 62 | val layout = inflater.inflate(R.layout.dialog_fragment_app, null) 63 | val listAppItem = layout.findViewById(R.id.listApp) as RecyclerView 64 | if (adapter == null) { 65 | adapter = AppAdapter(arrayListOf()) 66 | } 67 | 68 | Thread { 69 | val activ = activity 70 | activ ?: return@Thread 71 | val dataPkg = hashMapOf() 72 | val obbPkg = hashMapOf() 73 | 74 | val pm = activ.packageManager ?: return@Thread 75 | val pkgList = arrayListOf() 76 | val pkgSet = HashSet() 77 | 78 | val mainIntent = Intent(Intent.ACTION_MAIN) 79 | mainIntent.addCategory(Intent.CATEGORY_LAUNCHER) 80 | val apps = pm.queryIntentActivities(mainIntent, 0) //PackageManager.MATCH_ALL 81 | for (app in apps) { 82 | val appPkg: String = app.activityInfo.packageName 83 | if (!pkgSet.contains(appPkg)) { 84 | val item = AppItem() 85 | item.pkg = appPkg 86 | item.name = app.activityInfo.loadLabel(pm).toString() 87 | item.desk = true 88 | if (isAndroidData) { 89 | val dir = File("/storage/emulated/0/Android/data", appPkg) 90 | item.exits = dir!=null && dir.exists() 91 | item.hasPermission = dataPkg.contains(appPkg) 92 | if (item.hasPermission) { 93 | item.uri = dataPkg[appPkg] 94 | } 95 | } else { 96 | val dir = File("/storage/emulated/0/Android/obb", appPkg) 97 | item.exits = dir!=null && dir.exists() 98 | item.hasPermission = obbPkg.contains(appPkg) 99 | if (item.hasPermission) { 100 | item.uri = obbPkg[appPkg] 101 | } 102 | } 103 | if (!item.exits) { 104 | continue 105 | } 106 | pkgList.add(item) 107 | pkgSet.add(appPkg) 108 | } 109 | } 110 | pkgList.sortBy { it.name } 111 | 112 | val applications = pm.getInstalledApplications(0) 113 | for (application in applications) { 114 | val appPkg = application.packageName 115 | if (!pkgSet.contains(appPkg)) { 116 | val item = AppItem() 117 | item.pkg = appPkg 118 | item.name = application.loadLabel(pm).toString() 119 | item.desk = false 120 | if (isAndroidData) { 121 | val dir = File("/storage/emulated/0/Android/data", appPkg) 122 | item.exits = dir!=null && dir.exists() 123 | item.hasPermission = dataPkg.contains(appPkg) 124 | if (item.hasPermission) { 125 | item.uri = dataPkg[appPkg] 126 | } 127 | } else { 128 | val dir = File("/storage/emulated/0/Android/obb", appPkg) 129 | item.exits = dir!=null && dir.exists() 130 | item.hasPermission = obbPkg.contains(appPkg) 131 | if (item.hasPermission) { 132 | item.uri = obbPkg[appPkg] 133 | } 134 | } 135 | if (!item.exits) { 136 | continue 137 | } 138 | pkgList.add(item) 139 | pkgSet.add(appPkg) 140 | } 141 | } 142 | pkgList.sortWith(compareBy({ !it.hasPermission }, { !it.desk }, { it.name })) 143 | 144 | activity?.runOnUiThread { 145 | val appSelectRecyclerAdapter = AppSelectRecyclerAdapter(pkgList) 146 | appSelectRecyclerAdapter.setOnAppClickListener { appItem -> 147 | onSelectedListener?.onSelected(appItem) 148 | } 149 | listAppItem.adapter = appSelectRecyclerAdapter 150 | //adapter?.setNewData(pkgList) 151 | } 152 | }.start() 153 | 154 | 155 | listAppItem.adapter = adapter 156 | adapter?.setOnItemClickListener { baseQuickAdapter, view, i -> 157 | if (adapter != null) { 158 | val item = adapter!!.getItem(i) 159 | onSelectedListener?.onSelected(item) 160 | } 161 | } 162 | return MaterialAlertDialogBuilder(act) 163 | //.setIcon(R.drawable.file_icon_apk) 164 | .setTitle(R.string.select) //.setMessage(property) 165 | .setView(layout) 166 | .setPositiveButton(R.string.cancel) { dialog, which -> dismiss() } 167 | .create() 168 | } 169 | return super.onCreateDialog(savedInstanceState) 170 | } 171 | 172 | inner class AppItem { 173 | var pkg: String? = null 174 | var name: String? = null 175 | var desk: Boolean = false//show in launcher 176 | var exits: Boolean = false 177 | var hasPermission: Boolean = false 178 | var uri: Uri? = null 179 | } 180 | 181 | inner class AppAdapter(data: MutableList) : BaseQuickAdapter(R.layout.item_select_app, data) { 182 | 183 | override fun convert(helper: BaseViewHolder, appItem: AppItem) { 184 | helper.setText(R.id.tvName, appItem.name) 185 | helper.setText(R.id.tvPkg, appItem.pkg) 186 | val iv = helper.getView(R.id.ivIcon) 187 | try { 188 | appItem.pkg?.let { 189 | val packageInfo = TestApp.instance.packageManager.getPackageInfo(it, 0) 190 | if (packageInfo != null) { 191 | Glide.with(iv) 192 | .load(packageInfo) 193 | .placeholder(R.drawable.file_icon_apk) 194 | .into(iv) 195 | //Log.w(TAG, "load PackageInfo: $it") 196 | } 197 | } 198 | } catch (e: Exception) { 199 | e.printStackTrace() 200 | } 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/test/AppSelectRecyclerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.android.test; 2 | 3 | import android.content.Context; 4 | import android.content.pm.PackageInfo; 5 | import android.text.TextUtils; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.ImageView; 10 | import android.widget.TextView; 11 | 12 | import androidx.annotation.NonNull; 13 | import androidx.recyclerview.widget.RecyclerView; 14 | 15 | import com.bumptech.glide.Glide; 16 | import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView; 17 | 18 | import java.io.File; 19 | import java.util.List; 20 | 21 | public class AppSelectRecyclerAdapter extends RecyclerView.Adapter 22 | implements FastScrollRecyclerView.SectionedAdapter { 23 | 24 | private List appList; 25 | private OnAppClickListener onAppClickListener; 26 | 27 | 28 | interface OnAppClickListener { 29 | void onAppClicked(AppSelectDialogFragment.AppItem appItem); 30 | } 31 | 32 | public void setOnAppClickListener(OnAppClickListener onAppClickListener) { 33 | this.onAppClickListener = onAppClickListener; 34 | } 35 | 36 | public AppSelectRecyclerAdapter(List appList) { 37 | this.appList = appList; 38 | } 39 | 40 | @Override 41 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 42 | return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false)); 43 | } 44 | 45 | @Override 46 | public int getItemViewType(int position) { 47 | return R.layout.item_select_app; 48 | } 49 | 50 | @Override 51 | public void onBindViewHolder(ViewHolder holder, int position) { 52 | AppSelectDialogFragment.AppItem app = getItem(position); 53 | String pkg = app.getPkg(); 54 | holder.appItem.setOnClickListener(view -> { 55 | if (onAppClickListener != null) { 56 | onAppClickListener.onAppClicked(app); 57 | } 58 | }); 59 | holder.tvName.setText(app.getName()); 60 | if (app.getExits()) { 61 | holder.tvName.setText("📁" + app.getName()); 62 | } 63 | holder.tvPkg.setText(pkg); 64 | holder.ivTag.setVisibility(app.getHasPermission() ? View.VISIBLE : View.GONE); 65 | 66 | try { 67 | Context ctx = holder.ivIcon.getContext(); 68 | PackageInfo packageInfo = ctx.getPackageManager().getPackageInfo(pkg, 0); 69 | if (packageInfo != null) { 70 | GlideApp.with(holder.ivIcon) 71 | .load(packageInfo) 72 | .placeholder(R.drawable.file_icon_apk) 73 | .into(holder.ivIcon); 74 | //Log.w(TAG, "load PackageInfo: $it") 75 | } 76 | } catch (Exception e) { 77 | e.printStackTrace(); 78 | } 79 | 80 | } 81 | 82 | @Override 83 | public int getItemCount() { 84 | return appList == null ? 0 : appList.size(); 85 | } 86 | 87 | @NonNull 88 | @Override 89 | public String getSectionName(int position) { 90 | AppSelectDialogFragment.AppItem app = getItem(position); 91 | String name = app.getName(); 92 | if (!TextUtils.isEmpty(name)) { 93 | char firstChar = name.charAt(0); 94 | return String.valueOf(firstChar).toUpperCase(); 95 | } 96 | return ""; 97 | } 98 | 99 | @NonNull 100 | private AppSelectDialogFragment.AppItem getItem(int position) { 101 | return appList.get(position); 102 | } 103 | 104 | static class ViewHolder extends RecyclerView.ViewHolder { 105 | public View appItem; 106 | public TextView tvName; 107 | public TextView tvPkg; 108 | public ImageView ivIcon; 109 | public ImageView ivTag; 110 | 111 | ViewHolder(View itemView) { 112 | super(itemView); 113 | 114 | appItem = itemView.findViewById(R.id.appItem); 115 | tvName = itemView.findViewById(R.id.tvName); 116 | tvPkg = itemView.findViewById(R.id.tvPkg); 117 | ivIcon = itemView.findViewById(R.id.ivIcon); 118 | ivTag = itemView.findViewById(R.id.ivTag); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /app/src/main/java/com/android/test/DocumentVM.kt: -------------------------------------------------------------------------------- 1 | package com.android.test 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | import android.os.Build 8 | import android.provider.DocumentsContract 9 | import androidx.annotation.RequiresApi 10 | 11 | class DocumentVM { 12 | companion object { 13 | 14 | const val DOC_AUTHORITY = "com.android.externalstorage.documents" 15 | 16 | 17 | @JvmStatic 18 | @RequiresApi(Build.VERSION_CODES.O) 19 | fun requestFolderPermission(activity: Activity, requestCode: Int, id: String?) { 20 | val i = getUriOpenIntent(getFolderUri(id, false)) 21 | 22 | val flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or 23 | Intent.FLAG_GRANT_READ_URI_PERMISSION or 24 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 25 | i.addFlags(flags) 26 | 27 | activity.startActivityForResult(i, requestCode) 28 | } 29 | 30 | @JvmStatic 31 | fun getFolderUri(id: String?, tree: Boolean): Uri { 32 | return if (tree) DocumentsContract.buildTreeDocumentUri(DOC_AUTHORITY, id) else DocumentsContract.buildDocumentUri(DOC_AUTHORITY, id) 33 | } 34 | 35 | @JvmStatic 36 | @RequiresApi(Build.VERSION_CODES.O) 37 | fun getUriOpenIntent(uri: Uri): Intent { 38 | return Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) 39 | .putExtra("android.provider.extra.SHOW_ADVANCED", true) 40 | .putExtra("android.content.extra.SHOW_ADVANCED", true) 41 | .putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri) 42 | } 43 | 44 | @JvmStatic 45 | fun checkFolderPermission(context: Context, id: String?): Boolean { 46 | return if (atLeastR()) { 47 | val treeUri: Uri = getFolderUri(id, true) 48 | //Log.e(TAG, "treeUri:" + treeUri) 49 | isInPersistedUriPermissions(context, treeUri) 50 | } else { 51 | true 52 | } 53 | } 54 | 55 | @JvmStatic 56 | @RequiresApi(Build.VERSION_CODES.KITKAT) 57 | fun isInPersistedUriPermissions(context: Context, uri: Uri): Boolean { 58 | val pList = context.contentResolver.persistedUriPermissions 59 | //Log.e(TAG, "pList:" + pList.size) 60 | for (uriPermission in pList) { 61 | //Log.e(TAG, "uriPermission:$uriPermission") 62 | if (uriPermission.uri == uri && (uriPermission.isReadPermission || uriPermission.isWritePermission)) { 63 | return true 64 | } else { 65 | //Log.e(TAG, "up:" + uriPermission.uri) 66 | } 67 | } 68 | return false 69 | } 70 | 71 | fun atLeastTiramisu(): Boolean { 72 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU 73 | } 74 | 75 | fun atLeastR(): Boolean { 76 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R 77 | } 78 | } 79 | 80 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/test/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.android.test 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.SharedPreferences 7 | import android.net.Uri 8 | import android.os.Bundle 9 | import android.os.Environment 10 | import android.util.Log 11 | import android.view.View 12 | import android.widget.TextView 13 | import androidx.appcompat.app.AlertDialog 14 | import androidx.appcompat.app.AppCompatActivity 15 | import androidx.documentfile.provider.DocumentFile 16 | import com.github.k1rakishou.fsaf.FileChooser 17 | import com.github.k1rakishou.fsaf.FileManager 18 | import com.github.k1rakishou.fsaf.callback.FSAFActivityCallbacks 19 | import com.github.k1rakishou.fsaf_test_app.TestBaseDirectory 20 | import com.github.k1rakishou.fsaf_test_app.tests.TestSuite 21 | import java.io.File 22 | import java.util.* 23 | 24 | class MainActivity : AppCompatActivity(), FSAFActivityCallbacks { 25 | 26 | private lateinit var testSuite: TestSuite 27 | 28 | private lateinit var fileManager: FileManager 29 | private lateinit var fileChooser: FileChooser 30 | 31 | private lateinit var sharedPreferences: SharedPreferences 32 | 33 | private val testBaseDirectory = TestBaseDirectory({ 34 | getTreeUri() 35 | }, { 36 | null 37 | }) 38 | 39 | companion object { 40 | private const val TAG = "MainActivity" 41 | 42 | const val DOCID_ANDROID_DATA = "primary:Android/data" 43 | const val DOCID_ANDROID_OBB = "primary:Android/obb" 44 | 45 | const val REQ_SAF_R_DATA = 202030 46 | const val REQ_SAF_R_OBB = 202036 47 | 48 | const val TREE_URI = "tree_uri" 49 | } 50 | 51 | var tv: TextView? = null 52 | 53 | override fun onCreate(savedInstanceState: Bundle?) { 54 | super.onCreate(savedInstanceState) 55 | setContentView(R.layout.activity_main) 56 | 57 | sharedPreferences = getSharedPreferences("test", MODE_PRIVATE) 58 | 59 | fileManager = FileManager(applicationContext) 60 | fileChooser = FileChooser(applicationContext) 61 | testSuite = TestSuite(fileManager, this) 62 | //fileChooser.setCallbacks(this) 63 | 64 | if (getTreeUri() != null) { 65 | fileManager.registerBaseDir(TestBaseDirectory::class.java, testBaseDirectory) 66 | } 67 | 68 | tv = findViewById(R.id.tv) 69 | findViewById(R.id.btnGo).setOnClickListener { 70 | var docId = DOCID_ANDROID_DATA 71 | if (DocumentVM.atLeastR()) { 72 | //docId += "/" + act.packageName 73 | val appSelectDialogFragment = AppSelectDialogFragment.newInstance(true) 74 | appSelectDialogFragment.onSelectedListener = object:AppSelectDialogFragment.OnSelectedListener{ 75 | override fun onSelected(appItem: AppSelectDialogFragment.AppItem?) { 76 | appItem?.let { 77 | docId += "/${it.pkg}" 78 | if (it.hasPermission && it.uri != null) { 79 | goSAF(it.uri!!) 80 | //Log.e(TAG, "onSelected: $docId") 81 | } else { 82 | DocumentVM.requestFolderPermission(this@MainActivity, REQ_SAF_R_DATA, docId) 83 | } 84 | } 85 | appSelectDialogFragment.dismiss() 86 | } 87 | } 88 | appSelectDialogFragment.show(supportFragmentManager, "AppSelect4AndroidData") 89 | } 90 | } 91 | 92 | findViewById(R.id.btnObb).setOnClickListener { 93 | var docId = DOCID_ANDROID_OBB 94 | if (DocumentVM.atLeastR()) { 95 | //docId += "/" + act.packageName 96 | val appSelectDialogFragment = AppSelectDialogFragment.newInstance(false) 97 | appSelectDialogFragment.onSelectedListener = object:AppSelectDialogFragment.OnSelectedListener{ 98 | override fun onSelected(appItem: AppSelectDialogFragment.AppItem?) { 99 | appItem?.let { 100 | docId += "/${it.pkg}" 101 | if (it.hasPermission && it.uri != null) { 102 | goSAF(it.uri!!) 103 | //Log.e(TAG, "onSelected: $docId") 104 | } else { 105 | DocumentVM.requestFolderPermission(this@MainActivity, REQ_SAF_R_OBB, docId) 106 | } 107 | } 108 | appSelectDialogFragment.dismiss() 109 | } 110 | } 111 | appSelectDialogFragment.show(supportFragmentManager, "AppSelect4AndroidObb") 112 | } 113 | } 114 | 115 | findViewById(R.id.btnTest).setOnClickListener { 116 | runTests() 117 | } 118 | 119 | findViewById(R.id.myApp).setOnClickListener { 120 | val pkg = "com.folderv.file" 121 | try { 122 | startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$pkg"))) 123 | startActivity( 124 | Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/dev?id=9196025730305614222")) 125 | ) 126 | } catch (e: Exception) { 127 | startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=$pkg"))) 128 | } 129 | } 130 | 131 | if(isZh(this)){ 132 | findViewById(R.id.tvCoolapk).visibility = View.VISIBLE 133 | } 134 | 135 | } 136 | 137 | @Synchronized 138 | private fun goSAF(uri: Uri, docId: String? = null, hide: Boolean? = false) { 139 | // read and write Storage Access Framework https://developer.android.com/guide/topics/providers/document-provider 140 | val root = DocumentFile.fromTreeUri(this, uri); 141 | // make a new dir 142 | //val dir = root?.createDirectory("test") 143 | 144 | //list children 145 | root?.listFiles()?.let { 146 | val sb = StringBuilder() 147 | for (documentFile in it) { 148 | if(documentFile.isDirectory){ 149 | sb.append("📁") 150 | } 151 | sb.append(documentFile.name).append('\n') 152 | } 153 | tv?.text = sb.toString() 154 | } 155 | } 156 | 157 | private fun goAndroidData(path: String?) { 158 | val uri = DocumentVM.getFolderUri(DOCID_ANDROID_DATA, true) 159 | goSAF(uri, path) 160 | } 161 | 162 | private fun goAndroidObb(path: String?) { 163 | val uri = DocumentVM.getFolderUri(DOCID_ANDROID_OBB, true) 164 | goSAF(uri, path) 165 | } 166 | 167 | override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { 168 | val act: Activity? = this 169 | val data = intent?.data 170 | if (requestCode == REQ_SAF_R_DATA) { 171 | if (act != null) { 172 | Log.d(TAG, "onActivityResult: $data") 173 | if (!DocumentVM.checkFolderPermission(act,DOCID_ANDROID_DATA)) { 174 | if (resultCode == Activity.RESULT_OK) { 175 | if (data != null) { 176 | goSAF(data) 177 | 178 | removeTreeUri() 179 | storeTreeUri(data) 180 | 181 | try { 182 | val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION 183 | contentResolver.takePersistableUriPermission(data, flags) 184 | } catch (e: Exception) { 185 | e.printStackTrace() 186 | } 187 | 188 | //runTests() 189 | } 190 | } else { 191 | //showToast("canceled " + resultCode) //TODO 192 | } 193 | } else { 194 | goAndroidData(null) 195 | } 196 | } 197 | } 198 | else if (requestCode == REQ_SAF_R_OBB) { 199 | if (act != null) { 200 | Log.d(TAG, "onActivityResult: $data") 201 | if (!DocumentVM.checkFolderPermission(act, DOCID_ANDROID_OBB)) { 202 | if (resultCode == Activity.RESULT_OK) { 203 | if (data != null) { 204 | goSAF(data) 205 | 206 | removeTreeUri() 207 | storeTreeUri(data) 208 | 209 | try { 210 | val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION 211 | contentResolver.takePersistableUriPermission(data, flags) 212 | } catch (e: Exception) { 213 | e.printStackTrace() 214 | } 215 | 216 | //runTests() 217 | } 218 | } else { 219 | //showToast("canceled " + resultCode) //TODO 220 | } 221 | } else { 222 | goAndroidObb(null) 223 | } 224 | } 225 | } 226 | 227 | super.onActivityResult(requestCode, resultCode, intent) 228 | } 229 | 230 | fun isZh(context: Context): Boolean { 231 | val locale: Locale = context.resources.configuration.locale 232 | val language = locale.language 233 | return language.endsWith("zh") 234 | } 235 | 236 | private fun runTests() { 237 | try { 238 | val baseSAFDir = fileManager.newBaseDirectoryFile() 239 | if (baseSAFDir == null) { 240 | throw NullPointerException("baseSAFDir is null!") 241 | } 242 | 243 | val baseFileApiDir = fileManager.fromRawFile( 244 | File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "test") 245 | ) 246 | 247 | fileManager.create(baseFileApiDir) 248 | 249 | testSuite.runTests( 250 | baseSAFDir, 251 | baseFileApiDir 252 | ) 253 | 254 | val message = "=== ALL TESTS HAVE PASSED ===" 255 | println(message) 256 | showDialog(message) 257 | } catch (error: Throwable) { 258 | error.printStackTrace() 259 | showDialog(error.message ?: "Unknown error") 260 | } 261 | } 262 | 263 | private fun showDialog(message: String) { 264 | val dialog = AlertDialog.Builder(this) 265 | .setMessage(message) 266 | .setPositiveButton("OK") { dialog, _ -> dialog.dismiss() } 267 | .create() 268 | 269 | dialog.show() 270 | } 271 | 272 | private fun storeTreeUri(uri: Uri) { 273 | val dir = checkNotNull(fileManager.fromUri(uri)) { "fileManager.fromUri(${uri}) failure" } 274 | 275 | check(fileManager.exists(dir)) { "Does not exist" } 276 | check(fileManager.isDirectory(dir)) { "Not a dir" } 277 | 278 | fileManager.registerBaseDir(testBaseDirectory) 279 | sharedPreferences.edit().putString(TREE_URI, uri.toString()).apply() 280 | Log.d(TAG, "storeTreeUri: $uri") 281 | } 282 | 283 | private fun removeTreeUri() { 284 | val treeUri = getTreeUri() 285 | if (treeUri == null) { 286 | println("Already removed") 287 | return 288 | } 289 | 290 | fileChooser.forgetSAFTree(treeUri) 291 | fileManager.unregisterBaseDir() 292 | sharedPreferences.edit().remove(TREE_URI).apply() 293 | } 294 | 295 | private fun getTreeUri(): Uri? { 296 | return sharedPreferences.getString(TREE_URI, null) 297 | ?.let { str -> Uri.parse(str) } 298 | } 299 | 300 | override fun fsafStartActivityForResult(intent: Intent, requestCode: Int) { 301 | // 302 | } 303 | 304 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/test/MyGlideModule.kt: -------------------------------------------------------------------------------- 1 | package com.android.test 2 | 3 | import android.content.Context 4 | import android.content.pm.PackageInfo 5 | import android.graphics.Bitmap 6 | import com.bumptech.glide.Glide 7 | import com.bumptech.glide.GlideBuilder 8 | import com.bumptech.glide.Registry 9 | import com.bumptech.glide.annotation.GlideModule 10 | import com.bumptech.glide.load.DecodeFormat 11 | import com.bumptech.glide.module.AppGlideModule 12 | import com.bumptech.glide.request.RequestOptions 13 | import me.zhanghai.android.appiconloader.glide.AppIconModelLoader 14 | 15 | 16 | @GlideModule 17 | class MyGlideModule : AppGlideModule() { 18 | override fun applyOptions(context: Context, builder: GlideBuilder) { 19 | builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_ARGB_8888)) 20 | } 21 | 22 | override fun registerComponents(context: Context, glide: Glide, registry: Registry) { 23 | val iconSize = 144 24 | registry.prepend(PackageInfo::class.java, Bitmap::class.java, AppIconModelLoader.Factory(iconSize, false, context)) 25 | super.registerComponents(context, glide, registry) 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/android/test/TestApp.java: -------------------------------------------------------------------------------- 1 | package com.android.test; 2 | 3 | import androidx.multidex.MultiDexApplication; 4 | 5 | public class TestApp extends MultiDexApplication { 6 | public static TestApp instance; 7 | 8 | @Override 9 | public void onCreate() { 10 | super.onCreate(); 11 | instance = this; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/k1rakishou/fsaf_test_app/TestBaseDirectory.kt: -------------------------------------------------------------------------------- 1 | package com.github.k1rakishou.fsaf_test_app 2 | 3 | import android.net.Uri 4 | import com.github.k1rakishou.fsaf.manager.base_directory.BaseDirectory 5 | import java.io.File 6 | 7 | class TestBaseDirectory( 8 | private val getBaseDirUriFunc: () -> Uri?, 9 | private val getBaseDirFileFunc: () -> File? 10 | ) : BaseDirectory() { 11 | 12 | override fun getDirUri(): Uri? = getBaseDirUriFunc.invoke() 13 | override fun getDirFile(): File? = getBaseDirFileFunc.invoke() 14 | override fun currentActiveBaseDirType(): ActiveBaseDirType = ActiveBaseDirType.SafBaseDir 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/k1rakishou/fsaf_test_app/extensions/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.github.k1rakishou.fsaf_test_app.extensions 2 | 3 | import android.content.ContentResolver 4 | import java.util.regex.Pattern 5 | 6 | internal const val CONTENT_TYPE = "${ContentResolver.SCHEME_CONTENT}://" 7 | internal const val FILE_TYPE = "${ContentResolver.SCHEME_FILE}://" 8 | internal val uriTypes = arrayOf(CONTENT_TYPE, FILE_TYPE) 9 | 10 | internal const val ENCODED_SEPARATOR = "%2F" 11 | internal const val FILE_SEPARATOR1 = "/" 12 | internal const val FILE_SEPARATOR2 = "\\" 13 | 14 | // Either "%2F" or "/" or "\" 15 | private val SPLIT_PATTERN = Pattern.compile("%2F|/|\\\\") 16 | 17 | internal fun String.splitIntoSegments(): List { 18 | if (this.isEmpty()) { 19 | return emptyList() 20 | } 21 | 22 | val uriType = uriTypes.firstOrNull { type -> this.startsWith(type) } 23 | val string = if (uriType != null) { 24 | this.substring(uriType.length, this.length) 25 | } else { 26 | this 27 | } 28 | 29 | return if (string.contains(FILE_SEPARATOR1) 30 | || string.contains(FILE_SEPARATOR2) 31 | || string.contains(ENCODED_SEPARATOR) 32 | ) { 33 | val split = string 34 | .split(SPLIT_PATTERN) 35 | .filter { name -> name.isNotBlank() } 36 | 37 | split 38 | } else { 39 | listOf(string) 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/k1rakishou/fsaf_test_app/tests/BadPathSymbolResolutionTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.k1rakishou.fsaf_test_app.tests 2 | 3 | import android.content.Context 4 | import com.github.k1rakishou.fsaf.BadPathSymbolResolutionStrategy 5 | import com.github.k1rakishou.fsaf.FileManager 6 | import com.github.k1rakishou.fsaf.file.AbstractFile 7 | 8 | class BadPathSymbolResolutionTest( 9 | private val appContext: Context, 10 | tag: String 11 | ) : BaseTest(tag) { 12 | 13 | fun runTests(baseDir: AbstractFile) { 14 | val fileManager = FileManager( 15 | appContext = appContext, 16 | badPathSymbolResolutionStrategy = BadPathSymbolResolutionStrategy.Ignore 17 | ) 18 | 19 | runTest(fileManager, baseDir) { 20 | val dirName = "dir (1)" 21 | 22 | val createdDir = fileManager.createDir(baseDir, dirName) 23 | if (createdDir == null || !fileManager.exists(createdDir) || !fileManager.isDirectory(createdDir)) { 24 | throw TestException("Couldn't create directory") 25 | } 26 | 27 | val resultDirName = fileManager.getName(createdDir) 28 | if (resultDirName != dirName) { 29 | throw TestException("Bad dir name. Expected: ${dirName} got ${resultDirName}") 30 | } 31 | 32 | val filesCount = 10 33 | 34 | repeat(filesCount) { index -> 35 | val fileName = "test ${index} 2 3.txt" 36 | 37 | val file = fileManager.createFile(createdDir, fileName) 38 | if (file == null || !fileManager.exists(file) || !fileManager.isFile(file)) { 39 | throw TestException("Couldn't create file") 40 | } 41 | 42 | val resultFileName = fileManager.getName(file) 43 | if (resultFileName != fileName) { 44 | throw TestException("Bad file name. Expected: ${fileName} got ${resultFileName}") 45 | } 46 | } 47 | 48 | val files = fileManager.listFiles(createdDir) 49 | if (files.size != filesCount) { 50 | throw TestException("Expected ${filesCount} got ${files.size}") 51 | } 52 | 53 | repeat(filesCount) { index -> 54 | val fileName = "test ${index} 2 3.txt" 55 | val actualName = fileManager.getName(files[index]) 56 | 57 | if (fileName != actualName) { 58 | throw TestException("Bad file name. Expected: ${fileName} got ${actualName}") 59 | } 60 | } 61 | } 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/k1rakishou/fsaf_test_app/tests/BaseTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.k1rakishou.fsaf_test_app.tests 2 | 3 | import com.github.k1rakishou.fsaf.FileManager 4 | import com.github.k1rakishou.fsaf.file.AbstractFile 5 | 6 | abstract class BaseTest( 7 | private val tag: String 8 | ) { 9 | 10 | protected fun log(message: String) { 11 | println("$tag, $message") 12 | } 13 | 14 | protected fun checkDirEmpty(fileManager: FileManager, dir: AbstractFile) { 15 | val files = fileManager.listFiles(dir) 16 | if (files.isNotEmpty()) { 17 | throw TestException("Couldn't not delete some files in the base directory: ${files}") 18 | } 19 | } 20 | 21 | protected fun runTest(fileManager: FileManager, dir: AbstractFile, block: () -> Unit) { 22 | fileManager.deleteContent(dir) 23 | 24 | block() 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/k1rakishou/fsaf_test_app/tests/CopyTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.k1rakishou.fsaf_test_app.tests 2 | 3 | import com.github.k1rakishou.fsaf.FileManager 4 | import com.github.k1rakishou.fsaf.TraverseMode 5 | import com.github.k1rakishou.fsaf.file.AbstractFile 6 | import com.github.k1rakishou.fsaf.file.DirectorySegment 7 | import java.io.DataInputStream 8 | import java.io.DataOutputStream 9 | import kotlin.system.measureTimeMillis 10 | 11 | class CopyTest( 12 | tag: String 13 | ) : BaseTest(tag) { 14 | private val dirs = 5 15 | private val files = 25 16 | 17 | fun runTests(fileManager: FileManager, _scrDir: AbstractFile, _dstDir: AbstractFile, copyTest: CopyTestType) { 18 | runTest(fileManager, _scrDir) { 19 | val srcDir = fileManager.createDir(_scrDir, "src") 20 | if (srcDir == null || !fileManager.exists(srcDir) || !fileManager.isDirectory(srcDir)) { 21 | throw TestException("Couldn't create src directory") 22 | } 23 | 24 | val dstDir = fileManager.createDir(_dstDir, "dst") 25 | if (dstDir == null || !fileManager.exists(dstDir) || !fileManager.isDirectory(dstDir)) { 26 | throw TestException("Couldn't create dst directory") 27 | } 28 | 29 | val time = measureTimeMillis { 30 | copyTest(fileManager, srcDir, dstDir) 31 | } 32 | 33 | log("copyTest (${copyTest.text}) took ${time}ms") 34 | } 35 | } 36 | 37 | private fun copyTest(fileManager: FileManager, srcDir: AbstractFile, dstDir: AbstractFile) { 38 | createFiles(fileManager, srcDir) 39 | 40 | val copyResult = fileManager.copyDirectoryWithContent(srcDir, dstDir, true) 41 | if (!copyResult) { 42 | throw TestException("Couldn't copy file from one directory to another") 43 | } 44 | 45 | val destDirFiles = mutableListOf() 46 | 47 | fileManager.traverseDirectory(dstDir, true, TraverseMode.Both) { file -> 48 | destDirFiles.add(file) 49 | } 50 | 51 | val expectedFilesCount = dirs * (dirs + files) 52 | if (destDirFiles.size != expectedFilesCount) { 53 | throw TestException("Some files were not copied, " + 54 | "expectedSize = $expectedFilesCount, actual = ${destDirFiles.size}") 55 | } 56 | 57 | destDirFiles.forEach { file -> 58 | if (fileManager.isFile(file)) { 59 | fileManager.getInputStream(file)?.use { inputStream -> 60 | DataInputStream(inputStream).use { dis -> 61 | val expected = fileManager.getName(file) 62 | val actual = dis.readUTF() 63 | 64 | if (expected != actual) { 65 | throw TestException("Couldn't read the same value out of the file, " + 66 | "expected = ${expected}, actual = ${actual}") 67 | } 68 | } 69 | } ?: throw TestException("Couldn't open input stream for file ${file.getFullPath()}") 70 | } 71 | } 72 | } 73 | 74 | private fun createFiles( 75 | fileManager: FileManager, 76 | srcDir: AbstractFile 77 | ) { 78 | if (!fileManager.exists(srcDir) || !fileManager.isDirectory(srcDir)) { 79 | throw TestException("Couldn't create directory") 80 | } 81 | 82 | for (dir in 0 until dirs) { 83 | val dirName = dir.toString() 84 | 85 | val createdDir = fileManager.create( 86 | srcDir, 87 | DirectorySegment(dirName), 88 | DirectorySegment("$dirName$dirName"), 89 | DirectorySegment("$dirName$dirName$dirName"), 90 | DirectorySegment("$dirName$dirName$dirName$dirName"), 91 | DirectorySegment("$dirName$dirName$dirName$dirName$dirName") 92 | ) 93 | 94 | if (createdDir == null 95 | || !fileManager.exists(createdDir) 96 | || !fileManager.isDirectory(createdDir) 97 | ) { 98 | throw TestException("Couldn't create directories") 99 | } 100 | 101 | for (file in 0 until files) { 102 | val fileName = "${file}.txt" 103 | 104 | val createdFile = fileManager.createFile(createdDir, fileName) 105 | if (createdFile == null 106 | || !fileManager.exists(createdFile) 107 | || !fileManager.isFile(createdFile) 108 | ) { 109 | throw TestException("Couldn't create file ${fileName}") 110 | } 111 | 112 | fileManager.getOutputStream(createdFile)?.use { outputStream -> 113 | DataOutputStream(outputStream).use { dos -> 114 | dos.writeUTF(fileName) 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } 121 | 122 | 123 | enum class CopyTestType(val text: String) { 124 | FromSafDirToRegularDir("SAF dir to Regular dir"), 125 | FromRegularDitToSafDir("Regular dir to SAF dir") 126 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/k1rakishou/fsaf_test_app/tests/CreateFilesTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.k1rakishou.fsaf_test_app.tests 2 | 3 | import com.github.k1rakishou.fsaf.FileManager 4 | import com.github.k1rakishou.fsaf.file.AbstractFile 5 | import com.github.k1rakishou.fsaf.file.DirectorySegment 6 | import com.github.k1rakishou.fsaf.file.FileSegment 7 | import java.io.DataInputStream 8 | import java.io.DataOutputStream 9 | import kotlin.system.measureTimeMillis 10 | 11 | class CreateFilesTest( 12 | tag: String 13 | ) : BaseTest(tag) { 14 | 15 | fun runTests(fileManager: FileManager, baseDir: AbstractFile) { 16 | runTest(fileManager, baseDir) { 17 | val time = measureTimeMillis { 18 | testCreateBunchOfFilesEachAtATime(fileManager, baseDir) 19 | } 20 | 21 | log("testCreateBunchOfFilesEachAtATime took ${time}ms") 22 | } 23 | 24 | runTest(fileManager, baseDir) { 25 | testCreateFileWithTheSameNameShouldNotCreateNewFile(fileManager, baseDir) 26 | } 27 | } 28 | 29 | private fun testCreateFileWithTheSameNameShouldNotCreateNewFile( 30 | fileManager: FileManager, 31 | baseDir: AbstractFile 32 | ) { 33 | val dir1 = fileManager.createDir(baseDir, "test") 34 | if (dir1 == null || !fileManager.exists(dir1) || !fileManager.isDirectory(dir1)) { 35 | throw TestException("Couldn't create directory") 36 | } 37 | 38 | if (fileManager.create(dir1) == null) { 39 | throw TestException("Couldn't create already existing directory") 40 | } 41 | 42 | val dir2 = fileManager.createDir(baseDir, "test") 43 | if (dir2 == null || !fileManager.exists(dir2) || !fileManager.isDirectory(dir2)) { 44 | throw TestException("Couldn't create directory") 45 | } 46 | 47 | if (fileManager.create(dir2) == null) { 48 | throw TestException("Couldn't create already existing directory") 49 | } 50 | 51 | val totalFiles = fileManager.listFiles(baseDir).size 52 | if (totalFiles != 1) { 53 | throw TestException("New file was created when it shouldn't have been, totalFiles = $totalFiles") 54 | } 55 | } 56 | 57 | private fun testCreateBunchOfFilesEachAtATime(fileManager: FileManager, baseDir: AbstractFile) { 58 | val dir = fileManager.createDir( 59 | baseDir, 60 | "test" 61 | ) 62 | 63 | if (dir == null || !fileManager.exists(dir) || !fileManager.isDirectory(dir)) { 64 | throw TestException("Couldn't create directory") 65 | } 66 | 67 | val files = 25 68 | 69 | for (i in 0 until files) { 70 | val fileName = "${i}.txt" 71 | 72 | val createdFile = fileManager.create( 73 | dir, 74 | DirectorySegment(i.toString()), 75 | FileSegment(fileName) 76 | ) 77 | 78 | if (createdFile == null || !fileManager.exists(createdFile) || !fileManager.isFile(createdFile)) { 79 | throw TestException("Couldn't create file name") 80 | } 81 | 82 | if (fileManager.getName(createdFile) != fileName) { 83 | throw TestException("Bad name ${fileManager.getName(createdFile)}") 84 | } 85 | 86 | fileManager.getOutputStream(createdFile)?.use { os -> 87 | DataOutputStream(os).use { dos -> 88 | dos.writeUTF(fileName) 89 | } 90 | } ?: throw TestException("Couldn't get output stream, file = ${createdFile.getFullPath()}") 91 | 92 | fileManager.getInputStream(createdFile)?.use { `is` -> 93 | DataInputStream(`is`).use { dis -> 94 | val readString = dis.readUTF() 95 | 96 | if (readString != fileName) { 97 | throw TestException("Wrong value read, expected = ${fileName}, actual = ${readString}") 98 | } 99 | } 100 | } ?: throw TestException("Couldn't get input stream, file = ${createdFile.getFullPath()}") 101 | } 102 | } 103 | 104 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/k1rakishou/fsaf_test_app/tests/DeleteTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.k1rakishou.fsaf_test_app.tests 2 | 3 | import com.github.k1rakishou.fsaf.FileManager 4 | import com.github.k1rakishou.fsaf.file.AbstractFile 5 | import com.github.k1rakishou.fsaf.file.DirectorySegment 6 | import com.github.k1rakishou.fsaf.file.FileSegment 7 | import com.github.k1rakishou.fsaf_test_app.TestBaseDirectory 8 | import kotlin.system.measureTimeMillis 9 | 10 | class DeleteTest( 11 | tag: String 12 | ) : BaseTest(tag) { 13 | 14 | fun runTests(fileManager: FileManager, baseDir: AbstractFile) { 15 | runTest(fileManager, baseDir) { 16 | val time = measureTimeMillis { 17 | test1(fileManager, baseDir) 18 | } 19 | 20 | log("test1 took ${time}ms") 21 | } 22 | 23 | runTest(fileManager, baseDir) { 24 | val time = measureTimeMillis { 25 | test2(fileManager, baseDir) 26 | } 27 | 28 | log("test2 took ${time}ms") 29 | } 30 | } 31 | 32 | private fun test2(fileManager: FileManager, baseDir: AbstractFile) { 33 | checkNotNull(fileManager.createUnsafe(baseDir, "/123/456/789")) { "Create returned null" } 34 | checkNotNull(fileManager.createUnsafe(baseDir, "/123/456/789/1.txt")) { "Create returned null" } 35 | checkNotNull(fileManager.createUnsafe(baseDir, "/123/456/789/2.txt")) { "Create returned null" } 36 | checkNotNull(fileManager.createUnsafe(baseDir, "/123/456/789/3.txt")) { "Create returned null" } 37 | checkNotNull(fileManager.createUnsafe(baseDir, "/123/456/789/4.txt")) { "Create returned null" } 38 | checkNotNull(fileManager.createUnsafe(baseDir, "/123/456/789/5.txt")) { "Create returned null" } 39 | checkNotNull(fileManager.createUnsafe(baseDir, "/123/456/789/6.txt")) { "Create returned null" } 40 | checkNotNull(fileManager.createUnsafe(baseDir, "/123/456/789/7.txt")) { "Create returned null" } 41 | checkNotNull(fileManager.createUnsafe(baseDir, "/123/456/789/8.txt")) { "Create returned null" } 42 | checkNotNull(fileManager.createUnsafe(baseDir, "/123/456/789/9.txt")) { "Create returned null" } 43 | checkNotNull(fileManager.createUnsafe(baseDir, "/123/456/789/10.txt")) { "Create returned null" } 44 | checkNotNull(fileManager.createUnsafe(baseDir, "/123/456/789/11.txt")) { "Create returned null" } 45 | checkNotNull(fileManager.createUnsafe(baseDir, "/123/456/789/12.txt")) { "Create returned null" } 46 | checkNotNull(fileManager.createUnsafe(baseDir, "/123/456/789/13.txt")) { "Create returned null" } 47 | checkNotNull(fileManager.createUnsafe(baseDir, "/123/456/789/14.txt")) { "Create returned null" } 48 | checkNotNull(fileManager.createUnsafe(baseDir, "/123/456/789/15.txt")) { "Create returned null" } 49 | 50 | val outerDirectory = baseDir.cloneUnsafe("123") 51 | check(fileManager.delete(outerDirectory)) { "Couldn't delete 123 directory" } 52 | 53 | check(!fileManager.exists(baseDir.cloneUnsafe("123"))) { "123 still exists" } 54 | check(fileManager.findFile(baseDir, "123") == null) { "123 still exists" } 55 | } 56 | 57 | fun test1(fileManager: FileManager, baseDir: AbstractFile) { 58 | val externalFile = fileManager.create( 59 | baseDir, 60 | DirectorySegment("123"), 61 | DirectorySegment("456"), 62 | DirectorySegment("789"), 63 | FileSegment("test123.txt") 64 | ) 65 | 66 | if (externalFile == null || !fileManager.exists(externalFile)) { 67 | throw TestException("Couldn't create test123.txt") 68 | } 69 | 70 | kotlin.run { 71 | val file = fileManager.newBaseDirectoryFile()!! 72 | .cloneUnsafe("/123/456/678/test123.txt") 73 | 74 | check(fileManager.delete(file)) { "Couldn't delete test123.txt" } 75 | check(!fileManager.exists(file)) { "test123.txt still exists" } 76 | } 77 | 78 | kotlin.run { 79 | val file = fileManager.newBaseDirectoryFile()!! 80 | .cloneUnsafe("/123/456/678") 81 | 82 | check(fileManager.delete(file)) { "Couldn't delete 678" } 83 | check(!fileManager.exists(file)) { "678 still exists" } 84 | } 85 | 86 | kotlin.run { 87 | val file = fileManager.newBaseDirectoryFile()!! 88 | .cloneUnsafe("/123/456") 89 | 90 | check(fileManager.delete(file)) { "Couldn't delete 456" } 91 | check(!fileManager.exists(file)) { "456 still exists" } 92 | } 93 | 94 | kotlin.run { 95 | val file = fileManager.newBaseDirectoryFile()!! 96 | .cloneUnsafe("/123") 97 | 98 | check(fileManager.delete(file)) { "Couldn't delete 123" } 99 | check(!fileManager.exists(file)) { "123 still exists" } 100 | } 101 | 102 | kotlin.run { 103 | val file = fileManager.newBaseDirectoryFile()!! 104 | .cloneUnsafe("/123") 105 | 106 | check(fileManager.delete(file)) { "fileManager.delete even though file shouldn't exist" } 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/k1rakishou/fsaf_test_app/tests/FindTests.kt: -------------------------------------------------------------------------------- 1 | package com.github.k1rakishou.fsaf_test_app.tests 2 | 3 | import com.github.k1rakishou.fsaf.FileManager 4 | import com.github.k1rakishou.fsaf.file.AbstractFile 5 | import com.github.k1rakishou.fsaf.file.DirectorySegment 6 | import com.github.k1rakishou.fsaf.file.FileSegment 7 | import kotlin.system.measureTimeMillis 8 | 9 | class FindTests( 10 | tag: String 11 | ) : BaseTest(tag) { 12 | 13 | fun runTests(fileManager: FileManager, baseDir: AbstractFile) { 14 | runTest(fileManager, baseDir) { 15 | val time = measureTimeMillis { 16 | test1(fileManager, baseDir) 17 | } 18 | 19 | log("test1 took ${time}ms") 20 | } 21 | 22 | runTest(fileManager, baseDir) { 23 | val time = measureTimeMillis { 24 | test2(fileManager, baseDir) 25 | } 26 | 27 | log("test2 took ${time}ms") 28 | } 29 | 30 | runTest(fileManager, baseDir) { 31 | val time = measureTimeMillis { 32 | test3(fileManager, baseDir) 33 | } 34 | 35 | log("test3 took ${time}ms") 36 | } 37 | } 38 | 39 | private fun test3(fileManager: FileManager, baseDir: AbstractFile) { 40 | val clonedBaseDir = baseDir.cloneUnsafe("/1/2/3/4/5/6/7/8/") 41 | val result = fileManager.flattenSegments(clonedBaseDir) 42 | if (result != null) { 43 | throw IllegalStateException("Expected null but got ${result.getFullPath()}") 44 | } 45 | } 46 | 47 | private fun test2(fileManager: FileManager, baseDir: AbstractFile) { 48 | checkNotNull( 49 | fileManager.create( 50 | baseDir, 51 | DirectorySegment("1"), 52 | DirectorySegment("2"), 53 | DirectorySegment("3"), 54 | DirectorySegment("4"), 55 | DirectorySegment("5"), 56 | DirectorySegment("6"), 57 | DirectorySegment("7"), 58 | DirectorySegment("8"), 59 | FileSegment("test123.txt") 60 | ) 61 | ) { "Couldn't create \"/1/2/3/4/5/6/7/8/test123.txt\"" } 62 | 63 | val clonedBaseDir = baseDir.cloneUnsafe("/1/2/3/4/5/6/7/8/") 64 | val flattenedDir = checkNotNull(fileManager.flattenSegments(clonedBaseDir)) { 65 | "Couldn't flatten segments for ${clonedBaseDir.getFullPath()}" 66 | } 67 | 68 | fileManager.findFile(flattenedDir, "test123.txt").let { test123 -> 69 | checkNotNull(test123) { "Couldn't find file test123" } 70 | 71 | check(fileManager.exists(test123)) { "test123.txt does not exist" } 72 | check(fileManager.isFile(test123)) { "test123.txt is not a file" } 73 | check(!fileManager.isDirectory(test123)) { "test123.txt is a directory" } 74 | check(fileManager.getLength(test123) == 0L) { "test123.txt is not empty" } 75 | } 76 | } 77 | 78 | private fun test1(fileManager: FileManager, baseDir: AbstractFile) { 79 | checkNotNull( 80 | fileManager.create( 81 | baseDir, 82 | DirectorySegment("1"), 83 | DirectorySegment("2"), 84 | DirectorySegment("3"), 85 | DirectorySegment("4"), 86 | DirectorySegment("5"), 87 | DirectorySegment("6"), 88 | DirectorySegment("7"), 89 | DirectorySegment("8"), 90 | FileSegment("test123.txt") 91 | ) 92 | ) { "Couldn't create \"/1/2/3/4/5/6/7/8/test123.txt\"" } 93 | 94 | fileManager.findFile(baseDir, "1").let { dir1 -> 95 | checkNotNull(dir1) { "Couldn't find dir 1" } 96 | 97 | fileManager.findFile(dir1, "2").let { dir2 -> 98 | checkNotNull(dir2) { "Couldn't find dir 2" } 99 | 100 | fileManager.findFile(dir2, "3").let { dir3 -> 101 | checkNotNull(dir3) { "Couldn't find dir 3" } 102 | 103 | fileManager.findFile(dir3, "4").let { dir4 -> 104 | checkNotNull(dir4) { "Couldn't find dir 4" } 105 | 106 | fileManager.findFile(dir4, "5").let { dir5 -> 107 | checkNotNull(dir5) { "Couldn't find dir 5" } 108 | 109 | fileManager.findFile(dir5, "6").let { dir6 -> 110 | checkNotNull(dir6) { "Couldn't find dir 6" } 111 | 112 | fileManager.findFile(dir6, "7").let { dir7 -> 113 | checkNotNull(dir7) { "Couldn't find dir 7" } 114 | 115 | fileManager.findFile(dir7, "8").let { dir8 -> 116 | checkNotNull(dir8) { "Couldn't find dir 8" } 117 | 118 | fileManager.findFile(dir8, "test123.txt").let { test123 -> 119 | checkNotNull(test123) { "Couldn't find file test123" } 120 | 121 | check(fileManager.exists(test123)) { "test123.txt does not exist" } 122 | check(fileManager.isFile(test123)) { "test123.txt is not a file" } 123 | check(!fileManager.isDirectory(test123)) { "test123.txt is a directory" } 124 | check(fileManager.getLength(test123) == 0L) { "test123.txt is not empty" } 125 | } 126 | } 127 | } 128 | } 129 | } 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/k1rakishou/fsaf_test_app/tests/SimpleTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.k1rakishou.fsaf_test_app.tests 2 | 3 | import com.github.k1rakishou.fsaf.FileManager 4 | import com.github.k1rakishou.fsaf.TraverseMode 5 | import com.github.k1rakishou.fsaf.file.AbstractFile 6 | import com.github.k1rakishou.fsaf.file.DirectorySegment 7 | import com.github.k1rakishou.fsaf.file.FileSegment 8 | import com.github.k1rakishou.fsaf_test_app.TestBaseDirectory 9 | import com.github.k1rakishou.fsaf_test_app.extensions.splitIntoSegments 10 | import kotlin.system.measureTimeMillis 11 | 12 | class SimpleTest( 13 | tag: String 14 | ) : BaseTest(tag) { 15 | 16 | fun runTests(fileManager: FileManager, baseDir: AbstractFile) { 17 | runTest(fileManager, baseDir) { 18 | val time = measureTimeMillis { 19 | test1(fileManager, baseDir) 20 | } 21 | 22 | log("test1 took ${time}ms") 23 | } 24 | 25 | runTest(fileManager, baseDir) { 26 | val time = measureTimeMillis { 27 | test2(fileManager, baseDir) 28 | } 29 | 30 | log("test2 took ${time}ms") 31 | } 32 | 33 | runTest(fileManager, baseDir) { 34 | val time = measureTimeMillis { 35 | test3(fileManager, baseDir) 36 | } 37 | 38 | log("test3 took ${time}ms") 39 | } 40 | 41 | runTest(fileManager, baseDir) { 42 | val time = measureTimeMillis { 43 | basicFileTests(fileManager, baseDir) 44 | } 45 | 46 | log("basicFileTests took ${time}ms") 47 | } 48 | 49 | runTest(fileManager, baseDir) { 50 | val time = measureTimeMillis { 51 | badFileNamesTest(fileManager, baseDir) 52 | } 53 | 54 | log("badFileNamesTest took ${time}ms") 55 | } 56 | 57 | runTest(fileManager, baseDir) { 58 | val time = measureTimeMillis { 59 | listFilesOrderingTest(fileManager, baseDir) 60 | } 61 | 62 | log("listFilesOrderingTest took ${time}ms") 63 | } 64 | } 65 | 66 | private fun listFilesOrderingTest(fileManager: FileManager, baseDir: AbstractFile) { 67 | fileManager.create(baseDir, DirectorySegment("1"))?.also { dir1 -> 68 | checkNotNull(fileManager.create(dir1, FileSegment("1.txt"))) { "Couldn't create 1.txt" } 69 | checkNotNull(fileManager.create(dir1, FileSegment("156.txt"))) { "Couldn't create 156.txt" } 70 | checkNotNull(fileManager.create(dir1, FileSegment("10.txt"))) { "Couldn't create 10.txt" } 71 | 72 | fileManager.create(dir1, DirectorySegment("234"))?.also { dir2 -> 73 | checkNotNull(fileManager.create(dir2, FileSegment("2.txt"))) { "Couldn't create 2.txt" } 74 | checkNotNull(fileManager.create(dir2, FileSegment("256.txt"))) { "Couldn't create 256.txt" } 75 | checkNotNull(fileManager.create(dir2, FileSegment("20.txt"))) { "Couldn't create 20.txt" } 76 | 77 | fileManager.create(dir2, DirectorySegment("333444"))?.also { dir3 -> 78 | checkNotNull(fileManager.create(dir3, FileSegment("2_1_3.txt"))) { "Couldn't create 2_1_3.txt" } 79 | 80 | fileManager.create(dir3, DirectorySegment("555666"))?.also { dir4 -> 81 | checkNotNull(fileManager.create(dir4, FileSegment("innermost.txt"))) { "Couldn't create innermost.txt" } 82 | } ?: throw IllegalStateException("Couldn't create dir 555666") 83 | } ?: throw IllegalStateException("Couldn't create dir 333444") 84 | } ?: throw IllegalStateException("Couldn't create dir 234") 85 | 86 | fileManager.create(dir1, DirectorySegment("4562"))?.also { dir2 -> 87 | checkNotNull(fileManager.create(dir2, FileSegment("2.txt"))) { "Couldn't create 2.txt" } 88 | checkNotNull(fileManager.create(dir2, FileSegment("256.txt"))) { "Couldn't create 256.txt" } 89 | checkNotNull(fileManager.create(dir2, FileSegment("20.txt"))) { "Couldn't create 20.txt" } 90 | } ?: throw IllegalStateException("Couldn't create dir 456") 91 | 92 | fileManager.create(dir1, DirectorySegment("78"))?.also { dir2 -> 93 | checkNotNull(fileManager.create(dir2, FileSegment("2.txt"))) { "Couldn't create 2.txt" } 94 | checkNotNull(fileManager.create(dir2, FileSegment("256.txt"))) { "Couldn't create 256.txt" } 95 | checkNotNull(fileManager.create(dir2, FileSegment("20.txt"))) { "Couldn't create 20.txt" } 96 | } ?: throw IllegalStateException("Couldn't create dir 789") 97 | } ?: throw IllegalStateException("Couldn't create dir 1") 98 | 99 | val files = mutableListOf() 100 | fileManager.traverseDirectory(baseDir, true, TraverseMode.Both) { file -> files += file } 101 | 102 | val checkedDirectories = mutableSetOf() 103 | 104 | files.forEach { file -> 105 | if (fileManager.isDirectory(file)) { 106 | checkedDirectories += file.getFullPath().splitIntoSegments().joinToString(separator = "/") 107 | } else { 108 | val path = file.getFullPath().splitIntoSegments().dropLast(1).joinToString(separator = "/") 109 | if (path !in checkedDirectories) { 110 | throw IllegalStateException("File was added before it's parent directory") 111 | } 112 | } 113 | } 114 | } 115 | 116 | private fun badFileNamesTest(fileManager: FileManager, baseDir: AbstractFile) { 117 | val externalFile = fileManager.create( 118 | baseDir, 119 | DirectorySegment("12 3") 120 | ) 121 | 122 | if (externalFile == null || !fileManager.exists(externalFile)) { 123 | throw TestException("Couldn't create 12_3 directory") 124 | } 125 | 126 | check(fileManager.getName(externalFile) == "12_3") { 127 | "bad directory name after replacing bad symbols: ${fileManager.getName(externalFile)}" 128 | } 129 | 130 | val externalFile2 = fileManager.create(externalFile, FileSegment("123 4 5 .txt")) 131 | if (externalFile2 == null || !fileManager.exists(externalFile2)) { 132 | throw TestException("Couldn't create 123_4_5_.txt file") 133 | } 134 | 135 | check(fileManager.getName(externalFile2) == "123_4_5_.txt") { 136 | "bad directory name after replacing bad symbols: ${fileManager.getName(externalFile2)}" 137 | } 138 | 139 | val externalFile3 = fileManager.create( 140 | externalFile, 141 | DirectorySegment("test.dir"), 142 | FileSegment("Kuroba-dev v4.10.2-a9551c9.apk") 143 | ) 144 | 145 | if (externalFile3 == null || !fileManager.exists(externalFile3)) { 146 | throw TestException("Couldn't create Kuroba-dev v4.10.2-a9551c9.apk file") 147 | } 148 | 149 | check(fileManager.getName(externalFile3) == "Kuroba-dev_v4.10.2-a9551c9.apk") { 150 | "bad directory name after replacing bad symbols: ${fileManager.getName(externalFile3)}" 151 | } 152 | 153 | val fileWithMultiplePeriods = fileManager.create( 154 | externalFile, 155 | DirectorySegment("test....dir"), 156 | FileSegment("t.e.s.t......txt") 157 | ) 158 | 159 | if (fileWithMultiplePeriods == null || !fileManager.exists(fileWithMultiplePeriods)) { 160 | throw TestException("Couldn't create t.e.s.t......txt file") 161 | } 162 | 163 | check(fileManager.getName(fileWithMultiplePeriods) == "t.e.s.t......txt") { 164 | "bad directory name after replacing bad symbols: ${fileManager.getName(fileWithMultiplePeriods)}" 165 | } 166 | } 167 | 168 | private fun test1(fileManager: FileManager, baseDir: AbstractFile) { 169 | val externalFile = fileManager.create( 170 | baseDir, 171 | DirectorySegment("123"), 172 | DirectorySegment("456"), 173 | DirectorySegment("789"), 174 | FileSegment("test123.txt") 175 | ) 176 | 177 | if (externalFile == null || !fileManager.exists(externalFile)) { 178 | throw TestException("Couldn't create test123.txt") 179 | } 180 | 181 | if (!fileManager.isFile(externalFile)) { 182 | throw TestException("test123.txt is not a file") 183 | } 184 | 185 | if (fileManager.isDirectory(externalFile)) { 186 | throw TestException("test123.txt is a directory") 187 | } 188 | 189 | if (fileManager.getName(externalFile) != "test123.txt") { 190 | throw TestException("externalFile name != test123.txt") 191 | } 192 | 193 | val externalFile2Exists = baseDir.clone( 194 | DirectorySegment("123"), 195 | DirectorySegment("456"), 196 | DirectorySegment("789") 197 | ) 198 | 199 | if (!fileManager.exists(externalFile2Exists)) { 200 | throw TestException("789 directory does not exist") 201 | } 202 | 203 | val dirToDelete = baseDir.clone( 204 | DirectorySegment("123") 205 | ) 206 | 207 | if (!fileManager.delete(dirToDelete) && fileManager.exists(dirToDelete)) { 208 | throw TestException("Couldn't delete test123.txt") 209 | } 210 | 211 | checkDirEmpty(fileManager, baseDir) 212 | } 213 | 214 | private fun test2(fileManager: FileManager, baseDir: AbstractFile) { 215 | val externalFile = fileManager.create( 216 | baseDir, 217 | DirectorySegment("1234"), 218 | DirectorySegment("4566"), 219 | FileSegment("filename.json") 220 | ) 221 | 222 | if (externalFile == null || !fileManager.exists(externalFile)) { 223 | throw TestException("Couldn't create filename.json") 224 | } 225 | 226 | if (!fileManager.isFile(externalFile)) { 227 | throw TestException("filename.json is not a file") 228 | } 229 | 230 | if (fileManager.isDirectory(externalFile)) { 231 | throw TestException("filename.json is not a directory") 232 | } 233 | 234 | if (fileManager.getName(externalFile) != "filename.json") { 235 | throw TestException("externalFile1 name != filename.json") 236 | } 237 | 238 | val dir = baseDir.clone( 239 | DirectorySegment("1234"), 240 | DirectorySegment("4566") 241 | ) 242 | 243 | if (fileManager.getName(dir) != "4566") { 244 | throw TestException("dir.name != 4566, name = " + fileManager.getName(dir)) 245 | } 246 | 247 | val foundFile = fileManager.findFile(dir, "filename.json") 248 | if (foundFile == null || !fileManager.exists(foundFile)) { 249 | throw TestException("Couldn't find filename.json") 250 | } 251 | } 252 | 253 | private fun test3(fileManager: FileManager, baseDir: AbstractFile) { 254 | val externalFile = fileManager.create( 255 | baseDir, 256 | DirectorySegment("123"), 257 | DirectorySegment("456"), 258 | DirectorySegment("789"), 259 | FileSegment("test123.txt") 260 | ) 261 | 262 | if (externalFile == null || !fileManager.exists(externalFile)) { 263 | throw TestException("Couldn't create test123.txt") 264 | } 265 | 266 | for (i in 0 until 1000) { 267 | if (!fileManager.exists(externalFile)) { 268 | throw TestException("Does not exist") 269 | } 270 | 271 | if (!fileManager.isFile(externalFile)) { 272 | throw TestException("Not a file") 273 | } 274 | 275 | if (fileManager.isDirectory(externalFile)) { 276 | throw TestException("Is a directory") 277 | } 278 | } 279 | } 280 | 281 | private fun basicFileTests(fileManager: FileManager, baseDir: AbstractFile) { 282 | val createdDir1 = run { 283 | val createdDir1 = fileManager.createDir(baseDir, "1") 284 | ?: throw TestException("Couldn't create dir 1") 285 | val createdDir2 = fileManager.createDir(createdDir1, "2") 286 | ?: throw TestException("Couldn't create dir 2") 287 | val createdDir3 = fileManager.createDir(createdDir2, "3") 288 | ?: throw TestException("Couldn't create dir 3") 289 | fileManager.createFile(createdDir3, "file.txt") 290 | ?: throw TestException("Couldn't create file.txt") 291 | 292 | return@run createdDir1 293 | } 294 | 295 | check(fileManager.isDirectory(createdDir1)) { "Dir 1 is not a dir" } 296 | check(!fileManager.isFile(createdDir1)) { "Dir 1 is a file" } 297 | check(fileManager.exists(createdDir1)) { "Dir 1 does not exist" } 298 | check(fileManager.getName(createdDir1) == "1") { "Dir 1 has wrong name" } 299 | check(fileManager.canRead(createdDir1)) { "Cannot read dir 1" } 300 | check(fileManager.canWrite(createdDir1)) { "Cannot write to dir 1" } 301 | 302 | val createdDir2 = checkNotNull(fileManager.findFile(createdDir1, "2")) { "Couldn't find dir 2" } 303 | check(fileManager.isDirectory(createdDir2)) { "Dir 2 is not a dir" } 304 | check(!fileManager.isFile(createdDir2)) { "Dir 2 is a file" } 305 | check(fileManager.exists(createdDir2)) { "Dir 2 does not exist" } 306 | check(fileManager.getName(createdDir2) == "2") { "Dir 2 has wrong name" } 307 | check(fileManager.canRead(createdDir2)) { "Cannot read dir 2" } 308 | check(fileManager.canWrite(createdDir2)) { "Cannot write to dir 2" } 309 | 310 | val createdDir3 = checkNotNull(fileManager.findFile(createdDir2, "3")) { "Couldn't find dir 3" } 311 | check(fileManager.isDirectory(createdDir3)) { "Dir 3 is not a dir" } 312 | check(!fileManager.isFile(createdDir3)) { "Dir 3 is a file" } 313 | check(fileManager.exists(createdDir3)) { "Dir 3 does not exist" } 314 | check(fileManager.getName(createdDir3) == "3") { "Dir 3 has wrong name" } 315 | check(fileManager.canRead(createdDir3)) { "Cannot read dir 3" } 316 | check(fileManager.canWrite(createdDir3)) { "Cannot write to dir 3" } 317 | 318 | val createdFile = checkNotNull(fileManager.findFile(createdDir3, "file.txt")) { "Couldn't find file.txt" } 319 | check(!fileManager.isDirectory(createdFile)) { "file.txt is a dir" } 320 | check(fileManager.isFile(createdFile)) { "file.txt is not a file" } 321 | check(fileManager.exists(createdFile)) { "file.txt does not exist" } 322 | check(fileManager.getName(createdFile) == "file.txt") { "file.txt has wrong name" } 323 | check(fileManager.canRead(createdFile)) { "Cannot read file.txt" } 324 | check(fileManager.canWrite(createdFile)) { "Cannot write to file.txt" } 325 | 326 | val nonExistingDir = fileManager.newBaseDirectoryFile()!! 327 | .clone(DirectorySegment("211314")) 328 | 329 | if (fileManager.exists(nonExistingDir)) { 330 | throw TestException("TestBaseDirectory exists when it shouldn't") 331 | } 332 | } 333 | 334 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/k1rakishou/fsaf_test_app/tests/SnapshotTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.k1rakishou.fsaf_test_app.tests 2 | 3 | import android.content.Context 4 | import com.github.k1rakishou.fsaf.FileManager 5 | import com.github.k1rakishou.fsaf.file.AbstractFile 6 | import com.github.k1rakishou.fsaf.file.DirectorySegment 7 | import com.github.k1rakishou.fsaf.file.FileSegment 8 | import java.io.DataInputStream 9 | import java.io.DataOutputStream 10 | import kotlin.system.measureTimeMillis 11 | 12 | class SnapshotTest( 13 | private val context: Context, 14 | tag: String 15 | ) : BaseTest(tag) { 16 | 17 | fun runTests(fileManager: FileManager, baseDir: AbstractFile) { 18 | runTest(fileManager, baseDir) { 19 | val time = measureTimeMillis { 20 | test1(fileManager, baseDir) 21 | } 22 | 23 | log("test1 took ${time}ms") 24 | } 25 | 26 | runTest(fileManager, baseDir) { 27 | val time = measureTimeMillis { 28 | test2(fileManager, baseDir) 29 | } 30 | 31 | log("test2 took ${time}ms") 32 | } 33 | } 34 | 35 | private fun test2(fileManager: FileManager, baseDir: AbstractFile) { 36 | val snapshotFileManager = fileManager.createSnapshot(baseDir, true) 37 | val testString = "test string" 38 | 39 | snapshotFileManager.create(baseDir, listOf(DirectorySegment("123")))!!.also { innerDir1 -> 40 | snapshotFileManager.create(innerDir1, listOf(FileSegment("test.txt")))!!.also { file -> 41 | check(snapshotFileManager.canRead(file)) { "cannot read ${file.getFullPath()}" } 42 | check(snapshotFileManager.canWrite(file)) { "cannot write to ${file.getFullPath()}" } 43 | check(snapshotFileManager.isFile(file)) { "file ${file.getFullPath()} is not a file" } 44 | check(!snapshotFileManager.isDirectory(file)) { "file ${file.getFullPath()} is a directory" } 45 | 46 | snapshotFileManager.getOutputStream(file)!!.use { stream -> 47 | DataOutputStream(stream).use { dos -> 48 | dos.writeUTF(testString) 49 | } 50 | } 51 | 52 | snapshotFileManager.getInputStream(file)!!.use { stream -> 53 | DataInputStream(stream).use { dis -> 54 | val actual = dis.readUTF() 55 | 56 | check(actual == testString) { "Expected to read ${testString} but actual is $actual" } 57 | } 58 | } 59 | 60 | check(snapshotFileManager.delete(file)) { "Couldn't delete ${file.getFullPath()}" } 61 | check(!snapshotFileManager.exists(file)) { "file ${file.getFullPath()} still exists" } 62 | } 63 | 64 | check(snapshotFileManager.delete(innerDir1)) { "Couldn't delete ${innerDir1.getFullPath()}" } 65 | check(!snapshotFileManager.exists(innerDir1)) { "directory ${innerDir1} still exists" } 66 | } 67 | 68 | check(fileManager.exists(baseDir)) { "BaseDir was deleted during test!" } 69 | } 70 | 71 | private fun test1(fileManager: FileManager, baseDir: AbstractFile) { 72 | val dir = fileManager.createDir( 73 | baseDir, 74 | "test" 75 | ) 76 | 77 | if (dir == null || !fileManager.exists(dir) || !fileManager.isDirectory(dir)) { 78 | throw TestException("Couldn't create directory") 79 | } 80 | 81 | createFiles(fileManager, dir) 82 | 83 | val snapshotFileManager = fileManager.createSnapshot(dir, true) 84 | val innerDir1 = snapshotFileManager.listFiles(dir).firstOrNull() 85 | ?: throw TestException("Failed to find inner_dir1") 86 | val innerDir2 = snapshotFileManager.listFiles(innerDir1).firstOrNull() 87 | ?: throw TestException("Failed to find inner_dir2") 88 | 89 | val files = snapshotFileManager.listFiles(innerDir2) 90 | val tests = 5 91 | 92 | for (i in 0 until tests) { 93 | val time = measureTimeMillis { 94 | val fileNames = files.map { file -> fileManager.getName(file) }.toSet() 95 | 96 | for ((index, file) in files.withIndex()) { 97 | val expectedName = "${index}.txt" 98 | 99 | if (expectedName !in fileNames) { 100 | throw TestException("[Iteration $i] File name ${expectedName} does not exist in fileNames") 101 | } 102 | 103 | if (!fileManager.exists(file)) { 104 | throw TestException("[Iteration $i] File ${file.getFullPath()} does not exist") 105 | } 106 | 107 | if (!fileManager.isFile(file)) { 108 | throw TestException("[Iteration $i] File ${file.getFullPath()} is not a file") 109 | } 110 | 111 | fileManager.getLength(file) 112 | fileManager.lastModified(file) 113 | 114 | if (!fileManager.canRead(file)) { 115 | throw TestException("[Iteration $i] Cannot read ${file.getFullPath()}") 116 | } 117 | 118 | if (!fileManager.canWrite(file)) { 119 | throw TestException("[Iteration $i] Cannot write to ${file.getFullPath()}") 120 | } 121 | } 122 | } 123 | 124 | log("withSnapshot test ${i} out of $tests, time = ${time}ms") 125 | } 126 | } 127 | 128 | private fun createFiles( 129 | fileManager: FileManager, 130 | dir: AbstractFile 131 | ) { 132 | val count = 25 133 | 134 | val innerDir1 = fileManager.createDir(dir, "inner_dir1") 135 | ?: throw TestException("Failed to create innerDir1") 136 | val innerDir2 = fileManager.createDir(innerDir1, "innerDir2") 137 | ?: throw TestException("Failed to create innerDir2") 138 | 139 | for (i in 0 until count) { 140 | val fileName = "${i}.txt" 141 | 142 | val createdFile = fileManager.createFile( 143 | innerDir2, 144 | fileName 145 | ) 146 | 147 | if (createdFile == null 148 | || !fileManager.exists(createdFile) 149 | || !fileManager.isFile(createdFile) 150 | ) { 151 | throw TestException("Couldn't create file name") 152 | } 153 | 154 | if (fileManager.getName(createdFile) != fileName) { 155 | throw TestException("Bad name ${fileManager.getName(createdFile)}") 156 | } 157 | 158 | fileManager.getOutputStream(createdFile)?.use { os -> 159 | DataOutputStream(os).use { dos -> 160 | dos.writeUTF(fileName) 161 | } 162 | } ?: throw TestException("Couldn't get output stream, file = ${createdFile.getFullPath()}") 163 | 164 | fileManager.getInputStream(createdFile)?.use { `is` -> 165 | DataInputStream(`is`).use { dis -> 166 | val readString = dis.readUTF() 167 | 168 | if (readString != fileName) { 169 | throw TestException("Wrong value read, expected = ${fileName}, actual = ${readString}") 170 | } 171 | } 172 | } ?: throw TestException("Couldn't get input stream, file = ${createdFile.getFullPath()}") 173 | } 174 | } 175 | 176 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/k1rakishou/fsaf_test_app/tests/TestException.kt: -------------------------------------------------------------------------------- 1 | package com.github.k1rakishou.fsaf_test_app.tests 2 | 3 | class TestException(message: String) : Exception(message) -------------------------------------------------------------------------------- /app/src/main/java/com/github/k1rakishou/fsaf_test_app/tests/TestSuite.kt: -------------------------------------------------------------------------------- 1 | package com.github.k1rakishou.fsaf_test_app.tests 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import com.github.k1rakishou.fsaf.FileManager 6 | import com.github.k1rakishou.fsaf.file.AbstractFile 7 | import com.github.k1rakishou.fsaf.file.ExternalFile 8 | import com.github.k1rakishou.fsaf_test_app.TestBaseDirectory 9 | import kotlin.system.measureTimeMillis 10 | 11 | class TestSuite( 12 | private val fileManager: FileManager, 13 | private val context: Context 14 | ) { 15 | private val TAG = "TestSuite" 16 | 17 | fun runTests(baseDirSAF: AbstractFile, baseDirFile: AbstractFile) { 18 | try { 19 | println("$TAG =============== START TESTS ===============") 20 | println("$TAG baseDirSAF = ${baseDirSAF.getFullPath()}") 21 | println("$TAG baseDirFile = ${baseDirFile.getFullPath()}") 22 | 23 | check(fileManager.baseDirectoryExists()) { 24 | "Base directory does not exist!" 25 | } 26 | 27 | check(fileManager.exists(baseDirSAF)) { 28 | "Base directory does not exist! path = ${baseDirSAF.getFullPath()}" 29 | } 30 | check(fileManager.exists(baseDirFile)) { 31 | "Base directory does not exist! path = ${baseDirFile.getFullPath()}" 32 | } 33 | 34 | runTestsWithSAFFiles(fileManager, baseDirSAF, baseDirFile) 35 | runTestsWithJavaFiles(fileManager, baseDirSAF, baseDirFile) 36 | 37 | check(fileManager.deleteContent(baseDirFile)) { "deleteContent baseDirFile returned false" } 38 | check(fileManager.deleteContent(baseDirSAF)) { "deleteContent baseDirSAF returned false" } 39 | 40 | if (baseDirSAF is ExternalFile) { 41 | val baseDirUri = Uri.parse(baseDirSAF.getFullPath()) 42 | val dir = checkNotNull(fileManager.fromUri(baseDirUri)) { "fileManager.fromUri(${baseDirUri}) failure" } 43 | 44 | check(fileManager.exists(dir)) { "Does not exist" } 45 | check(fileManager.isDirectory(dir)) { "Not a dir" } 46 | } 47 | 48 | println("$TAG =============== END TESTS ===============") 49 | } catch (error: Throwable) { 50 | println("$TAG =============== ERROR ===============") 51 | throw error 52 | } 53 | } 54 | 55 | private fun runTestsWithSAFFiles( 56 | fileManager: FileManager, 57 | baseDirSAF: AbstractFile, 58 | baseDirFile: AbstractFile 59 | ) { 60 | val time = measureTimeMillis { 61 | SimpleTest("$TAG(SAF) SimpleTest").runTests(fileManager, baseDirSAF) 62 | CreateFilesTest("$TAG(SAF) CreateFilesTest").runTests(fileManager, baseDirSAF) 63 | SnapshotTest(context, "$TAG(SAF) SnapshotTest").runTests(fileManager, baseDirSAF) 64 | DeleteTest("$TAG(SAF) DeleteTest").runTests(fileManager, baseDirSAF) 65 | CopyTest("$TAG(SAF) CopyTest").runTests( 66 | fileManager, 67 | baseDirSAF, 68 | baseDirFile, 69 | CopyTestType.FromSafDirToRegularDir 70 | ) 71 | FindTests("$TAG(SAF) FindTests").runTests(fileManager, baseDirSAF) 72 | BadPathSymbolResolutionTest(context.applicationContext, "$TAG(SAF) BadPathSymbolResolutionTest").runTests(baseDirSAF) 73 | } 74 | 75 | println("$TAG runTestsWithSAFFiles took ${time}ms") 76 | } 77 | 78 | private fun runTestsWithJavaFiles( 79 | fileManager: FileManager, 80 | baseDirSAF: AbstractFile, 81 | baseDirFile: AbstractFile 82 | ) { 83 | val time = measureTimeMillis { 84 | SimpleTest("$TAG(Java) SimpleTest").runTests(fileManager, baseDirFile) 85 | CreateFilesTest("$TAG(Java) CreateFilesTest").runTests(fileManager, baseDirFile) 86 | SnapshotTest(context, "$TAG(Java) SnapshotTest").runTests(fileManager, baseDirFile) 87 | DeleteTest("$TAG(Java) DeleteTest").runTests(fileManager, baseDirFile) 88 | CopyTest("$TAG(Java) CopyTest").runTests( 89 | fileManager, 90 | baseDirFile, 91 | baseDirSAF, 92 | CopyTestType.FromRegularDitToSafDir 93 | ) 94 | FindTests("$TAG(Java) FindTests").runTests(fileManager, baseDirFile) 95 | BadPathSymbolResolutionTest(context.applicationContext, "$TAG(Java) BadPathSymbolResolutionTest").runTests(baseDirSAF) 96 | } 97 | 98 | println("$TAG runTestsWithJavaFiles took ${time}ms") 99 | } 100 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/file_icon_apk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folderv/androidDataWithoutRootAPI33/230a2c445e1590b4340109da25ed976e80b3fbdb/app/src/main/res/drawable-xxxhdpi/file_icon_apk.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check_circle_green_24dp.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 13 | 14 |