Repository: Raival-e/File-Explorer Branch: master Commit: 3ee7490d759a Files: 191 Total size: 522.9 KB Directory structure: gitextract_t7u9uygv/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ └── feature_request.yml │ └── workflows/ │ ├── DeleteWorkflowRuns.yml │ └── android.yml ├── .gitignore ├── .idea/ │ ├── .gitignore │ ├── .name │ ├── codeStyles/ │ │ ├── Project.xml │ │ └── codeStyleConfig.xml │ ├── compiler.xml │ ├── deploymentTargetDropDown.xml │ ├── gradle.xml │ ├── misc.xml │ └── vcs.xml ├── .replit ├── LICENSE ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── assets/ │ │ └── textmate/ │ │ ├── README.md │ │ ├── dark.json │ │ ├── json/ │ │ │ ├── language-configuration.json │ │ │ └── syntax/ │ │ │ └── json.tmLanguage.json │ │ ├── kotlin/ │ │ │ ├── language-configuration.json │ │ │ └── syntax/ │ │ │ └── kotlin.tmLanguage │ │ ├── light.tmTheme │ │ └── xml/ │ │ ├── language-configuration.json │ │ └── syntax/ │ │ └── xml.tmLanguage.json │ ├── java/ │ │ └── com/ │ │ └── raival/ │ │ └── fileexplorer/ │ │ ├── App.kt │ │ ├── activity/ │ │ │ ├── BaseActivity.kt │ │ │ ├── MainActivity.kt │ │ │ ├── SettingsActivity.kt │ │ │ ├── TextEditorActivity.kt │ │ │ ├── adapter/ │ │ │ │ └── BookmarksAdapter.kt │ │ │ ├── editor/ │ │ │ │ ├── autocomplete/ │ │ │ │ │ ├── CustomCompletionItemAdapter.kt │ │ │ │ │ └── CustomCompletionLayout.kt │ │ │ │ ├── language/ │ │ │ │ │ ├── java/ │ │ │ │ │ │ ├── JavaCodeLanguage.kt │ │ │ │ │ │ └── JavaFormatter.kt │ │ │ │ │ ├── json/ │ │ │ │ │ │ ├── JsonFormatter.kt │ │ │ │ │ │ └── JsonLanguage.kt │ │ │ │ │ ├── kotlin/ │ │ │ │ │ │ └── KotlinCodeLanguage.kt │ │ │ │ │ └── xml/ │ │ │ │ │ └── XmlLanguage.kt │ │ │ │ ├── scheme/ │ │ │ │ │ ├── DarkScheme.kt │ │ │ │ │ └── LightScheme.kt │ │ │ │ └── view/ │ │ │ │ └── SymbolInputView.kt │ │ │ └── model/ │ │ │ ├── MainViewModel.kt │ │ │ └── TextEditorViewModel.kt │ │ ├── common/ │ │ │ ├── BackgroundTask.kt │ │ │ ├── dialog/ │ │ │ │ ├── CustomDialog.kt │ │ │ │ └── OptionsDialog.kt │ │ │ └── view/ │ │ │ ├── BottomBarView.kt │ │ │ └── TabView.kt │ │ ├── extension/ │ │ │ ├── File.kt │ │ │ ├── Int.kt │ │ │ ├── Long.kt │ │ │ └── String.kt │ │ ├── glide/ │ │ │ ├── FileExplorerGlideModule.java │ │ │ ├── apk/ │ │ │ │ ├── ApkIconDataFetcher.java │ │ │ │ ├── ApkIconModelLoader.java │ │ │ │ └── ApkIconModelLoaderFactory.java │ │ │ ├── icon/ │ │ │ │ ├── IconDataFetcher.java │ │ │ │ ├── IconModelLoader.java │ │ │ │ └── IconModelLoaderFactory.java │ │ │ └── model/ │ │ │ └── IconRes.kt │ │ ├── tab/ │ │ │ ├── BaseDataHolder.kt │ │ │ ├── BaseTabFragment.kt │ │ │ ├── apps/ │ │ │ │ ├── AppsTabDataHolder.kt │ │ │ │ ├── AppsTabFragment.kt │ │ │ │ ├── adapter/ │ │ │ │ │ └── AppListAdapter.kt │ │ │ │ ├── model/ │ │ │ │ │ └── Apk.kt │ │ │ │ └── resolver/ │ │ │ │ └── ApkResolver.kt │ │ │ └── file/ │ │ │ ├── FileExplorerTabDataHolder.kt │ │ │ ├── FileExplorerTabFragment.kt │ │ │ ├── adapter/ │ │ │ │ ├── FileListAdapter.kt │ │ │ │ └── PathHistoryAdapter.kt │ │ │ ├── dialog/ │ │ │ │ ├── FileInfoDialog.kt │ │ │ │ ├── SearchDialog.kt │ │ │ │ └── TasksDialog.kt │ │ │ ├── misc/ │ │ │ │ ├── APKSignerUtils.kt │ │ │ │ ├── BuildUtils.kt │ │ │ │ ├── FileMimeTypes.kt │ │ │ │ ├── FileOpener.kt │ │ │ │ ├── FileUtils.kt │ │ │ │ ├── IconHelper.kt │ │ │ │ ├── ZipUtils.kt │ │ │ │ └── md5/ │ │ │ │ ├── HashUtils.kt │ │ │ │ ├── MessageDigestAlgorithm.kt │ │ │ │ └── StringUtils.kt │ │ │ ├── model/ │ │ │ │ ├── FileItem.kt │ │ │ │ └── Task.kt │ │ │ ├── observer/ │ │ │ │ └── FileListObserver.kt │ │ │ ├── options/ │ │ │ │ └── FileOptionsHandler.kt │ │ │ └── task/ │ │ │ ├── CompressTask.kt │ │ │ ├── CopyTask.kt │ │ │ ├── CutTask.kt │ │ │ └── ExtractTask.kt │ │ └── util/ │ │ ├── Log.kt │ │ ├── PrefsUtils.kt │ │ └── Utils.kt │ └── res/ │ ├── drawable/ │ │ ├── app_icon_foreground.xml │ │ ├── fastscroll_thumb.xml │ │ ├── ic_baseline_add_24.xml │ │ ├── ic_baseline_arrow_back_24.xml │ │ ├── ic_baseline_assignment_24.xml │ │ ├── ic_baseline_bookmark_add_24.xml │ │ ├── ic_baseline_bookmark_remove_24.xml │ │ ├── ic_baseline_bug_report_24.xml │ │ ├── ic_baseline_chevron_right_24.xml │ │ ├── ic_baseline_delete_sweep_24.xml │ │ ├── ic_baseline_file_copy_24.xml │ │ ├── ic_baseline_folder_24.xml │ │ ├── ic_baseline_folder_open_24.xml │ │ ├── ic_baseline_info_24.xml │ │ ├── ic_baseline_layers_24.xml │ │ ├── ic_baseline_logout_24.xml │ │ ├── ic_baseline_more_vert_24.xml │ │ ├── ic_baseline_open_in_browser_24.xml │ │ ├── ic_baseline_open_in_new_24.xml │ │ ├── ic_baseline_restart_alt_24.xml │ │ ├── ic_baseline_save_24.xml │ │ ├── ic_baseline_select_all_24.xml │ │ ├── ic_baseline_sort_24.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_round_code_24.xml │ │ ├── ic_round_compress_24.xml │ │ ├── ic_round_content_cut_24.xml │ │ ├── ic_round_delete_forever_24.xml │ │ ├── ic_round_edit_24.xml │ │ ├── ic_round_edit_note_24.xml │ │ ├── ic_round_exit_to_app_24.xml │ │ ├── ic_round_home_24.xml │ │ ├── ic_round_insert_drive_file_24.xml │ │ ├── ic_round_key_24.xml │ │ ├── ic_round_manage_search_24.xml │ │ ├── ic_round_menu_24.xml │ │ ├── ic_round_play_arrow_24.xml │ │ ├── ic_round_redo_24.xml │ │ ├── ic_round_search_24.xml │ │ ├── ic_round_settings_24.xml │ │ ├── ic_round_share_24.xml │ │ ├── ic_round_tab_24.xml │ │ ├── ic_round_timelapse_24.xml │ │ └── ic_round_undo_24.xml │ ├── layout/ │ │ ├── activity_main.xml │ │ ├── activity_main_drawer.xml │ │ ├── activity_main_drawer_bookmark_item.xml │ │ ├── apps_tab_app_item.xml │ │ ├── apps_tab_fragment.xml │ │ ├── bottom_bar_menu_item.xml │ │ ├── common_custom_dialog.xml │ │ ├── common_options_dialog.xml │ │ ├── common_options_dialog_item.xml │ │ ├── file_explorer_tab_file_item.xml │ │ ├── file_explorer_tab_fragment.xml │ │ ├── file_explorer_tab_info_dialog.xml │ │ ├── file_explorer_tab_info_dialog_item.xml │ │ ├── file_explorer_tab_path_history_view.xml │ │ ├── file_explorer_tab_placeholder.xml │ │ ├── file_explorer_tab_task_dialog.xml │ │ ├── file_explorer_tab_task_dialog_item.xml │ │ ├── input.xml │ │ ├── progress_view.xml │ │ ├── search_fragment.xml │ │ ├── settings_activity.xml │ │ ├── text_editor_activity.xml │ │ └── text_editor_completion_item.xml │ ├── menu/ │ │ ├── main_menu.xml │ │ ├── tab_menu.xml │ │ └── text_editor_menu.xml │ ├── mipmap-anydpi-v26/ │ │ ├── app_icon.xml │ │ └── app_icon_round.xml │ ├── values/ │ │ ├── app_icon_background.xml │ │ ├── colors.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── themes.xml │ ├── values-night/ │ │ ├── colors.xml │ │ └── themes.xml │ └── xml/ │ └── provider_paths.xml ├── build.gradle ├── fastlane/ │ └── metadata/ │ └── android/ │ └── en-US/ │ ├── full_description.txt │ └── short_description.txt ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── testkey.keystore ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ # Original source: https://github.com/AndroidIDEOfficial/AndroidIDE/blob/main/.github/ISSUE_TEMPLATE/BUG.yml name: Bug Report description: File a bug report title: "[Bug]: " labels: ["bug"] body: - type: markdown attributes: value: | Thanks for taking the time to fill out this bug report! Please provide a proper title and clear description to this issue. - type: textarea id: what-happened attributes: label: What happened? description: Describe the issue properly. placeholder: Describe the error validations: required: true - type: textarea id: expected-behavior attributes: label: What's the expected behavior? description: Tell us what is the expected behavior. placeholder: Describe the expected behavior. validations: required: true - type: dropdown id: version attributes: label: What version of File Explorer you are using? multiple: false options: - latest GitHub action - latest release (v1.1.0) - from IzzyOnDroid validations: required: true - type: textarea id: logs attributes: label: Relevant log output description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. render: shell - type: checkboxes id: not-a-duplicate attributes: label: Duplicate issues description: Please make sure that there are no similar issues opened. Duplicate issues will be closed directly. If there are any similar looking issues, leave a comment there. options: - label: This issue has not been reported yet. required: true ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: Feature Request description: Requesting a new feature title: "[Feature]: " labels: ["feature request"] body: - type: markdown attributes: value: | Thanks for taking the time to fill out this feature request! Please provide a proper title and clear description to this feature. You must request one feature at a time, if you want to request multiple features, create a new request for each one. - type: textarea id: feature-details attributes: label: Feature Description description: Describe the feature properly. placeholder: Describe the feature validations: required: true - type: textarea id: implementation attributes: label: How can this feature be implemented? description: Tell us any idea on how can we implement this feature placeholder: Describe the possible implementation validations: required: false - type: checkboxes id: not-a-duplicate attributes: label: Duplicate feature description: Please make sure that there are no similar feature opened. Duplicate feature will be closed directly. If there are any similar looking features, leave a comment there. options: - label: This feature has not been requested yet. required: true ================================================ FILE: .github/workflows/DeleteWorkflowRuns.yml ================================================ # Original at https://github.com/Sketchware-Pro/Sketchware-Pro/blob/main/.github/workflows/DeleteWorkflowRuns.yml name: 'Delete old workflow runs' on: workflow_dispatch: inputs: days: description: 'Number of retains days.' required: true default: '20' minimum_runs: description: 'The minimum runs to keep for each workflow.' required: true default: '6' jobs: deleteWorkflowRuns: runs-on: ubuntu-latest steps: - name: Delete workflow runs uses: Mattraks/delete-workflow-runs@v2.0.3 with: token: ${{ github.token }} repository: ${{ github.repository }} retain_days: ${{ github.event.inputs.days }} keep_minimum_runs: ${{ github.event.inputs.minimum_runs }} ================================================ FILE: .github/workflows/android.yml ================================================ # Original at https://github.com/Sketchware-Pro/Sketchware-Pro/blob/main/.github/workflows/android.yml name: Android CI on: push: paths: - '.github/workflows/android.yml' - 'app/**' - 'build-logic/**' - 'kotlinc/**' - 'gradle/**' - 'build.gradle' - 'gradle.properties' - 'gradlew' - 'gradlew.bat' - 'public-stable-ids.txt' - 'settings.gradle' pull_request: paths: - '.github/workflows/android.yml' - 'app/**' - 'build-logic/**' - 'kotlinc/**' - 'gradle/**' - 'build.gradle' - 'gradle.properties' - 'gradlew' - 'gradlew.bat' - 'public-stable-ids.txt' - 'settings.gradle' workflow_dispatch: jobs: build: name: Build release APK runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up JDK 18 uses: actions/setup-java@v3 with: java-version: 18 distribution: temurin cache: gradle - name: Grant execute permissions for gradlew run: chmod +x gradlew - name: Build release apk uses: gradle/gradle-build-action@v2 with: arguments: assembleRelease - name: Upload APK uses: actions/upload-artifact@v3 with: name: apk-release path: app/build/outputs/apk/release ================================================ FILE: .gitignore ================================================ *.iml .gradle /local.properties /.idea/caches /.idea/libraries /.idea/modules.xml /.idea/workspace.xml /.idea/navEditor.xml /.idea/assetWizardSettings.xml .DS_Store /build /captures .externalNativeBuild .cxx local.properties ================================================ FILE: .idea/.gitignore ================================================ # Default ignored files /shelf/ /workspace.xml ================================================ FILE: .idea/.name ================================================ File Explorer ================================================ FILE: .idea/codeStyles/Project.xml ================================================ ================================================ FILE: .idea/codeStyles/codeStyleConfig.xml ================================================ ================================================ FILE: .idea/compiler.xml ================================================ ================================================ FILE: .idea/deploymentTargetDropDown.xml ================================================ ================================================ FILE: .idea/gradle.xml ================================================ ================================================ FILE: .idea/misc.xml ================================================ ================================================ FILE: .idea/vcs.xml ================================================ ================================================ FILE: .replit ================================================ language = "bash" run = "" ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================ ![CI](https://github.com/Raival-e/File-Explorer/actions/workflows/android.yml/badge.svg) [![License](https://img.shields.io/github/license/Raival-e/File-Explorer)](https://github.com/Raival-e/File-Explorer/blob/master/LICENSE) ![Commit Activity](https://img.shields.io/github/commit-activity/m/Raival-e/File-Explorer) [![Total downloads](https://img.shields.io/github/downloads/Raival-e/File-Explorer/total)](https://github.com/Raival-e/File-Explorer/releases) ![Repository Size](https://img.shields.io/github/repo-size/Raival-e/File-Explorer) > [!WARNING] > This project is no longer maintained. Check out the new version [here](https://github.com/Raival-e/File-Explorer-Compose). # File Explorer A full-featured and lightweight file manager with Material 3 Dynamic colors # Screenshots
# Features - Open source and simple. - All basic file management functionality (e.g. copy, paste,.. etc) are supported. - Support for multiple tabs, and Tasks which make managing files much easier. - Powerful Code Editor ([Sora Editor](https://github.com/Rosemoe/sora-editor)). - Deep search that allows you to search in files contents. # Upcoming features - [ ] Built-in audio/video player and PDF/image viewer - [ ] Archive viewer - [ ] Support for exploring root/external storage # Download [Get it on IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/com.raival.fileexplorer) - Latest release from [here](https://github.com/Raival-e/File-Explorer/releases/tag/v1.1.0). - Latest debug build from [Github Actions](https://github.com/Raival-e/File-Explorer/actions). ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle ================================================ plugins { id 'com.android.application' id 'kotlin-android' } android { compileSdk 32 defaultConfig { applicationId "com.raival.fileexplorer" minSdk 26 targetSdk 32 versionCode 2 versionName "1.1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } packagingOptions { resources.excludes.add("license/*") } signingConfigs { debug { storeFile file('../testkey.keystore') storePassword 'testkey' keyAlias 'testkey' keyPassword 'testkey' } release { storeFile file("../testkey.keystore") storePassword "testkey" keyAlias "testkey" keyPassword "testkey" } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" signingConfig signingConfigs.release } } compileOptions { coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_11 } kotlinOptions { jvmTarget = '1.8' } } dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.9' implementation fileTree(dir: "libs", include: ["*.jar"]) implementation "androidx.appcompat:appcompat:1.5.1" implementation "com.google.android.material:material:1.7.0" implementation "com.google.code.gson:gson:2.9.1" implementation 'commons-io:commons-io:2.11.0' implementation 'com.github.bumptech.glide:glide:4.14.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.14.0' implementation "com.pixplicity.easyprefs:EasyPrefs:1.10.0" implementation 'net.lingala.zip4j:zip4j:2.11.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' implementation "io.github.Rosemoe.sora-editor:editor:0.17.1", { exclude group: "androidx.annotation", module: "annotation" } implementation "io.github.Rosemoe.sora-editor:language-textmate:0.17.1" implementation "io.github.Rosemoe.sora-editor:language-java:0.17.1" testImplementation "junit:junit:4.13.2" androidTestImplementation "androidx.test.ext:junit:1.1.3" androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0" } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/assets/textmate/README.md ================================================ # Disclaimer These files were copied from [CodeAssist](https://github.com/tyron12233/CodeAssist), and minor modifications were applied to some of them. Original source: https://github.com/tyron12233/CodeAssist/tree/main/app/src/main/assets/textmate ================================================ FILE: app/src/main/assets/textmate/dark.json ================================================ { "name": "darcula", "settings": [ { "settings": { "background": "#1F1A1B", "foreground": "#cccccc", "lineHighlight": "#2B2B2B", "blockLineColor": "#575757", "currentBlockLineColor": "#7a7a7a", "selection": "#214283", "completionWindowBackground": "#1F1A1B", "completionWindowStroke": "#555555" } }, { "name": "Package declaration", "scope": "storage.modifier.package", "settings": { "foreground": "#CCCCCC" } }, { "name": "Import declaration", "scope": "storage.modifier.import", "settings": { "foreground": "#CCCCCC" } }, { "name": "Class names (Identifiers starting with uppercase)", "scope": "storage.type.java", "settings": { "foreground": "#CCCCCC" } }, { "name": "Annotation", "scope": "storage.type.annotation", "settings": { "foreground": "#BBB529" } }, { "name": "Comment", "scope": "comment", "settings": { "foreground": "#707070" } }, { "name": "Operator Keywords", "scope": "keyword.operator,keyword.operator.logical,keyword.operator.relational,keyword.operator.assignment,keyword.operator.comparison,keyword.operator.ternary,keyword.operator.arithmetic,keyword.operator.spread", "settings": { "foreground": "#CCCCCC" } }, { "name": "Strings", "scope": "string,string.character.escape,string.template.quoted,string.template.quoted.punctuation,string.template.quoted.punctuation.single,string.template.quoted.punctuation.double,string.type.declaration.annotation,string.template.quoted.punctuation.tag", "settings": { "foreground": "#6A8759" } }, { "name": "String Interpolation Begin and End", "scope": "punctuation.definition.template-expression.begin,punctuation.definition.template-expression.end", "settings": { "foreground": "#CC8242" } }, { "name": "String Interpolation Body", "scope": "expression.string,meta.template.expression", "settings": { "foreground": "#CCCCCC" } }, { "name": "Number", "scope": "constant.numeric", "settings": { "foreground": "#7A9EC2" } }, { "name": "Built-in constant", "scope": "constant.language,variable.language", "settings": { "foreground": "#CC8242" } }, { "name": "User-defined constant", "scope": "constant.character, constant.other", "settings": { "foreground": "#9E7BB0" } }, { "name": "Keyword", "scope": "keyword,keyword.operator.new,keyword.operator.delete,keyword.operator.static,keyword.operator.this,keyword.operator.expression", "settings": { "foreground": "#CC8242" } }, { "name": "Method return type", "scope": "meta.method.return-type", "settings": { "foreground": "#A9B7C6" } }, { "name": "Method call identifier", "scope": "meta.method-call", "settings": { "foreground": "#A9B7C6" } }, { "name": "Types, Class Types", "scope": "entity.name.type,meta.return.type,meta.type.annotation,meta.type.parameters,support.type.primitive", "settings": { "foreground": "#7A9EC2" } }, { "name": "Storage type", "scope": "storage,storage.type,storage.modifier,storage.arrow", "settings": { "foreground": "#CC8242" } }, { "name": "Class constructor", "scope": "class.instance.constructor,new.expr entity.name.type", "settings": { "foreground": "#FFC66D" } }, { "name": "Function", "scope": "support.function, entity.name.function", "settings": { "foreground": "#FFC66D" } }, { "name": "Function Types", "scope": "annotation.meta.ts, annotation.meta.tsx", "settings": { "foreground": "#CCCCCC" } }, { "name": "Function Argument", "scope": "variable.parameter, operator.rest.parameters", "settings": { "foreground": "#A9B7C6" } }, { "name": "Variable, Property", "scope": "variable.property,variable.other.property,variable.other.object.property,variable.object.property,support.variable.property", "settings": { "foreground": "#9E7BB0" } }, { "name": "Variable name", "scope": "entity.name.variable", "settings": { "foreground": "#A9B7C6" } }, { "name": "CONSTANT", "scope": "variable.other.constant", "settings": { "foreground": "#9876AA" } }, { "name": "Module Name", "scope": "quote.module", "settings": { "foreground": "#6A8759" } }, { "name": "Markup Headings", "scope": "markup.heading", "settings": { "foreground": "#CC8242" } }, { "name": "Tag name", "scope": "punctuation.definition.tag.html, punctuation.definition.tag.begin, punctuation.definition.tag.end, entity.name.tag", "settings": { "foreground": "#FFC66D" } }, { "name": "Tag attribute", "scope": "entity.other.attribute-name", "settings": { "foreground": "#CCCCCC" } }, { "name": "Object Keys", "scope": "meta.object-literal.key", "settings": { "foreground": "#9E7BB0" } }, { "name": "TypeScript Class Modifiers", "scope": "storage.modifier.ts", "settings": { "foreground": "#CC8242" } }, { "name": "TypeScript Type Casting", "scope": "ts.cast.expr,ts.meta.entity.class.method.new.expr.cast,ts.meta.entity.type.name.new.expr.cast,ts.meta.entity.type.name.var-single-variable.annotation,tsx.cast.expr,tsx.meta.entity.class.method.new.expr.cast,tsx.meta.entity.type.name.new.expr.cast,tsx.meta.entity.type.name.var-single-variable.annotation", "settings": { "foreground": "#7A9EC2" } }, { "name": "TypeScript Type Declaration", "scope": "ts.meta.type.support,ts.meta.type.entity.name,ts.meta.class.inherited-class,tsx.meta.type.support,tsx.meta.type.entity.name,tsx.meta.class.inherited-class,type-declaration,enum-declaration", "settings": { "foreground": "#7A9EC2" } }, { "name": "TypeScript Method Declaration", "scope": "function-declaration,method-declaration,method-overload-declaration,type-fn-type-parameters", "settings": { "foreground": "#FFC66D" } }, { "name": "Documentation Block", "scope": "comment.block.documentation", "settings": { "foreground": "#6A8759" } }, { "name": "Documentation Highlight (JSDoc)", "scope": "storage.type.class.jsdoc", "settings": { "foreground": "#CC8242" } }, { "name": "Import-Export-All (*) Keyword", "scope": "constant.language.import-export-all", "settings": { "foreground": "#CCCCCC" } }, { "name": "Object Key Seperator", "scope": "objectliteral.key.separator, punctuation.separator.key-value", "settings": { "foreground": "#CCCCCC" } }, { "name": "Regex", "scope": "regex", "settings": { "fontStyle": " italic" } }, { "name": "Typescript Namespace", "scope": "ts.meta.entity.name.namespace,tsx.meta.entity.name.namespace", "settings": { "foreground": "#CCCCCC" } }, { "name": "Regex Character-class", "scope": "regex.character-class", "settings": { "foreground": "#CCCCCC" } }, { "name": "Class Name", "scope": "entity.name.type.class", "settings": { "foreground": "#A9B7C6" } }, { "name": "Class Inheritances", "scope": "entity.other.inherited-class", "settings": { "foreground": "#7A9EC2" } }, { "name": "Documentation Entity", "scope": "entity.name.type.instance.jsdoc", "settings": { "foreground": "#FFC66D" } }, { "name": "YAML entity", "scope": "yaml.entity.name,yaml.string.entity.name", "settings": { "foreground": "#CC8242" } }, { "name": "YAML string value", "scope": "yaml.string.out", "settings": { "foreground": "#CCCCCC" } }, { "name": "Ignored (Exceptions Rules)", "scope": "meta.brace.square.ts,block.support.module,block.support.type.module,block.support.function.variable,punctuation.definition.typeparameters.begin,punctuation.definition.typeparameters.end", "settings": { "foreground": "#CCCCCC" } }, { "name": "Regex", "scope": "string.regexp", "settings": { "foreground": "#CC8242" } }, { "name": "Regex Group/Set", "scope": "punctuation.definition.group.regexp,punctuation.definition.character-class.regexp", "settings": { "foreground": "#FFC66D" } }, { "name": "Regex Character Class", "scope": "constant.other.character-class.regexp, constant.character.escape.ts", "settings": { "foreground": "#CCCCCC" } }, { "name": "Regex Or Operator", "scope": "expr.regex.or.operator", "settings": { "foreground": "#CCCCCC" } }, { "name": "Tag string", "scope": "string.template.tag,string.template.punctuation.tag,string.quoted.punctuation.tag,string.quoted.embedded.tag, string.quoted.double.tag", "settings": { "foreground": "#6A8759" } }, { "name": "Tag function parenthesis", "scope": "tag.punctuation.begin.arrow.parameters.embedded,tag.punctuation.end.arrow.parameters.embedded", "settings": { "foreground": "#CCCCCC" } }, { "name": "Object-literal key class", "scope": "object-literal.object.member.key.field.other,object-literal.object.member.key.accessor,object-literal.object.member.key.array.brace.square", "settings": { "foreground": "#CCCCCC" } }, { "name": "CSS Property-value", "scope": "property-list.property-value,property-list.constant", "settings": { "foreground": "#A5C261" } }, { "name": "CSS Property variable", "scope": "support.type.property-name.variable.css,support.type.property-name.variable.scss,variable.scss", "settings": { "foreground": "#7A9EC2" } }, { "name": "CSS Property entity", "scope": "entity.other.attribute-name.class.css,entity.other.attribute-name.class.scss,entity.other.attribute-name.parent-selector-suffix.css,entity.other.attribute-name.parent-selector-suffix.scss", "settings": { "foreground": "#FFC66D" } }, { "name": "CSS Property-value", "scope": "property-list.property-value.rgb-value, keyword.other.unit.css,keyword.other.unit.scss", "settings": { "foreground": "#7A9EC2" } }, { "name": "CSS Property-value function", "scope": "property-list.property-value.function", "settings": { "foreground": "#FFC66D" } }, { "name": "CSS constant variables", "scope": "support.constant.property-value.css,support.constant.property-value.scss", "settings": { "foreground": "#A5C261" } }, { "name": "CSS Tag", "scope": "css.entity.name.tag,scss.entity.name.tag", "settings": { "foreground": "#CC8242" } }, { "name": "CSS ID, Selector", "scope": "meta.selector.css, entity.attribute-name.id, entity.other.attribute-name.pseudo-class.css,entity.other.attribute-name.pseudo-element.css", "settings": { "foreground": "#FFC66D" } }, { "name": "CSS Keyword", "scope": "keyword.scss,keyword.css", "settings": { "foreground": "#CC8242" } }, { "name": "Triple-slash Directive Tag", "scope": "triple-slash.tag", "settings": { "foreground": "#CCCCCC", "fontStyle": "italic" } }, { "scope": "token.info-token", "settings": { "foreground": "#6796e6" } }, { "scope": "token.warn-token", "settings": { "foreground": "#cd9731" } }, { "scope": "token.error-token", "settings": { "foreground": "#f44747" } }, { "scope": "token.debug-token", "settings": { "foreground": "#b267e6" } }, { "name": "Python operators", "scope": "keyword.operator.logical.python", "settings": { "foreground": "#CC8242" } }, { "name": "Dart class type", "scope": "support.class.dart", "settings": { "foreground": "#CC8242" } }, { "name": "PHP variables", "scope": [ "variable.language.php", "variable.other.php" ], "settings": { "foreground": "#9E7BB0" } }, { "name": "Perl specific", "scope": [ "variable.other.readwrite.perl" ], "settings": { "foreground": "#9E7BB0" } }, { "name": "PHP variables", "scope": [ "variable.other.property.php" ], "settings": { "foreground": "#CC8242" } }, { "name": "PHP variables", "scope": [ "support.variable.property.php" ], "settings": { "foreground": "#FFC66D" } }, { "name": "XML Namespace prefix", "scope": "entity.name.tag.namesapce.xml", "settings": { "foreground": "#9876AA" } } ] } ================================================ FILE: app/src/main/assets/textmate/json/language-configuration.json ================================================ { "comments": { "lineComment": "//", "blockComment": [ "/*", "*/" ] }, "brackets": [ [ "{", "}" ], [ "[", "]" ] ], "autoClosingPairs": [ [ "{", "}" ], [ "[", "]" ], { "open": "\"", "close": "\"", "notIn": [ "string" ] }, { "open": "'", "close": "'", "notIn": [ "string" ] }, { "open": "/**", "close": " */", "notIn": [ "string" ] } ], "surroundingPairs": [ [ "{", "}" ], [ "[", "]" ], [ "\"", "\"" ] ], "folding": { "offSide": false, "markers": { "start": "^\\s*//\\s*#region", "end": "^\\s*//\\s*#endregion" } } } ================================================ FILE: app/src/main/assets/textmate/json/syntax/json.tmLanguage.json ================================================ { "scopeName": "source.json", "name": "JSON", "fileTypes": [ "avsc", "babelrc", "bowerrc", "composer.lock", "geojson", "gltf", "htmlhintrc", "ipynb", "jscsrc", "jshintrc", "jslintrc", "json", "jsonl", "jsonld", "languagebabel", "ldj", "ldjson", "Pipfile.lock", "schema", "stylintrc", "template", "tern-config", "tern-project", "tfstate", "tfstate.backup", "topojson", "webapp", "webmanifest" ], "patterns": [ { "include": "#value" } ], "repository": { "array": { "begin": "\\[", "beginCaptures": { "0": { "name": "punctuation.definition.array.begin.json" } }, "end": "(,)?[\\s\\n]*(\\])", "endCaptures": { "1": { "name": "invalid.illegal.trailing-array-separator.json" }, "2": { "name": "punctuation.definition.array.end.json" } }, "name": "meta.structure.array.json", "patterns": [ { "include": "#value" }, { "match": ",", "name": "punctuation.separator.array.json" }, { "match": "[^\\s\\]]", "name": "invalid.illegal.expected-array-separator.json" } ] }, "constant": { "match": "\\b(true|false|null)\\b", "name": "constant.language.json" }, "number": { "match": "-?(?=[1-9]|0(?!\\d))\\d+(\\.\\d+)?([eE][+-]?\\d+)?", "name": "constant.numeric.json" }, "object": { "begin": "{", "beginCaptures": { "0": { "name": "punctuation.definition.dictionary.begin.json" } }, "end": "}", "endCaptures": { "0": { "name": "punctuation.definition.dictionary.end.json" } }, "name": "meta.structure.dictionary.json", "patterns": [ { "begin": "(?=\")", "end": "(?<=\")", "name": "meta.structure.dictionary.key.json", "patterns": [ { "include": "#string" } ] }, { "begin": ":", "beginCaptures": { "0": { "name": "punctuation.separator.dictionary.key-value.json" } }, "end": "(,)(?=[\\s\\n]*})|(,)|(?=})", "endCaptures": { "1": { "name": "invalid.illegal.trailing-dictionary-separator.json" }, "2": { "name": "punctuation.separator.dictionary.pair.json" } }, "name": "meta.structure.dictionary.value.json", "patterns": [ { "include": "#value" }, { "match": "[^\\s,]", "name": "invalid.illegal.expected-dictionary-separator.json" } ] }, { "match": "[^\\s}]", "name": "invalid.illegal.expected-dictionary-separator.json" } ] }, "string": { "begin": "\"", "beginCaptures": { "0": { "name": "punctuation.definition.string.begin.json" } }, "end": "\"", "endCaptures": { "0": { "name": "punctuation.definition.string.end.json" } }, "name": "string.quoted.double.json", "patterns": [ { "match": "(?x)\n\\\\ # a literal backslash\n( # followed by\n [\"\\\\/bfnrt] # one of these characters\n | # or\n u[0-9a-fA-F]{4} # a u and four hex digits\n)", "name": "constant.character.escape.json" }, { "match": "\\\\.", "name": "invalid.illegal.unrecognized-string-escape.json" } ] }, "value": { "patterns": [ { "include": "#constant" }, { "include": "#number" }, { "include": "#string" }, { "include": "#array" }, { "include": "#object" } ] } } } ================================================ FILE: app/src/main/assets/textmate/kotlin/language-configuration.json ================================================ { "comments": { "lineComment": "//", "blockComment": [ "/*", "*/" ] }, "brackets": [ [ "{", "}" ], [ "[", "]" ], [ "(", ")" ], [ "<", ">" ] ], "autoClosingPairs": [ { "open": "{", "close": "}" }, { "open": "[", "close": "]" }, { "open": "(", "close": ")" }, { "open": "'", "close": "'", "notIn": [ "string", "comment" ] }, { "open": "\"", "close": "\"", "notIn": [ "string" ] }, { "open": "/*", "close": " */", "notIn": [ "string" ] } ], "surroundingPairs": [ [ "{", "}" ], [ "[", "]" ], [ "(", ")" ], [ "<", ">" ], [ "'", "'" ], [ "\"", "\"" ] ], "folding": { "offSide": false, "markers": { "start": "^\\s*//\\s*#region", "end": "^\\s*//\\s*#endregion" } } } ================================================ FILE: app/src/main/assets/textmate/kotlin/syntax/kotlin.tmLanguage ================================================ fileTypes kt kts foldingStartMarker (\{\s*(//.*)?$|^\s*// \{\{\{) foldingStopMarker ^\s*(\}|// \}\}\}$) name Kotlin patterns include #comments captures 1 name keyword.other.kotlin 2 name entity.name.package.kotlin match ^\s*(package)\b(?:\s*([^ ;$]+)\s*)? captures 1 name keyword.other.import.kotlin match ^\s*(import)\s+([^ $.]+(?:\.(?:[`][^$`]+[`]|[^` $.]+))+)(?:\s+(as)\s+([`][^$`]+[`]|[^` $.]+))? name meta.import.kotlin include #code repository annotations patterns begin (@[^ (]+)(\()? beginCaptures 1 name storage.type.annotation.kotlin 2 name punctuation.definition.annotation-arguments.begin.kotlin end (\)|\s|$) endCaptures 1 name punctuation.definition.annotation-arguments.end.kotlin name meta.declaration.annotation.kotlin patterns captures 1 name constant.other.key.kotlin 2 name keyword.operator.assignment.kotlin match (\w*)\s*(=) include #code match , name punctuation.seperator.property.kotlin match @\w* name storage.type.annotation.kotlin builtin-functions patterns match \b(apply|also|let|takeIf|run|takeUnless|with|print|println)\b\s*(?={|\() captures 1 name support.function.kotlin match \b(mutableListOf|listOf|mutableMapOf|mapOf|mutableSetOf|setOf)\b\s*(?={|\() captures 1 name support.function.kotlin comments patterns captures 0 name punctuation.definition.comment.kotlin match /\*\*/ name comment.block.empty.kotlin include #comments-inline comments-inline patterns begin /\* captures 0 name punctuation.definition.comment.kotlin end \*/ name comment.block.kotlin captures 1 name comment.line.double-slash.kotlin 2 name punctuation.definition.comment.kotlin match \s*((//).*$\n?) class-literal begin (?=\b(?:class|interface|object)\s+\w+)\b end (?=\}|$) name meta.class.kotlin patterns include #keyword-literal begin \b(class|object|interface)\b\s+(\w+) beginCaptures 1 name storage.modifier.kotlin 2 name entity.name.class.kotlin end (?=\{|\(|:|$) patterns include #keyword-literal include #annotations include #types begin (:)\s*(\w+) beginCaptures 1 name keyword.operator.declaration.kotlin 2 name entity.other.inherited-class.kotlin end (?={|=|$) patterns include #types include #braces include #parens literal-functions begin (?=\b(?:fun)\b) end (?=$|=|\}) patterns begin \b(fun)\b beginCaptures 1 name keyword.other.kotlin end (?=\() patterns captures 2 name entity.name.function.kotlin match ([\.<\?>\w]+\.)?(\w+|(`[^`]*`)) include #types begin (:) beginCaptures 1 name keyword.operator.declaration.kotlin end (?={|=|$) patterns include #types include #parens include #braces parameters patterns begin (:) beginCaptures 1 name keyword.operator.declaration.kotlin end (?=,|=|\)) patterns include #types match \w+(?=:) name variable.parameter.function.kotlin include #keyword-literal keyword-literal patterns match (\!in|\!is|as\?) name keyword.operator.kotlin match \b(in|is|as|assert)\b name keyword.operator.kotlin match \b(const)\b name storage.modifier.kotlin match \b(val|var)\b name storage.type.kotlin match \b(\_)\b name punctuation.definition.variable.kotlin match \b(data|inline|tailrec|operator|infix|typealias|reified)\b name storage.type.kotlin match \b(external|public|private|protected|internal|abstract|final|sealed|enum|open|annotation|override|vararg|typealias|expect|actual|suspend|yield|out|in)\b name storage.modifier.kotlin match \b(try|catch|finally|throw)\b name keyword.control.catch-exception.kotlin match \b(if|else|when)\b name keyword.control.conditional.kotlin match \b(while|for|do|return|break|continue)\b name keyword.control.kotlin match \b(constructor|init)\b name entity.name.function.constructor match \b(companion|object)\b name storage.type.kotlin keyword-operator patterns match \b(and|or|not|inv)\b name keyword.operator.bitwise.kotlin match (==|!=|===|!==|<=|>=|<|>) name keyword.operator.comparison.kotlin match (=) name keyword.operator.assignment.kotlin match (:(?!:)) name keyword.operator.declaration.kotlin match (\?:) name keyword.operator.elvis.kotlin match (\-\-|\+\+) name keyword.operator.increment-decrement.kotlin match (\-|\+|\*|\/|%) name keyword.operator.arithmetic.kotlin match (\+\=|\-\=|\*\=|\/\=) name keyword.operator.arithmetic.assign.kotlin match (\!|\&\&|\|\|) name keyword.operator.logical.kotlin match (\.\.) name keyword.operator.range.kotlin keyword-punctuation patterns match (::) name punctuation.accessor.reference.kotlin match (\?\.) name punctuation.accessor.dot.safe.kotlin match (\.) name punctuation.accessor.dot.kotlin match (\,) name punctuation.seperator.kotlin match (\;) name punctuation.terminator.kotlin keyword-constant patterns match \b(true|false|null|class)\b name constant.language.kotlin match \b(this|super)\b name variable.language.kotlin match \b(0(x|X)[0-9A-Fa-f_]*)[L]?\b name constant.numeric.hex.kotlin match \b(0(b|B)[0-1_]*)[L]?\b name constant.numeric.binary.kotlin match \b([0-9][0-9_]*\.[0-9][0-9_]*[fFL]?)\b name constant.numeric.float.kotlin match \b([0-9][0-9_]*[fFL]?)\b name constant.numeric.integer.kotlin literal-string patterns begin " beginCaptures 0 name punctuation.definition.string.begin.kotlin end " endCaptures 0 name punctuation.definition.string.end.kotlin name string.quoted.double.kotlin patterns include #string-content literal-raw-string patterns begin """ beginCaptures 0 name punctuation.definition.string.begin.kotlin end """ endCaptures 0 name punctuation.definition.string.end.kotlin name string.quoted.triple.kotlin patterns include #string-content string-content patterns name constant.character.escape.newline.kotlin match \\\s*\n name constant.character.escape.kotlin match \\(x[\da-fA-F]{2}|u[\da-fA-F]{4}|.) begin (\$)(\{) beginCaptures 1 name punctuation.definition.keyword.kotlin 2 name punctuation.section.block.begin.kotlin end \} endCaptures 0 name punctuation.section.block.end.kotlin name entity.string.template.element.kotlin patterns include #code types patterns match \b(Nothing|Any|Unit|String|CharSequence|Int|Boolean|Char|Long|Double|Float|Short|Byte|Array|List|Map|Set|dynamic)\b(\?)? name support.class.kotlin match \b(IntArray|BooleanArray|CharArray|LongArray|DoubleArray|FloatArray|ShortArray|ByteArray)\b(\?)? name support.class.kotlin match ((?:[a-zA-Z]\w*\.)*[A-Z]+\w*[a-z]+\w*)(\?) name entity.name.type.class.kotlin patterns include #keyword-punctuation include #types match \b(?:[a-z]\w*(\.))*[A-Z]+\w*\b captures 1 name keyword.operator.dereference.kotlin name entity.name.type.class.kotlin begin \( beginCaptures 0 name punctuation.section.group.begin.kotlin end \) endCaptures 0 name punctuation.section.group.end.kotlin patterns include #types include #keyword-punctuation include #keyword-operator parens patterns begin \( beginCaptures 0 name punctuation.section.group.begin.kotlin end \) endCaptures 0 name punctuation.section.group.end.kotlin name meta.group.kotlin patterns include #keyword-punctuation include #parameters include #code braces patterns begin \{ beginCaptures 0 name punctuation.section.group.begin.kotlin end \} endCaptures 0 name punctuation.section.group.end.kotlin name meta.block.kotlin patterns include #code brackets patterns begin \[ beginCaptures 0 name punctuation.section.brackets.begin.kotlin end \] endCaptures 0 name punctuation.section.brackets.end.kotlin name meta.brackets.kotlin patterns include #code code patterns include #comments include #comments-inline include #annotations include #class-literal include #parens include #braces include #brackets include #keyword-literal include #types include #keyword-operator include #keyword-constant include #keyword-punctuation include #builtin-functions include #literal-functions include #builtin-classes include #literal-raw-string include #literal-string scopeName source.kotlin uuid d9380650-5edc-447d-8dbd-98838c7d0adf ================================================ FILE: app/src/main/assets/textmate/light.tmTheme ================================================ author Martin Kühl comment Based on the Quiet Light theme for Espresso by Ian Beck. name Quiet Light settings settings background #F5F5F5 caret #000000 foreground #333333 invisibles #AAAAAA lineHighlight #E4F6D4 selection #C9D0D9 name Comments scope comment, punctuation.definition.comment settings fontStyle italic foreground #AAAAAA name Comments: Preprocessor scope comment.block.preprocessor settings fontStyle foreground #AAAAAA name Comments: Documentation scope comment.documentation, comment.block.documentation settings foreground #448C27 name Invalid - Deprecated scope invalid.deprecated settings background #96000014 name Invalid - Illegal scope invalid.illegal settings background #96000014 foreground #660000 name Operators scope keyword.operator settings foreground #777777 name Keywords scope keyword, storage settings foreground #4B83CD name Types scope storage.type, support.type settings foreground #7A3E9D name Language Constants scope constant.language, support.constant, variable.language settings foreground #AB6526 name Variables scope variable, support.variable settings foreground #7A3E9D name Functions scope entity.name.function, support.function settings fontStyle bold foreground #AA3731 name Classes scope entity.name.type, entity.other.inherited-class, support.class settings fontStyle bold foreground #7A3E9D name Exceptions scope entity.name.exception settings foreground #660000 name Sections scope entity.name.section settings fontStyle bold name Numbers, Characters scope constant.numeric, constant.character, constant settings foreground #AB6526 name Strings scope string settings foreground #448C27 name Strings: Escape Sequences scope constant.character.escape settings foreground #777777 name Strings: Regular Expressions scope string.regexp settings foreground #4B83CD name Strings: Symbols scope constant.other.symbol settings foreground #AB6526 name Punctuation scope punctuation settings foreground #777777 name Embedded Source scope string source, text source settings background #EAEBE6 name ----------------------------------- settings name HTML: Doctype Declaration scope meta.tag.sgml.doctype, meta.tag.sgml.doctype string, meta.tag.sgml.doctype entity.name.tag, meta.tag.sgml punctuation.definition.tag.html settings foreground #AAAAAA name HTML: Tags scope meta.tag, punctuation.definition.tag.html, punctuation.definition.tag.begin.html, punctuation.definition.tag.end.html settings foreground #91B3E0 name HTML: Tag Names scope entity.name.tag settings foreground #4B83CD name HTML: Attribute Names scope meta.tag entity.other.attribute-name, entity.other.attribute-name.html settings foreground #91B3E0 name HTML: Entities scope constant.character.entity, punctuation.definition.entity settings foreground #AB6526 name ----------------------------------- settings name CSS: Selectors scope meta.selector, meta.selector entity, meta.selector entity punctuation, entity.name.tag.css settings foreground #7A3E9D name CSS: Property Names scope meta.property-name, support.type.property-name settings foreground #AB6526 name CSS: Property Values scope meta.property-value, meta.property-value constant.other, support.constant.property-value settings foreground #448C27 name CSS: Important Keyword scope keyword.other.important settings fontStyle bold name ----------------------------------- settings name Markup: Changed scope markup.changed settings background #FFFFDD foreground #000000 name Markup: Deletion scope markup.deleted settings background #FFDDDD foreground #000000 name Markup: Emphasis scope markup.italic settings fontStyle italic name Markup: Error scope markup.error settings background #96000014 foreground #660000 name Markup: Insertion scope markup.inserted settings background #DDFFDD foreground #000000 name Markup: Link scope meta.link settings foreground #4B83CD name Markup: Output scope markup.output, markup.raw settings foreground #777777 name Markup: Prompt scope markup.prompt settings foreground #777777 name Markup: Heading scope markup.heading settings foreground #AA3731 name Markup: Strong scope markup.bold settings fontStyle bold name Markup: Traceback scope markup.traceback settings foreground #660000 name Markup: Underline scope markup.underline settings fontStyle underline name Markup Quote scope markup.quote settings foreground #7A3E9D name Markup Lists scope markup.list settings foreground #4B83CD name Markup Styling scope markup.bold, markup.italic settings foreground #448C27 name Markup Inline scope markup.inline.raw settings fontStyle foreground #AB6526 name ----------------------------------- settings name Extra: Diff Range scope meta.diff.range, meta.diff.index, meta.separator settings background #DDDDFF foreground #434343 name Extra: Diff From scope meta.diff.header.from-file settings background #FFDDDD foreground #434343 name Extra: Diff To scope meta.diff.header.to-file settings background #DDFFDD foreground #434343 uuid 231D6A91-5FD1-4CBE-BD2A-0F36C08693F1 ================================================ FILE: app/src/main/assets/textmate/xml/language-configuration.json ================================================ { "comments": { "lineComment": [ "" ] }, "brackets": [ [ "<", "/>" ], [ "[", "]" ], [ "(", ")" ] ], "autoClosingPairs": [ [ "{", "}" ], [ "[", "]" ], [ "(", ")" ], { "open": "\"", "close": "\"", "notIn": [ "string" ] }, { "open": "'", "close": "'", "notIn": [ "string" ] }, { "open": "/**", "close": " */", "notIn": [ "string" ] } ], "surroundingPairs": [ [ "{", "}" ], [ "[", "]" ], [ "(", ")" ], [ "\"", "\"" ], [ "'", "'" ], [ "<", ">" ] ], "folding": { "offSide": false, "markers": { "start": "^\\s*//\\s*#region", "end": "^\\s*//\\s*#endregion" } } } ================================================ FILE: app/src/main/assets/textmate/xml/syntax/xml.tmLanguage.json ================================================ { "scopeName": "text.xml", "name": "XML", "fileTypes": [ "aiml", "atom", "axml", "bpmn", "config", "cpt", "csl", "csproj", "csproj.user", "dae", "dia", "dita", "ditamap", "dtml", "fodg", "fodp", "fods", "fodt", "fsproj", "fxml", "gir", "glade", "gpx", "graphml", "icls", "iml", "isml", "jmx", "jsp", "kml", "kst", "launch", "menu", "mxml", "nunit", "nuspec", "opml", "owl", "pom", "ppj", "proj", "pt", "pubxml", "pubxml.user", "rdf", "rng", "rss", "sdf", "shproj", "siml", "sld", "storyboard", "StyleCop", "svg", "targets", "tld", "vbox", "vbox-prev", "vbproj", "vbproj.user", "vcproj", "vcproj.filters", "vcxproj", "vcxproj.filters", "wixmsp", "wixmst", "wixobj", "wixout", "wsdl", "wxs", "xaml", "xbl", "xib", "xlf", "xliff", "xml", "xpdl", "xsd", "xul", "ui" ], "firstLineMatch": "(?x)\n# XML declaration\n(?:\n ^ <\\? xml\n\n # VersionInfo\n \\s+ version\n \\s* = \\s*\n (['\"])\n 1 \\. [0-9]+\n \\1\n\n # EncodingDecl\n (?:\n \\s+ encoding\n \\s* = \\s*\n\n # EncName\n (['\"])\n [A-Za-z]\n [-A-Za-z0-9._]*\n \\2\n )?\n\n # SDDecl\n (?:\n \\s+ standalone\n \\s* = \\s*\n (['\"])\n (?:yes|no)\n \\3\n )?\n\n \\s* \\?>\n)\n|\n# Modeline\n(?i:\n # Emacs\n -\\*-(?:\\s*(?=[^:;\\s]+\\s*-\\*-)|(?:.*?[;\\s]|(?<=-\\*-))mode\\s*:\\s*)\n xml\n (?=[\\s;]|(?]?\\d+|m)?|\\sex)(?=:(?=\\s*set?\\s[^\\n:]+:)|:(?!\\s*set?\\s))(?:(?:\\s|\\s*:\\s*)\\w*(?:\\s*=(?:[^\\n\\\\\\s]|\\\\.)*)?)*[\\s:](?:filetype|ft|syntax)\\s*=\n xml\n (?=\\s|:|$)\n)", "patterns": [ { "begin": "(<\\?)\\s*([-_a-zA-Z0-9]+)", "captures": { "1": { "name": "punctuation.definition.tag.xml" }, "2": { "name": "entity.name.tag.xml" } }, "end": "(\\?>)", "name": "meta.tag.preprocessor.xml", "patterns": [ { "match": " ([a-zA-Z-]+)", "name": "entity.other.attribute-name.xml" }, { "include": "#doublequotedString" }, { "include": "#singlequotedString" } ] }, { "begin": "()", "name": "meta.tag.sgml.doctype.xml", "patterns": [ { "include": "#internalSubset" } ] }, { "include": "#comments" }, { "begin": "(<)((?:([-_a-zA-Z0-9]+)(:))?([-_a-zA-Z0-9:]+))(?=(\\s[^>]*)?>)", "beginCaptures": { "1": { "name": "punctuation.definition.tag.xml" }, "2": { "name": "entity.name.tag.xml" }, "3": { "name": "entity.name.tag.namespace.xml" }, "4": { "name": "punctuation.separator.namespace.xml" }, "5": { "name": "entity.name.tag.localname.xml" } }, "end": "(>)()", "endCaptures": { "1": { "name": "punctuation.definition.tag.xml" }, "2": { "name": "punctuation.definition.tag.xml" }, "3": { "name": "entity.name.tag.xml" }, "4": { "name": "entity.name.tag.namespace.xml" }, "5": { "name": "punctuation.separator.namespace.xml" }, "6": { "name": "entity.name.tag.localname.xml" }, "7": { "name": "punctuation.definition.tag.xml" } }, "name": "meta.tag.no-content.xml", "patterns": [ { "include": "#tagStuff" } ] }, { "begin": "()", "name": "meta.tag.xml", "patterns": [ { "include": "#tagStuff" } ] }, { "include": "#entity" }, { "include": "#bare-ampersand" }, { "begin": "<%@", "beginCaptures": { "0": { "name": "punctuation.section.embedded.begin.xml" } }, "end": "%>", "endCaptures": { "0": { "name": "punctuation.section.embedded.end.xml" } }, "name": "source.java-props.embedded.xml", "patterns": [ { "match": "page|include|taglib", "name": "keyword.other.page-props.xml" } ] }, { "begin": "<%[!=]?(?!--)", "beginCaptures": { "0": { "name": "punctuation.section.embedded.begin.xml" } }, "end": "(?!--)%>", "endCaptures": { "0": { "name": "punctuation.section.embedded.end.xml" } }, "name": "source.java.embedded.xml", "patterns": [ { "include": "source.java" } ] }, { "begin": "", "endCaptures": { "0": { "name": "punctuation.definition.string.end.xml" } }, "name": "string.unquoted.cdata.xml" } ], "repository": { "EntityDecl": { "begin": "()", "patterns": [ { "include": "#doublequotedString" }, { "include": "#singlequotedString" } ] }, "bare-ampersand": { "match": "&", "name": "invalid.illegal.bad-ampersand.xml" }, "doublequotedString": { "begin": "\"", "beginCaptures": { "0": { "name": "punctuation.definition.string.begin.xml" } }, "end": "\"", "endCaptures": { "0": { "name": "punctuation.definition.string.end.xml" } }, "name": "string.quoted.double.xml", "patterns": [ { "include": "#entity" }, { "include": "#bare-ampersand" } ] }, "entity": { "captures": { "1": { "name": "punctuation.definition.constant.xml" }, "3": { "name": "punctuation.definition.constant.xml" } }, "match": "(&)([:a-zA-Z_][:a-zA-Z0-9_.-]*|#[0-9]+|#x[0-9a-fA-F]+)(;)", "name": "constant.character.entity.xml" }, "internalSubset": { "begin": "(\\[)", "captures": { "1": { "name": "punctuation.definition.constant.xml" } }, "end": "(\\])", "name": "meta.internalsubset.xml", "patterns": [ { "include": "#EntityDecl" }, { "include": "#parameterEntity" }, { "include": "#comments" } ] }, "parameterEntity": { "captures": { "1": { "name": "punctuation.definition.constant.xml" }, "3": { "name": "punctuation.definition.constant.xml" } }, "match": "(%)([:a-zA-Z_][:a-zA-Z0-9_.-]*)(;)", "name": "constant.character.parameter-entity.xml" }, "singlequotedString": { "begin": "'", "beginCaptures": { "0": { "name": "punctuation.definition.string.begin.xml" } }, "end": "'", "endCaptures": { "0": { "name": "punctuation.definition.string.end.xml" } }, "name": "string.quoted.single.xml", "patterns": [ { "include": "#entity" }, { "include": "#bare-ampersand" } ] }, "tagStuff": { "patterns": [ { "captures": { "1": { "name": "entity.other.attribute-name.namespace.xml" }, "2": { "name": "entity.other.attribute-name.xml" }, "3": { "name": "punctuation.separator.namespace.xml" }, "4": { "name": "entity.other.attribute-name.localname.xml" } }, "match": "(?:^|\\s+)(?:([-\\w.]+)((:)))?([-\\w.:]+)\\s*=" }, { "include": "#doublequotedString" }, { "include": "#singlequotedString" } ] } } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/App.kt ================================================ package com.raival.fileexplorer import android.app.Application import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.os.Process import android.widget.Toast import com.pixplicity.easyprefs.library.Prefs import com.raival.fileexplorer.util.Log import kotlin.system.exitProcess class App : Application() { override fun onCreate() { Thread.setDefaultUncaughtExceptionHandler { _: Thread?, throwable: Throwable? -> Log.e("AppCrash", "", throwable) Process.killProcess(Process.myPid()) exitProcess(2) } super.onCreate() appContext = this Prefs.Builder() .setContext(applicationContext) .setPrefsName("Prefs") .setMode(MODE_PRIVATE) .build() Log.start(appContext) } companion object { lateinit var appContext: Context @JvmStatic fun showMsg(message: String?) { Toast.makeText(appContext, message, Toast.LENGTH_SHORT).show() } @JvmStatic fun copyString(string: String?) { (appContext.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip( ClipData.newPlainText("clipboard", string) ) } } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/activity/BaseActivity.kt ================================================ package com.raival.fileexplorer.activity import android.Manifest import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Environment import android.provider.Settings import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import com.bumptech.glide.Glide import com.google.android.material.elevation.SurfaceColors import com.raival.fileexplorer.App import com.raival.fileexplorer.App.Companion.showMsg import com.raival.fileexplorer.R import com.raival.fileexplorer.util.PrefsUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch abstract class BaseActivity : AppCompatActivity() { @JvmField protected var currentTheme: String = PrefsUtils.Settings.themeMode /** * This method is called after checking storage permissions */ open fun init() {} override fun onCreate(savedInstanceState: Bundle?) { if (currentTheme == SettingsActivity.THEME_MODE_DARK) { setTheme(R.style.Theme_FileExplorer_Dark) } else if (currentTheme == SettingsActivity.THEME_MODE_LIGHT) { setTheme(R.style.Theme_FileExplorer_Light) } super.onCreate(savedInstanceState) window.statusBarColor = SurfaceColors.SURFACE_2.getColor(this) } protected fun checkPermissions() { if (grantStoragePermissions()) { init() } } private fun grantStoragePermissions(): Boolean { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (!Environment.isExternalStorageManager()) { val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) intent.data = Uri.fromParts("package", packageName, null) startActivityForResult(intent, 121121) return false } } else { if (ContextCompat.checkSelfPermission( this, Manifest.permission.READ_EXTERNAL_STORAGE ) == PackageManager.PERMISSION_DENIED || ContextCompat.checkSelfPermission( this, Manifest.permission.WRITE_EXTERNAL_STORAGE ) == PackageManager.PERMISSION_DENIED ) { ActivityCompat.requestPermissions( this, arrayOf( Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE ), 9011 ) return false } } return true } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == 9011) { init() } } override fun onDestroy() { super.onDestroy() CoroutineScope(Dispatchers.Default).launch { Glide.get(App.appContext).clearDiskCache() } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == 121121) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Environment.isExternalStorageManager()) { init() } else { showMsg("Storage permission is required") finish() } } } } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/activity/MainActivity.kt ================================================ package com.raival.fileexplorer.activity import android.annotation.SuppressLint import android.content.Intent import android.net.Uri import android.os.Bundle import android.os.Environment import android.view.Menu import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.HorizontalScrollView import android.widget.LinearLayout import android.widget.TextView import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.view.menu.MenuBuilder import androidx.appcompat.widget.PopupMenu import androidx.drawerlayout.widget.DrawerLayout import androidx.fragment.app.FragmentContainerView import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.RecyclerView import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.textview.MaterialTextView import com.raival.fileexplorer.App.Companion.showMsg import com.raival.fileexplorer.R import com.raival.fileexplorer.activity.adapter.BookmarksAdapter import com.raival.fileexplorer.activity.model.MainViewModel import com.raival.fileexplorer.common.dialog.CustomDialog import com.raival.fileexplorer.common.view.BottomBarView import com.raival.fileexplorer.common.view.TabView import com.raival.fileexplorer.common.view.TabView.OnUpdateTabViewListener import com.raival.fileexplorer.extension.* import com.raival.fileexplorer.tab.BaseDataHolder import com.raival.fileexplorer.tab.BaseTabFragment import com.raival.fileexplorer.tab.apps.AppsTabDataHolder import com.raival.fileexplorer.tab.apps.AppsTabFragment import com.raival.fileexplorer.tab.file.FileExplorerTabDataHolder import com.raival.fileexplorer.tab.file.FileExplorerTabFragment import com.raival.fileexplorer.tab.file.misc.FileOpener import com.raival.fileexplorer.tab.file.misc.FileUtils import com.raival.fileexplorer.util.Log import com.raival.fileexplorer.util.PrefsUtils import com.raival.fileexplorer.util.Utils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.io.File class MainActivity : BaseActivity() { private var confirmExit = false lateinit var tabView: TabView lateinit var toolbar: MaterialToolbar lateinit var bottomBarView: BottomBarView private lateinit var drawerLayout: View private lateinit var drawer: DrawerLayout private lateinit var drawerStorageSpaceProgress: LinearProgressIndicator private lateinit var drawerStorageSpace: TextView private lateinit var drawerRootSpaceProgress: LinearProgressIndicator private lateinit var drawerRootSpace: TextView private lateinit var bookmarksList: RecyclerView private lateinit var fragmentContainerView: FragmentContainerView private val mainViewModel: MainViewModel get() { return ViewModelProvider(this)[MainViewModel::class.java] } private val tabFragments: List get() { val list: MutableList = ArrayList() for (fragment in supportFragmentManager.fragments) { if (fragment is BaseTabFragment) { list.add(fragment) } } return list } private val activeFragment: BaseTabFragment get() = supportFragmentManager.findFragmentById(R.id.fragment_container) as BaseTabFragment /** * Called after read & write permissions are granted */ override fun init() { if (tabFragments.isEmpty()) { loadDefaultTab() } else { fragmentContainerView.post { restoreTabs() } } } private fun restoreTabs() { val activeFragmentTag = activeFragment.tag for (i in mainViewModel.getDataHolders().indices) { val dataHolder = mainViewModel.getDataHolders()[i] // The active fragment will create its own TabView, so we skip it if (dataHolder.tag != activeFragmentTag) { when (dataHolder) { is FileExplorerTabDataHolder -> { tabView.insertNewTabAt(i, dataHolder.tag, false).setName( dataHolder.activeDirectory!!.getShortLabel( FileExplorerTabFragment.MAX_NAME_LENGTH ) ) } is AppsTabDataHolder -> { tabView.insertNewTabAt(i, dataHolder.tag, false).setName("Apps") } // handle other types of DataHolders here } } } } /** * The default fragment cannot be deleted, and its tag is unique (starts with "0_") */ private fun loadDefaultTab() { supportFragmentManager.beginTransaction() .replace( R.id.fragment_container, FileExplorerTabFragment(), BaseTabFragment.DEFAULT_TAB_FRAGMENT_PREFIX + generateRandomTag() ) .setReorderingAllowed(true) .commit() } fun generateRandomTag(): String { return Utils.getRandomString(16) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) tabView = findViewById(R.id.tabs) fragmentContainerView = findViewById(R.id.fragment_container) toolbar = findViewById(R.id.toolbar) bottomBarView = findViewById(R.id.bottom_bar_view) drawer = findViewById(R.id.drawer) setSupportActionBar(toolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar!!.setHomeButtonEnabled(true) supportActionBar!!.setHomeAsUpIndicator(R.drawable.ic_round_menu_24) toolbar.setNavigationOnClickListener(null) val toggle: ActionBarDrawerToggle = object : ActionBarDrawerToggle(this, drawer, toolbar, R.string.app_name, R.string.app_name) { override fun onDrawerOpened(drawerView: View) { super.onDrawerOpened(drawerView) refreshBookmarks() } } drawer.addDrawerListener(toggle) toggle.syncState() tabView.setOnUpdateTabViewListener(object : OnUpdateTabViewListener { override fun onUpdate(tab: TabView.Tab?, event: Int) { if (tab == null) return if (event == TabView.ON_SELECT) { if (tab.tag.startsWith(BaseTabFragment.FILE_EXPLORER_TAB_FRAGMENT_PREFIX) || tab.tag.startsWith(BaseTabFragment.DEFAULT_TAB_FRAGMENT_PREFIX) ) { if (supportFragmentManager.findFragmentById(R.id.fragment_container)?.tag != tab.tag) { supportFragmentManager.beginTransaction() .replace( R.id.fragment_container, FileExplorerTabFragment(), tab.tag ) .setReorderingAllowed(true) .commit() } } if (tab.tag.startsWith(BaseTabFragment.APPS_TAB_FRAGMENT_PREFIX)) { if (supportFragmentManager.findFragmentById(R.id.fragment_container)?.tag != tab.tag) { supportFragmentManager.beginTransaction() .replace(R.id.fragment_container, AppsTabFragment(), tab.tag) .setReorderingAllowed(true) .commit() } } // Handle other types of tabs here... } else if (event == TabView.ON_LONG_CLICK) { val popupMenu = PopupMenu(this@MainActivity, tab.view) popupMenu.inflate(R.menu.tab_menu) // Default tab is un-closable if (tab.tag.startsWith(BaseTabFragment.DEFAULT_TAB_FRAGMENT_PREFIX)) { popupMenu.menu.findItem(R.id.close).isVisible = false popupMenu.menu.findItem(R.id.close_all).isVisible = false } popupMenu.setOnMenuItemClickListener { item: MenuItem -> when (item.itemId) { R.id.close -> { val activeFragment = activeFragment if (tab.tag == activeFragment.tag) { activeFragment.closeTab() } else { mainViewModel.getDataHolders() .removeIf { dataHolder1: BaseDataHolder -> dataHolder1.tag == tab.tag } closeTab(tab.tag) } return@setOnMenuItemClickListener true } R.id.close_all -> { val activeFragment = activeFragment // Remove unselected tabs for (tag in tabView.tags) { if (!tag.startsWith(BaseTabFragment.DEFAULT_TAB_FRAGMENT_PREFIX) && tag != activeFragment.tag) { mainViewModel.getDataHolders() .removeIf { dataHolder1: BaseDataHolder -> dataHolder1.tag == tag } closeTab(tag) } } // Remove the active tab activeFragment.closeTab() return@setOnMenuItemClickListener true } R.id.close_others -> { val activeFragment = activeFragment for (tag in tabView.tags) { if (!tag.startsWith(BaseTabFragment.DEFAULT_TAB_FRAGMENT_PREFIX) && tag != activeFragment.tag && tag != tab.tag) { mainViewModel.getDataHolders() .removeIf { dataHolder1: BaseDataHolder -> dataHolder1.tag == tag } closeTab(tag) } } if (activeFragment.tag != tab.tag) activeFragment.closeTab() return@setOnMenuItemClickListener true } else -> false } } popupMenu.show() } } }) findViewById(R.id.tabs_options).setOnClickListener { addNewTab() } checkPermissions() setupDrawer() } override fun onResume() { super.onResume() val newTheme = PrefsUtils.Settings.themeMode if (newTheme != currentTheme) { recreate() } else { bottomBarView.onUpdatePrefs() } } @SuppressLint("NotifyDataSetChanged") fun refreshBookmarks() { bookmarksList.adapter?.notifyDataSetChanged() } fun onBookmarkSelected(file: File) { if (file.isDirectory) { val fragment = FileExplorerTabFragment(file) addNewTab( fragment, BaseTabFragment.FILE_EXPLORER_TAB_FRAGMENT_PREFIX + generateRandomTag() ) } else { FileOpener(this).openFile(file) } if (drawer.isDrawerOpen(drawerLayout)) drawer.close() } private fun setupDrawer() { drawerLayout = findViewById(R.id.drawer_layout) drawerStorageSpaceProgress = drawerLayout.findViewById(R.id.storage_space_progress) drawerRootSpaceProgress = drawerLayout.findViewById(R.id.root_space_progress) drawerRootSpace = drawerLayout.findViewById(R.id.root_space) drawerStorageSpace = drawerLayout.findViewById(R.id.storage_space) bookmarksList = drawerLayout.findViewById(R.id.rv) drawerLayout.findViewById(R.id.apps).setOnClickListener { addNewTab( AppsTabFragment(), BaseTabFragment.APPS_TAB_FRAGMENT_PREFIX + generateRandomTag() ) drawer.close() } bookmarksList.adapter = BookmarksAdapter(this) drawerLayout.findViewById(R.id.toolbar).apply { setTitle(R.string.app_name) subtitle = LINK menu.apply { clear() add("GitHub") .setOnMenuItemClickListener { openGithubPage() } .setIcon(R.drawable.ic_baseline_open_in_browser_24) .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) add("Settings") .setOnMenuItemClickListener { openSettings() } .setIcon(R.drawable.ic_round_settings_24) .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) } } updateStorageSpace() updateRootSpace() } private fun openSettings(): Boolean { startActivity(Intent().setClass(this, SettingsActivity::class.java)) return true } private fun openGithubPage(): Boolean { startActivity(Intent(Intent.ACTION_VIEW).setData(Uri.parse(LINK))) return true } @SuppressLint("SetTextI18n") private fun updateRootSpace() { val used = Environment.getRootDirectory().getUsedMemoryBytes() val total = Environment.getRootDirectory().getTotalMemoryBytes() val available = Environment.getRootDirectory().getAvailableMemoryBytes() drawerRootSpaceProgress.progress = (used.toDouble() / total.toDouble() * 100).toInt() drawerRootSpace.text = (FileUtils.getFormattedSize(used) + " used, " + FileUtils.getFormattedSize(available) + " available") } @SuppressLint("SetTextI18n") private fun updateStorageSpace() { val used = Environment.getExternalStorageDirectory().getUsedMemoryBytes() val total = Environment.getExternalStorageDirectory().getTotalMemoryBytes() val available = Environment.getExternalStorageDirectory().getAvailableMemoryBytes() drawerStorageSpaceProgress.progress = (used.toDouble() / total.toDouble() * 100).toInt() drawerStorageSpace.text = (FileUtils.getFormattedSize(used) + " used, " + FileUtils.getFormattedSize(available) + " available") } fun addNewTab(fragment: BaseTabFragment, tag: String) { supportFragmentManager.beginTransaction() .replace(R.id.fragment_container, fragment, tag) .setReorderingAllowed(true) .commit() } @SuppressLint("SetTextI18n") private fun addNewTab() { val customDialog = CustomDialog() val input = customDialog.createInput(this, "e.g. /sdcard/...") input.editText?.setSingleLine() val textView = MaterialTextView(this) textView.setPadding(0, 8.toDp(), 0, 0) textView.alpha = 0.7f textView.text = "Quick Links:" val layout = ChipGroup(this).apply { isScrollContainer = true } // Chips layout.addView(createChip("Internal Storage") { addNewTab( FileExplorerTabFragment(), BaseTabFragment.FILE_EXPLORER_TAB_FRAGMENT_PREFIX + generateRandomTag() ) customDialog.dismiss() }) layout.addView(createChip("Downloads") { addNewTab( FileExplorerTabFragment(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)), BaseTabFragment.FILE_EXPLORER_TAB_FRAGMENT_PREFIX + generateRandomTag() ) customDialog.dismiss() }) layout.addView(createChip("Documents") { addNewTab( FileExplorerTabFragment(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)), BaseTabFragment.FILE_EXPLORER_TAB_FRAGMENT_PREFIX + generateRandomTag() ) customDialog.dismiss() }) layout.addView(createChip("DCIM") { addNewTab( FileExplorerTabFragment(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)), BaseTabFragment.FILE_EXPLORER_TAB_FRAGMENT_PREFIX + generateRandomTag() ) customDialog.dismiss() }) layout.addView(createChip("Music") { addNewTab( FileExplorerTabFragment(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC)), BaseTabFragment.FILE_EXPLORER_TAB_FRAGMENT_PREFIX + generateRandomTag() ) customDialog.dismiss() }) customDialog.setTitle("Set tab path") .addView(input) .addView(textView) .addView(HorizontalScrollView(this).apply { layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT ) addView(layout) }) .setPositiveButton("Go", { val file = File( input.editText!!.text.toString() ) if (file.exists()) { if (file.canRead()) { if (file.isFile) { FileOpener(this).openFile(file) } else { addNewTab( FileExplorerTabFragment(file), BaseTabFragment.FILE_EXPLORER_TAB_FRAGMENT_PREFIX + generateRandomTag() ) } } else { showMsg(Log.UNABLE_TO + " read the provided file") } } else { showMsg("The destination path doesn't exist!") } }, true) .show(supportFragmentManager, "") } private fun createChip(title: String, onClick: () -> Unit): View { return Chip(this).apply { layoutParams = LinearLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT ) text = title setOnClickListener { onClick.invoke() } } } @SuppressLint("RestrictedApi") override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.main_menu, menu) (menu as MenuBuilder).setOptionalIconsVisible(true) return super.onCreateOptionsMenu(menu) } override fun onOptionsItemSelected(item: MenuItem): Boolean { val title = item.title.toString() if (title == "Logs") { showLogFile() } return super.onOptionsItemSelected(item) } private fun showLogFile() { val logFile = File(getExternalFilesDir(null)!!.absolutePath + "/debug/log.txt") if (logFile.exists() && logFile.isFile) { val intent = Intent() intent.setClass(this, TextEditorActivity::class.java) intent.putExtra("file", logFile.absolutePath) startActivity(intent) return } showMsg("No logs found") } override fun onBackPressed() { if (drawer.isDrawerOpen(drawerLayout)) { drawer.close() return } if (activeFragment.onBackPressed()) { return } if (!confirmExit) { confirmExit = true showMsg("Press again to exit") CoroutineScope(Dispatchers.Main).launch { delay(2000) confirmExit = false } return } super.onBackPressed() } fun closeTab(tag: String) { // Remove the tab from TabView. TabView will select another tab which will replace the corresponding fragment. // The DataHolder must be removed by the fragment itself, as deletion process differs for each tab. // Default fragment (the one added when the app is opened) won't be closed. if (tag.startsWith("0_")) return tabView.removeTab(tag) } companion object { private const val LINK = "https://github.com/Raival-e/File-Explorer" } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/activity/SettingsActivity.kt ================================================ package com.raival.fileexplorer.activity import android.os.Bundle import android.view.View import android.widget.TextView import androidx.appcompat.widget.SwitchCompat import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.slider.Slider import com.raival.fileexplorer.R import com.raival.fileexplorer.common.dialog.CustomDialog import com.raival.fileexplorer.common.dialog.OptionsDialog import com.raival.fileexplorer.tab.file.misc.FileUtils import com.raival.fileexplorer.util.PrefsUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch class SettingsActivity : BaseActivity() { private lateinit var logModeValue: TextView private lateinit var themeModeValue: TextView private lateinit var deepSearchSizeLimitValue: TextView private lateinit var showBottomToolbarLabels: SwitchCompat override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.settings_activity) val materialToolbar = findViewById(R.id.toolbar) setSupportActionBar(materialToolbar) supportActionBar?.title = "Settings" supportActionBar!!.setDisplayHomeAsUpEnabled(true) supportActionBar!!.setHomeButtonEnabled(true) materialToolbar.setNavigationOnClickListener { onBackPressed() } themeModeValue = findViewById(R.id.settings_theme_value) themeModeValue.text = PrefsUtils.Settings.themeMode findViewById(R.id.settings_theme).setOnClickListener { OptionsDialog("Select theme mode") .addOption( label = THEME_MODE_AUTO, listener = { setThemeMode(THEME_MODE_AUTO) }, dismissOnClick = true ) .addOption( label = THEME_MODE_DARK, listener = { setThemeMode(THEME_MODE_DARK) }, dismissOnClick = true ) .addOption( label = THEME_MODE_LIGHT, listener = { setThemeMode(THEME_MODE_LIGHT) }, dismissOnClick = true ) .show(supportFragmentManager, "") } showBottomToolbarLabels = findViewById(R.id.settings_bottom_toolbar_labels_value) showBottomToolbarLabels.isChecked = PrefsUtils.Settings.showBottomToolbarLabels findViewById(R.id.settings_bottom_toolbar_labels).setOnClickListener { showBottomToolbarLabels.isChecked = !showBottomToolbarLabels.isChecked PrefsUtils.Settings.showBottomToolbarLabels = showBottomToolbarLabels.isChecked } logModeValue = findViewById(R.id.settings_log_mode_value) logModeValue.text = PrefsUtils.Settings.logMode findViewById(R.id.settings_log_mode).setOnClickListener { OptionsDialog("Select log mode") .addOption(LOG_MODE_DISABLE, { setLogMode(LOG_MODE_DISABLE) }, true) .addOption( LOG_MODE_ERRORS_ONLY, { setLogMode(LOG_MODE_ERRORS_ONLY) }, true ) .addOption(LOG_MODE_ALL, { setLogMode(LOG_MODE_ALL) }, true) .show(supportFragmentManager, "") } deepSearchSizeLimitValue = findViewById(R.id.settings_deep_search_limit_value) deepSearchSizeLimitValue.text = FileUtils.getFormattedSize( PrefsUtils.Settings.deepSearchFileSizeLimit, "%.0f" ) findViewById(R.id.settings_deep_search_limit).setOnClickListener { val seekBar = Slider(this).apply { valueFrom = 0f valueTo = 80f stepSize = 1f value = PrefsUtils.Settings.deepSearchFileSizeLimit.toFloat() / 1024 / 1024 } val customDialog = CustomDialog() customDialog.setTitle("Max file size limit (MB)") .setMsg( "Any file larger than " + FileUtils.getFormattedSize(PrefsUtils.Settings.deepSearchFileSizeLimit) + " will be ignored" ) .addView(seekBar) .setPositiveButton("Save", { PrefsUtils.Settings.deepSearchFileSizeLimit = seekBar.value.toLong() * 1024 * 1024 deepSearchSizeLimitValue.text = FileUtils.getFormattedSize( PrefsUtils.Settings.deepSearchFileSizeLimit, "%.0f" ) }, true) .setNegativeButton("Cancel", null, true) .show(supportFragmentManager, "") seekBar.addOnChangeListener(Slider.OnChangeListener { _: Slider?, value: Float, _: Boolean -> customDialog.setMsg( "Any file larger than " + FileUtils.getFormattedSize((value * 1024 * 1024).toLong(), "%.0f") + " will be ignored" ) }) } } private fun setLogMode(mode: String) { logModeValue.text = mode PrefsUtils.Settings.logMode = mode } private fun setThemeMode(mode: String) { themeModeValue.text = mode PrefsUtils.Settings.themeMode = mode CoroutineScope(Dispatchers.Main).launch { delay(150) recreate() } } companion object { const val LOG_MODE_DISABLE = "Disable" const val LOG_MODE_ERRORS_ONLY = "Errors only" const val LOG_MODE_ALL = "All logs" const val THEME_MODE_AUTO = "Auto" const val THEME_MODE_DARK = "Dark" const val THEME_MODE_LIGHT = "Light" } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/activity/TextEditorActivity.kt ================================================ package com.raival.fileexplorer.activity import android.annotation.SuppressLint import android.graphics.Color import android.graphics.Typeface import android.os.Bundle import android.text.Editable import android.text.TextWatcher import android.view.Menu import android.view.MenuItem import android.view.View import androidx.appcompat.view.menu.MenuBuilder import androidx.appcompat.widget.Toolbar import androidx.lifecycle.ViewModelProvider import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.elevation.SurfaceColors import com.google.android.material.textfield.TextInputLayout import com.raival.fileexplorer.App.Companion.showMsg import com.raival.fileexplorer.R import com.raival.fileexplorer.activity.editor.autocomplete.CustomCompletionItemAdapter import com.raival.fileexplorer.activity.editor.autocomplete.CustomCompletionLayout import com.raival.fileexplorer.activity.editor.language.java.JavaCodeLanguage import com.raival.fileexplorer.activity.editor.language.json.JsonLanguage import com.raival.fileexplorer.activity.editor.language.kotlin.KotlinCodeLanguage import com.raival.fileexplorer.activity.editor.language.xml.XmlLanguage import com.raival.fileexplorer.activity.editor.scheme.DarkScheme import com.raival.fileexplorer.activity.editor.scheme.LightScheme import com.raival.fileexplorer.activity.editor.view.SymbolInputView import com.raival.fileexplorer.activity.model.TextEditorViewModel import com.raival.fileexplorer.tab.file.misc.FileMimeTypes import com.raival.fileexplorer.util.Log import com.raival.fileexplorer.util.PrefsUtils import com.raival.fileexplorer.util.Utils import io.github.rosemoe.sora.lang.EmptyLanguage import io.github.rosemoe.sora.lang.Language import io.github.rosemoe.sora.langs.textmate.TextMateColorScheme import io.github.rosemoe.sora.widget.CodeEditor import io.github.rosemoe.sora.widget.EditorSearcher.SearchOptions import io.github.rosemoe.sora.widget.component.EditorAutoCompletion import io.github.rosemoe.sora.widget.component.Magnifier import io.github.rosemoe.sora.widget.schemes.EditorColorScheme import org.eclipse.tm4e.core.registry.IThemeSource import java.io.File import java.io.IOException import java.util.* class TextEditorActivity : BaseActivity() { private lateinit var editor: CodeEditor private lateinit var searchPanel: View private lateinit var editorViewModel: TextEditorViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.text_editor_activity) editorViewModel = ViewModelProvider(this).get(TextEditorViewModel::class.java) editor = findViewById(R.id.editor) val materialToolbar = findViewById(R.id.toolbar) searchPanel = findViewById(R.id.search_panel) setupSearchPanel() val inputView = findViewById(R.id.symbol_input) inputView.bindEditor(editor) .setTextColor(Utils.getColorAttribute(R.attr.colorOnSurface, this)) .setBackgroundColor(SurfaceColors.SURFACE_2.getColor(this)) inputView.addSymbol("->", " ") .addSymbols(arrayOf("_", "=", "{", "}", "<", ">", "|", "\\", "?", "+", "-", "*", "/")) editor.apply { getComponent(EditorAutoCompletion::class.java).setLayout(CustomCompletionLayout()) getComponent(EditorAutoCompletion::class.java) .setAdapter(CustomCompletionItemAdapter()) typefaceText = Typeface.createFromAsset(assets, "font/JetBrainsMono-Regular.ttf") props.useICULibToSelectWords = false props.symbolPairAutoCompletion = false props.deleteMultiSpaces = -1 props.deleteEmptyLineFast = false } loadEditorPrefs() if (editorViewModel.file == null) editorViewModel.file = File(intent.getStringExtra("file")!!) detectLanguage(editorViewModel.file!!) materialToolbar.title = editorViewModel.file!!.name setSupportActionBar(materialToolbar) supportActionBar?.apply { setDisplayHomeAsUpEnabled(true) setHomeButtonEnabled(true) } materialToolbar.setNavigationOnClickListener { onBackPressed() } if (!editorViewModel.file!!.exists()) { showMsg("File not found") finish() } if (editorViewModel.file!!.isDirectory) { showMsg("Invalid file") finish() } try { if (editorViewModel.content != null) { editor.setText(editorViewModel.content.toString()) } else { editor.setText(editorViewModel.file?.readText()) } } catch (exception: Exception) { Log.e( TAG, Log.SOMETHING_WENT_WRONG + " while reading file: " + editorViewModel.file!!.absolutePath, exception ) showMsg("Failed to read file: " + editorViewModel.file!!.absolutePath) finish() } } private fun detectLanguage(file: File) { when (file.extension.lowercase(Locale.getDefault())) { FileMimeTypes.javaType -> setEditorLanguage(LANGUAGE_JAVA) FileMimeTypes.kotlinType -> setEditorLanguage(LANGUAGE_KOTLIN) "json" -> setEditorLanguage(LANGUAGE_JSON) "xml" -> setEditorLanguage(LANGUAGE_XML) else -> setEditorLanguage(-1) } } private fun setupSearchPanel() { val findInput = searchPanel.findViewById(R.id.find_input) findInput.hint = "Find text" val replaceInput = searchPanel.findViewById(R.id.replace_input) replaceInput.hint = "Replacement" findInput.editText?.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} override fun afterTextChanged(editable: Editable) { if (editable.isNotEmpty()) { editor.searcher.search( editable.toString(), SearchOptions(false, false) ) } else { editor.searcher.stopSearch() } } }) searchPanel.apply { findViewById(R.id.next) .setOnClickListener { if (editor.searcher.hasQuery()) editor.searcher.gotoNext() } findViewById(R.id.previous) .setOnClickListener { if (editor.searcher.hasQuery()) editor.searcher.gotoPrevious() } findViewById(R.id.replace).setOnClickListener { if (editor.searcher.hasQuery()) editor.searcher.replaceThis( replaceInput.editText?.text.toString() ) } findViewById(R.id.replace_all).setOnClickListener { if (editor.searcher.hasQuery()) editor.searcher.replaceAll( replaceInput.editText?.text.toString() ) } } } public override fun onStop() { super.onStop() editorViewModel.content = editor.text } override fun onBackPressed() { if (searchPanel.visibility == View.VISIBLE) { searchPanel.visibility = View.GONE editor.searcher.stopSearch() return } try { if (editorViewModel.file?.readText() != editor.text.toString()) { MaterialAlertDialogBuilder(this) .setTitle("Save File") .setMessage("Do you want to save this file before exit?") .setPositiveButton("Yes") { _, _ -> saveFile(editor.text.toString()) finish() } .setNegativeButton("No") { _, _ -> finish() } .show() return } } catch (exception: Exception) { Log.w(TAG, exception) } super.onBackPressed() } @SuppressLint("RestrictedApi") override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.text_editor_menu, menu) (menu as MenuBuilder).setOptionalIconsVisible(true) menu.findItem(R.id.editor_option_wordwrap).isChecked = PrefsUtils.TextEditor.textEditorWordwrap menu.findItem(R.id.editor_option_magnifier).isChecked = PrefsUtils.TextEditor.textEditorMagnifier menu.findItem(R.id.editor_option_pin_line_number).isChecked = PrefsUtils.TextEditor.textEditorPinLineNumber menu.findItem(R.id.editor_option_line_number).isChecked = PrefsUtils.TextEditor.textEditorShowLineNumber menu.findItem(R.id.editor_option_read_only).isChecked = PrefsUtils.TextEditor.textEditorReadOnly menu.findItem(R.id.editor_option_autocomplete).isChecked = PrefsUtils.TextEditor.textEditorAutocomplete return super.onCreateOptionsMenu(menu) } private fun loadEditorPrefs() { editor.setPinLineNumber(PrefsUtils.TextEditor.textEditorPinLineNumber) editor.isWordwrap = PrefsUtils.TextEditor.textEditorWordwrap editor.isLineNumberEnabled = PrefsUtils.TextEditor.textEditorShowLineNumber editor.getComponent(Magnifier::class.java).isEnabled = PrefsUtils.TextEditor.textEditorMagnifier editor.isEditable = !PrefsUtils.TextEditor.textEditorReadOnly editor.getComponent(EditorAutoCompletion::class.java).isEnabled = PrefsUtils.TextEditor.textEditorAutocomplete } override fun onOptionsItemSelected(item: MenuItem): Boolean { val id = item.itemId if (id == R.id.editor_format) { editor.formatCodeAsync() } else if (id == R.id.editor_language_def) { item.isChecked = true editor.setEditorLanguage(null) } else if (id == R.id.editor_language_java) { item.isChecked = true setEditorLanguage(LANGUAGE_JAVA) } else if (id == R.id.editor_language_kotlin) { item.isChecked = true setEditorLanguage(LANGUAGE_KOTLIN) } else if (id == R.id.editor_option_read_only) { item.isChecked = !item.isChecked PrefsUtils.TextEditor.textEditorReadOnly = item.isChecked editor.isEditable = !item.isChecked } else if (id == R.id.editor_option_search) { if (searchPanel.visibility == View.GONE) { searchPanel.visibility = View.VISIBLE } else { searchPanel.visibility = View.GONE editor.searcher.stopSearch() } } else if (id == R.id.editor_option_save) { saveFile(editor.text.toString()) showMsg("Saved successfully") } else if (id == R.id.editor_option_text_undo) { editor.undo() } else if (id == R.id.editor_option_text_redo) { editor.redo() } else if (id == R.id.editor_option_wordwrap) { item.isChecked = !item.isChecked PrefsUtils.TextEditor.textEditorWordwrap = item.isChecked editor.isWordwrap = item.isChecked } else if (id == R.id.editor_option_magnifier) { item.isChecked = !item.isChecked editor.getComponent(Magnifier::class.java).isEnabled = item.isChecked PrefsUtils.TextEditor.textEditorMagnifier = item.isChecked } else if (id == R.id.editor_option_line_number) { item.isChecked = !item.isChecked PrefsUtils.TextEditor.textEditorShowLineNumber = item.isChecked editor.isLineNumberEnabled = item.isChecked } else if (id == R.id.editor_option_pin_line_number) { item.isChecked = !item.isChecked PrefsUtils.TextEditor.textEditorPinLineNumber = item.isChecked editor.setPinLineNumber(item.isChecked) } else if (id == R.id.editor_option_autocomplete) { item.isChecked = !item.isChecked PrefsUtils.TextEditor.textEditorAutocomplete = item.isChecked editor.getComponent(EditorAutoCompletion::class.java).isEnabled = item.isChecked } return super.onOptionsItemSelected(item) } private fun setEditorLanguage(language: Int) { when (language) { LANGUAGE_JAVA -> { editor.apply { colorScheme = getColorScheme(false) setEditorLanguage(javaLanguage) } } LANGUAGE_KOTLIN -> { editor.apply { colorScheme = getColorScheme(true) setEditorLanguage(kotlinLang) } } LANGUAGE_XML -> { editor.apply { colorScheme = getColorScheme(true) setEditorLanguage(xmlLang) } } LANGUAGE_JSON -> { editor.apply { colorScheme = getColorScheme(true) setEditorLanguage(jsonLang) } } else -> { editor.apply { colorScheme = getColorScheme(false) setEditorLanguage(EmptyLanguage()) } } } } private val javaLanguage: Language get() = JavaCodeLanguage() private val jsonLang: Language get() = JsonLanguage((getColorScheme(true) as TextMateColorScheme).themeSource) private val xmlLang: Language get() = XmlLanguage((getColorScheme(true) as TextMateColorScheme).themeSource) private val kotlinLang: Language get() = KotlinCodeLanguage((getColorScheme(true) as TextMateColorScheme).themeSource) private fun getColorScheme(isTextmate: Boolean): EditorColorScheme { return if (Utils.isDarkMode) getDarkScheme(isTextmate) else getLightScheme(isTextmate) } private fun getLightScheme(isTextmate: Boolean): EditorColorScheme { val scheme: EditorColorScheme = if (isTextmate) { try { TextMateColorScheme.create( IThemeSource.fromInputStream( assets.open("textmate/light.tmTheme"), "light.tmTheme", null ) ) } catch (e: Exception) { Log.e( TAG, Log.SOMETHING_WENT_WRONG + " while creating light scheme for textmate language", e ) showMsg(Log.UNABLE_TO + " load: textmate/light.tmTheme") LightScheme() } } else { LightScheme() } scheme.apply { setColor( EditorColorScheme.WHOLE_BACKGROUND, SurfaceColors.SURFACE_0.getColor(this@TextEditorActivity) ) setColor( EditorColorScheme.LINE_NUMBER_BACKGROUND, SurfaceColors.SURFACE_0.getColor(this@TextEditorActivity) ) setColor( EditorColorScheme.COMPLETION_WND_BACKGROUND, SurfaceColors.SURFACE_1.getColor(this@TextEditorActivity) ) setColor(EditorColorScheme.HIGHLIGHTED_DELIMITERS_FOREGROUND, Color.RED) } return scheme } private fun getDarkScheme(isTextmate: Boolean): EditorColorScheme { val scheme: EditorColorScheme = if (isTextmate) { try { TextMateColorScheme.create( IThemeSource.fromInputStream( assets.open("textmate/dark.json"), "dark.json", null ) ) } catch (e: Exception) { Log.e( TAG, Log.SOMETHING_WENT_WRONG + " while creating dark scheme for textmate language", e ) showMsg(Log.UNABLE_TO + " load: textmate/dark.json") DarkScheme() } } else { DarkScheme() } scheme.apply { setColor( EditorColorScheme.WHOLE_BACKGROUND, SurfaceColors.SURFACE_0.getColor(this@TextEditorActivity) ) setColor( EditorColorScheme.LINE_NUMBER_BACKGROUND, SurfaceColors.SURFACE_0.getColor(this@TextEditorActivity) ) setColor( EditorColorScheme.COMPLETION_WND_BACKGROUND, SurfaceColors.SURFACE_1.getColor(this@TextEditorActivity) ) setColor(EditorColorScheme.HIGHLIGHTED_DELIMITERS_FOREGROUND, Color.RED) } return scheme } private fun saveFile(content: String) { try { editorViewModel.file?.writeText(content) } catch (e: IOException) { Log.e(TAG, Log.UNABLE_TO + " write to file " + editorViewModel.file, e) showMsg(Log.SOMETHING_WENT_WRONG + ", check app debug for more details") } } companion object { private const val TAG = "TextEditorActivity" private const val LANGUAGE_JAVA = 0 private const val LANGUAGE_KOTLIN = 1 private const val LANGUAGE_JSON = 2 private const val LANGUAGE_XML = 3 } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/activity/adapter/BookmarksAdapter.kt ================================================ package com.raival.fileexplorer.activity.adapter import android.annotation.SuppressLint import android.graphics.Color import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.raival.fileexplorer.App.Companion.showMsg import com.raival.fileexplorer.R import com.raival.fileexplorer.activity.MainActivity import com.raival.fileexplorer.tab.file.misc.IconHelper import com.raival.fileexplorer.util.PrefsUtils import com.raival.fileexplorer.util.Utils import java.io.File class BookmarksAdapter(private val activity: MainActivity) : RecyclerView.Adapter() { private var list = arrayListOf() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { @SuppressLint("InflateParams") val v = activity.layoutInflater.inflate(R.layout.activity_main_drawer_bookmark_item, null) v.layoutParams = RecyclerView.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) return ViewHolder(v) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.bind() } override fun getItemCount(): Int { return PrefsUtils.TextEditor.fileExplorerTabBookmarks.also { list = it }.size } inner class ViewHolder(v: View) : RecyclerView.ViewHolder(v) { var name: TextView var details: TextView var icon: ImageView var background: View init { name = v.findViewById(R.id.name) details = v.findViewById(R.id.details) icon = v.findViewById(R.id.icon) background = v.findViewById(R.id.background) } @SuppressLint("NotifyDataSetChanged") fun bind() { val position = adapterPosition val file = File(list[position]) if (file.isFile && file.name.endsWith(".extension")) { name.text = file.name.substring(0, file.name.length - 10) } else { name.text = file.name } if (!file.exists()) { name.setTextColor(Color.RED) details.setTextColor(Color.RED) background.setOnClickListener { showMsg("This file doesn't exist anymore") list.remove(file.absolutePath) PrefsUtils.General.setFileExplorerTabBookmarks(list) list.clear() notifyDataSetChanged() } } else { name.setTextColor(Utils.getColorAttribute(R.attr.colorOnSurface, activity)) details.setTextColor(Utils.getColorAttribute(R.attr.colorOnSurface, activity)) background.setOnClickListener { activity.onBookmarkSelected(file) } } details.text = file.absolutePath IconHelper.setFileIcon(icon, file) background.setOnLongClickListener { list.remove(file.absolutePath) PrefsUtils.General.setFileExplorerTabBookmarks(list) list.clear() notifyDataSetChanged() true } } } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/activity/editor/autocomplete/CustomCompletionItemAdapter.kt ================================================ package com.raival.fileexplorer.activity.editor.autocomplete import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView import com.raival.fileexplorer.R import com.raival.fileexplorer.extension.toDp import io.github.rosemoe.sora.widget.component.EditorCompletionAdapter class CustomCompletionItemAdapter : EditorCompletionAdapter() { override fun getItemHeight(): Int = 45.toDp() public override fun getView( pos: Int, view: View?, parent: ViewGroup, isCurrentCursorPosition: Boolean ): View { val v: View = view ?: LayoutInflater.from(context) .inflate(R.layout.text_editor_completion_item, parent, false) val item = getItem(pos) var tv = v.findViewById(R.id.result_item_label) val iv = v.findViewById(R.id.result_item_image) tv.text = item.label tv = v.findViewById(R.id.result_item_desc) tv.text = item.desc v.tag = pos iv.text = item.desc.subSequence(0, 1) return v } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/activity/editor/autocomplete/CustomCompletionLayout.kt ================================================ package com.raival.fileexplorer.activity.editor.autocomplete import android.content.Context import android.graphics.drawable.GradientDrawable import android.os.SystemClock import android.view.MotionEvent import android.view.View import android.widget.* import android.widget.AdapterView.OnItemClickListener import com.raival.fileexplorer.App.Companion.showMsg import com.raival.fileexplorer.extension.toDp import io.github.rosemoe.sora.widget.component.CompletionLayout import io.github.rosemoe.sora.widget.component.EditorAutoCompletion import io.github.rosemoe.sora.widget.schemes.EditorColorScheme class CustomCompletionLayout : CompletionLayout { private lateinit var mListView: ListView private lateinit var mProgressBar: ProgressBar private lateinit var mBackground: GradientDrawable private lateinit var mEditorAutoCompletion: EditorAutoCompletion override fun onApplyColorScheme(colorScheme: EditorColorScheme) { mBackground.setStroke( 1, colorScheme.getColor(EditorColorScheme.COMPLETION_WND_BACKGROUND) ) mBackground.setColor(colorScheme.getColor(EditorColorScheme.COMPLETION_WND_BACKGROUND)) } override fun setEditorCompletion(completion: EditorAutoCompletion) { mEditorAutoCompletion = completion } override fun inflate(context: Context): View { val layout = RelativeLayout(context) mProgressBar = ProgressBar(context) layout.addView(mProgressBar) val params = mProgressBar.layoutParams as RelativeLayout.LayoutParams params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT) params.height = 30.toDp() params.width = params.height mBackground = GradientDrawable() mBackground.cornerRadius = 8f.toDp() layout.background = mBackground mListView = ListView(context) mListView.dividerHeight = 0 layout.addView(mListView, LinearLayout.LayoutParams(-1, -1)) mListView.onItemClickListener = OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long -> try { mEditorAutoCompletion.select(position) } catch (e: Exception) { showMsg(e.toString()) } } setLoading(true) return layout } override fun getCompletionList(): AdapterView<*> { return mListView } override fun setLoading(loading: Boolean) { mProgressBar.visibility = if (loading) View.VISIBLE else View.INVISIBLE } override fun ensureListPositionVisible(position: Int, incrementPixels: Int) { mListView.post { while (mListView.firstVisiblePosition + 1 > position && mListView.canScrollList(-1)) { performScrollList(incrementPixels / 2) } while (mListView.lastVisiblePosition - 1 < position && mListView.canScrollList(1)) { performScrollList(-incrementPixels / 2) } } } private fun performScrollList(offset: Int) { val adpView = completionList val down = SystemClock.uptimeMillis() var ev = MotionEvent.obtain(down, down, MotionEvent.ACTION_DOWN, 0f, 0f, 0) adpView.onTouchEvent(ev) ev.recycle() ev = MotionEvent.obtain(down, down, MotionEvent.ACTION_MOVE, 0f, offset.toFloat(), 0) adpView.onTouchEvent(ev) ev.recycle() ev = MotionEvent.obtain(down, down, MotionEvent.ACTION_CANCEL, 0f, offset.toFloat(), 0) adpView.onTouchEvent(ev) ev.recycle() } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/activity/editor/language/java/JavaCodeLanguage.kt ================================================ package com.raival.fileexplorer.activity.editor.language.java import io.github.rosemoe.sora.lang.format.Formatter import io.github.rosemoe.sora.lang.smartEnter.NewlineHandleResult import io.github.rosemoe.sora.lang.smartEnter.NewlineHandler import io.github.rosemoe.sora.langs.java.JavaLanguage import io.github.rosemoe.sora.langs.java.JavaTextTokenizer import io.github.rosemoe.sora.langs.java.Tokens import io.github.rosemoe.sora.text.ContentReference import io.github.rosemoe.sora.text.TextUtils open class JavaCodeLanguage : JavaLanguage() { private val javaFormatter = JavaFormatter() private val newlineHandlers = arrayOf(BraceHandler()) override fun getFormatter(): Formatter { return javaFormatter } override fun getIndentAdvance(text: ContentReference, line: Int, column: Int): Int { val content = text.getLine(line).substring(0, column) return getIndentAdvance(content) } override fun useTab(): Boolean { return false } private fun getIndentAdvance(content: String): Int { val t = JavaTextTokenizer(content) var token: Tokens var advance = 0 while (t.nextToken().also { token = it } !== Tokens.EOF) { if (token === Tokens.LBRACE) { advance++ } if (token === Tokens.LPAREN) { advance++ } if (advance > 0) { if (token === Tokens.RBRACE) { advance-- } if (token === Tokens.RPAREN) { advance-- } } } advance = 0.coerceAtLeast(advance) if (advance > 0) return 4 return 0 } override fun getNewlineHandlers(): Array { return newlineHandlers } inner class BraceHandler : NewlineHandler { override fun matchesRequirement(beforeText: String, afterText: String): Boolean { return (beforeText.endsWith("{") && afterText.startsWith("}")) || (beforeText.endsWith("(") && afterText.startsWith(")")) } override fun handleNewline( beforeText: String, afterText: String, tabSize: Int ): NewlineHandleResult { val count: Int = TextUtils.countLeadingSpaceCount(beforeText, tabSize) val advanceBefore: Int = getIndentAdvance(beforeText) val advanceAfter: Int = getIndentAdvance(afterText) var text: String val sb: StringBuilder = StringBuilder("\n") .append(TextUtils.createIndent(count + advanceBefore, tabSize, useTab())) .append('\n') .append( TextUtils.createIndent(count + advanceAfter, tabSize, useTab()) .also { text = it }) val shiftLeft = text.length + 1 return NewlineHandleResult(sb, shiftLeft) } } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/activity/editor/language/java/JavaFormatter.kt ================================================ package com.raival.fileexplorer.activity.editor.language.java import io.github.rosemoe.sora.lang.format.Formatter import io.github.rosemoe.sora.text.Content import io.github.rosemoe.sora.text.TextRange import java.util.* import java.util.stream.Collectors class JavaFormatter : Formatter { private var receiver: Formatter.FormatResultReceiver? = null private var isRunning = false override fun format(text: Content, cursorRange: TextRange) { isRunning = true receiver?.onFormatSucceed(format(text.toString()), cursorRange) isRunning = false } override fun formatRegion(text: Content, rangeToFormat: TextRange, cursorRange: TextRange) { isRunning = true val line = text.getLine(rangeToFormat.start.line) val indents = if (line.trim().isEmpty()) line else line.substring( 0, line.indexOf(line.trim().toString()) ) val textToFormat = text.subContent( rangeToFormat.start.line, rangeToFormat.start.column, rangeToFormat.end.line, rangeToFormat.end.column ) val formattedRegion = format(textToFormat.toString()).split("\n") .stream().map { indents.toString() + it }.collect(Collectors.joining("\n")) text.replace( rangeToFormat.start.line, rangeToFormat.start.column, rangeToFormat.end.line, rangeToFormat.end.column, formattedRegion ) receiver?.onFormatSucceed(text.toString(), cursorRange) isRunning = false } override fun setReceiver(receiver: Formatter.FormatResultReceiver?) { this.receiver = receiver } override fun isRunning(): Boolean { return isRunning } override fun destroy() { receiver = null } private fun format(source: String): String { val result = StringBuilder() val charArray = trimSource(source).toCharArray() val length = charArray.size var index = 0 var indents = 0 var lineIndents = 0 var lineStartIndex = 0 var isSingleLineComment = false var isMultiLineComment = false var isJavaDoc = false var isEscape = false var isChar = false var isString = false while (index < length) { val currentChar = charArray[index] val nextCharIndex = index + 1 val isValidNextChar = isValidIndex(nextCharIndex, charArray) if (isSingleLineComment) { if (currentChar == '\n') { result.append(currentChar) lineIndents = 0 lineStartIndex = result.length addIndent(result, indents) isSingleLineComment = false } else { result.append(currentChar) } } else if (isEscape) { result.append(currentChar) isEscape = false } else if (currentChar == '\\') { result.append(currentChar) isEscape = true } else if (isChar) { if (currentChar == '\'') { result.append(currentChar) isChar = false } else { result.append(currentChar) } } else if (isString) { if (currentChar == '\"') { result.append(currentChar) isString = false } else { result.append(currentChar) } } else { if (isMultiLineComment) { if (currentChar == '*') { if (isValidNextChar) { val nextChar = charArray[nextCharIndex] if (nextChar == '/') { isMultiLineComment = false isJavaDoc = false } } } } else { if (currentChar == '/') { if (isValidNextChar) { val nextChar = charArray[nextCharIndex] if (nextChar == '/') { result.append(currentChar) result.append(nextChar) isSingleLineComment = true index = nextCharIndex + 1 continue } if (nextChar == '*') { result.append(currentChar) result.append(nextChar) isMultiLineComment = true index = nextCharIndex + 1 continue } } } if (currentChar == '\'') { isChar = true } if (currentChar == '\"') { isString = true } } if (!isJavaDoc) { if (currentChar == '{' || currentChar == '(') { if (lineIndents <= 0) { ++indents } ++lineIndents if (lineIndents <= 0) lineIndents = 1 } if (currentChar == '}' || currentChar == ')') { if (lineIndents == 0) { --indents --lineIndents if (result.length > lineStartIndex) { if (result[lineStartIndex] == '\t') { result.deleteCharAt(lineStartIndex) } } } else if (lineIndents == 1) { --indents } if (lineIndents > 0) --lineIndents } } result.append(currentChar) if (currentChar == '\n') { lineIndents = 0 lineStartIndex = result.length addIndent(result, indents) if (isMultiLineComment) { if (isValidNextChar) { val nextChar = charArray[nextCharIndex] if (nextChar == '*') { isJavaDoc = true result.append(" ") } else { isJavaDoc = false } } } if (isValidNextChar) { val nextChar = charArray[nextCharIndex] if (nextChar == '?' || nextChar == ':' || nextChar == '&' || nextChar == '|' || nextChar == '+' ) { result.append("\t") } } } } ++index } return result.toString().replace("\t", " ") } private fun isValidIndex(index: Int, source: CharArray): Boolean { return index < source.size && index > -1 } private fun addIndent(builder: StringBuilder, indents: Int) { for (i in 0 until indents) { builder.append('\t') } } private fun trimSource(source: String): String { return Arrays.stream(source.split("\n").toTypedArray()) .map { obj: String -> obj.trim { it <= ' ' } } .collect(Collectors.joining("\n")) } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/activity/editor/language/json/JsonFormatter.kt ================================================ package com.raival.fileexplorer.activity.editor.language.json import io.github.rosemoe.sora.lang.format.Formatter import io.github.rosemoe.sora.text.Content import io.github.rosemoe.sora.text.TextRange import org.json.JSONArray import org.json.JSONObject class JsonFormatter : Formatter { private var receiver: Formatter.FormatResultReceiver? = null private var isRunning = false override fun format(text: Content, cursorRange: TextRange) { isRunning = true receiver?.onFormatSucceed(format(text.toString()), cursorRange) isRunning = false } override fun formatRegion(text: Content, rangeToFormat: TextRange, cursorRange: TextRange) { } private fun format(txt: String): String { if (txt.isEmpty()) return txt val isObject = txt.trim()[0] == '{' return try { if (isObject) JSONObject(txt).toString(2) else JSONArray(txt).toString(2) } catch (e: Exception) { txt } } override fun setReceiver(receiver: Formatter.FormatResultReceiver?) { this.receiver = receiver } override fun isRunning(): Boolean { return isRunning } override fun destroy() { receiver = null } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/activity/editor/language/json/JsonLanguage.kt ================================================ package com.raival.fileexplorer.activity.editor.language.json import com.raival.fileexplorer.App import io.github.rosemoe.sora.lang.format.Formatter import io.github.rosemoe.sora.lang.smartEnter.NewlineHandleResult import io.github.rosemoe.sora.lang.smartEnter.NewlineHandler import io.github.rosemoe.sora.langs.java.JavaTextTokenizer import io.github.rosemoe.sora.langs.java.Tokens import io.github.rosemoe.sora.langs.textmate.TextMateLanguage import io.github.rosemoe.sora.text.ContentReference import io.github.rosemoe.sora.text.TextUtils import org.eclipse.tm4e.core.registry.IGrammarSource import org.eclipse.tm4e.core.registry.IThemeSource import java.io.InputStreamReader import java.io.Reader class JsonLanguage( iThemeSource: IThemeSource, iGrammarSource: IGrammarSource = IGrammarSource.fromInputStream( App.appContext.assets.open("textmate/json/syntax/json.tmLanguage.json"), "json.tmLanguage.json", null ), languageConfiguration: Reader = InputStreamReader(App.appContext.assets.open("textmate/json/language-configuration.json")), createIdentifiers: Boolean = true ) : TextMateLanguage(iGrammarSource, languageConfiguration, iThemeSource, createIdentifiers) { private val jsonFormatter = JsonFormatter() private val newlineHandlers = arrayOf(BraceHandler()) override fun getFormatter(): Formatter { return jsonFormatter } override fun getIndentAdvance(text: ContentReference, line: Int, column: Int): Int { val content = text.getLine(line).substring(0, column) return getIndentAdvance(content) } override fun useTab(): Boolean { return false } private fun getIndentAdvance(content: String): Int { val t = JavaTextTokenizer(content) var token: Tokens var advance = 0 while (t.nextToken().also { token = it } !== Tokens.EOF) { if (token === Tokens.LBRACE) { advance++ } if (token === Tokens.LBRACK) { advance++ } if (advance > 0) { if (token === Tokens.RBRACE) { advance-- } if (token === Tokens.RBRACE) { advance-- } } } advance = 0.coerceAtLeast(advance) if (advance > 0) return 4 return 0 } override fun getNewlineHandlers(): Array { return newlineHandlers } inner class BraceHandler : NewlineHandler { override fun matchesRequirement(beforeText: String, afterText: String): Boolean { return (beforeText.endsWith("{") && afterText.startsWith("}")) || (beforeText.endsWith("[") && afterText.startsWith("]")) } override fun handleNewline( beforeText: String, afterText: String, tabSize: Int ): NewlineHandleResult { val count: Int = TextUtils.countLeadingSpaceCount(beforeText, tabSize) val advanceBefore: Int = getIndentAdvance(beforeText) val advanceAfter: Int = getIndentAdvance(afterText) var text: String val sb: StringBuilder = StringBuilder("\n") .append(TextUtils.createIndent(count + advanceBefore, tabSize, useTab())) .append('\n') .append( TextUtils.createIndent(count + advanceAfter, tabSize, useTab()) .also { text = it }) val shiftLeft = text.length + 1 return NewlineHandleResult(sb, shiftLeft) } } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/activity/editor/language/kotlin/KotlinCodeLanguage.kt ================================================ package com.raival.fileexplorer.activity.editor.language.kotlin import com.raival.fileexplorer.App import com.raival.fileexplorer.activity.editor.language.java.JavaFormatter import io.github.rosemoe.sora.lang.format.Formatter import io.github.rosemoe.sora.lang.smartEnter.NewlineHandleResult import io.github.rosemoe.sora.lang.smartEnter.NewlineHandler import io.github.rosemoe.sora.langs.java.JavaTextTokenizer import io.github.rosemoe.sora.langs.java.Tokens import io.github.rosemoe.sora.langs.textmate.TextMateLanguage import io.github.rosemoe.sora.text.ContentReference import io.github.rosemoe.sora.text.TextUtils import org.eclipse.tm4e.core.registry.IGrammarSource import org.eclipse.tm4e.core.registry.IThemeSource import java.io.InputStreamReader import java.io.Reader class KotlinCodeLanguage( iThemeSource: IThemeSource, iGrammarSource: IGrammarSource = IGrammarSource.fromInputStream( App.appContext.assets.open("textmate/kotlin/syntax/kotlin.tmLanguage"), "kotlin.tmLanguage", null ), languageConfiguration: Reader = InputStreamReader(App.appContext.assets.open("textmate/kotlin/language-configuration.json")), createIdentifiers: Boolean = true ) : TextMateLanguage(iGrammarSource, languageConfiguration, iThemeSource, createIdentifiers) { private val javaFormatter = JavaFormatter() private val newlineHandlers = arrayOf(BraceHandler()) override fun getFormatter(): Formatter { return javaFormatter } override fun getIndentAdvance(text: ContentReference, line: Int, column: Int): Int { val content = text.getLine(line).substring(0, column) return getIndentAdvance(content) } override fun useTab(): Boolean { return false } private fun getIndentAdvance(content: String): Int { val t = JavaTextTokenizer(content) var token: Tokens var advance = 0 while (t.nextToken().also { token = it } !== Tokens.EOF) { if (token === Tokens.LBRACE) { advance++ } if (token === Tokens.LPAREN) { advance++ } if (advance > 0) { if (token === Tokens.RBRACE) { advance-- } if (token === Tokens.RPAREN) { advance-- } } } advance = 0.coerceAtLeast(advance) if (advance > 0) return 4 return 0 } override fun getNewlineHandlers(): Array { return newlineHandlers } inner class BraceHandler : NewlineHandler { override fun matchesRequirement(beforeText: String, afterText: String): Boolean { return (beforeText.endsWith("{") && afterText.startsWith("}")) || (beforeText.endsWith("(") && afterText.startsWith(")")) } override fun handleNewline( beforeText: String, afterText: String, tabSize: Int ): NewlineHandleResult { val count: Int = TextUtils.countLeadingSpaceCount(beforeText, tabSize) val advanceBefore: Int = getIndentAdvance(beforeText) val advanceAfter: Int = getIndentAdvance(afterText) var text: String val sb: StringBuilder = StringBuilder("\n") .append(TextUtils.createIndent(count + advanceBefore, tabSize, useTab())) .append('\n') .append( TextUtils.createIndent(count + advanceAfter, tabSize, useTab()) .also { text = it }) val shiftLeft = text.length + 1 return NewlineHandleResult(sb, shiftLeft) } } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/activity/editor/language/xml/XmlLanguage.kt ================================================ package com.raival.fileexplorer.activity.editor.language.xml import com.raival.fileexplorer.App import io.github.rosemoe.sora.langs.textmate.TextMateLanguage import org.eclipse.tm4e.core.registry.IGrammarSource import org.eclipse.tm4e.core.registry.IThemeSource import java.io.InputStreamReader import java.io.Reader class XmlLanguage( iThemeSource: IThemeSource, iGrammarSource: IGrammarSource = IGrammarSource.fromInputStream( App.appContext.assets.open("textmate/xml/syntax/xml.tmLanguage.json"), "xml.tmLanguage.json", null ), languageConfiguration: Reader = InputStreamReader(App.appContext.assets.open("textmate/xml/language-configuration.json")), createIdentifiers: Boolean = true ) : TextMateLanguage(iGrammarSource, languageConfiguration, iThemeSource, createIdentifiers) { } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/activity/editor/scheme/DarkScheme.kt ================================================ package com.raival.fileexplorer.activity.editor.scheme import io.github.rosemoe.sora.widget.schemes.EditorColorScheme class DarkScheme : EditorColorScheme() { override fun applyDefault() { super.applyDefault() setColor(ANNOTATION, -0x444ad7) setColor(FUNCTION_NAME, -0x332f27) setColor(IDENTIFIER_NAME, -0x332f27) setColor(IDENTIFIER_VAR, -0x678956) setColor(LITERAL, -0x9578a7) setColor(OPERATOR, -0x332f27) setColor(COMMENT, -0x7f7f80) setColor(KEYWORD, -0x3387ce) setColor(WHOLE_BACKGROUND, -0xd4d4d5) setColor(TEXT_NORMAL, -0x332f27) setColor(LINE_NUMBER_BACKGROUND, -0xcecccb) setColor(LINE_NUMBER, -0x9f9c9a) setColor(LINE_DIVIDER, -0x9f9c9a) setColor(SCROLL_BAR_THUMB, -0x59595a) setColor(SCROLL_BAR_THUMB_PRESSED, -0xa9a9aa) setColor(SELECTED_TEXT_BACKGROUND, -0xc98948) setColor(MATCHED_TEXT_BACKGROUND, -0xcda6c3) setColor(CURRENT_LINE, -0xcdcdce) setColor(SELECTION_INSERT, -0x332f27) setColor(SELECTION_HANDLE, -0x332f27) setColor(BLOCK_LINE, -0xa8a8a9) setColor(BLOCK_LINE_CURRENT, -0x22a8a8a9) setColor(NON_PRINTABLE_CHAR, -0x222223) setColor(TEXT_SELECTED, -0x332f27) } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/activity/editor/scheme/LightScheme.kt ================================================ package com.raival.fileexplorer.activity.editor.scheme import io.github.rosemoe.sora.widget.schemes.EditorColorScheme class LightScheme : EditorColorScheme() { override fun applyDefault() { super.applyDefault() setColor(ANNOTATION, -0x9b9b9c) setColor(FUNCTION_NAME, -0x1000000) setColor(IDENTIFIER_NAME, -0x1000000) setColor(IDENTIFIER_VAR, -0x479cc2) setColor(LITERAL, -0xd5ff01) setColor(OPERATOR, -0xc60000) setColor(COMMENT, -0xc080a1) setColor(KEYWORD, -0x80ff8c) setColor(WHOLE_BACKGROUND, -0x1) setColor(TEXT_NORMAL, -0x1000000) setColor(LINE_NUMBER_BACKGROUND, -0x1) setColor(LINE_NUMBER, -0x878788) setColor(SELECTED_TEXT_BACKGROUND, -0xcc6601) setColor(MATCHED_TEXT_BACKGROUND, -0x2b2b2c) setColor(CURRENT_LINE, -0x170d02) setColor(SELECTION_INSERT, -0xfc1415) setColor(SELECTION_HANDLE, -0xfc1415) setColor(BLOCK_LINE, -0x272728) setColor(BLOCK_LINE_CURRENT, 0) setColor(TEXT_SELECTED, -0x1) } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/activity/editor/view/SymbolInputView.kt ================================================ package com.raival.fileexplorer.activity.editor.view import android.content.Context import android.util.AttributeSet import android.util.TypedValue import android.widget.Button import android.widget.LinearLayout import com.raival.fileexplorer.R import com.raival.fileexplorer.util.Utils import io.github.rosemoe.sora.widget.CodeEditor class SymbolInputView : LinearLayout { private var textColor = 0 private lateinit var editor: CodeEditor constructor(context: Context?) : super(context) constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( context, attrs, defStyleAttr ) init { setBackgroundColor(Utils.getColorAttribute(R.attr.backgroundColor, context)) setTextColor(Utils.getColorAttribute(R.attr.colorOnSurface, context)) orientation = HORIZONTAL } fun bindEditor(editor: CodeEditor): SymbolInputView { this.editor = editor return this } fun setTextColor(color: Int): SymbolInputView { for (i in 0 until childCount) { (getChildAt(i) as Button).setTextColor(color) } textColor = color return this } fun addSymbols(symbols: Array): SymbolInputView { for (symbol in symbols) addSymbol(symbol) return this } fun addSymbol( display: String, content: String = display, cursorPos: Int = content.length ): SymbolInputView { val btn = Button(context, null, android.R.attr.buttonStyleSmall) btn.text = display val out = TypedValue() context.theme.resolveAttribute(android.R.attr.selectableItemBackground, out, true) btn.setBackgroundResource(out.resourceId) btn.setTextColor(textColor) addView(btn, LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)) btn.setOnClickListener { if (this::editor.isInitialized) editor.insertText(content, cursorPos) } return this } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/activity/model/MainViewModel.kt ================================================ package com.raival.fileexplorer.activity.model import androidx.lifecycle.ViewModel import com.raival.fileexplorer.tab.BaseDataHolder import com.raival.fileexplorer.tab.file.model.Task class MainViewModel : ViewModel() { @JvmField val tasks = arrayListOf() private val dataHolders: MutableList = arrayListOf() fun addDataHolder(dataHolder: BaseDataHolder) { dataHolders.add(dataHolder) } fun getDataHolders(): ArrayList { return dataHolders as ArrayList } fun getDataHolder(tag: String): BaseDataHolder? { for (dataHolder in dataHolders) { if (dataHolder.tag == tag) return dataHolder } return null } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/activity/model/TextEditorViewModel.kt ================================================ package com.raival.fileexplorer.activity.model import androidx.lifecycle.ViewModel import io.github.rosemoe.sora.text.Content import java.io.File class TextEditorViewModel : ViewModel() { var file: File? = null var content: Content? = null } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/common/BackgroundTask.kt ================================================ package com.raival.fileexplorer.common import android.annotation.SuppressLint import android.app.Activity import android.os.Handler import android.os.Looper import android.view.View import android.widget.TextView import androidx.appcompat.app.AlertDialog import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.raival.fileexplorer.R import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch class BackgroundTask { private lateinit var preTask: Runnable private lateinit var task: Runnable private lateinit var postTask: Runnable private lateinit var alertDialog: AlertDialog fun setTasks(preTask: Runnable, task: Runnable, postTask: Runnable) { this.preTask = preTask this.task = task this.postTask = postTask } fun run() { CoroutineScope(Dispatchers.IO).launch { Handler(Looper.getMainLooper()).post(preTask) task.run() Handler(Looper.getMainLooper()).post(postTask) }.start() } fun dismiss() { if (this::alertDialog.isInitialized) alertDialog.dismiss() } @SuppressLint("ResourceType") fun showProgressDialog(msg: String, activity: Activity) { alertDialog = MaterialAlertDialogBuilder(activity) .setCancelable(false) .setView(getProgressView(msg, activity)) .show() } private fun getProgressView(msg: String, activity: Activity): View { @SuppressLint("InflateParams") val v = activity.layoutInflater.inflate(R.layout.progress_view, null) (v.findViewById(R.id.msg) as TextView).text = msg return v } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/common/dialog/CustomDialog.kt ================================================ package com.raival.fileexplorer.common.dialog import android.content.Context import android.graphics.drawable.Drawable import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.widget.LinearLayout import android.widget.TextView import androidx.appcompat.content.res.AppCompatResources import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.button.MaterialButton import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.textfield.TextInputLayout import com.raival.fileexplorer.R class CustomDialog : BottomSheetDialogFragment() { private val views = ArrayList() private lateinit var icon: Drawable private lateinit var title: String private lateinit var msg: String private lateinit var positiveButton: String private lateinit var positiveListener: Listener private lateinit var negativeButton: String private lateinit var negativeListener: Listener private lateinit var neutralButton: String private lateinit var neutralListener: Listener private lateinit var msgView: TextView override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { dialog?.window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) return inflater.inflate(R.layout.common_custom_dialog, container, true) } override fun getTheme(): Int { return R.style.ThemeOverlay_Material3_BottomSheetDialog } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val titleView = view.findViewById(R.id.dialog_title) msgView = view.findViewById(R.id.dialog_msg) val imageView = view.findViewById(R.id.dialog_icon) val containerView = view.findViewById(R.id.dialog_container) val positiveButtonView = view.findViewById(R.id.dialog_positive_button) val negativeButtonView = view.findViewById(R.id.dialog_negative_button) val neutralButtonView = view.findViewById(R.id.dialog_neutral_button) if (this::icon.isInitialized) { imageView.visibility = View.VISIBLE imageView.setImageDrawable(icon) } if (this::title.isInitialized) { titleView.visibility = View.VISIBLE titleView.text = title } if (this::msg.isInitialized) { msgView.visibility = View.VISIBLE msgView.text = msg } if (views.size > 0) { containerView.visibility = View.VISIBLE for (view1 in views) { containerView.addView(view1) } } if (this::positiveButton.isInitialized) { positiveButtonView.visibility = View.VISIBLE positiveButtonView.text = positiveButton positiveButtonView.setOnClickListener { view1: View? -> positiveListener.listener.onClick(view1) if (positiveListener.dismiss) dismiss() } } if (this::negativeButton.isInitialized) { negativeButtonView.visibility = View.VISIBLE negativeButtonView.text = negativeButton negativeButtonView.setOnClickListener { view1: View? -> negativeListener.listener.onClick(view1) if (negativeListener.dismiss) dismiss() } } if (this::neutralButton.isInitialized) { neutralButtonView.visibility = View.VISIBLE neutralButtonView.text = neutralButton neutralButtonView.setOnClickListener { view1: View? -> neutralListener.listener.onClick(view1) if (neutralListener.dismiss) dismiss() } } } fun setTitle(title: String): CustomDialog { this.title = title return this } fun setMsg(msg: String): CustomDialog { this.msg = msg if (this::msgView.isInitialized) msgView.text = msg return this } fun setIcon(resId: Int): CustomDialog { icon = AppCompatResources.getDrawable(requireActivity(), resId)!! return this } fun setIconDrawable(drawable: Drawable): CustomDialog { this.icon = drawable return this } fun addView(view: View): CustomDialog { views.add(view) return this } fun setPositiveButton( label: String, listener: View.OnClickListener?, dismiss: Boolean ): CustomDialog { positiveButton = label positiveListener = Listener(listener ?: emptyListener(), dismiss) return this } fun setNegativeButton( label: String, listener: View.OnClickListener?, dismiss: Boolean ): CustomDialog { negativeButton = label negativeListener = Listener(listener ?: emptyListener(), dismiss) return this } fun setNeutralButton( label: String, listener: View.OnClickListener?, dismiss: Boolean ): CustomDialog { neutralButton = label neutralListener = Listener(listener ?: emptyListener(), dismiss) return this } fun createInput(context: Context, hint: String): TextInputLayout { return (LayoutInflater.from(context) .inflate(R.layout.input, null, false) as TextInputLayout) .apply { setHint(hint) } } private fun emptyListener(): View.OnClickListener = View.OnClickListener { } private class Listener(var listener: View.OnClickListener, var dismiss: Boolean) } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/common/dialog/OptionsDialog.kt ================================================ package com.raival.fileexplorer.common.dialog import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.raival.fileexplorer.R class OptionsDialog(var title: String) : BottomSheetDialogFragment() { private val options = ArrayList() private var container: LinearLayout? = null fun addOption( label: String?, listener: View.OnClickListener?, dismissOnClick: Boolean ): OptionsDialog { return addOption(label, 0, listener, dismissOnClick) } fun addOption( label: String?, resId: Int, listener: View.OnClickListener?, dismissOnClick: Boolean ): OptionsDialog { val optionHolder = OptionHolder() optionHolder.dismissOnClick = dismissOnClick optionHolder.label = label optionHolder.listener = listener optionHolder.res = resId options.add(optionHolder) return this } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return inflater.inflate(R.layout.common_options_dialog, container, false) } override fun getTheme(): Int { return R.style.ThemeOverlay_Material3_BottomSheetDialog } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) container = view.findViewById(R.id.container) (view.findViewById(R.id.title) as TextView).text = title view.findViewById(R.id.msg).visibility = View.GONE addOptions() } private fun addOptions() { for (optionHolder in options) { val v = layoutInflater.inflate(R.layout.common_options_dialog_item, container, false) if (optionHolder.res != 0) { val icon = v.findViewById(R.id.icon) icon.visibility = View.VISIBLE icon.setImageResource(optionHolder.res) } (v.findViewById(R.id.label) as TextView).text = optionHolder.label v.findViewById(R.id.background).setOnClickListener { view: View? -> if (optionHolder.listener != null) optionHolder.listener!!.onClick(view) if (optionHolder.dismissOnClick) v.post { dismiss() } } container!!.addView(v) } } private class OptionHolder { var label: String? = null var res = 0 var listener: View.OnClickListener? = null var dismissOnClick = false } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/common/view/BottomBarView.kt ================================================ package com.raival.fileexplorer.common.view import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import com.raival.fileexplorer.R import com.raival.fileexplorer.util.PrefsUtils class BottomBarView : LinearLayout { constructor(context: Context?) : super(context) constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( context, attrs, defStyleAttr ) fun addItem(tag: String?, icon: Int, clickListener: OnClickListener?) { val view = (context .getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater) .inflate(R.layout.bottom_bar_menu_item, this, false) view.setOnClickListener(clickListener) view.tooltipText = tag val label = view.findViewById(R.id.label) if (PrefsUtils.Settings.showBottomToolbarLabels) { label.text = tag } else { label.visibility = GONE } val image = view.findViewById(R.id.icon) image.setImageResource(icon) addView(view) } fun addItem(tag: String?, view: View) { view.tooltipText = tag addView(view) } fun clear() { removeAllViews() } fun onUpdatePrefs() { for (i in 0 until childCount) { val view = getChildAt(i) val label = view.findViewById(R.id.label) if (label != null) { label.visibility = if (PrefsUtils.Settings.showBottomToolbarLabels) VISIBLE else GONE } } } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/common/view/TabView.kt ================================================ package com.raival.fileexplorer.common.view import android.animation.ObjectAnimator import android.content.Context import android.graphics.drawable.GradientDrawable import android.util.AttributeSet import android.util.TypedValue import android.view.Gravity import android.view.View import android.view.ViewGroup import android.widget.HorizontalScrollView import android.widget.LinearLayout import android.widget.TextView import com.raival.fileexplorer.R import com.raival.fileexplorer.extension.toDp import com.raival.fileexplorer.util.Utils class TabView : HorizontalScrollView { private var tabsArray = ArrayList() private var textColor = 0 private var indicatorColor = 0 private var textSize = 0 private lateinit var onUpdateListener: OnUpdateTabViewListener private lateinit var onBulkRemoveListener: OnBulkRemoveListener private var container: LinearLayout = LinearLayout(context) constructor(context: Context?) : super(context) constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) init { addView( container, ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ) ) isFillViewport = true isVerticalScrollBarEnabled = false isHorizontalScrollBarEnabled = false textColor = Utils.getColorAttribute(R.attr.colorPrimary, context) indicatorColor = Utils.getColorAttribute(R.attr.colorPrimary, context) textSize = 14 } @JvmOverloads fun addNewTab(tag: String, select: Boolean = true): Tab { return insertNewTabAt(tabsArray.size, tag, select) } fun removeTabs(ids: Array) { val selectedTabPos = selectedTabPosition for (id in ids) { val pos = getTabPositionByTag(id) if (!valid(pos)) continue tabsArray.removeAt(pos) container.removeView(container.getChildAt(pos)) } if (tabsArray.size == 0 && this::onUpdateListener.isInitialized) { onUpdateListener.onUpdate(null, ON_EMPTY) return } val tab = tabsArray[validatePosition(selectedTabPos)] tab.select(true) if (this::onUpdateListener.isInitialized) onUpdateListener.onUpdate(tab, ON_SELECT) } private fun validatePosition(i: Int): Int { return if (i >= tabsArray.size) tabsArray.size - 1 else i.coerceAtLeast(0) } @JvmOverloads fun insertNewTabAt(pos: Int, tag: String, select: Boolean = true): Tab { if (!isUnique(tag)) { val oldSelectedTab = selectedTab oldSelectedTab!!.select(false) if (this::onUpdateListener.isInitialized) onUpdateListener.onUpdate( oldSelectedTab, ON_UNSELECT ) val newTab = getTabByTag(tag) newTab!!.select(true) if (this::onUpdateListener.isInitialized) onUpdateListener.onUpdate(newTab, ON_SELECT) return newTab } val oldSelectedTab = selectedTab val tab = Tab(tag, createTabView(), select) tabsArray.add(pos, tab) if (this::onUpdateListener.isInitialized) onUpdateListener.onUpdate(tab, ON_CREATE) if (select) { if (oldSelectedTab != null) { oldSelectedTab.select(false) if (this::onUpdateListener.isInitialized) onUpdateListener.onUpdate( oldSelectedTab, ON_UNSELECT ) } if (this::onUpdateListener.isInitialized) { onUpdateListener.onUpdate(tab, ON_SELECT) } } container.addView(tab.view, pos) scrollToTab(tab) return tab } private fun removeTabAt(pos: Int) { if (!valid(pos)) return if (tabsArray.size == 1 || selectedTabPosition != pos) { container.removeViewAt(pos) val tempTab = tabsArray[pos] tabsArray.removeAt(pos) if (this::onUpdateListener.isInitialized) { onUpdateListener.onUpdate(tempTab, ON_REMOVE) onUpdateListener.onUpdate(null, ON_EMPTY) } return } container.removeViewAt(pos) val tempTab = tabsArray[pos] tabsArray.removeAt(pos) if (this::onUpdateListener.isInitialized) onUpdateListener.onUpdate(tempTab, ON_REMOVE) val limit = tabsArray.size - 1 val newTabToSelect: Tab = if (pos > limit) { tabsArray[pos - 1] } else { tabsArray[pos] } newTabToSelect.select(true) if (this::onUpdateListener.isInitialized) onUpdateListener.onUpdate( newTabToSelect, ON_SELECT ) } fun removeTab(id: String) { for ((i, tab) in tabsArray.withIndex()) { if (tab.tag == id) { removeTabAt(i) return } } } fun removeSelectedTab() { removeTabAt(selectedTabPosition) } fun removeAllTabs() { if (this::onBulkRemoveListener.isInitialized) onBulkRemoveListener.onBulkRemove( tabsArray, true ) tabsArray.clear() container.removeAllViews() if (this::onUpdateListener.isInitialized) onUpdateListener.onUpdate(null, ON_EMPTY) } private fun removeAllTabsExcept(pos: Int) { if (!valid(pos)) return val exception = tabsArray[pos] tabsArray.removeAt(pos) if (this::onBulkRemoveListener.isInitialized) onBulkRemoveListener.onBulkRemove( tabsArray, false ) tabsArray.clear() container.removeAllViews() tabsArray.add(exception) container.addView(exception.view) if (!exception.isSelected) { exception.select(true) if (this::onUpdateListener.isInitialized) onUpdateListener.onUpdate( exception, ON_SELECT ) } if (tabsArray.size == 0 && this::onUpdateListener.isInitialized) onUpdateListener.onUpdate( null, ON_EMPTY ) } fun removeAllTabsExceptSelectedTab() { removeAllTabsExcept(selectedTabPosition) } private fun valid(pos: Int): Boolean { return if (tabsArray.isEmpty()) false else pos > 0 && pos < tabsArray.size } private fun isUnique(s: String): Boolean { for (tab in tabsArray) { if (tab.tag == s) return false } return true } fun setTextSize(s: Int) { textSize = s } fun setOnUpdateTabViewListener(l: OnUpdateTabViewListener) { onUpdateListener = l } fun setOnBulkRemoveListener(l: OnBulkRemoveListener) { onBulkRemoveListener = l } fun getTabByTag(tag: String?): Tab? { for (tab in tabsArray) { if (tab.tag == tag) return tab } return null } private fun getTabPositionByTag(tag: String): Int { for ((i, tab) in tabsArray.withIndex()) { if (tab.tag == tag) return i } return -1 } val tabCount: Int get() = tabsArray.size private val selectedTabPosition: Int get() { for ((i, tab) in tabsArray.withIndex()) { if (tab.isSelected) return i } return -1 } val selectedTabId: String? get() { for (tab in tabsArray) { if (tab.isSelected) return tab.tag } return null } val selectedTab: Tab? get() { for (tab in tabsArray) { if (tab.isSelected) return tab } return null } fun tabAnimation(v: View?, event: String?) { when (event) { "onSelect" -> { val indicator = v!!.findViewWithTag("tab_indicator") ObjectAnimator.ofFloat( indicator, "translationY", 2f.toDp(), (-3f).toDp(), 0f ).setDuration(500).start() } "onReselect" -> { val indicator = v!!.findViewWithTag("tab_indicator") ObjectAnimator.ofFloat(indicator, "translationY", 0f, (1.5f).toDp(), 0f) .setDuration(400).start() } "onUnselect" -> { val indicator = v!!.findViewWithTag("tab_indicator") ObjectAnimator.ofFloat(indicator, "translationY", 0f, 2f.toDp()) .setDuration(400).start() } } } private fun createTabView(): View { val padding = 8.toDp() val bg = LinearLayout(context) bg.orientation = LinearLayout.VERTICAL bg.tag = "tab_background" bg.setPadding(0, 0, padding, 0) bg.gravity = Gravity.CENTER_VERTICAL or Gravity.CENTER_HORIZONTAL bg.minimumWidth = 80.toDp() bg.layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT ) val out = TypedValue() context.theme.resolveAttribute(android.R.attr.selectableItemBackground, out, true) bg.setBackgroundResource(out.resourceId) val text = TextView(context) text.text = "" text.setTextColor(textColor) text.textSize = textSize.toFloat() text.tag = "tab_text" text.gravity = Gravity.CENTER_VERTICAL or Gravity.CENTER_HORIZONTAL val line = LinearLayout(context) val gd = GradientDrawable() val dp8 = 8f.toDp() gd.setColor(indicatorColor) gd.cornerRadii = floatArrayOf( dp8, dp8, dp8, dp8, 0f, 0f, 0f, 0f ) line.background = gd line.tag = "tab_indicator" bg.addView(text, LinearLayout.LayoutParams(-2, 0, 1f)) bg.addView(line, LinearLayout.LayoutParams(-1, 3.toDp())) return bg } fun scrollToTab(tab: Tab?) { if (tab?.view == null) return post { smoothScrollTo(tab.view.x.toInt() - 50.toDp(), 0) } } val tags: ArrayList get() { val tags = ArrayList() for (tab in tabsArray) { tags.add(tab.tag) } return tags } /** * Interface * * * Events called when create/insert new tab: * onCreate > onUnselect? > onSelect? * * * Events called when remove a single tab new tab: * onRemove > onSelect? * * * Events called when remove moe than one tab: * onBulkRemove > onSelect? * * * Events called when remove all tabs: * onRemoveAll * * * Event called when update a tab: * onUpdate */ interface OnUpdateTabViewListener { fun onUpdate(tab: Tab?, event: Int) } interface OnBulkRemoveListener { fun onBulkRemove(array: ArrayList?, all: Boolean) } inner class Tab(val tag: String, val view: View, var isSelected: Boolean) { private var name: String = "" init { select(isSelected) view.setOnClickListener { if (selectedTab == null) { select(true) if (this@TabView::onUpdateListener.isInitialized) onUpdateListener.onUpdate( this, ON_SELECT ) return@setOnClickListener } if (this.isSelected) { if (this@TabView::onUpdateListener.isInitialized) onUpdateListener.onUpdate( this, ON_RESELECT ) tabAnimation(this.view, "onReselect") return@setOnClickListener } val oldSelectedTab = selectedTab oldSelectedTab!!.select(false) if (this@TabView::onUpdateListener.isInitialized) onUpdateListener.onUpdate( oldSelectedTab, ON_UNSELECT ) select(true) if (this@TabView::onUpdateListener.isInitialized) onUpdateListener.onUpdate( this, ON_SELECT ) } view.setOnLongClickListener { if (this@TabView::onUpdateListener.isInitialized) { onUpdateListener.onUpdate(this, ON_LONG_CLICK) return@setOnLongClickListener true } false } } fun setName(s: String) { name = s (view.findViewWithTag("tab_text") as TextView).text = s } fun getName(): String = name fun select(s: Boolean) { isSelected = s if (s) { view.alpha = 1f tabAnimation(view, "onSelect") scrollToTab(getTabByTag(tag)) } else { view.alpha = 0.7f tabAnimation(view, "onUnselect") } } } companion object { const val ON_CREATE = 1 const val ON_SELECT = 2 const val ON_LONG_CLICK = 3 const val ON_RESELECT = 4 const val ON_UNSELECT = -1 const val ON_REMOVE = -2 const val ON_EMPTY = 0 } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/extension/File.kt ================================================ package com.raival.fileexplorer.extension import android.annotation.SuppressLint import android.content.ActivityNotFoundException import android.content.Intent import android.net.Uri import android.os.Environment import android.os.StatFs import android.webkit.MimeTypeMap import androidx.core.content.FileProvider import com.raival.fileexplorer.App import com.raival.fileexplorer.App.Companion.showMsg import com.raival.fileexplorer.tab.file.misc.FileMimeTypes import com.raival.fileexplorer.tab.file.misc.FileUtils import java.io.File import java.text.SimpleDateFormat fun File.getFileDetails(): String { val sb = StringBuilder() sb.append(getLastModifiedDate()) sb.append(" | ") if (this.isFile) { sb.append(length().toFormattedSize()) } else { sb.append(getFormattedFileCount()) } return sb.toString() } fun File.getShortLabel(maxLength: Int): String { var name = Uri.parse(this.absolutePath).lastPathSegment if (isExternalStorageFolder()) { name = FileUtils.INTERNAL_STORAGE } if (name!!.length > maxLength) { name = name.substring(0, maxLength - 3) + "..." } return name } fun File.isExternalStorageFolder(): Boolean { return this.absolutePath == Environment.getExternalStorageDirectory().absolutePath } fun File.getAvailableMemoryBytes(): Long { val statFs = StatFs(this.absolutePath) return statFs.blockSizeLong * statFs.availableBlocksLong } fun File.getTotalMemoryBytes(): Long { val statFs = StatFs(this.absolutePath) return statFs.blockSizeLong * statFs.blockCountLong } fun File.getUsedMemoryBytes(): Long { return getTotalMemoryBytes() - getAvailableMemoryBytes() } fun File.getFormattedFileCount(): String { val noItemsString = "Empty folder" if (this.isFile) { return noItemsString } var files = 0 var folders = 0 val fileList = this.listFiles() ?: return noItemsString for (item in fileList) { if (item.isFile) files++ else folders++ } val sb = java.lang.StringBuilder() if (folders > 0) { sb.append(folders) sb.append(" folder") if (folders > 1) sb.append("s") if (files > 0) sb.append(", ") } if (files > 0) { sb.append(files) sb.append(" file") if (files > 1) sb.append("s") } return if (folders == 0 && files == 0) noItemsString else sb.toString() } fun File.getMimeTypeFromFile(): String { return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) ?: getMimeTypeFromExtension() } fun File.getMimeTypeFromExtension(): String { val type = FileMimeTypes.mimeTypes[extension] return type ?: FileMimeTypes.default } fun File.openFileWith(anonymous: Boolean) { val i = Intent(Intent.ACTION_VIEW) val uri = FileProvider.getUriForFile( App.appContext, App.appContext.packageName + ".provider", this ) i.setDataAndType(uri, if (anonymous) "*/*" else getMimeTypeFromFile()) i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) i.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) try { App.appContext.startActivity(i) } catch (e: ActivityNotFoundException) { if (!anonymous) { openFileWith(true) } else { showMsg("Couldn't find any app that can open this type of files") } } catch (e: Exception) { //Log.i(TAG, e); showMsg("Failed to open this file") } } fun File.getFolderSize(): Long { var size: Long = 0 val list = listFiles() if (list != null) { for (child in list) { size = if (child.isFile) { size + child.length() } else { size + child.getFolderSize() } } } return size } fun File.getAllFilesInDir(extension: String): ArrayList { if (!exists() || isFile) { return ArrayList() } val list = arrayListOf() val content = listFiles() if (content != null) { for (file in content) { if (file.isFile && file.name.endsWith(".$extension")) { list.add(file.absolutePath) } else { list.addAll(file.getAllFilesInDir(extension)) } } } return list } @SuppressLint("SimpleDateFormat") fun File.getLastModifiedDate(REGULAR_DATE_FORMAT: String = "MMM dd , hh:mm a"): String { return SimpleDateFormat(REGULAR_DATE_FORMAT).format(lastModified()) } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/extension/Int.kt ================================================ package com.raival.fileexplorer.extension import android.util.TypedValue import com.raival.fileexplorer.App fun Int.toDp(): Int = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), App.appContext.resources.displayMetrics ).toInt() fun Float.toDp(): Float = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, this, App.appContext.resources.displayMetrics ) ================================================ FILE: app/src/main/java/com/raival/fileexplorer/extension/Long.kt ================================================ package com.raival.fileexplorer.extension import java.text.DecimalFormat import kotlin.math.log10 import kotlin.math.pow fun Long.toFormattedSize(): String { if (this <= 0) return "0 B" val units = arrayOf("B", "kB", "MB", "GB", "TB") val digitGroups = (log10(this.toDouble()) / log10(1024.0)).toInt() return DecimalFormat("#,##0.#").format( this / 1024.0.pow(digitGroups.toDouble()) ) + " " + units[digitGroups] } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/extension/String.kt ================================================ package com.raival.fileexplorer.extension import com.google.gson.Gson import com.google.gson.reflect.TypeToken fun String.getStringList(): ArrayList { return Gson().fromJson(this, object : TypeToken>() {}.type) } fun String.surroundWithBrackets(): String { return "[$this]" } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/glide/FileExplorerGlideModule.java ================================================ package com.raival.fileexplorer.glide; import android.content.Context; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import com.bumptech.glide.Glide; import com.bumptech.glide.Registry; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.module.AppGlideModule; import com.raival.fileexplorer.glide.apk.ApkIconModelLoaderFactory; import com.raival.fileexplorer.glide.icon.IconModelLoaderFactory; import com.raival.fileexplorer.glide.model.IconRes; @GlideModule public class FileExplorerGlideModule extends AppGlideModule { @Override public void registerComponents(@NonNull Context context, @NonNull Glide glide, Registry registry) { registry.prepend(String.class, Drawable.class, new ApkIconModelLoaderFactory(context)); registry.prepend(IconRes.class, Drawable.class, new IconModelLoaderFactory(context)); } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/glide/apk/ApkIconDataFetcher.java ================================================ package com.raival.fileexplorer.glide.apk; import android.content.Context; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.data.DataFetcher; import com.raival.fileexplorer.R; import com.raival.fileexplorer.tab.file.misc.FileUtils; import java.io.File; public class ApkIconDataFetcher implements DataFetcher { private final Context context; private final String model; public ApkIconDataFetcher(Context context, String model) { this.context = context; this.model = model; } @Override public void loadData(@NonNull Priority priority, @NonNull DataCallback callback) { Drawable apkIcon = FileUtils.INSTANCE.getApkIcon(new File(model)); if (apkIcon == null) apkIcon = ContextCompat.getDrawable(context, R.drawable.unknown_file_extension); callback.onDataReady(apkIcon); } @Override public void cleanup() { // Intentionally empty only because we're not opening an InputStream or another I/O resource! } @Override public void cancel() { // No cancellation procedure } @NonNull @Override public Class getDataClass() { return Drawable.class; } @NonNull @Override public DataSource getDataSource() { return DataSource.LOCAL; } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/glide/apk/ApkIconModelLoader.java ================================================ package com.raival.fileexplorer.glide.apk; import android.content.Context; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.signature.ObjectKey; import com.raival.fileexplorer.tab.file.misc.FileMimeTypes; import java.util.Locale; public class ApkIconModelLoader implements ModelLoader { private final Context context; public ApkIconModelLoader(Context context) { this.context = context; } @Nullable @Override public LoadData buildLoadData(@NonNull String s, int width, int height, @NonNull Options options) { return new LoadData<>(new ObjectKey(s), new ApkIconDataFetcher(context, s)); } @Override public boolean handles(String s) { return s.toLowerCase(Locale.ROOT).endsWith(FileMimeTypes.apkType); } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/glide/apk/ApkIconModelLoaderFactory.java ================================================ package com.raival.fileexplorer.glide.apk; import android.content.Context; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; import com.bumptech.glide.load.model.MultiModelLoaderFactory; public class ApkIconModelLoaderFactory implements ModelLoaderFactory { private final Context context; public ApkIconModelLoaderFactory(Context context) { this.context = context; } @NonNull @Override public ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) { return new ApkIconModelLoader(context); } @Override public void teardown() { } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/glide/icon/IconDataFetcher.java ================================================ package com.raival.fileexplorer.glide.icon; import android.content.Context; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.data.DataFetcher; import com.raival.fileexplorer.glide.model.IconRes; public class IconDataFetcher implements DataFetcher { private final Context context; private final IconRes model; public IconDataFetcher(Context context, IconRes model) { this.context = context; this.model = model; } @Override public void loadData(@NonNull Priority priority, @NonNull DataCallback callback) { Context ctx = (model.getContext() != null) ? model.getContext() : context; Drawable drawable = ContextCompat.getDrawable(ctx, model.getResId()); callback.onDataReady(drawable); } @Override public void cleanup() { // Intentionally empty only because we're not opening an InputStream or another I/O resource! } @Override public void cancel() { // No cancellation procedure } @NonNull @Override public Class getDataClass() { return Drawable.class; } @NonNull @Override public DataSource getDataSource() { return DataSource.LOCAL; } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/glide/icon/IconModelLoader.java ================================================ package com.raival.fileexplorer.glide.icon; import android.content.Context; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.signature.ObjectKey; import com.raival.fileexplorer.glide.model.IconRes; public class IconModelLoader implements ModelLoader { private final Context context; public IconModelLoader(Context context) { this.context = context; } @Nullable @Override public LoadData buildLoadData(@NonNull IconRes s, int width, int height, @NonNull Options options) { return new LoadData<>(new ObjectKey(s), new IconDataFetcher(context, s)); } @Override public boolean handles(@NonNull IconRes s) { return true; } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/glide/icon/IconModelLoaderFactory.java ================================================ package com.raival.fileexplorer.glide.icon; import android.content.Context; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; import com.bumptech.glide.load.model.MultiModelLoaderFactory; import com.raival.fileexplorer.glide.model.IconRes; public class IconModelLoaderFactory implements ModelLoaderFactory { private final Context context; public IconModelLoaderFactory(Context context) { this.context = context; } @NonNull @Override public ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) { return new IconModelLoader(context); } @Override public void teardown() { } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/glide/model/IconRes.kt ================================================ package com.raival.fileexplorer.glide.model import android.content.Context class IconRes(val resId: Int, val context: Context? = null) ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/BaseDataHolder.kt ================================================ package com.raival.fileexplorer.tab abstract class BaseDataHolder { abstract val tag: String } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/BaseTabFragment.kt ================================================ package com.raival.fileexplorer.tab import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import com.google.android.material.appbar.MaterialToolbar import com.raival.fileexplorer.activity.MainActivity import com.raival.fileexplorer.activity.model.MainViewModel import com.raival.fileexplorer.common.view.BottomBarView import com.raival.fileexplorer.common.view.TabView /** * Each TabFragment must handle the creation of its DataHolder and the related TabView using * the provided APIs or custom ones. */ abstract class BaseTabFragment : Fragment() { var mainViewModel: MainViewModel? = null get() { if (field == null) { field = ViewModelProvider(requireActivity()).get(MainViewModel::class.java) } return field } private set var bottomBarView: BottomBarView? = null get() { if (field == null) field = (requireActivity() as MainActivity).bottomBarView return field } private set var toolbar: MaterialToolbar? = null get() { if (field == null) field = (requireActivity() as MainActivity).toolbar return field } private set private var tabView: TabView.Tab? = null var dataHolder: BaseDataHolder? = null get() { if (field == null && mainViewModel!!.getDataHolder(tag!!).also { field = it } == null) { field = createNewDataHolder() mainViewModel!!.addDataHolder(field!!) } return field } private set abstract fun onBackPressed(): Boolean abstract fun createNewDataHolder(): BaseDataHolder fun getTabView(): TabView.Tab? { if (tabView == null && (requireActivity() as MainActivity).tabView.getTabByTag(tag) .also { tabView = it } == null ) { tabView = (requireActivity() as MainActivity).tabView.addNewTab(tag!!) // set Default name tabView!!.setName("Untitled") } return tabView } open fun closeTab() { mainViewModel!!.getDataHolders() .removeIf { dataHolder1: BaseDataHolder -> dataHolder1.tag == tag } (requireActivity() as MainActivity).closeTab(tag!!) } override fun onResume() { super.onResume() // create TabView if necessary (important when a tab fragment doesn't update its TabView) getTabView() } companion object { const val DEFAULT_TAB_FRAGMENT_PREFIX = "0_FileExplorerTabFragment_" const val FILE_EXPLORER_TAB_FRAGMENT_PREFIX = "FileExplorerTabFragment_" const val APPS_TAB_FRAGMENT_PREFIX = "AppsTabFragment_" const val ARCHIVE_TAB_FRAGMENT_PREFIX = "ArchiveTabFragment_" } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/apps/AppsTabDataHolder.kt ================================================ package com.raival.fileexplorer.tab.apps import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.raival.fileexplorer.tab.BaseDataHolder import com.raival.fileexplorer.tab.apps.model.Apk import com.raival.fileexplorer.tab.apps.resolver.ApkResolver import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class AppsTabDataHolder(override val tag: String) : BaseDataHolder() { private val apps = MutableLiveData>() fun getApps(showSystemApps: Boolean, sortNewerFirst: Boolean): LiveData> { if (apps.value == null) { loadApps(showSystemApps, sortNewerFirst) } return apps } fun updateAppsList(showSystemApps: Boolean, sortNewerFirst: Boolean) { loadApps(showSystemApps, sortNewerFirst) } private fun loadApps(showSystemApps: Boolean, sortNewerFirst: Boolean) { CoroutineScope(Dispatchers.IO).launch { val apks = ApkResolver().load(showSystemApps, sortNewerFirst).get() withContext(Dispatchers.Main) { apps.setValue(apks) } }.start() } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/apps/AppsTabFragment.kt ================================================ package com.raival.fileexplorer.tab.apps import android.annotation.SuppressLint import android.os.Bundle import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.CompoundButton import android.widget.LinearLayout import android.widget.Space import androidx.recyclerview.widget.RecyclerView import com.google.android.material.checkbox.MaterialCheckBox import com.google.android.material.progressindicator.CircularProgressIndicator import com.raival.fileexplorer.R import com.raival.fileexplorer.tab.BaseDataHolder import com.raival.fileexplorer.tab.BaseTabFragment import com.raival.fileexplorer.tab.apps.adapter.AppListAdapter import com.raival.fileexplorer.tab.apps.model.Apk class AppsTabFragment : BaseTabFragment() { private lateinit var recyclerView: RecyclerView private lateinit var progressIndicator: CircularProgressIndicator private lateinit var showSystemApps: MaterialCheckBox private lateinit var sortApps: MaterialCheckBox override fun onBackPressed(): Boolean { super.closeTab() return true } override fun createNewDataHolder(): BaseDataHolder { return AppsTabDataHolder(tag!!) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val view = inflater.inflate(R.layout.apps_tab_fragment, container, false) recyclerView = view.findViewById(R.id.rv) progressIndicator = view.findViewById(R.id.progress) return view } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) prepareBottomBarView() (dataHolder as AppsTabDataHolder).getApps(showSystemApps = false, sortNewerFirst = true) .observe(viewLifecycleOwner) { list: ArrayList -> recyclerView.adapter = AppListAdapter(list, this) progressIndicator.visibility = View.GONE recyclerView.visibility = View.VISIBLE showSystemApps.isEnabled = true sortApps.isEnabled = true } getTabView()!!.setName("Apps") } @SuppressLint("SetTextI18n") private fun prepareBottomBarView() { bottomBarView!!.clear() bottomBarView!!.addView(Space(requireActivity()), LinearLayout.LayoutParams(0, 0, 1f)) showSystemApps = MaterialCheckBox(requireActivity()).apply { layoutParams = LinearLayout.LayoutParams(-2, -1, 1f) text = "System Apps" gravity = Gravity.CENTER_VERTICAL setOnCheckedChangeListener { _: CompoundButton?, _: Boolean -> (dataHolder as AppsTabDataHolder).updateAppsList( showSystemApps.isChecked, sortApps.isChecked ) progressIndicator.visibility = View.VISIBLE recyclerView.visibility = View.GONE showSystemApps.isEnabled = false sortApps.isEnabled = false } } bottomBarView!!.addItem("Show System Apps", showSystemApps) bottomBarView!!.addView(Space(requireActivity()), LinearLayout.LayoutParams(0, 0, 1f)) sortApps = MaterialCheckBox(requireActivity()).apply { layoutParams = LinearLayout.LayoutParams(-2, -1, 1f) text = "Newer First" gravity = Gravity.CENTER_VERTICAL isChecked = true setOnCheckedChangeListener { _: CompoundButton?, _: Boolean -> (dataHolder as AppsTabDataHolder).updateAppsList( showSystemApps.isChecked, sortApps.isChecked ) progressIndicator.visibility = View.VISIBLE recyclerView.visibility = View.GONE showSystemApps.isEnabled = false sortApps.isEnabled = false } } bottomBarView!!.addItem("Sort Apps", sortApps) bottomBarView!!.addView(Space(requireActivity()), LinearLayout.LayoutParams(0, 0, 1f)) } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/apps/adapter/AppListAdapter.kt ================================================ package com.raival.fileexplorer.tab.apps.adapter import android.annotation.SuppressLint import android.os.Environment import android.os.Handler import android.os.Looper import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.raival.fileexplorer.App.Companion.showMsg import com.raival.fileexplorer.R import com.raival.fileexplorer.common.BackgroundTask import com.raival.fileexplorer.tab.apps.AppsTabFragment import com.raival.fileexplorer.tab.apps.model.Apk import com.raival.fileexplorer.tab.file.misc.FileUtils import java.io.File import java.util.concurrent.atomic.AtomicBoolean open class AppListAdapter(private val list: ArrayList, private val fragment: AppsTabFragment) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { @SuppressLint("InflateParams") val view = fragment.layoutInflater.inflate(R.layout.apps_tab_app_item, null) view.layoutParams = RecyclerView.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) return ViewHolder(view) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.bind() } override fun getItemCount(): Int { return list.size } private fun showSaveDialog(file: Apk) { MaterialAlertDialogBuilder(fragment.requireContext()) .setIcon(file.icon) .setTitle(file.name) .setMessage("Do you want to save this app to Download folder?") .setPositiveButton("Yes") { _, _ -> saveApkFile(file) } .setNegativeButton("No", null) .show() } private fun saveApkFile(file: Apk) { val backgroundTask = BackgroundTask() val error = AtomicBoolean(false) backgroundTask.setTasks({ backgroundTask.showProgressDialog( "Copying...", fragment.requireActivity() ) }, { try { FileUtils.copyFile( file.source, file.name + ".apk", File( Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS ), true ) } catch (e: Exception) { error.set(true) e.printStackTrace() Handler(Looper.getMainLooper()).post { showMsg(e.toString()) } } }) { if (!error.get()) showMsg("APK file has been saved in " + "/Downloads/" + file.name) backgroundTask.dismiss() } backgroundTask.run() } inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { var icon: ImageView var name: TextView var pkg: TextView var details: TextView init { icon = itemView.findViewById(R.id.app_icon) name = itemView.findViewById(R.id.app_name) pkg = itemView.findViewById(R.id.app_pkg) details = itemView.findViewById(R.id.app_details) } fun bind() { val position = adapterPosition val apk = list[position] name.text = apk.name pkg.text = apk.pkg details.text = apk.size icon.setImageDrawable(apk.icon) itemView.findViewById(R.id.background) .setOnClickListener { showSaveDialog(apk) } } } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/apps/model/Apk.kt ================================================ package com.raival.fileexplorer.tab.apps.model import android.graphics.drawable.Drawable import java.io.File class Apk( val name: String, val pkg: String, val size: String, val icon: Drawable, val lastModified: Long, val source: File ) ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/apps/resolver/ApkResolver.kt ================================================ package com.raival.fileexplorer.tab.apps.resolver import android.annotation.SuppressLint import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import android.content.pm.PackageManager import com.raival.fileexplorer.App import com.raival.fileexplorer.extension.toFormattedSize import com.raival.fileexplorer.tab.apps.model.Apk import java.io.File class ApkResolver { private val list = ArrayList() private var sortApps = false @SuppressLint("PackageManagerGetSignatures") fun load(showSystemApps: Boolean, sortNewerFirst: Boolean): ApkResolver { list.clear() sortApps = sortNewerFirst val pm = App.appContext.packageManager val apps = pm.getInstalledApplications( PackageManager.MATCH_UNINSTALLED_PACKAGES or PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS ) var androidInfo: PackageInfo? = null try { androidInfo = pm.getPackageInfo("android", PackageManager.GET_SIGNATURES) } catch (ignored: PackageManager.NameNotFoundException) { } for (info in apps) { if (info.sourceDir == null) { continue } val pkgInfo: PackageInfo? = try { pm.getPackageInfo(info.packageName, PackageManager.GET_SIGNATURES) } catch (e: PackageManager.NameNotFoundException) { null } val isSystemApp = isAppInSystemPartition(info) || isSignedBySystem(pkgInfo, androidInfo) if (isSystemApp && !showSystemApps) continue val apk = Apk( pm.getApplicationLabel(info).toString(), info.packageName, File(info.publicSourceDir).length().toFormattedSize(), info.loadIcon(pm), File(info.publicSourceDir).lastModified(), File(info.publicSourceDir) ) list.add(apk) } return this } fun get(): ArrayList { if (sortApps) list.sortWith { apk1: Apk, apk2: Apk -> apk2.lastModified.compareTo(apk1.lastModified) } return list } private fun isSignedBySystem(piApp: PackageInfo?, piSys: PackageInfo?): Boolean { return piApp != null && piSys != null && piApp.signatures != null && piSys.signatures[0] == piApp.signatures[0] } companion object { fun isAppInSystemPartition(applicationInfo: ApplicationInfo): Boolean { return ((applicationInfo.flags and (ApplicationInfo.FLAG_SYSTEM or ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) != 0) } } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/file/FileExplorerTabDataHolder.kt ================================================ package com.raival.fileexplorer.tab.file import android.os.Parcelable import com.raival.fileexplorer.tab.BaseDataHolder import com.raival.fileexplorer.tab.file.model.FileItem import java.io.File class FileExplorerTabDataHolder(override val tag: String) : BaseDataHolder() { @JvmField var activeDirectory: File? = null @JvmField var recyclerViewStates: HashMap = HashMap() var searchList = ArrayList() @JvmField var selectedFiles = ArrayList() } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/file/FileExplorerTabFragment.kt ================================================ package com.raival.fileexplorer.tab.file import android.annotation.SuppressLint import android.os.Bundle import android.os.Environment import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.HorizontalScrollView import android.widget.LinearLayout import androidx.appcompat.widget.PopupMenu import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup import com.google.android.material.textview.MaterialTextView import com.raival.fileexplorer.App.Companion.showMsg import com.raival.fileexplorer.R import com.raival.fileexplorer.activity.MainActivity import com.raival.fileexplorer.common.dialog.CustomDialog import com.raival.fileexplorer.extension.getFormattedFileCount import com.raival.fileexplorer.extension.getShortLabel import com.raival.fileexplorer.extension.toDp import com.raival.fileexplorer.tab.BaseDataHolder import com.raival.fileexplorer.tab.BaseTabFragment import com.raival.fileexplorer.tab.file.adapter.FileListAdapter import com.raival.fileexplorer.tab.file.adapter.PathHistoryAdapter import com.raival.fileexplorer.tab.file.dialog.SearchDialog import com.raival.fileexplorer.tab.file.dialog.TasksDialog import com.raival.fileexplorer.tab.file.misc.FileOpener import com.raival.fileexplorer.tab.file.misc.FileUtils import com.raival.fileexplorer.tab.file.model.FileItem import com.raival.fileexplorer.tab.file.options.FileOptionsHandler import com.raival.fileexplorer.util.Log import com.raival.fileexplorer.util.PrefsUtils import java.io.File import java.io.IOException import java.util.* class FileExplorerTabFragment : BaseTabFragment { val files = ArrayList() var pathHistory = ArrayList() private lateinit var fileList: RecyclerView private lateinit var pathHistoryRv: RecyclerView private lateinit var placeHolder: View private lateinit var fileOptionsHandler: FileOptionsHandler private var requireRefresh = false var previousDirectory: File? = null @JvmField var currentDirectory: File? = null constructor() : super() constructor(directory: File) : super() { currentDirectory = directory } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val view = inflater.inflate(R.layout.file_explorer_tab_fragment, container, false) fileList = view.findViewById(R.id.rv) pathHistoryRv = view.findViewById(R.id.path_history) placeHolder = view.findViewById(R.id.place_holder) val homeButton = view.findViewById(R.id.home) homeButton.setOnClickListener { setCurrentDirectory(defaultHomeDirectory) } homeButton.setOnLongClickListener { showSetPathDialog() } return view } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { prepareBottomBarView() initFileList() loadData() // restore RecyclerView state restoreRecyclerViewState() } private fun loadData() { setCurrentDirectory((dataHolder as FileExplorerTabDataHolder).activeDirectory!!) } private fun prepareBottomBarView() { bottomBarView!!.clear() bottomBarView!!.addItem("Tasks", R.drawable.ic_baseline_assignment_24) { TasksDialog(this).show( parentFragmentManager, "" ) } bottomBarView!!.addItem("Search", R.drawable.ic_round_search_24) { val searchFragment = SearchDialog(this, currentDirectory!!) searchFragment.show(parentFragmentManager, "") setSelectAll(false) } bottomBarView!!.addItem( "Create", R.drawable.ic_baseline_add_24 ) { showAddNewFileDialog() } bottomBarView!!.addItem( "Sort", R.drawable.ic_baseline_sort_24 ) { view: View -> showSortOptionsMenu(view) } bottomBarView!!.addItem( "Select All", R.drawable.ic_baseline_select_all_24 ) { setSelectAll(true) } bottomBarView!!.addItem( "refresh", R.drawable.ic_baseline_restart_alt_24 ) { refresh() } } private fun showSortOptionsMenu(view: View) { val popupMenu = PopupMenu(requireActivity(), view) popupMenu.menu.add("Sort by:").isEnabled = false popupMenu.menu.add("Name (A-Z)").setCheckable(true).isChecked = PrefsUtils.FileExplorerTab.sortingMethod == PrefsUtils.SORT_NAME_A2Z popupMenu.menu.add("Name (Z-A)").setCheckable(true).isChecked = PrefsUtils.FileExplorerTab.sortingMethod == PrefsUtils.SORT_NAME_Z2A popupMenu.menu.add("Size (Bigger)").setCheckable(true).isChecked = PrefsUtils.FileExplorerTab.sortingMethod == PrefsUtils.SORT_SIZE_BIGGER popupMenu.menu.add("Size (Smaller)").setCheckable(true).isChecked = PrefsUtils.FileExplorerTab.sortingMethod == PrefsUtils.SORT_SIZE_SMALLER popupMenu.menu.add("Date (Newer)").setCheckable(true).isChecked = PrefsUtils.FileExplorerTab.sortingMethod == PrefsUtils.SORT_DATE_NEWER popupMenu.menu.add("Date (Older)").setCheckable(true).isChecked = PrefsUtils.FileExplorerTab.sortingMethod == PrefsUtils.SORT_DATE_OLDER popupMenu.menu.add("Other options:").isEnabled = false popupMenu.menu.add("Folders first").setCheckable(true).isChecked = PrefsUtils.FileExplorerTab.listFoldersFirst() popupMenu.setOnMenuItemClickListener { menuItem: MenuItem -> menuItem.isChecked = !menuItem.isChecked when (menuItem.title.toString()) { "Name (A-Z)" -> { PrefsUtils.FileExplorerTab.sortingMethod = PrefsUtils.SORT_NAME_A2Z } "Name (Z-A)" -> { PrefsUtils.FileExplorerTab.sortingMethod = PrefsUtils.SORT_NAME_Z2A } "Size (Bigger)" -> { PrefsUtils.FileExplorerTab.sortingMethod = PrefsUtils.SORT_SIZE_BIGGER } "Size (Smaller)" -> { PrefsUtils.FileExplorerTab.sortingMethod = PrefsUtils.SORT_SIZE_SMALLER } "Date (Older)" -> { PrefsUtils.FileExplorerTab.sortingMethod = PrefsUtils.SORT_DATE_OLDER } "Date (Newer)" -> { PrefsUtils.FileExplorerTab.sortingMethod = PrefsUtils.SORT_DATE_NEWER } "Folders first" -> { PrefsUtils.FileExplorerTab.setListFoldersFirst(menuItem.isChecked) } } refresh() true } popupMenu.show() } @SuppressLint("SetTextI18n") private fun showSetPathDialog(): Boolean { val customDialog = CustomDialog() val input = customDialog.createInput(requireActivity(), "e.g. /sdcard/...") input.editText?.setSingleLine() val textView = MaterialTextView(requireContext()) textView.setPadding(0, 8.toDp(), 0, 0) textView.alpha = 0.7f textView.text = "Quick Links:" val layout = ChipGroup(requireContext()).apply { isScrollContainer = true } // Chips layout.addView(createChip("Internal Storage") { setCurrentDirectory( defaultHomeDirectory ) customDialog.dismiss() }) layout.addView(createChip("Downloads") { setCurrentDirectory( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) ) customDialog.dismiss() }) layout.addView(createChip("Documents") { setCurrentDirectory( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) ) customDialog.dismiss() }) layout.addView(createChip("DCIM") { setCurrentDirectory( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) ) customDialog.dismiss() }) layout.addView(createChip("Music") { setCurrentDirectory( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC) ) customDialog.dismiss() }) customDialog.setTitle("Jump to path") .addView(input) .addView(textView) .addView(HorizontalScrollView(requireContext()).apply { layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT ) addView(layout) }) .setPositiveButton("Go", { val file = File( input.editText!!.text.toString() ) if (file.exists()) { if (file.canRead()) { if (file.isFile) { FileOpener(requireActivity() as MainActivity).openFile(file) } else { setCurrentDirectory(file) } } else { showMsg(Log.UNABLE_TO + " read the provided file") } } else { showMsg("The destination path doesn't exist!") } }, true) .show(parentFragmentManager, "") return true } private fun createChip(title: String, onClick: () -> Unit): View { return Chip(requireContext()).apply { layoutParams = LinearLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT ) text = title setOnClickListener { onClick.invoke() } } } private fun showAddNewFileDialog() { val customDialog = CustomDialog() val input = customDialog.createInput(requireActivity(), "File name") input.editText?.setSingleLine() FileUtils.setFileValidator(input, currentDirectory!!) CustomDialog() .setTitle("Create new file") .addView(input) .setPositiveButton("File", { createFile( input.editText!!.text.toString(), false ) }, true) .setNegativeButton("Folder", { createFile( input.editText!!.text.toString(), true ) }, true) .setNeutralButton("Cancel", null, true) .show(parentFragmentManager, "") } private fun createFile(name: String, isFolder: Boolean) { val file = File(currentDirectory, name) if (isFolder) { if (!file.mkdir()) { showMsg(Log.UNABLE_TO + " create folder: " + file.absolutePath) } else { refresh() focusOn(file) } } else { try { if (!file.createNewFile()) { showMsg(Log.UNABLE_TO + " " + FileUtils.CREATE_FILE + ": " + file.absolutePath) } else { refresh() focusOn(file) } } catch (e: IOException) { Log.e(TAG, e) showMsg(e.toString()) } } } override fun closeTab() { // Close the tab (if not default tab) if (tag != null && !tag!!.startsWith("0_")) { super.closeTab() } } override fun onBackPressed(): Boolean { // Unselect selected files (if any) if (selectedFiles.size > 0) { setSelectAll(false) return true } // Go back if possible val parent = currentDirectory?.parentFile if (parent != null && parent.exists() && parent.canRead()) { setCurrentDirectory(currentDirectory?.parentFile!!) // restore RecyclerView state restoreRecyclerViewState() // Clear any selected files from the DataHolder (it also gets cleared // when a FileItem is clicked) (dataHolder as FileExplorerTabDataHolder).selectedFiles.clear() return true } // Close the tab (if not default tab) if (tag != null && !tag!!.startsWith("0_")) { closeTab() return true } return false } override fun createNewDataHolder(): BaseDataHolder { val dataHolder = FileExplorerTabDataHolder(tag!!) dataHolder.activeDirectory = if (currentDirectory == null) defaultHomeDirectory else currentDirectory return dataHolder } override fun onStop() { super.onStop() requireRefresh = true } override fun onPause() { super.onPause() requireRefresh = true } override fun onResume() { super.onResume() if (requireRefresh) { requireRefresh = false refresh() } } @SuppressLint("NotifyDataSetChanged") fun setSelectAll(select: Boolean) { if (!select) (dataHolder as FileExplorerTabDataHolder).selectedFiles.clear() for (item in files) { item.isSelected = select if (select) { (dataHolder as FileExplorerTabDataHolder).selectedFiles.add(item.file) } } // Don't call refresh(), because it will recreate the tab and reset the selection fileList.adapter?.notifyDataSetChanged() } val selectedFiles: ArrayList get() { val list = ArrayList() for (item in files) { if (item.isSelected) list.add(item) } return list } /** * Show/Hide placeholder */ fun showPlaceholder(isShow: Boolean) { placeHolder.visibility = if (isShow) View.VISIBLE else View.GONE } /** * Used to update the title of attached tabView */ private fun updateTabTitle() { getTabView()!!.setName(name) } /** * This method is called once from #onViewCreated(View, Bundle) */ private fun initFileList() { fileList.adapter = FileListAdapter(this) fileList.setHasFixedSize(true) initPathHistory() } private fun initPathHistory() { pathHistoryRv.layoutManager = LinearLayoutManager(requireActivity(), LinearLayoutManager.HORIZONTAL, false) pathHistoryRv.adapter = PathHistoryAdapter(this) } /** * RecyclerView state should be saved when the fragment is destroyed and recreated. * #getDataHolder() isn't used here because we don't want to create a new DataHolder if the fragment is about * to close (note that the DataHolder gets removed just right before the fragment is closed) */ override fun onDestroy() { super.onDestroy() val fileExplorerTabDataHolder = mainViewModel!!.getDataHolder( tag!! ) as FileExplorerTabDataHolder? fileExplorerTabDataHolder?.recyclerViewStates?.put( currentDirectory!!, fileList.layoutManager!! .onSaveInstanceState()!! ) } /** * This method handles the following (in order): * - Updating currentDirectory and previousDirectory fields * - Updating recyclerViewStates in DataHolder * - Sorting files based on the preferences * - Updating tabView title * - Update pathHistory list * - Refreshing adapters (fileList & pathHistory) * - Updating activeDirectory in DataHolder * * @param file the directory to open */ fun setCurrentDirectory(file: File) { if (currentDirectory != null) previousDirectory = currentDirectory currentDirectory = file // Save only when previousDirectory is set (so that it can restore the state before onDestroy()) if (previousDirectory != null) (dataHolder as FileExplorerTabDataHolder).recyclerViewStates[previousDirectory!!] = fileList.layoutManager!! .onSaveInstanceState()!! prepareFiles() updateTabTitle() updatePathHistoryList() refreshFileList() (dataHolder as FileExplorerTabDataHolder).activeDirectory = currentDirectory } /** * This method automatically removes the restored state from DataHolder recyclerViewStates * This method is called when: * - Create the fragment * - #onBackPressed() * - when select a directory from pathHistory RecyclerView */ fun restoreRecyclerViewState() { val savedState = (dataHolder as FileExplorerTabDataHolder).recyclerViewStates[currentDirectory] if (savedState != null) { fileList.layoutManager?.onRestoreInstanceState(savedState) (dataHolder as FileExplorerTabDataHolder).recyclerViewStates.remove(currentDirectory) } } /** * Refreshes both fileList and pathHistory recyclerview (used by #setCurrentDirectory(File) ONLY) */ @SuppressLint("NotifyDataSetChanged") private fun refreshFileList() { fileList.adapter?.notifyDataSetChanged() pathHistoryRv.adapter?.notifyDataSetChanged() pathHistoryRv.scrollToPosition(pathHistoryRv.adapter!!.itemCount - 1) fileList.scrollToPosition(0) if (toolbar != null) toolbar!!.subtitle = currentDirectory?.getFormattedFileCount() } /** * Used to refresh the tab */ fun refresh() { setCurrentDirectory(currentDirectory!!) restoreRecyclerViewState() } private val defaultHomeDirectory: File get() = Environment.getExternalStorageDirectory() private fun prepareFiles() { // Make sure current file is ready if (currentDirectory == null) { loadData() return } // Clear previous list files.clear() // Load all files in the current File val files = currentDirectory!!.listFiles() if (files != null) { for (comparator in FileUtils.comparators) { Arrays.sort(files, comparator) } for (file in files) { val fileItem = FileItem(file) if ((dataHolder as FileExplorerTabDataHolder).selectedFiles.contains(fileItem.file)) { fileItem.isSelected = true } this.files.add(fileItem) } } } fun focusOn(file: File) { for (i in files.indices) { if (file == files[i].file) { fileList.scrollToPosition(i) return } } } /** * @return the name associated with this tab (currently used for tabView) */ val name: String get() = currentDirectory?.getShortLabel(MAX_NAME_LENGTH)!! fun showFileOptions(fileItem: FileItem?) { if (!this::fileOptionsHandler.isInitialized) { fileOptionsHandler = FileOptionsHandler(this) } fileOptionsHandler.showOptions(fileItem!!) } fun openFile(fileItem: FileItem) { FileOpener(requireActivity() as MainActivity).openFile(fileItem.file) } private fun updatePathHistoryList() { val list = ArrayList() var file = currentDirectory while (file != null && file.canRead()) { list.add(file) file = file.parentFile } list.reverse() if (list.size > 0) pathHistory = list } companion object { const val MAX_NAME_LENGTH = 26 private const val TAG = "FileExplorerTabFragment" } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/file/adapter/FileListAdapter.kt ================================================ package com.raival.fileexplorer.tab.file.adapter import android.annotation.SuppressLint import android.graphics.drawable.ColorDrawable import android.text.TextUtils import android.view.View import android.view.View.OnLongClickListener import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.raival.fileexplorer.R import com.raival.fileexplorer.extension.getFileDetails import com.raival.fileexplorer.tab.file.FileExplorerTabDataHolder import com.raival.fileexplorer.tab.file.FileExplorerTabFragment import com.raival.fileexplorer.tab.file.misc.IconHelper import com.raival.fileexplorer.tab.file.observer.FileListObserver import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File class FileListAdapter(private val parentFragment: FileExplorerTabFragment) : RecyclerView.Adapter() { private val selectedFileDrawable: ColorDrawable private val highlightedFileDrawable: ColorDrawable init { registerAdapterDataObserver(FileListObserver(parentFragment, this)) selectedFileDrawable = ColorDrawable(parentFragment.requireContext().getColor(R.color.selectedFileHighlight)) highlightedFileDrawable = ColorDrawable(parentFragment.requireContext().getColor(R.color.previousFileHighlight)) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { @SuppressLint("InflateParams") val view = parentFragment.layoutInflater.inflate(R.layout.file_explorer_tab_file_item, null) view.layoutParams = RecyclerView.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) return ViewHolder(view) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.bind() } override fun getItemCount(): Int { return parentFragment.files.size } inner class ViewHolder(v: View) : RecyclerView.ViewHolder(v) { var name: TextView var details: TextView var icon: ImageView var background: View private var divider: View private lateinit var prevFile: File init { name = v.findViewById(R.id.file_name) details = v.findViewById(R.id.file_details) icon = v.findViewById(R.id.file_icon) background = v.findViewById(R.id.background) divider = v.findViewById(R.id.divider) } /** * Update the ui of each item */ fun bind() { val position = adapterPosition val fileItem = parentFragment.files[position] if (!this::prevFile.isInitialized || !fileItem.file.isDirectory || !prevFile.isDirectory) { IconHelper.setFileIcon(icon, fileItem.file) } if (TextUtils.isEmpty(fileItem.name)) { fileItem.name = fileItem.file.name } name.text = fileItem.name if (fileItem.details.isEmpty()) { val pos = adapterPosition CoroutineScope(Dispatchers.IO).launch { fileItem.details = fileItem.file.getFileDetails() if (position == pos) { withContext(Dispatchers.Main) { details.text = fileItem.details } } } } else { details.text = fileItem.details } if (position == itemCount - 1) { divider.visibility = View.GONE } else { divider.visibility = View.VISIBLE } // Hidden files will be 50% transparent if (fileItem.file.isHidden) { if (icon.alpha == 1f) icon.alpha = 0.5f } else { if (icon.alpha < 1) icon.alpha = 1f } // Set a proper background color if (fileItem.isSelected) { if (background.foreground !== selectedFileDrawable) background.foreground = selectedFileDrawable } else if (parentFragment.previousDirectory != null && fileItem.file.absolutePath == parentFragment.previousDirectory?.absolutePath ) { if (background.foreground !== highlightedFileDrawable) background.foreground = highlightedFileDrawable } else { if (background.foreground != null) background.foreground = null } // Select/unselect item by pressing the icon itemView.findViewById(R.id.icon_container).setOnClickListener { fileItem.isSelected = !fileItem.isSelected if (fileItem.isSelected) { (parentFragment.dataHolder as FileExplorerTabDataHolder?)!!.selectedFiles.add( fileItem.file ) } else { (parentFragment.dataHolder as FileExplorerTabDataHolder?)!!.selectedFiles.remove( fileItem.file ) } notifyItemChanged(position) } // Handle click event background.setOnClickListener { if (fileItem.file.isFile) { parentFragment.openFile(fileItem) } else { parentFragment.setCurrentDirectory(fileItem.file) // Clear any selected files from the DataHolder (it also gets cleared // in onBackPressed (when go back) (parentFragment.dataHolder as FileExplorerTabDataHolder?)!!.selectedFiles.clear() } } val longClickListener = OnLongClickListener { parentFragment.showFileOptions(fileItem) true } // Apply the listener for both the background and the icon itemView.findViewById(R.id.icon_container) .setOnLongClickListener(longClickListener) background.setOnLongClickListener(longClickListener) prevFile = fileItem.file } } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/file/adapter/PathHistoryAdapter.kt ================================================ package com.raival.fileexplorer.tab.file.adapter import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.raival.fileexplorer.R import com.raival.fileexplorer.extension.isExternalStorageFolder import com.raival.fileexplorer.tab.file.FileExplorerTabFragment import com.raival.fileexplorer.tab.file.dialog.FileInfoDialog import com.raival.fileexplorer.tab.file.misc.FileUtils import com.raival.fileexplorer.util.Utils class PathHistoryAdapter(private val parentFragment: FileExplorerTabFragment) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { @SuppressLint("InflateParams") val v = LayoutInflater.from(parent.context).inflate( R.layout.file_explorer_tab_path_history_view, null ) v.layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT ) return ViewHolder(v) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.bind() } override fun getItemCount(): Int { return parentFragment.pathHistory.size } inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { var label: TextView fun bind() { val position = adapterPosition val file = parentFragment.pathHistory[position] label.text = if (file.isExternalStorageFolder()) FileUtils.INTERNAL_STORAGE else file.name label.setTextColor( if (position == itemCount - 1) Utils.getColorAttribute( R.attr.colorPrimary, parentFragment.requireActivity() ) else Utils.getColorAttribute( R.attr.colorOutline, parentFragment.requireActivity() ) ) itemView.setOnClickListener { parentFragment.setCurrentDirectory(file) // Restore RecyclerView state parentFragment.restoreRecyclerViewState() } itemView.setOnLongClickListener { FileInfoDialog(file).setUseDefaultFileInfo(true).show( parentFragment.parentFragmentManager, "" ) true } } init { label = itemView.findViewById(R.id.text) } } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/file/dialog/FileInfoDialog.kt ================================================ package com.raival.fileexplorer.tab.file.dialog import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.textfield.TextInputLayout import com.raival.fileexplorer.R import com.raival.fileexplorer.extension.getFolderSize import com.raival.fileexplorer.extension.getFormattedFileCount import com.raival.fileexplorer.extension.getLastModifiedDate import com.raival.fileexplorer.extension.toFormattedSize import com.raival.fileexplorer.tab.file.misc.IconHelper import com.raival.fileexplorer.tab.file.misc.md5.HashUtils import com.raival.fileexplorer.tab.file.misc.md5.MessageDigestAlgorithm import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File import java.security.MessageDigest class FileInfoDialog(private val file: File) : BottomSheetDialogFragment() { private val infoList = ArrayList() private var useDefaultFileInfo = false private lateinit var container: ViewGroup fun setUseDefaultFileInfo(`is`: Boolean): FileInfoDialog { useDefaultFileInfo = `is` return this } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return inflater.inflate(R.layout.file_explorer_tab_info_dialog, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) (view.findViewById(R.id.file_name) as TextView).text = file.name IconHelper.setFileIcon(view.findViewById(R.id.file_icon), file) container = view.findViewById(R.id.container) if (!useDefaultFileInfo) { for (holder in infoList) { addItemView(holder, container) } } else { if (file.isFile) { addDefaultFileInfo() } else { addDefaultFolderInfo() } } } private fun addDefaultFolderInfo() { addItemView(InfoHolder("Path:", file.absolutePath, true), container) addItemView( InfoHolder( "Modified:", file.getLastModifiedDate(), true ), container ) val count = addItemView(InfoHolder("Content:", "Counting...", true), container) CoroutineScope(Dispatchers.IO).launch { val s = file.getFormattedFileCount() withContext(Dispatchers.Main) { count.text = s } }.start() val size = addItemView(InfoHolder("Size:", "Counting...", true), container) CoroutineScope(Dispatchers.IO).launch { val s = file.getFolderSize().toFormattedSize() withContext(Dispatchers.Main) { size.text = s } }.start() } private fun addDefaultFileInfo() { addItemView(InfoHolder("Path:", file.absolutePath, true), container) addItemView( InfoHolder( "Extension:", file.extension, true ), container ) addItemView( InfoHolder( "Modified:", file.getLastModifiedDate(), true ), container ) addItemView( InfoHolder( "Size:", file.length().toFormattedSize(), true ), container ) val md5 = addItemView(InfoHolder("MD5:", "calculating...", true), container) CoroutineScope(Dispatchers.IO).launch { val s = HashUtils.getCheckSumFromFile( MessageDigest.getInstance(MessageDigestAlgorithm.MD5), file ) withContext(Dispatchers.Main) { md5.text = s } }.start() val sha1 = addItemView(InfoHolder("SHA1:", "calculating...", true), container) CoroutineScope(Dispatchers.IO).launch { val s = HashUtils.getCheckSumFromFile( MessageDigest.getInstance(MessageDigestAlgorithm.SHA_1), file ) withContext(Dispatchers.Main) { sha1.text = s } }.start() val sha256 = addItemView(InfoHolder("SHA256:", "calculating...", true), container) CoroutineScope(Dispatchers.IO).launch { val s = HashUtils.getCheckSumFromFile( MessageDigest.getInstance(MessageDigestAlgorithm.SHA_256), file ) withContext(Dispatchers.Main) { sha256.text = s } }.start() val sha512 = addItemView(InfoHolder("SHA_512:", "calculating...", true), container) CoroutineScope(Dispatchers.IO).launch { val s = HashUtils.getCheckSumFromFile( MessageDigest.getInstance(MessageDigestAlgorithm.SHA_512), file ) withContext(Dispatchers.Main) { sha512.text = s } }.start() } private fun addItemView(holder: InfoHolder, container: ViewGroup?): TextView { @SuppressLint("InflateParams") val view = layoutInflater.inflate(R.layout.file_explorer_tab_info_dialog_item, null, false) val v = (view.findViewById(R.id.item) as TextInputLayout).apply { editText!!.apply { keyListener = null setTextIsSelectable(true) setText(holder.info) } hint = holder.name } container!!.addView(view) return v.editText!! } fun addItem(name: String, info: String?, clickable: Boolean): FileInfoDialog { infoList.add(InfoHolder(name, info, clickable)) return this } override fun getTheme(): Int { return R.style.ThemeOverlay_Material3_BottomSheetDialog } class InfoHolder(var name: String, var info: String?, var clickable: Boolean) } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/file/dialog/SearchDialog.kt ================================================ package com.raival.fileexplorer.tab.file.dialog import android.annotation.SuppressLint import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.widget.* import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.textfield.TextInputLayout import com.raival.fileexplorer.R import com.raival.fileexplorer.extension.getFileDetails import com.raival.fileexplorer.tab.file.FileExplorerTabDataHolder import com.raival.fileexplorer.tab.file.FileExplorerTabFragment import com.raival.fileexplorer.tab.file.misc.IconHelper import com.raival.fileexplorer.tab.file.model.FileItem import com.raival.fileexplorer.util.Log import com.raival.fileexplorer.util.PrefsUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.apache.commons.io.FileUtils import org.apache.commons.io.LineIterator import java.io.File import java.util.regex.Pattern class SearchDialog : BottomSheetDialogFragment { private val tab: FileExplorerTabFragment private val filesToSearchIn: ArrayList private lateinit var recyclerView: RecyclerView private lateinit var input: TextInputLayout private lateinit var deepSearch: CheckBox private lateinit var optimizedSearching: CheckBox private lateinit var regEx: CheckBox private lateinit var suffix: CheckBox private lateinit var prefix: CheckBox private lateinit var searchButton: Button private lateinit var progress: ProgressBar private lateinit var fileCount: TextView private lateinit var query: String private var active = false constructor(tab: FileExplorerTabFragment, directory: File) { filesToSearchIn = ArrayList() filesToSearchIn.add(directory) this.tab = tab } constructor(tab: FileExplorerTabFragment, files: ArrayList) { this.tab = tab filesToSearchIn = files } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { dialog?.window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) return inflater.inflate(R.layout.search_fragment, container, false) } override fun getTheme(): Int { return R.style.ThemeOverlay_Material3_BottomSheetDialog } @SuppressLint("SetTextI18n", "NotifyDataSetChanged") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) recyclerView = view.findViewById(R.id.rv) input = view.findViewById(R.id.input) deepSearch = view.findViewById(R.id.search_option_deep_search) optimizedSearching = view.findViewById(R.id.search_option_optimized_searching) regEx = view.findViewById(R.id.search_option_regex) suffix = view.findViewById(R.id.search_option_suffix) prefix = view.findViewById(R.id.search_option_prefix) searchButton = view.findViewById(R.id.search_button) progress = view.findViewById(R.id.progress) fileCount = view.findViewById(R.id.file_count) recyclerView.layoutManager = LinearLayoutManager(requireContext()) recyclerView.adapter = RecyclerViewAdapter() progress.visibility = View.GONE searchButton.setOnClickListener { if (active) { searchButton.text = "Search" progress.visibility = View.GONE recyclerView.adapter?.notifyDataSetChanged() active = false isCancelable = true } else { isCancelable = false searchButton.text = "Stop" (tab.dataHolder as FileExplorerTabDataHolder?)!!.searchList.clear() recyclerView.adapter?.notifyDataSetChanged() progress.visibility = View.VISIBLE loseFocus(input) active = true query = input.editText?.text.toString() CoroutineScope(Dispatchers.IO).launch { for (file in filesToSearchIn) { if (!active) break searchIn( file, deepSearch.isChecked, optimizedSearching.isChecked, regEx.isChecked, prefix.isChecked, suffix.isChecked ) } withContext(Dispatchers.Main) { searchButton.text = "Search" progress.visibility = View.GONE recyclerView.adapter!!.notifyDataSetChanged() active = false isCancelable = true updateFileCount() } } } } if ((tab.dataHolder as FileExplorerTabDataHolder?)!!.searchList.size > 0) { fileCount.visibility = View.VISIBLE updateFileCount() } } @SuppressLint("SetTextI18n") private fun updateFileCount() { fileCount.text = (tab.dataHolder as FileExplorerTabDataHolder?)!!.searchList.size.toString() + " results found" } private fun searchIn( file: File, isDeepSearch: Boolean, optimized: Boolean, useRegex: Boolean, startWith: Boolean, endWith: Boolean ) { if (file.isFile) { if (isDeepSearch) { if (PrefsUtils.Settings.deepSearchFileSizeLimit >= file.length()) { if (optimized) { var lineIterator: LineIterator? = null try { lineIterator = FileUtils.lineIterator(file) while (lineIterator.hasNext()) { if (useRegex) { if (Pattern.compile(query).matcher(lineIterator.nextLine()) .find() ) { addFileItem(file) break } } else { if (lineIterator.nextLine().contains(query)) { addFileItem(file) break } } } } catch (e: Exception) { Log.e(TAG, e) } finally { LineIterator.closeQuietly(lineIterator) } } else { try { if (useRegex) { if (Pattern.compile(query).matcher(file.readText()).find()) { addFileItem(file) } } else { if (file.readText().contains(query)) { addFileItem(file) } } } catch (e: Exception) { Log.e(TAG, e) } } } } else { if (startWith) { if (file.name.startsWith(query)) addFileItem(file) } else if (endWith) { if (file.name.endsWith(query)) addFileItem(file) } else { if (file.name.contains(query)) addFileItem(file) } } } else { val children = file.listFiles() if (children != null) { for (child in children) { if (!active) break searchIn(child, isDeepSearch, optimized, useRegex, startWith, endWith) } } } } @SuppressLint("NotifyDataSetChanged") private fun addFileItem(file: File) { (tab.dataHolder as FileExplorerTabDataHolder?)!!.searchList.add(FileItem(file)) recyclerView.post { updateFileCount() recyclerView.adapter?.notifyDataSetChanged() } } private fun loseFocus(view: View) { view.isEnabled = false view.isEnabled = true } private inner class RecyclerViewAdapter : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val inflater = requireActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater @SuppressLint("InflateParams") val view = inflater.inflate(R.layout.file_explorer_tab_file_item, null) val lp = RecyclerView.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) view.layoutParams = lp return ViewHolder(view) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.bind() } override fun getItemCount(): Int { return (tab.dataHolder as FileExplorerTabDataHolder?)!!.searchList.size } inner class ViewHolder(v: View) : RecyclerView.ViewHolder(v) { var name: TextView var details: TextView var icon: ImageView var background: View fun bind() { val position = adapterPosition val fileItem = (tab.dataHolder as FileExplorerTabDataHolder?)!!.searchList[position] name.text = fileItem.file.name details.text = fileItem.file.getFileDetails() IconHelper.setFileIcon(icon, fileItem.file) icon.alpha = if (fileItem.file.isHidden) 0.5f else 1f background.setOnClickListener { tab.setCurrentDirectory(fileItem.file.parentFile!!) tab.refresh() tab.focusOn(fileItem.file) dismiss() } } init { name = v.findViewById(R.id.file_name) details = v.findViewById(R.id.file_details) icon = v.findViewById(R.id.file_icon) background = v.findViewById(R.id.background) } } } companion object { private const val TAG = "Search Dialog" } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/file/dialog/TasksDialog.kt ================================================ package com.raival.fileexplorer.tab.file.dialog import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.lifecycle.ViewModelProvider import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.raival.fileexplorer.App.Companion.showMsg import com.raival.fileexplorer.R import com.raival.fileexplorer.activity.model.MainViewModel import com.raival.fileexplorer.tab.file.FileExplorerTabFragment import com.raival.fileexplorer.tab.file.model.Task import com.raival.fileexplorer.tab.file.model.Task.OnFinishListener import com.raival.fileexplorer.tab.file.model.Task.OnUpdateListener class TasksDialog(private val fileExplorerTabFragment: FileExplorerTabFragment) : BottomSheetDialogFragment() { private lateinit var container: ViewGroup private lateinit var mainViewModel: MainViewModel private lateinit var alertDialog: AlertDialog private lateinit var placeHolder: View override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return inflater.inflate(R.layout.file_explorer_tab_task_dialog, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) container = view.findViewById(R.id.container) placeHolder = view.findViewById(R.id.place_holder) mainViewModel = ViewModelProvider(requireActivity()).get(MainViewModel::class.java) val tasks = mainViewModel.tasks if (tasks.isNotEmpty()) { placeHolder.visibility = View.GONE } for (task in tasks) { addTask(task, task.isValid) } } override fun getTheme(): Int { return R.style.ThemeOverlay_Material3_BottomSheetDialog } private fun addTask(task: Task, valid: Boolean) { val v = layoutInflater.inflate(R.layout.file_explorer_tab_task_dialog_item, container, false) (v.findViewById(R.id.label) as TextView).text = task.name (v.findViewById(R.id.task_details) as TextView).text = task.details v.findViewById(R.id.background).setOnClickListener { if (!valid) { showMsg("This task is invalid, some files are missing, long click to execute it anyway") return@setOnClickListener } run(task) } v.findViewById(R.id.remove).setOnClickListener { mainViewModel.tasks.remove(task) container.removeView(v) if (container.childCount == 0) { placeHolder.visibility = View.VISIBLE } } v.setOnLongClickListener label@{ if (!valid) { run(task) return@label true } false } container.addView(v) } @SuppressLint("SetTextI18n") private fun run(task: Task) { dismiss() task.setActiveDirectory(fileExplorerTabFragment.currentDirectory!!) val view = progressView val progressText = view.findViewById(R.id.msg) progressText.text = "Processing..." alertDialog = MaterialAlertDialogBuilder(requireActivity()) .setCancelable(false) .setView(view) .show() task.start( object : OnUpdateListener { override fun onUpdate(progress: String) { progressText.text = progress } }, object : OnFinishListener { override fun onFinish(result: String) { mainViewModel.tasks.remove(task) showMsg(result) fileExplorerTabFragment.refresh() alertDialog.dismiss() } } ) } @get:SuppressLint("InflateParams") private val progressView: View get() = requireActivity().layoutInflater.inflate(R.layout.progress_view, null) } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/file/misc/APKSignerUtils.kt ================================================ package com.raival.fileexplorer.tab.file.misc import com.raival.fileexplorer.App import com.raival.fileexplorer.tab.file.misc.BuildUtils.unzipFromAssets import java.io.File object APKSignerUtils { val pk8: File get() { val check = File(App.appContext.filesDir.toString() + "/build/testkey.pk8") if (check.exists()) { return check } unzipFromAssets( App.appContext, "build/testkey.pk8.zip", check.parentFile?.absolutePath ) return check } val pem: File get() { val check = File(App.appContext.filesDir.toString() + "/build/testkey.x509.pem") if (check.exists()) { return check } unzipFromAssets( App.appContext, "build/testkey.x509.pem.zip", check.parentFile?.absolutePath ) return check } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/file/misc/BuildUtils.kt ================================================ package com.raival.fileexplorer.tab.file.misc import android.content.Context import android.util.Log import com.raival.fileexplorer.App import java.io.File import java.io.FileOutputStream import java.io.IOException import java.io.InputStream import java.util.zip.ZipEntry import java.util.zip.ZipInputStream object BuildUtils { private const val BUFFER_SIZE = 1024 * 10 private const val TAG = "Decompress" val lambdaStubsJarFile: File get() { val check = File(App.appContext.filesDir.toString() + "/build/core-lambda-stubs.jar") if (check.exists()) { return check } unzipFromAssets( App.appContext, "build/lambda-stubs.zip", check.parentFile?.absolutePath ) return check } val rtJarFile: File get() { val customRt = File(App.appContext.getExternalFilesDir(null), "build/rt.jar") if (customRt.exists() && customRt.isFile) { return customRt } val check = File(App.appContext.filesDir.toString() + "/build/rt.jar") if (check.exists()) { return check } unzipFromAssets( App.appContext, "build/rt.zip", check.parentFile?.absolutePath ) return check } @JvmStatic fun unzipFromAssets(context: Context, zipFile: String, destination: String?) { var des = destination try { if (des == null || des.isEmpty()) des = context.filesDir.absolutePath val stream = context.assets.open(zipFile) unzip(stream, des) } catch (e: IOException) { e.printStackTrace() } } private fun dirChecker(destination: String?, dir: String) { val f = File(destination, dir) if (!f.isDirectory) { val success = f.mkdirs() if (!success) { Log.w(TAG, "Failed to create folder " + f.name) } } } private fun unzip(stream: InputStream, destination: String?) { dirChecker(destination, "") val buffer = ByteArray(BUFFER_SIZE) try { val zin = ZipInputStream(stream) var ze: ZipEntry while (zin.nextEntry.also { ze = it } != null) { Log.v(TAG, "Unzipping " + ze.name) if (ze.isDirectory) { dirChecker(destination, ze.name) } else { val f = File(destination, ze.name) if (!f.exists()) { val success = f.createNewFile() if (!success) { Log.w( TAG, com.raival.fileexplorer.util.Log.UNABLE_TO + " " + FileUtils.CREATE_FILE + " " + f.name ) continue } val fileOutputStream = FileOutputStream(f) var count: Int while (zin.read(buffer).also { count = it } != -1) { fileOutputStream.write(buffer, 0, count) } zin.closeEntry() fileOutputStream.close() } } } zin.close() } catch (e: Exception) { Log.e(TAG, "unzip", e) } } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/file/misc/FileMimeTypes.kt ================================================ package com.raival.fileexplorer.tab.file.misc object FileMimeTypes { const val apkType = "apk" const val rarType = "rar" const val pdfType = "pdf" const val javaType = "java" const val kotlinType = "kt" const val xmlType = "xml" const val aiType = "ai" const val docType = "doc" const val docxType = "docx" const val xlsType = "xls" const val xlsxType = "xlsx" const val pptType = "ppt" const val pptxType = "pptx" const val sqlType = "sql" const val svgType = "svg" const val default = "*/*" @JvmField val fontType = arrayOf("ttf", "otf") @JvmField val audioType = arrayOf("mp3", "4mp", "aup", "ogg", "3ga", "m4b", "wav", "acc", "m4a") @JvmField val videoType = arrayOf("mp4", "mov", "avi", "mkv", "wmv", "m4v", "3gp", "webm") @JvmField val archiveType = arrayOf("zip", "7z", "tar", "jar", "gz", "xz", "xapk", "obb", apkType) @JvmField val textType = arrayOf("txt", "text", "log", "dsc", "apt", "rtf", "rtx") @JvmField val codeType = arrayOf(javaType, xmlType, "py", "css", kotlinType, "cs", "xml", "json") @JvmField val imageType = arrayOf("png", "jpeg", "jpg", "heic", "tiff", "gif", "webp", svgType, "bmp") val mimeTypes = HashMap().apply { put("asm", "text/x-asm"); put("def", "text/plain"); put("in", "text/plain"); put("rc", "text/plain"); put("list", "text/plain"); put("log", "text/plain"); put("pl", "text/plain"); put("prop", "text/plain"); put("properties", "text/plain"); put("rc", "text/plain"); put("epub", "application/epub+zip"); put("ibooks", "application/x-ibooks+zip"); put("ifb", "text/calendar"); put("eml", "message/rfc822"); put("msg", "application/vnd.ms-outlook"); put("ace", "application/x-ace-compressed"); put("bz", "application/x-bzip"); put("bz2", "application/x-bzip2"); put("cab", "application/vnd.ms-cab-compressed"); put("gz", "application/x-gzip"); put("lrf", "application/octet-stream"); put("jar", "application/java-archive"); put("xz", "application/x-xz"); put("Z", "application/x-compress"); put("bat", "application/x-msdownload"); put("ksh", "text/plain"); put("sh", "application/x-sh"); put("db", "application/octet-stream"); put("db3", "application/octet-stream"); put("otf", "application/x-font-otf"); put("ttf", "application/x-font-ttf"); put("psf", "application/x-font-linux-psf"); put("cgm", "image/cgm"); put("btif", "image/prs.btif"); put("dwg", "image/vnd.dwg"); put("dxf", "image/vnd.dxf"); put("fbs", "image/vnd.fastbidsheet"); put("fpx", "image/vnd.fpx"); put("fst", "image/vnd.fst"); put("mdi", "image/vnd.ms-mdi"); put("npx", "image/vnd.net-fpx"); put("xif", "image/vnd.xiff"); put("pct", "image/x-pict"); put("pic", "image/x-pict"); put("adp", "audio/adpcm"); put("au", "audio/basic"); put("snd", "audio/basic"); put("m2a", "audio/mpeg"); put("m3a", "audio/mpeg"); put("oga", "audio/ogg"); put("spx", "audio/ogg"); put("aac", "audio/x-aac"); put("mka", "audio/x-matroska"); put("jpgv", "video/jpeg"); put("jpgm", "video/jpm"); put("jpm", "video/jpm"); put("mj2", "video/mj2"); put("mjp2", "video/mj2"); put("mpa", "video/mpeg"); put("ogv", "video/ogg"); put("flv", "video/x-flv"); put("mkv", "video/x-matroska"); } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/file/misc/FileOpener.kt ================================================ package com.raival.fileexplorer.tab.file.misc import android.content.Intent import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.raival.fileexplorer.activity.MainActivity import com.raival.fileexplorer.activity.TextEditorActivity import com.raival.fileexplorer.extension.openFileWith import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.io.File class FileOpener(private val mainActivity: MainActivity) { fun openFile(file: File) { if (!handleKnownFileExtensions(file)) { file.openFileWith(false) } } private fun handleKnownFileExtensions(file: File): Boolean { if (FileMimeTypes.textType.contains(file.extension.lowercase()) || FileMimeTypes.codeType.contains(file.extension.lowercase()) ) { val intent = Intent() intent.setClass(mainActivity, TextEditorActivity::class.java) intent.putExtra("file", file.absolutePath) mainActivity.startActivity(intent) return true } if (file.extension == FileMimeTypes.apkType) { val dialog = MaterialAlertDialogBuilder(mainActivity) .setMessage("Do you want to install this app?") .setPositiveButton("Install") { _, _ -> file.openFileWith(false) } CoroutineScope(Dispatchers.Main).launch { dialog.setTitle(FileUtils.getApkName(file)) dialog.setIcon(FileUtils.getApkIcon(file)) dialog.show() } return true } return false } companion object { private val TAG = FileOpener::class.java.simpleName } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/file/misc/FileUtils.kt ================================================ package com.raival.fileexplorer.tab.file.misc import android.app.Activity import android.content.Intent import android.content.pm.PackageInfo import android.graphics.drawable.Drawable import android.net.Uri import android.text.Editable import android.text.TextWatcher import android.webkit.MimeTypeMap import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import com.google.android.material.textfield.TextInputLayout import com.raival.fileexplorer.App import com.raival.fileexplorer.App.Companion.showMsg import com.raival.fileexplorer.R import com.raival.fileexplorer.util.Log import com.raival.fileexplorer.util.PrefsUtils import com.raival.fileexplorer.util.PrefsUtils.FileExplorerTab.listFoldersFirst import com.raival.fileexplorer.util.PrefsUtils.FileExplorerTab.sortingMethod import java.io.* import java.util.* object FileUtils { const val INTERNAL_STORAGE = "Internal Storage" const val CREATE_FILE = "create file" private fun sortFoldersFirst(): Comparator { return Comparator { file1: File, file2: File -> if (file1.isDirectory && !file2.isDirectory) { return@Comparator -1 } else if (!file1.isDirectory && file2.isDirectory) { return@Comparator 1 } else { return@Comparator 0 } } } private fun sortFilesFirst(): Comparator { return Comparator { file2: File, file1: File -> if (file1.isDirectory && !file2.isDirectory) { return@Comparator -1 } else if (!file1.isDirectory && file2.isDirectory) { return@Comparator 1 } else { return@Comparator 0 } } } val comparators: ArrayList> get() { val list = ArrayList>() when (sortingMethod) { PrefsUtils.SORT_NAME_A2Z -> { list.add(sortNameAsc()) } PrefsUtils.SORT_NAME_Z2A -> { list.add(sortNameDesc()) } PrefsUtils.SORT_SIZE_SMALLER -> { list.add(sortSizeAsc()) } PrefsUtils.SORT_SIZE_BIGGER -> { list.add(sortSizeDesc()) } PrefsUtils.SORT_DATE_NEWER -> { list.add(sortDateDesc()) } PrefsUtils.SORT_DATE_OLDER -> { list.add(sortDateAsc()) } } if (listFoldersFirst()) { list.add(sortFoldersFirst()) } else { list.add(sortFilesFirst()) } return list } private fun sortDateAsc(): Comparator { return Comparator.comparingLong { obj: File -> obj.lastModified() } } private fun sortDateDesc(): Comparator { return Comparator { file1: File, file2: File -> file2.lastModified().compareTo(file1.lastModified()) } } private fun sortNameAsc(): Comparator { return Comparator.comparing { file: File -> file.name.lowercase(Locale.getDefault()) } } private fun sortNameDesc(): Comparator { return Comparator { file1: File, file2: File -> file2.name.lowercase(Locale.getDefault()).compareTo( file1.name.lowercase( Locale.getDefault() ) ) } } private fun sortSizeAsc(): Comparator { return Comparator.comparingLong { obj: File -> obj.length() } } private fun sortSizeDesc(): Comparator { return Comparator { file1: File, file2: File -> file2.length().compareTo(file1.length()) } } fun isSingleFolder(selectedFiles: ArrayList): Boolean { return selectedFiles.size == 1 && !selectedFiles[0].isFile } fun isSingleFile(selectedFiles: ArrayList): Boolean { return selectedFiles.size == 1 && selectedFiles[0].isFile } fun isOnlyFiles(selectedFiles: ArrayList): Boolean { for (file in selectedFiles) { if (!file.isFile) return false } return true } fun isArchiveFiles(selectedFiles: ArrayList): Boolean { for (file in selectedFiles) { if (!FileMimeTypes.archiveType.contains(file.extension.lowercase())) return false } return true } fun copy(fileToCopy: File, destinationFolder: File, overwrite: Boolean) { if (!fileToCopy.exists()) throw Exception("File " + fileToCopy.absolutePath + " doesn't exist") if (fileToCopy.isFile) { copyFile(fileToCopy, destinationFolder, overwrite) } else copyFolder(fileToCopy, destinationFolder, overwrite) } fun copyFile(fileToCopy: File, destinationFolder: File, overwrite: Boolean) { copyFile(fileToCopy, fileToCopy.name, destinationFolder, overwrite) } /** * Copy file to a new folder * * @param fileToCopy: the file that needs to be copied * @param fileName: The name of the copied file in the destination folder * @param destinationFolder: The folder to copy the file into * @param overwrite: Whether or not to overwrite the already existed file in the destination folder * @throws Exception: Any errors that occur during the copying process */ fun copyFile( fileToCopy: File, fileName: String, destinationFolder: File, overwrite: Boolean ) { if (!destinationFolder.exists() && !destinationFolder.mkdirs()) { throw Exception(Log.UNABLE_TO + " create folder: " + destinationFolder) } val newFile = File(destinationFolder, fileName) if (newFile.exists() && !overwrite) return if (!newFile.exists() && !newFile.createNewFile()) { throw Exception(Log.UNABLE_TO + " " + CREATE_FILE + ": " + newFile) } val fileInputStream = FileInputStream(fileToCopy) val fileOutputStream = FileOutputStream(newFile, false) val buff = ByteArray(1024) var length: Int while (fileInputStream.read(buff).also { length = it } > 0) { fileOutputStream.write(buff, 0, length) } fileInputStream.close() fileOutputStream.close() } fun copyFolder(folderToCopy: File, destinationFolder: File, overwrite: Boolean) { copyFolder(folderToCopy, folderToCopy.name, destinationFolder, overwrite) } /** * Copy folder to another new folder * * @param folderToCopy: the folder that needs to be copied * @param folderName: The name of the copied folder in the destination folder * @param destinationFolder: The folder to copy into * @param overwrite: Whether or not to overwrite the already existed files in the destination folder * @throws Exception: Any errors that occur during the copying process */ fun copyFolder( folderToCopy: File, folderName: String, destinationFolder: File, overwrite: Boolean ) { val newFolder = File(destinationFolder, folderName) if (!newFolder.exists() && !newFolder.mkdirs()) { throw Exception(Log.UNABLE_TO + " create folder: " + newFolder) } if (newFolder.isFile) { throw Exception( """${Log.UNABLE_TO} create folder: $newFolder. A file with the same name exists.""" ) } val folderContent = folderToCopy.listFiles() if (folderContent != null) { for (file in folderContent) { if (file.isFile) { copyFile(file, file.name, newFolder, overwrite) } else { copyFolder(file, file.name, newFolder, overwrite) } } } } fun deleteFile(file: File) { if (!file.exists()) { throw Exception("File $file doesn't exist") } if (!file.isFile) { val fileArr = file.listFiles() if (fileArr != null) { for (subFile in fileArr) { if (subFile.isDirectory) { deleteFile(subFile) } if (subFile.isFile) { if (!subFile.delete()) throw Exception(Log.UNABLE_TO + " delete file: " + subFile) } } } } if (!file.delete()) throw Exception(Log.UNABLE_TO + " delete file: " + file) } fun deleteFiles(selectedFiles: ArrayList) { for (file in selectedFiles) { deleteFile(file) } } fun move(file: File, destination: File?) { if (file.isFile) { if (!file.renameTo(File(destination, file.name))) { throw IOException("Failed to move file: " + file.absolutePath) } } else { val parent = File(destination, file.name) if (parent.mkdir()) { val files = file.listFiles() if (files != null) { for (child in files) { move(child, parent) } } if (!file.delete()) { throw IOException("Failed to delete file: " + file.absolutePath) } } else { throw IOException("Failed to create folder: $parent") } } } fun setFileValidator(input: TextInputLayout, directory: File) { setFileValidator(input, null, directory) } fun setFileValidator(input: TextInputLayout, file: File?, directory: File) { setFileValidator(input, file, directory.list()?.toMutableList() as ArrayList) } fun setFileValidator(input: TextInputLayout, file: File?, nameList: ArrayList) { if (file != null) input.error = "This name is the same as before" else input.error = "Invalid file name" input.editText?.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} override fun afterTextChanged(editable: Editable) { if (isValidFileName(editable.toString())) { if (!nameList.contains(editable.toString())) { input.error = null } else if (file != null && editable.toString() == file.name) { input.error = "This name is the same as before" } else { input.error = "This name is in use" } } else { input.error = "Invalid file name" } } }) } fun isValidFileName(name: String): Boolean { return if (name.isEmpty()) false else !hasInvalidChar( name ) } private fun hasInvalidChar(name: String): Boolean { for (ch in name.toCharArray()) { when (ch) { '"', '*', '/', ':', '>', '<', '?', '\\', '|', '\n', '\t', 0x7f.toChar() -> { return true } else -> {} } if (ch.code <= 0x1f) return true } return false } fun rename(file: File, newName: String): Boolean { return file.renameTo(File(file.parentFile, newName)) } fun shareFiles(filesToShare: ArrayList, activity: Activity) { if (filesToShare.size == 1) { val file = filesToShare[0] if (file.isDirectory) { showMsg("Folders cannot be shared") return } val uri = FileProvider.getUriForFile( App.appContext, App.appContext.packageName + ".provider", file ) val intent = Intent(Intent.ACTION_SEND) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) intent.type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension) intent.putExtra(Intent.EXTRA_STREAM, uri) activity.startActivity(Intent.createChooser(intent, "Share file")) return } val intent = Intent(Intent.ACTION_SEND_MULTIPLE) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) intent.type = "*/*" val uriList = ArrayList() for (file in filesToShare) { if (file.isFile) { val uri = FileProvider.getUriForFile( App.appContext, App.appContext.packageName + ".provider", file ) uriList.add(uri) } } intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList) activity.startActivity(Intent.createChooser(intent, "Share files")) } fun getFormattedSize(length: Long, format: String): String { if (length > 1073741824) return String.format( Locale.ENGLISH, format, length.toFloat() / 1073741824 ) + "GB" if (length > 1048576) return String.format( Locale.ENGLISH, format, length.toFloat() / 1048576 ) + "MB" return if (length > 1024) String.format( Locale.ENGLISH, format, length.toFloat() / 1024 ) + "KB" else length.toString() + "B" } fun getFormattedSize(length: Long): String { return getFormattedSize(length, "%.02f") } fun copyFromInputStream(inputStream: InputStream): String { val outputStream = ByteArrayOutputStream() val buf = ByteArray(1024) var i: Int try { while (inputStream.read(buf).also { i = it } != -1) { outputStream.write(buf, 0, i) } outputStream.close() inputStream.close() } catch (ignored: IOException) { } return outputStream.toString() } fun getApkIcon(file: File): Drawable? { val pi: PackageInfo? = App.appContext.packageManager.getPackageArchiveInfo(file.absolutePath, 0) return if (pi != null) { pi.applicationInfo.sourceDir = file.absolutePath pi.applicationInfo.publicSourceDir = file.absolutePath pi.applicationInfo.loadIcon(App.appContext.packageManager) } else { ContextCompat.getDrawable(App.appContext, R.drawable.unknown_file_extension) } } fun getApkName(file: File): String? { val pi = App.appContext.packageManager.getPackageArchiveInfo(file.absolutePath, 0) return if (pi != null) { App.appContext.packageManager.getApplicationLabel(pi.applicationInfo).toString() } else { pi?.applicationInfo?.name } } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/file/misc/IconHelper.kt ================================================ package com.raival.fileexplorer.tab.file.misc import android.widget.ImageView import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.signature.ObjectKey import com.raival.fileexplorer.R import com.raival.fileexplorer.glide.model.IconRes import java.io.File import java.util.zip.ZipEntry object IconHelper { fun setFileIcon(icon: ImageView, file: Any) { if ((file is File && !file.isFile) || (file is ZipEntry && file.isDirectory)) { Glide.with(icon.context) .load(IconRes(R.drawable.ic_baseline_folder_24, icon.context)) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(icon) return } val ext: String = if (file is File) file.extension.lowercase() else "" if (ext == FileMimeTypes.pdfType) { Glide.with(icon.context) .load(IconRes(R.drawable.pdf_file_extension)) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(icon) return } if (FileMimeTypes.textType.contains(ext)) { Glide.with(icon.context) .load(IconRes(R.drawable.txt_file_extension)) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(icon) return } if (ext == FileMimeTypes.javaType) { Glide.with(icon.context) .load(IconRes(R.drawable.java_file_extension)) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(icon) return } if (ext == FileMimeTypes.kotlinType) { Glide.with(icon.context) .load(IconRes(R.drawable.kt_file_extension)) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(icon) return } if (ext == FileMimeTypes.xmlType) { Glide.with(icon.context) .load(IconRes(R.drawable.xml_file_extension)) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(icon) return } if (FileMimeTypes.codeType.contains(ext)) { Glide.with(icon.context) .load(IconRes(R.drawable.code_file_extension)) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(icon) return } if (ext == FileMimeTypes.apkType) { Glide.with(icon.context) .load(if (file is File) file.absolutePath else R.drawable.apk_placeholder) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .error(R.drawable.apk_placeholder) .into(icon) return } if (FileMimeTypes.archiveType.contains(ext) || ext == FileMimeTypes.rarType) { Glide.with(icon.context) .load(IconRes(R.drawable.archive_file_extension)) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(icon) return } if (FileMimeTypes.videoType.contains(ext)) { if (file is File) { Glide.with(icon.context) .load(file) .signature(ObjectKey(file.lastModified())) .error(R.drawable.video_file_extension) .placeholder(R.drawable.video_file_extension) .into(icon) } else { Glide.with(icon.context) .load(R.drawable.video_file_extension) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(icon) } return } if (FileMimeTypes.audioType.contains(ext)) { Glide.with(icon.context) .load(IconRes(R.drawable.music_file_extension)) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(icon) return } if (FileMimeTypes.fontType.contains(ext)) { Glide.with(icon.context) .load(IconRes(R.drawable.font_file_extension)) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(icon) return } if (ext == FileMimeTypes.sqlType) { Glide.with(icon.context) .load(IconRes(R.drawable.sql_file_extension)) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(icon) return } if (ext == FileMimeTypes.aiType) { Glide.with(icon.context) .load(IconRes(R.drawable.vector_file_extension)) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(icon) return } if (ext == FileMimeTypes.svgType) { Glide.with(icon.context) .load(IconRes(R.drawable.svg_file_extension)) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(icon) return } if (FileMimeTypes.imageType.contains(ext)) { if (file is File) { Glide.with(icon.context) .applyDefaultRequestOptions(RequestOptions().override(100).encodeQuality(80)) .load(file) .signature(ObjectKey(file.lastModified())) .error(R.drawable.image_file_extension) .into(icon) } else { Glide.with(icon.context) .load(R.drawable.image_file_extension) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(icon) } return } if (ext == FileMimeTypes.docType || ext == FileMimeTypes.docxType) { Glide.with(icon.context) .load(IconRes(R.drawable.doc_file_extension)) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(icon) return } if (ext == FileMimeTypes.xlsType || ext == FileMimeTypes.xlsxType) { Glide.with(icon.context) .load(IconRes(R.drawable.xls_file_extension)) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(icon) return } if (ext == FileMimeTypes.pptType || ext == FileMimeTypes.pptxType) { Glide.with(icon.context) .load(IconRes(R.drawable.powerpoint_file_extension)) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(icon) return } Glide.with(icon.context) .load(IconRes(R.drawable.unknown_file_extension)) .diskCacheStrategy(DiskCacheStrategy.NONE) .skipMemoryCache(true) .into(icon) } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/file/misc/ZipUtils.kt ================================================ package com.raival.fileexplorer.tab.file.misc import net.lingala.zip4j.ZipFile import java.io.File fun archive(filesToCompress: ArrayList, zipFile: File?) { val zip = ZipFile(zipFile) for (file in filesToCompress) { if (file.isFile) { zip.addFile(file) } else { zip.addFolder(file) } } } fun extract(filesToExtract: ArrayList, directory: File) { for (file in filesToExtract) { if (file.isFile) { val output = File(directory, file.name.substring(0, file.name.lastIndexOf("."))) if (output.mkdir()) { ZipFile(file).extractAll(output.absolutePath) } else { ZipFile(file).extractAll(directory.absolutePath) } } } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/file/misc/md5/HashUtils.kt ================================================ package com.raival.fileexplorer.tab.file.misc.md5 import java.io.File import java.io.FileInputStream import java.io.InputStream import java.security.MessageDigest object HashUtils { private const val STREAM_BUFFER_LENGTH = 1024 fun getCheckSumFromFile(digest: MessageDigest, filePath: String): String { val file = File(filePath) return getCheckSumFromFile(digest, file) } fun getCheckSumFromFile(digest: MessageDigest, file: File): String { val fis = FileInputStream(file) val byteArray = updateDigest(digest, fis).digest() fis.close() val hexCode = StringUtils.encodeHex(byteArray, true) return String(hexCode) } private fun updateDigest(digest: MessageDigest, data: InputStream): MessageDigest { val buffer = ByteArray(STREAM_BUFFER_LENGTH) var read = data.read(buffer, 0, STREAM_BUFFER_LENGTH) while (read > -1) { digest.update(buffer, 0, read) read = data.read(buffer, 0, STREAM_BUFFER_LENGTH) } return digest } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/file/misc/md5/MessageDigestAlgorithm.kt ================================================ package com.raival.fileexplorer.tab.file.misc.md5 object MessageDigestAlgorithm { const val MD2 = "MD2" const val MD5 = "MD5" const val SHA_1 = "SHA-1" const val SHA_224 = "SHA-224" const val SHA_256 = "SHA-256" const val SHA_384 = "SHA-384" const val SHA_512 = "SHA-512" const val SHA_512_224 = "SHA-512/224" const val SHA_512_256 = "SHA-512/256" const val SHA3_224 = "SHA3-224" const val SHA3_256 = "SHA3-256" const val SHA3_384 = "SHA3-384" const val SHA3_512 = "SHA3-512" } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/file/misc/md5/StringUtils.kt ================================================ package com.raival.fileexplorer.tab.file.misc.md5 object StringUtils { private val DIGITS_LOWER = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f') private val DIGITS_UPPER = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F') fun encodeHex(data: ByteArray, toLowerCase: Boolean): CharArray { return encodeHex(data, if (toLowerCase) DIGITS_LOWER else DIGITS_UPPER) } fun encodeHex(data: ByteArray, toDigits: CharArray): CharArray { val l = data.size val out = CharArray(l shl 1) // two characters form the hex value. var i = 0 var j = 0 while (i < l) { out[j++] = toDigits[0xF0 and data[i].toInt() ushr 4] out[j++] = toDigits[0x0F and data[i].toInt()] i++ } return out } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/file/model/FileItem.kt ================================================ package com.raival.fileexplorer.tab.file.model import java.io.File class FileItem(var f: File) { @JvmField var isSelected = false @JvmField var file: File = f var details = "" var name = "" } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/file/model/Task.kt ================================================ package com.raival.fileexplorer.tab.file.model import java.io.File abstract class Task { abstract val name: String? abstract val details: String? abstract val isValid: Boolean abstract fun setActiveDirectory(directory: File) abstract fun start(onUpdate: OnUpdateListener, onFinish: OnFinishListener) interface OnUpdateListener { fun onUpdate(progress: String) } interface OnFinishListener { fun onFinish(result: String) } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/file/observer/FileListObserver.kt ================================================ package com.raival.fileexplorer.tab.file.observer import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver import com.raival.fileexplorer.tab.file.FileExplorerTabFragment import com.raival.fileexplorer.tab.file.adapter.FileListAdapter class FileListObserver( private val parentFragment: FileExplorerTabFragment, private val fileListAdapter: FileListAdapter? ) : AdapterDataObserver() { override fun onChanged() { super.onChanged() checkIfEmpty() } private fun checkIfEmpty() { parentFragment.showPlaceholder(fileListAdapter != null && fileListAdapter.itemCount == 0) } init { checkIfEmpty() } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/file/options/FileOptionsHandler.kt ================================================ package com.raival.fileexplorer.tab.file.options import android.annotation.SuppressLint import android.content.Intent import android.view.View import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.lifecycle.ViewModelProvider import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.raival.fileexplorer.App.Companion.showMsg import com.raival.fileexplorer.R import com.raival.fileexplorer.activity.MainActivity import com.raival.fileexplorer.activity.TextEditorActivity import com.raival.fileexplorer.activity.model.MainViewModel import com.raival.fileexplorer.common.BackgroundTask import com.raival.fileexplorer.common.dialog.CustomDialog import com.raival.fileexplorer.common.dialog.OptionsDialog import com.raival.fileexplorer.extension.openFileWith import com.raival.fileexplorer.tab.BaseTabFragment import com.raival.fileexplorer.tab.file.FileExplorerTabFragment import com.raival.fileexplorer.tab.file.dialog.FileInfoDialog import com.raival.fileexplorer.tab.file.dialog.SearchDialog import com.raival.fileexplorer.tab.file.misc.FileUtils import com.raival.fileexplorer.tab.file.model.FileItem import com.raival.fileexplorer.tab.file.model.Task.OnFinishListener import com.raival.fileexplorer.tab.file.model.Task.OnUpdateListener import com.raival.fileexplorer.tab.file.task.CompressTask import com.raival.fileexplorer.tab.file.task.CopyTask import com.raival.fileexplorer.tab.file.task.CutTask import com.raival.fileexplorer.tab.file.task.ExtractTask import com.raival.fileexplorer.util.PrefsUtils import java.io.File class FileOptionsHandler(private val parentFragment: FileExplorerTabFragment) { private var mainViewModel: MainViewModel? = null get() { if (field == null) { field = ViewModelProvider(parentFragment.requireActivity()).get( MainViewModel::class.java ) } return field } fun showOptions(fileItem: FileItem) { val selectedFiles = ArrayList() for (item in parentFragment.selectedFiles) { selectedFiles.add(item.file) } if (!fileItem.isSelected) selectedFiles.add(fileItem.file) val title: String = if (selectedFiles.size == 1) { fileItem.file.name } else { "" + selectedFiles.size + " Files selected" } val bottomDialog = OptionsDialog(title) bottomDialog.show(parentFragment.parentFragmentManager, "FileOptionsDialog") //______________| Options |_______________\\ if (selectedFiles.size == 1) { val list = PrefsUtils.TextEditor.fileExplorerTabBookmarks if (!list.contains(selectedFiles[0].toString())) { bottomDialog.addOption( "Add to bookmarks", R.drawable.ic_baseline_bookmark_add_24, { list.add(selectedFiles[0].absolutePath) PrefsUtils.General.setFileExplorerTabBookmarks(list) (parentFragment.requireActivity() as MainActivity).refreshBookmarks() showMsg("Added to bookmarks successfully") }, true ) } else { bottomDialog.addOption( "Remove from bookmarks", R.drawable.ic_baseline_bookmark_remove_24, { list.remove(selectedFiles[0].absolutePath) PrefsUtils.General.setFileExplorerTabBookmarks(list) (parentFragment.requireActivity() as MainActivity).refreshBookmarks() showMsg("Removed from bookmarks successfully") }, true ) } } if (FileUtils.isSingleFolder(selectedFiles)) { bottomDialog.addOption( "Open in a new tab", R.drawable.ic_round_tab_24, { if (parentFragment.requireActivity() is MainActivity) { val fragment = FileExplorerTabFragment(selectedFiles[0]) (parentFragment.requireActivity() as MainActivity).addNewTab( fragment, BaseTabFragment.FILE_EXPLORER_TAB_FRAGMENT_PREFIX + (parentFragment.requireActivity() as MainActivity).generateRandomTag() ) } }, true ) } if (FileUtils.isOnlyFiles(selectedFiles)) { bottomDialog.addOption("Share", R.drawable.ic_round_share_24, { FileUtils.shareFiles(selectedFiles, parentFragment.requireActivity()) parentFragment.setSelectAll(false) }, true) } if (FileUtils.isSingleFile(selectedFiles)) { bottomDialog.addOption( "Open with", R.drawable.ic_baseline_open_in_new_24, { selectedFiles[0].openFileWith(true) parentFragment.setSelectAll(false) }, true ) } bottomDialog.addOption("Copy", R.drawable.ic_baseline_file_copy_24, { mainViewModel!!.tasks.add(CopyTask(selectedFiles)) parentFragment.setSelectAll(false) notifyNewTask() }, true) if (FileUtils.isSingleFile(selectedFiles)) { bottomDialog.addOption( "Create a backup", R.drawable.ic_baseline_file_copy_24, { createBackupFile(selectedFiles[0]) parentFragment.setSelectAll(false) }, true ) } bottomDialog.addOption("Cut", R.drawable.ic_round_content_cut_24, { mainViewModel!!.tasks.add(CutTask(selectedFiles)) parentFragment.setSelectAll(false) notifyNewTask() }, true) if (selectedFiles.size == 1) { bottomDialog.addOption( "Rename", R.drawable.ic_round_edit_24, { showRenameDialog(selectedFiles) }, true ) } bottomDialog.addOption( "Delete", R.drawable.ic_round_delete_forever_24, { confirmDeletion(selectedFiles) }, true ) if (FileUtils.isSingleFile(selectedFiles)) { bottomDialog.addOption( "Edit with code editor", R.drawable.ic_round_edit_note_24, { openWithTextEditor( selectedFiles[0] ) }, true ) } if (FileUtils.isArchiveFiles(selectedFiles)) { bottomDialog.addOption("Extract", R.drawable.ic_baseline_logout_24, { mainViewModel!!.tasks.add(ExtractTask(selectedFiles)) parentFragment.setSelectAll(false) notifyNewTask() }, true) } bottomDialog.addOption( "Compress", R.drawable.ic_round_compress_24, { compressFiles(CompressTask(selectedFiles)) }, true ) if (selectedFiles.size == 1) { bottomDialog.addOption("Details", R.drawable.ic_baseline_info_24, { showFileInfoDialog( selectedFiles[0] ) }, true) } bottomDialog.addOption("Search", R.drawable.ic_round_manage_search_24, { val searchFragment = SearchDialog( parentFragment, selectedFiles ) searchFragment.show(parentFragment.parentFragmentManager, "") parentFragment.setSelectAll(false) }, true) } private fun createBackupFile(file: File) { val backgroundTask = BackgroundTask() backgroundTask.setTasks({ backgroundTask.showProgressDialog( "creating a backup file...", parentFragment.requireActivity() ) }, { try { FileUtils.copyFile( file, generateUniqueFileName( file.nameWithoutExtension + "_copy." + file.extension, file.parentFile!! ), file.parentFile!!, true ) } catch (e: Exception) { e.printStackTrace() showMsg(e.toString()) } }) { backgroundTask.dismiss() showMsg("New backup file has been created") parentFragment.refresh() } backgroundTask.run() } private fun generateUniqueFileName(name: String, directory: File): String { var file = File(directory, name) var i = 2 while (file.exists()) { file = File(directory, name) val newName = file.nameWithoutExtension + i + "." + file.extension file = File(directory, newName) i++ } return file.name } @SuppressLint("SetTextI18n") private fun compressFiles(task: CompressTask) { val customDialog = CustomDialog() val input = customDialog.createInput(parentFragment.requireActivity(), "Archive name") input.editText?.setText(".zip") FileUtils.setFileValidator(input, (parentFragment).currentDirectory!!) CustomDialog() .setTitle("Compress") .addView(input) .setPositiveButton("Save", { if (input.error == null) { if (task.isValid) { task.setActiveDirectory( File( parentFragment.currentDirectory, input.editText!! .text.toString() ) ) val view = progressView val progressText = view.findViewById(R.id.msg) progressText.text = "Processing..." val dialog = dialog.setView(view).show() task.start( object : OnUpdateListener { override fun onUpdate(progress: String) { progressText.text = progress } }, object : OnFinishListener { override fun onFinish(result: String) { showMsg(result) parentFragment.refresh() dialog.dismiss() } }) } else { showMsg("The files to compress are missing") } } else { showMsg("Compress canceled") } }, true) .show(parentFragment.childFragmentManager, "") } private fun openWithTextEditor(file: File) { val intent = Intent() intent.setClass(parentFragment.requireActivity(), TextEditorActivity::class.java) intent.putExtra("file", file.absolutePath) parentFragment.requireActivity().startActivity(intent) } private fun notifyNewTask() { showMsg("A new task has been added") } private fun showFileInfoDialog(file: File) { FileInfoDialog(file).setUseDefaultFileInfo(true) .show(parentFragment.parentFragmentManager, "") } @get:SuppressLint("InflateParams") private val progressView: View get() = parentFragment.requireActivity().layoutInflater.inflate( R.layout.progress_view, null ) private val dialog: AlertDialog.Builder get() = MaterialAlertDialogBuilder(parentFragment.requireActivity()) .setCancelable(false) private fun showRenameDialog(selectedFiles: ArrayList) { val customDialog = CustomDialog() val input = customDialog.createInput(parentFragment.requireActivity(), "File name") input.editText?.setText(selectedFiles[0].name) input.editText!!.setSingleLine() FileUtils.setFileValidator(input, selectedFiles[0], selectedFiles[0].parentFile) customDialog.setTitle("Rename") .addView(input) .setPositiveButton("Save", { if (input.error == null) { if (!FileUtils.rename( selectedFiles[0], input.editText!! .text.toString() ) ) { showMsg("Cannot rename this file") } else { showMsg("File has been renamed") parentFragment.refresh() } } else { showMsg("Rename canceled") } }, true) .show(parentFragment.parentFragmentManager, "") } private fun confirmDeletion(selectedFiles: ArrayList) { MaterialAlertDialogBuilder(parentFragment.requireContext()) .setTitle("Delete") .setMessage("Do you want to delete selected files? this action cannot be redone.") .setPositiveButton("Confirm") { _, _ -> doDelete(selectedFiles) } .setNegativeButton("Cancel", null) .show() } private fun doDelete(selectedFiles: ArrayList) { parentFragment.setSelectAll(false) var errorMsg = "" val backgroundTask = BackgroundTask() backgroundTask.setTasks({ backgroundTask.showProgressDialog( "Deleting files...", parentFragment.requireActivity() ) }, { try { FileUtils.deleteFiles(selectedFiles) } catch (e: Exception) { e.printStackTrace() errorMsg = "Something went wrong, check logs for more details" } }) { backgroundTask.dismiss() showMsg(if(errorMsg.isEmpty()) "Files have been deleted" else errorMsg) parentFragment.refresh() } backgroundTask.run() } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/file/task/CompressTask.kt ================================================ package com.raival.fileexplorer.tab.file.task import com.raival.fileexplorer.tab.file.misc.archive import com.raival.fileexplorer.tab.file.model.Task import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File class CompressTask(private val filesToCompress: ArrayList) : Task() { private var zipFile: File? = null override val name: String get() = "Compress" override val details: String get() { val sb = StringBuilder() var first = true for (file in filesToCompress) { if (!first) { sb.append(", ") } sb.append(file.name) first = false } return sb.toString() } override val isValid: Boolean get() { for (file in filesToCompress) { if (!file.exists()) return false } return true } override fun setActiveDirectory(directory: File) { zipFile = directory } override fun start(onUpdate: OnUpdateListener, onFinish: OnFinishListener) { CoroutineScope(Dispatchers.IO).launch { withContext(Dispatchers.Main) { onUpdate.onUpdate("Compressing....") } try { archive(filesToCompress, zipFile) withContext(Dispatchers.Main) { onFinish.onFinish("Files have been compressed successfully") } } catch (e: Exception) { e.printStackTrace() withContext(Dispatchers.Main) { onFinish.onFinish(e.toString()) } } } } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/file/task/CopyTask.kt ================================================ package com.raival.fileexplorer.tab.file.task import com.raival.fileexplorer.tab.file.misc.FileUtils import com.raival.fileexplorer.tab.file.model.Task import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File class CopyTask(private val filesToCopy: ArrayList) : Task() { private var activeDirectory: File? = null override val name: String get() = "Copy" override val details: String get() { val sb = StringBuilder() var first = true for (file in filesToCopy) { if (!first) { sb.append(", ") } sb.append(file.name) first = false } return sb.toString() } override val isValid: Boolean get() { for (file in filesToCopy) { if (!file.exists()) return false } return true } override fun setActiveDirectory(directory: File) { activeDirectory = directory } override fun start(onUpdate: OnUpdateListener, onFinish: OnFinishListener) { CoroutineScope(Dispatchers.IO).launch { var error = false try { var progress = 1 for (file in filesToCopy) { try { val finalProgress = progress withContext(Dispatchers.Main) { onUpdate.onUpdate( "[" + finalProgress + "/" + filesToCopy.size + "]" + "Copying " + file.name ) } FileUtils.copy(file, activeDirectory!!, true) } catch (exception: Exception) { error = true } ++progress } val finalError = error withContext(Dispatchers.Main) { onFinish.onFinish(if (finalError) "An error occurred, some files didn't get copied" else "Files copied successfully") } } catch (e: Exception) { e.printStackTrace() withContext(Dispatchers.Main) { onFinish.onFinish(e.toString()) } } } } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/file/task/CutTask.kt ================================================ package com.raival.fileexplorer.tab.file.task import com.raival.fileexplorer.tab.file.misc.FileUtils import com.raival.fileexplorer.tab.file.model.Task import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File class CutTask(private val filesToCut: ArrayList) : Task() { private var activeDirectory: File? = null override val name: String get() = "Cut" override val details: String get() { val sb = StringBuilder() var first = true for (file in filesToCut) { if (!first) { sb.append(", ") } sb.append(file.name) first = false } return sb.toString() } override val isValid: Boolean get() { for (file in filesToCut) { if (!file.exists()) return false } return true } override fun setActiveDirectory(directory: File) { activeDirectory = directory } override fun start(onUpdate: OnUpdateListener, onFinish: OnFinishListener) { CoroutineScope(Dispatchers.IO).launch { var error = false try { var progress = 1 for (file in filesToCut) { try { val finalProgress = progress withContext(Dispatchers.Main) { onUpdate.onUpdate( "[" + finalProgress + "/" + filesToCut.size + "]" + "Moving " + file.name ) } FileUtils.move(file, activeDirectory) } catch (exception: Exception) { error = true } ++progress } val finalError = error withContext(Dispatchers.Main) { onFinish.onFinish(if (finalError) "An error occurred, some files haven't been moved" else "Files have been moved successfully") } } catch (e: Exception) { e.printStackTrace() withContext(Dispatchers.Main) { onFinish.onFinish(e.toString()) } } } } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/tab/file/task/ExtractTask.kt ================================================ package com.raival.fileexplorer.tab.file.task import android.os.Handler import android.os.Looper import com.raival.fileexplorer.tab.file.misc.extract import com.raival.fileexplorer.tab.file.model.Task import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File class ExtractTask(private val filesToExtract: ArrayList) : Task() { private var activeDirectory: File? = null override val name: String get() = "Extract" override val details: String get() { val sb = StringBuilder() var first = true for (file in filesToExtract) { if (!first) { sb.append(", ") } sb.append(file.name) first = false } return sb.toString() } override val isValid: Boolean get() { for (file in filesToExtract) { if (!file.exists()) return false } return true } override fun setActiveDirectory(directory: File) { activeDirectory = directory } override fun start(onUpdate: OnUpdateListener, onFinish: OnFinishListener) { CoroutineScope(Dispatchers.IO).launch { Handler(Looper.getMainLooper()).post { onUpdate.onUpdate("Extracting....") } try { extract(filesToExtract, activeDirectory!!) withContext(Dispatchers.Main) { onFinish.onFinish("Files have been extracted successfully") } } catch (e: Exception) { e.printStackTrace() withContext(Dispatchers.Main) { onFinish.onFinish(e.toString()) } } }.start() } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/util/Log.kt ================================================ package com.raival.fileexplorer.util import android.annotation.SuppressLint import android.content.Context import android.util.Log import com.raival.fileexplorer.activity.SettingsActivity import com.raival.fileexplorer.extension.surroundWithBrackets import com.raival.fileexplorer.tab.file.misc.FileUtils import java.io.* import java.text.SimpleDateFormat object Log { private const val TAG = "CustomLog" // This is just a common phrase used in many classes const val UNABLE_TO = "Unable to" const val SOMETHING_WENT_WRONG = "Something went wrong while" private var logFile: File? = null fun start(context: Context) { logFile = File(context.getExternalFilesDir(null)!!.absolutePath + "/debug/log.txt") } fun e(tag: String?, e: Exception?) { e(tag, "", e) } @JvmOverloads fun e(tag: String?, msg: String, throwable: Throwable? = null) { write(tag, "Error", msg, throwable) } fun w(tag: String?, e: Exception?) { w(tag, "", e) } @JvmOverloads fun w(tag: String?, msg: String, throwable: Throwable? = null) { write(tag, "Warning", msg, throwable) } fun d(tag: String?, e: Exception?) { d(tag, "", e) } @JvmOverloads fun d(tag: String?, msg: String, throwable: Throwable? = null) { write(tag, "Warning", msg, throwable) } fun i(tag: String?, e: Exception?) { i(tag, "", e) } @JvmOverloads fun i(tag: String?, msg: String, throwable: Throwable? = null) { write(tag, "Info", msg, throwable) } @get:SuppressLint("SimpleDateFormat") private val currentTime: String get() = SimpleDateFormat("MM/dd/yyyy HH:mm:ss.SSS").format(System.currentTimeMillis()) fun getStackTrace(throwable: Throwable): String { val result: Writer = StringWriter() val printWriter = PrintWriter(result) throwable.printStackTrace(printWriter) val stacktraceAsString = result.toString() printWriter.close() return stacktraceAsString } private fun write(tag: String?, priority: String, msg: String, throwable: Throwable?) { if (!checkPrefs(priority)) return if (logFile == null) return if (!logFile!!.parentFile?.exists()!! && !logFile!!.parentFile?.mkdirs()!!) { Log.e(TAG, UNABLE_TO + " " + FileUtils.CREATE_FILE + ": " + logFile!!.parentFile) return } try { if (!logFile!!.exists() && !logFile!!.createNewFile()) { Log.e(TAG, UNABLE_TO + " " + FileUtils.CREATE_FILE + ": " + logFile) return } val logToWrite = StringBuilder() if (logFile!!.length() > 0) logToWrite.append(System.lineSeparator()) logToWrite.append(currentTime.surroundWithBrackets()) .append(priority.surroundWithBrackets()) .append(tag!!.surroundWithBrackets()) .append(":").append(" ") if (msg.isNotEmpty()) { logToWrite.append(msg) } if (throwable != null) { logToWrite.append(System.lineSeparator()).append(getStackTrace(throwable)) } val fileWriter = FileWriter(logFile, true) fileWriter.write(logToWrite.toString()) fileWriter.flush() fileWriter.close() } catch (e: Exception) { Log.e(TAG, UNABLE_TO + " write to log file" + System.lineSeparator() + e) Log.e(tag, msg, throwable) } } private fun checkPrefs(tag: String): Boolean { return if (PrefsUtils.Settings.logMode == SettingsActivity.LOG_MODE_ALL) true else PrefsUtils.Settings.logMode == SettingsActivity.LOG_MODE_ERRORS_ONLY && tag == "Error" } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/util/PrefsUtils.kt ================================================ package com.raival.fileexplorer.util import com.google.gson.Gson import com.pixplicity.easyprefs.library.Prefs import com.raival.fileexplorer.activity.SettingsActivity import com.raival.fileexplorer.extension.getStringList object PrefsUtils { const val SORT_NAME_A2Z = 1 const val SORT_NAME_Z2A = 2 const val SORT_SIZE_SMALLER = 3 const val SORT_SIZE_BIGGER = 4 const val SORT_DATE_OLDER = 5 const val SORT_DATE_NEWER = 6 object TextEditor { var textEditorWordwrap: Boolean get() = Prefs.getBoolean("text_editor_wordwrap", false) set(wordwrap) { Prefs.putBoolean("text_editor_wordwrap", wordwrap) } var textEditorShowLineNumber: Boolean get() = Prefs.getBoolean("text_editor_show_line_number", true) set(showLineNumber) { Prefs.putBoolean("text_editor_show_line_number", showLineNumber) } var textEditorPinLineNumber: Boolean get() = Prefs.getBoolean("text_editor_pin_line_number", true) set(pinLineNumber) { Prefs.putBoolean("text_editor_pin_line_number", pinLineNumber) } var textEditorMagnifier: Boolean get() = Prefs.getBoolean("text_editor_magnifier", true) set(magnifier) { Prefs.putBoolean("text_editor_magnifier", magnifier) } var textEditorReadOnly: Boolean get() = Prefs.getBoolean("text_editor_read_only", false) set(readOnly) { Prefs.putBoolean("text_editor_read_only", readOnly) } var textEditorAutocomplete: Boolean get() = Prefs.getBoolean("text_editor_autocomplete", false) set(autocomplete) { Prefs.putBoolean("text_editor_autocomplete", autocomplete) } val fileExplorerTabBookmarks: ArrayList get() = Prefs.getString("file_explorer_tab_bookmarks", "[]").getStringList() } object FileExplorerTab { @JvmStatic var sortingMethod: Int get() = Prefs.getInt("sorting_method", SORT_NAME_A2Z) set(method) { Prefs.putInt("sorting_method", method) } fun setListFoldersFirst(b: Boolean) { Prefs.putBoolean("list_folders_first", b) } @JvmStatic fun listFoldersFirst(): Boolean { return Prefs.getBoolean("list_folders_first", true) } } object Settings { var deepSearchFileSizeLimit: Long get() = Prefs.getLong( "settings_deep_search_file_size_limit", (6 * 1024 * 1024).toLong() ) set(limit) { Prefs.putLong("settings_deep_search_file_size_limit", limit) } @JvmStatic var themeMode: String get() = Prefs.getString("settings_theme_mode", SettingsActivity.THEME_MODE_AUTO) set(themeMode) { Prefs.putString("settings_theme_mode", themeMode) } var logMode: String get() = Prefs.getString("settings_log_mode", SettingsActivity.LOG_MODE_ERRORS_ONLY) set(logMode) { Prefs.putString("settings_log_mode", logMode) } var showBottomToolbarLabels: Boolean get() = Prefs.getBoolean("settings_show_bottom_toolbar_labels", true) set(showBottomToolbarLabels) { Prefs.putBoolean("settings_show_bottom_toolbar_labels", showBottomToolbarLabels) } } object General { fun setFileExplorerTabBookmarks(list: ArrayList) { Prefs.putString("file_explorer_tab_bookmarks", Gson().toJson(list)) } } } ================================================ FILE: app/src/main/java/com/raival/fileexplorer/util/Utils.kt ================================================ package com.raival.fileexplorer.util import android.content.Context import android.content.res.Configuration import android.util.TypedValue import androidx.annotation.ColorInt import com.raival.fileexplorer.App import com.raival.fileexplorer.activity.SettingsActivity import com.raival.fileexplorer.util.PrefsUtils.Settings.themeMode import java.util.* object Utils { private const val ALLOWED_CHARACTERS = "0123456789qwertyuiopasdfghjklzxcvbnm_" @ColorInt fun getColorAttribute(id: Int, context: Context): Int { val out = TypedValue() context.theme.resolveAttribute(id, out, true) return out.data } val isDarkMode: Boolean get() = when (themeMode) { SettingsActivity.THEME_MODE_DARK -> { true } SettingsActivity.THEME_MODE_LIGHT -> { false } else -> { (App.appContext.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES } } fun getRandomString(sizeOfRandomString: Int): String { val random = Random() val sb = StringBuilder(sizeOfRandomString) for (i in 0 until sizeOfRandomString) { sb.append(ALLOWED_CHARACTERS[random.nextInt(ALLOWED_CHARACTERS.length)]) } return sb.toString() } } ================================================ FILE: app/src/main/res/drawable/app_icon_foreground.xml ================================================ ================================================ FILE: app/src/main/res/drawable/fastscroll_thumb.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_add_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_arrow_back_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_assignment_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_bookmark_add_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_bookmark_remove_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_bug_report_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_chevron_right_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_delete_sweep_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_file_copy_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_folder_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_folder_open_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_info_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_layers_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_logout_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_more_vert_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_open_in_browser_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_open_in_new_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_restart_alt_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_save_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_select_all_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_baseline_sort_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launcher_foreground.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_round_code_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_round_compress_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_round_content_cut_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_round_delete_forever_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_round_edit_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_round_edit_note_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_round_exit_to_app_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_round_home_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_round_insert_drive_file_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_round_key_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_round_manage_search_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_round_menu_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_round_play_arrow_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_round_redo_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_round_search_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_round_settings_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_round_share_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_round_tab_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_round_timelapse_24.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_round_undo_24.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main_drawer.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main_drawer_bookmark_item.xml ================================================ ================================================ FILE: app/src/main/res/layout/apps_tab_app_item.xml ================================================ ================================================ FILE: app/src/main/res/layout/apps_tab_fragment.xml ================================================ ================================================ FILE: app/src/main/res/layout/bottom_bar_menu_item.xml ================================================ ================================================ FILE: app/src/main/res/layout/common_custom_dialog.xml ================================================ ================================================ FILE: app/src/main/res/layout/common_options_dialog.xml ================================================ ================================================ FILE: app/src/main/res/layout/common_options_dialog_item.xml ================================================ ================================================ FILE: app/src/main/res/layout/file_explorer_tab_file_item.xml ================================================ ================================================ FILE: app/src/main/res/layout/file_explorer_tab_fragment.xml ================================================ ================================================ FILE: app/src/main/res/layout/file_explorer_tab_info_dialog.xml ================================================ ================================================ FILE: app/src/main/res/layout/file_explorer_tab_info_dialog_item.xml ================================================ ================================================ FILE: app/src/main/res/layout/file_explorer_tab_path_history_view.xml ================================================ ================================================ FILE: app/src/main/res/layout/file_explorer_tab_placeholder.xml ================================================ ================================================ FILE: app/src/main/res/layout/file_explorer_tab_task_dialog.xml ================================================ ================================================ FILE: app/src/main/res/layout/file_explorer_tab_task_dialog_item.xml ================================================ ================================================ FILE: app/src/main/res/layout/input.xml ================================================ ================================================ FILE: app/src/main/res/layout/progress_view.xml ================================================ ================================================ FILE: app/src/main/res/layout/search_fragment.xml ================================================ ================================================ FILE: app/src/main/res/layout/settings_activity.xml ================================================ ================================================ FILE: app/src/main/res/layout/text_editor_activity.xml ================================================ ================================================ FILE: app/src/main/res/layout/text_editor_completion_item.xml ================================================ ================================================ FILE: app/src/main/res/menu/main_menu.xml ================================================ ================================================ FILE: app/src/main/res/menu/tab_menu.xml ================================================ ================================================ FILE: app/src/main/res/menu/text_editor_menu.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi-v26/app_icon.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi-v26/app_icon_round.xml ================================================ ================================================ FILE: app/src/main/res/values/app_icon_background.xml ================================================ #262A30 ================================================ FILE: app/src/main/res/values/colors.xml ================================================ #2F0066FF #0A000000 ================================================ FILE: app/src/main/res/values/ic_launcher_background.xml ================================================ #1B2221 ================================================ FILE: app/src/main/res/values/strings.xml ================================================ File Explorer ================================================ FILE: app/src/main/res/values/themes.xml ================================================ ================================================ FILE: app/src/main/res/values-night/colors.xml ================================================ #2F0066FF #0FFDFDFD ================================================ FILE: app/src/main/res/values-night/themes.xml ================================================ ================================================ FILE: app/src/main/res/xml/provider_paths.xml ================================================ ================================================ FILE: build.gradle ================================================ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { ext.kotlin_version = '1.7.20-RC' repositories { google() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:7.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } task clean(type: Delete) { delete rootProject.buildDir } ================================================ FILE: fastlane/metadata/android/en-US/full_description.txt ================================================ File Explorer is a full-featured and lightweight file manager with Material 3 Dynamic colors.
Features: - All basic file management functionality (e.g. copy, paste,.. etc) are supported. - Support for multiple tabs, and Tasks which make managing files much easier. - Powerful Code Editor (Sora Editor). - Deep search that allows you to search in files contents. ================================================ FILE: fastlane/metadata/android/en-US/short_description.txt ================================================ a full-featured file manager for android ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Thu Jul 14 16:17:27 UTC 2022 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app"s APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true # Automatically convert third-party libraries to use AndroidX android.enableJetifier=true ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "$( uname )" in #( CYGWIN* ) cygwin=true ;; #( Darwin* ) darwin=true ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD=$JAVA_HOME/jre/sh/java else JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Collect all arguments for the java command; # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # shell script including quotes and variable substitutions, so put them in # double quotes to make sure that they get re-expanded; and # * put everything else in single quotes, so that it's not re-expanded. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ org.gradle.wrapper.GradleWrapperMain \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: settings.gradle ================================================ dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() jcenter() // Warning: this repository is going to shut down soon } } rootProject.name = "File Explorer" include ':app'