├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── android.yml │ ├── codeql-analysis.yml │ ├── greetings.yml │ └── stale.yml ├── .gitignore ├── .gitmodules ├── .project ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── app ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ └── cp │ │ └── .gitignore │ ├── cpp │ ├── CMakeLists.txt │ └── app.cpp │ ├── ic_launcher-playstore.png │ ├── java │ └── org │ │ └── andbootmgr │ │ └── app │ │ ├── BackupRestoreFlow.kt │ │ ├── CreatePartFlow.kt │ │ ├── DeviceInfo.kt │ │ ├── DeviceLogic.kt │ │ ├── DroidBootFlow.kt │ │ ├── FixDroidBootFlow.kt │ │ ├── MainActivity.kt │ │ ├── Settings.kt │ │ ├── Start.kt │ │ ├── UpdateDroidBootFlow.kt │ │ ├── UpdateFlow.kt │ │ ├── Wizard.kt │ │ ├── ext.kt │ │ ├── themes │ │ ├── Simulator.kt │ │ └── Themes.kt │ │ └── util │ │ ├── AbmOkHttp.kt │ │ ├── ConfigFile.kt │ │ ├── SDLessUtils.kt │ │ ├── SDUtils.kt │ │ ├── SOUtils.java │ │ ├── StayAliveService.kt │ │ ├── Terminal.kt │ │ ├── Theme.kt │ │ └── Toolkit.kt │ └── res │ ├── drawable-anydpi │ ├── ic_about.xml │ └── ic_droidbooticon.xml │ ├── drawable │ ├── abm_notif.xml │ ├── ic_baseline_check_circle_24.xml │ ├── ic_baseline_error_24.xml │ ├── ic_baseline_keyboard_arrow_down_24.xml │ ├── ic_baseline_keyboard_arrow_up_24.xml │ ├── ic_launcher_foreground.xml │ ├── ic_roms.xml │ ├── ic_sailfish_os_logo.xml │ ├── ic_sd.xml │ ├── ic_settings.xml │ └── ut_logo.xml │ ├── mipmap-anydpi │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── values-ar │ └── strings.xml │ ├── values-de │ └── strings.xml │ ├── values-in │ └── strings.xml │ ├── values-ja │ └── strings.xml │ ├── values-ko │ └── strings.xml │ ├── values-pl │ └── strings.xml │ ├── values-pt-rBR │ └── strings.xml │ ├── values-pt │ └── strings.xml │ ├── values-ru │ └── strings.xml │ ├── values-tr │ └── strings.xml │ ├── values-uk │ └── strings.xml │ ├── values │ ├── dimens.xml │ ├── ic_launcher_background.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ └── data_extraction_rules.xml ├── build.gradle.kts ├── buildutils ├── clone-submodules.sh ├── screenshot │ ├── abm1.png │ ├── abm2.png │ ├── abm3.png │ ├── abm4.png │ ├── abm5.png │ └── abm6.png └── update-submodules.sh ├── crowdin.yml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── web_hi_res_512.png /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug, not checked 6 | assignees: luka177, nift4 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Smartphone (please complete the following information):** 27 | - Device: [e.g. iPhone6] 28 | - OS: [e.g. iOS8.1] 29 | - Recovery [e.g. stock browser, safari] 30 | - App Version [e.g. 22] 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement, not checked 6 | assignees: luka177, nift4 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout git repository 16 | uses: actions/checkout@v2 17 | - name: Silent git warning by configuring pull strategy 18 | run: git config --global pull.ff only 19 | - name: Download submodules 20 | run: ./buildutils/clone-submodules.sh 21 | - name: Update submodules 22 | run: ./buildutils/update-submodules.sh 23 | - name: set up JDK 17 24 | uses: actions/setup-java@v1 25 | with: 26 | java-version: 17 27 | - name: Extract signing keystore 28 | run: echo "${{ secrets.SIGNING_KEY }}" | base64 -d > ~/abm.keystore 29 | - name: Set signing configuration 30 | run: mkdir -p ~/.gradle && echo "${{ secrets.KEY_SETTINGS }}" | base64 -d > ~/.gradle/gradle.properties 31 | - name: Build with Gradle 32 | run: ./gradlew build 33 | - name: Upload apks (1/2) 34 | uses: actions/upload-artifact@v4 35 | with: 36 | name: other_apks 37 | path: app/build/outputs/apk/*/*.apk 38 | - name: Upload apks (2/2) 39 | uses: actions/upload-artifact@v4 40 | with: 41 | name: app-release.apk 42 | path: app/build/outputs/apk/release/app-release.apk 43 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '31 11 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'java' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/first-interaction@v1 10 | with: 11 | repo-token: ${{ secrets.GITHUB_TOKEN }} 12 | issue-message: 'Hello, and thanks for contributing at Android Boot Manager! As this is your first issue, we give you this message :) Please do not forget to make sure everything important is in the issue. Thanks!' 13 | pr-message: 'Hello, and thanks for your first PR! We love to see that you help to develop Android Boot Manager! :)' 14 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/stale@v1 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | stale-issue-message: 'Hi! This issue had no activity for some time. Is this a long-standing issue or is the user simply not replying anymore? To make sure our issue tracker is not spammed we close issues without activity after some time. If this issue should not be closed, please comment something! Thanks.' 17 | stale-pr-message: 'Hi! This PR is open for some time but not merged yet. If there are no comments soon, it will be closed! Please comment if this PR is still useful.' 18 | stale-issue-label: 'no-issue-activity' 19 | stale-pr-label: 'no-pr-activity' 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | app/build 3 | app/.cxx 4 | .gradle/* 5 | .kotlin/* 6 | local.properties 7 | # User-specific stuff 8 | .idea 9 | *.iml 10 | *.ipr 11 | .DS_Store 12 | # CMake 13 | cmake-build-*/ 14 | 15 | # Mongo Explorer plugin 16 | .idea/**/mongoSettings.xml 17 | 18 | # File-based project format 19 | *.iws 20 | 21 | # IntelliJ 22 | out/ 23 | 24 | # mpeltonen/sbt-idea plugin 25 | .idea_modules/ 26 | 27 | # JIRA plugin 28 | atlassian-ide-plugin.xml 29 | 30 | # Cursive Clojure plugin 31 | .idea/replstate.xml 32 | 33 | # Crashlytics plugin (for Android Studio and IntelliJ) 34 | com_crashlytics_export_strings.xml 35 | crashlytics.properties 36 | crashlytics-build.properties 37 | fabric.properties 38 | 39 | # Editor-based Rest Client 40 | .idea/httpRequests 41 | 42 | # Android studio 3.1+ serialized cache file 43 | .idea/caches/build_file_checksums.ser 44 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "app/src/main/assets/Toolkit"] 2 | path = app/src/main/assets/Toolkit 3 | url = https://github.com/Android-Boot-Manager/Toolkit 4 | branch = master 5 | [submodule "app/src/main/cpp/droidboot_gui"] 6 | path = app/src/main/cpp/droidboot_gui 7 | url = https://github.com/Android-Boot-Manager/droidboot_gui 8 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | App 4 | Android Boot Manager 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at nift4@protonmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please be nice to each other and follow our [Code of Conduct](https://github.com/Android-Boot-Manager/App/blob/master/CODE_OF_CONDUCT.md). Thanks :) 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Android Boot Manager logo 4 | 5 |
Android Boot Manager
6 |

7 | 8 |

9 | 10 | 11 | CI Status 13 | 14 |

15 | 16 |

**Multi-boot for smartphones!**

17 | 18 | ## What is it? 19 | 20 | Android Boot Manager is an Android app and a couple of bootloader modifications, which are all based 21 | on a library called droidboot_gui. Additionally, there are scripts that automate patching operating 22 | systems created by third parties to work in the multi-boot environment. 23 | 24 | The Android app is the configuration frontend which oversees installing and updating operating 25 | systems, partitioning the storage and installing and configuring the bootloader modifications. 26 | 27 | The bootloader modifications are responsible for showing a graphical selection UI at boot time that 28 | lets you select the operating system you intend to start. There are three types of supported 29 | bootloaders: 30 | 1. First-stage bootloader modifications, which means you modify/replace the bootloader that would 31 | usually start the Linux kernel. We prefer this. 32 | 2. Creating or porting a second-stage bootloader, which will be loaded instead of the kernel and 33 | later loading the kernel from the second-stage bootloader. This is also good. 34 | 3. The "no-bootloader workaround ramdisk", which runs inside the kernel and flashes the boot the 35 | user wants to boot, then reboots the device. This is really just a workaround. 36 | 37 | (A solution akin to MultiROM's kexec-hardboot would also be possible, however, we currently aren't 38 | pursuing this.) 39 | 40 | The scripts and device configuration files for the App are stored in the 41 | [Scripts](https://github.com/Android-Boot-Manager/Scripts) and 42 | [ABM-json](https://github.com/Android-Boot-Manager/ABM-json/tree/master/devices) repositories. 43 | These are intended to be very flexible and support many devices without changing the App code, 44 | at least not unless we release a major new feature. The operating systems are usually created by 45 | other communities and we only modify them to work in a multi-boot setup. Right now, Halium-based 46 | operating systems like [UBports' Ubuntu Touch](https://ubuntu-touch.io/) or 47 | [Droidian](https://droidian.org/) are supported best. We also support 48 | [SailfishOS](https://sailfishos.org/), and we are working on installing multiple Android systems as 49 | well. 50 | 51 | ## What devices are supported? 52 | 53 | 1. First-stage bootloaders 54 | - MediaTek lk 55 | - [Volla Phone (yggdrasil)](https://github.com/Android-Boot-Manager/droidboot_device_volla_yggdrasil) - MT6763 56 | - Volla Phone X (yggdrasilx) - MT6763 57 | - [Volla Phone 22 (mimameid)](https://github.com/Android-Boot-Manager/droidboot_device_volla_mimameid) - MT6768 58 | - [Volla Phone X23 (vidofnir)](https://github.com/Android-Boot-Manager/droidboot_device_gigaset_gx4) - MT6789 59 | - [U-Boot](https://github.com/Android-Boot-Manager/droidboot_device_generic_u-boot) 60 | - Qualcomm ABL EFI LinuxLoader 61 | - [F(x)tec Pro1](https://github.com/Android-Boot-Manager/droidboot_device_fxtec_pro1) - not ported to 0.3 yet 62 | 2. Second-stage bootloaders 63 | - [Project Renegade's EDKII for MSM](https://github.com/Android-Boot-Manager/droidboot_device_renegade-uefi) 64 | - [lk2nd](https://github.com/Android-Boot-Manager/droidboot_device_qcom_lk2nd) 65 | 66 | ## How do I install it? 67 | You need a rooted and supported device to get started. The detailed instructions can be found on our 68 | [MediaWiki instance](https://wiki.andbootmgr.org). 69 | 70 | ## Why? 71 | Why not? 72 | 73 | ## Screenshots 74 | | ![Screenshot 1](buildutils/screenshot/abm1.png) | ![Screenshot 2](buildutils/screenshot/abm2.png) | ![Screenshot 3](buildutils/screenshot/abm3.png) | 75 | |-------------------------------------------------|-------------------------------------------------|-------------------------------------------------| 76 | | ![Screenshot 4](buildutils/screenshot/abm4.png) | ![Screenshot 6](buildutils/screenshot/abm5.png) | ![Screenshot 6](buildutils/screenshot/abm6.png) | 77 | 78 | ## Credits 79 | - [M1cha](https://github.com/M1cha) for efidroid, arguably the best approach for multi-booting 80 | - [Mis012](https://github.com/Mis012) for re-boot2, which we borrowed some code from 81 | - [msm8916-mainline](https://github.com/msm8916-mainline) and [msm8953-mainline](https://github.com/msm8953-mainline) for lk2nd 82 | - [Renegade Project](https://github.com/edk2-porting) for edk2-msm 83 | - [U-Boot team](https://u-boot.org) 84 | - [Volla](https://volla.online) 85 | - [BigfootACA](https://github.com/BigfootACA) for SimpleInit 86 | - [calebccff](https://github.com/calebccff) 87 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 0.0.x | :white_check_mark: | 8 | | Alphas | :x: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | Use GitHub issues for security issues that cannot leak data (for example crashing the system or making the phone reboot) but for anything else, please email nift4@protonmail.com 13 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("org.jetbrains.kotlin.android") 4 | id("org.jetbrains.kotlin.plugin.compose") 5 | id("com.mikepenz.aboutlibraries.plugin") 6 | } 7 | 8 | android { 9 | namespace = "org.andbootmgr.app" 10 | compileSdk = 34 11 | buildFeatures { 12 | buildConfig = true 13 | compose = true 14 | } 15 | packaging { 16 | dex { 17 | useLegacyPackaging = false 18 | } 19 | jniLibs { 20 | useLegacyPackaging = false 21 | } 22 | resources { 23 | excludes += "META-INF/*.version" 24 | } 25 | } 26 | defaultConfig { 27 | applicationId = "org.andbootmgr.app" 28 | minSdk = 26 29 | //noinspection ExpiredTargetSdkVersion 30 | targetSdk = 32 31 | versionCode = 3001 32 | versionName = "0.3.0-m0" 33 | vectorDrawables { 34 | useSupportLibrary = true 35 | } 36 | externalNativeBuild { 37 | cmake { 38 | arguments += "-DANDROID_STL=c++_shared" 39 | } 40 | } 41 | } 42 | signingConfigs { 43 | register("release") { 44 | if (project.hasProperty("ABM_RELEASE_KEY_ALIAS")) { 45 | storeFile = file(project.properties["ABM_RELEASE_STORE_FILE"].toString()) 46 | storePassword = project.properties["ABM_RELEASE_STORE_PASSWORD"].toString() 47 | keyAlias = project.properties["ABM_RELEASE_KEY_ALIAS"].toString() 48 | keyPassword = project.properties["ABM_RELEASE_KEY_PASSWORD"].toString() 49 | } 50 | } 51 | getByName("debug") { 52 | if (project.hasProperty("ABM_DEBUG_KEY_ALIAS")) { 53 | storeFile = file(project.properties["ABM_DEBUG_STORE_FILE"].toString()) 54 | storePassword = project.properties["ABM_DEBUG_STORE_PASSWORD"].toString() 55 | keyAlias = project.properties["ABM_DEBUG_KEY_ALIAS"].toString() 56 | keyPassword = project.properties["ABM_DEBUG_KEY_PASSWORD"].toString() 57 | } 58 | } 59 | } 60 | buildTypes { 61 | release { 62 | isMinifyEnabled = true 63 | proguardFiles( 64 | getDefaultProguardFile("proguard-android-optimize.txt"), 65 | "proguard-rules.pro", 66 | ) 67 | if (project.hasProperty("ABM_RELEASE_KEY_ALIAS")) { 68 | signingConfig = signingConfigs["release"] 69 | } 70 | buildConfigField("boolean", "DEFAULT_NOOB_MODE", "false") // Noob mode default 71 | } 72 | debug { 73 | if (project.hasProperty("ABM_DEBUG_KEY_ALIAS")) { 74 | signingConfig = signingConfigs["debug"] 75 | } else if (project.hasProperty("ABM_RELEASE_KEY_ALIAS")) { 76 | signingConfig = signingConfigs["release"] 77 | } 78 | buildConfigField("boolean", "DEFAULT_NOOB_MODE", "false") // Noob mode default 79 | } 80 | } 81 | java { 82 | compileOptions { 83 | toolchain { 84 | languageVersion = JavaLanguageVersion.of(17) 85 | } 86 | } 87 | } 88 | 89 | kotlin { 90 | jvmToolchain(17) 91 | compilerOptions { 92 | freeCompilerArgs = listOf( 93 | "-Xno-param-assertions", 94 | "-Xno-call-assertions", 95 | "-Xno-receiver-assertions" 96 | ) 97 | } 98 | } 99 | composeOptions { 100 | kotlinCompilerExtensionVersion = "1.1.1" 101 | } 102 | externalNativeBuild { 103 | cmake { 104 | path = file("src/main/cpp/CMakeLists.txt") 105 | version = "3.22.1" 106 | } 107 | } 108 | applicationVariants.configureEach { 109 | tasks["merge${name.replaceFirstChar(Char::titlecase)}Assets"].dependsOn(tasks["setAssetTs"]) 110 | } 111 | } 112 | 113 | tasks.register("setAssetTs", Task::class) { 114 | doLast { 115 | File("$rootDir/app/src/main/assets/cp/_ts").writeText((System.currentTimeMillis() / 1000L).toString()) 116 | } 117 | } 118 | 119 | dependencies { 120 | implementation("androidx.core:core-splashscreen:1.0.1") 121 | implementation("androidx.appcompat:appcompat:1.7.0") 122 | implementation("androidx.legacy:legacy-support-v4:1.0.0") 123 | implementation("com.google.android.material:material:1.12.0") 124 | implementation("androidx.cardview:cardview:1.0.0") 125 | implementation("androidx.constraintlayout:constraintlayout:2.1.4") 126 | implementation("androidx.navigation:navigation-fragment-ktx:2.7.7") 127 | implementation("androidx.navigation:navigation-ui-ktx:2.7.7") 128 | implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") 129 | implementation("androidx.recyclerview:recyclerview:1.3.2") 130 | implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.4") 131 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4") 132 | implementation("androidx.compose.ui:ui:1.6.8") 133 | // Tooling support (Previews, etc.) 134 | implementation("androidx.compose.ui:ui-tooling:1.6.8") 135 | // Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.) 136 | implementation("androidx.compose.foundation:foundation:1.6.8") 137 | // Material Design 138 | implementation("androidx.compose.material3:material3:1.2.1") 139 | // Material design icons 140 | implementation("androidx.compose.material:material-icons-core:1.6.8") 141 | implementation("androidx.compose.material:material-icons-extended:1.6.8") 142 | // Integration with activities 143 | implementation("androidx.activity:activity-compose:1.9.1") 144 | // Integration with ViewModels 145 | implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4") 146 | // Integration with observables 147 | implementation("androidx.compose.runtime:runtime-livedata:1.6.8") 148 | implementation("androidx.compose.runtime:runtime-rxjava2:1.6.8") 149 | 150 | implementation("androidx.navigation:navigation-compose:2.7.7") 151 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.4") 152 | implementation("androidx.compose.ui:ui-tooling-preview:1.6.8") 153 | androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.6.8") 154 | debugImplementation("androidx.compose.ui:ui-test-manifest:1.6.8") 155 | implementation("com.github.skydoves:colorpicker-compose:1.1.2") 156 | 157 | 158 | val libsuVersion = "5.0.1" 159 | implementation("com.github.topjohnwu.libsu:core:${libsuVersion}") 160 | implementation("com.github.topjohnwu.libsu:io:${libsuVersion}") 161 | implementation("com.mikepenz:aboutlibraries:11.2.2") 162 | 163 | implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.2") 164 | 165 | } 166 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:\tools\adt-bundle-windows-x86_64-20131030\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # This is generated automatically by the Android Gradle plugin. 20 | -dontwarn org.bouncycastle.jsse.BCSSLParameters 21 | -dontwarn org.bouncycastle.jsse.BCSSLSocket 22 | -dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider 23 | -dontwarn org.conscrypt.Conscrypt$Version 24 | -dontwarn org.conscrypt.Conscrypt 25 | -dontwarn org.openjsse.javax.net.ssl.SSLParameters 26 | -dontwarn org.openjsse.javax.net.ssl.SSLSocket 27 | -dontwarn org.openjsse.net.ssl.OpenJSSE -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 23 | 27 | 29 | 30 | 31 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/assets/cp/.gitignore: -------------------------------------------------------------------------------- 1 | _ts -------------------------------------------------------------------------------- /app/src/main/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | # For more information about using CMake with Android Studio, read the 3 | # documentation: https://d.android.com/studio/projects/add-native-code.html. 4 | # For more examples on how to use CMake, see https://github.com/android/ndk-samples. 5 | 6 | # Sets the minimum CMake version required for this project. 7 | cmake_minimum_required(VERSION 3.22.1) 8 | 9 | # Declares the project name. The project name can be accessed via ${ PROJECT_NAME}, 10 | # Since this is the top level CMakeLists.txt, the project name is also accessible 11 | # with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level 12 | # build script scope). 13 | project("app") 14 | add_subdirectory(droidboot_gui) 15 | 16 | # Creates and names a library, sets it as either STATIC 17 | # or SHARED, and provides the relative paths to its source code. 18 | # You can define multiple libraries, and CMake builds them for you. 19 | # Gradle automatically packages shared libraries with your APK. 20 | # 21 | # In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define 22 | # the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME} 23 | # is preferred for the same purpose. 24 | # 25 | # In order to load a library into your app from Java/Kotlin, you must call 26 | # System.loadLibrary() and pass the name of the library defined here; 27 | # for GameActivity/NativeActivity derived applications, the same library name must be 28 | # used in the AndroidManifest.xml file. 29 | add_library(${CMAKE_PROJECT_NAME} SHARED app.cpp) 30 | 31 | # Specifies libraries CMake should link to your target library. You 32 | # can link libraries from various origins, such as libraries defined in this 33 | # build script, prebuilt third-party libraries, or Android system libraries. 34 | target_link_libraries(${CMAKE_PROJECT_NAME} droidboot_gui) 35 | -------------------------------------------------------------------------------- /app/src/main/cpp/app.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // Created by nick on 01.08.24. 3 | // 4 | 5 | #include 6 | 7 | extern "C" void simulator_start(JNIEnv* env, jobject thiz, jobject bitmap, jint w, jint h); 8 | extern "C" void simulator_stop(JNIEnv* env); 9 | extern "C" void simulator_key(jint key); 10 | 11 | extern "C" JNIEXPORT void JNICALL Java_org_andbootmgr_app_themes_Simulator_key(JNIEnv* env, jobject thiz, jint key) { 12 | simulator_key(key); 13 | } 14 | 15 | extern "C" JNIEXPORT void JNICALL Java_org_andbootmgr_app_themes_Simulator_stop(JNIEnv* env, jobject thiz) { 16 | simulator_stop(env); 17 | } 18 | 19 | extern "C" JNIEXPORT void JNICALL Java_org_andbootmgr_app_themes_Simulator_start(JNIEnv* env, jobject thiz, jobject bitmap, jint w, jint h) { 20 | simulator_start(env, thiz, bitmap, w, h); 21 | } -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Android-Boot-Manager/App/06c7e0b1a8fdac505d595fb96b11f2dfba338da7/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/org/andbootmgr/app/BackupRestoreFlow.kt: -------------------------------------------------------------------------------- 1 | package org.andbootmgr.app 2 | 3 | import android.net.Uri 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.material3.Button 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.LaunchedEffect 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.runtime.mutableStateOf 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.runtime.setValue 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.res.stringResource 18 | import androidx.compose.ui.text.style.TextAlign 19 | import com.topjohnwu.superuser.Shell 20 | import com.topjohnwu.superuser.io.SuFileInputStream 21 | import org.andbootmgr.app.util.SDUtils 22 | import java.io.File 23 | import java.io.IOException 24 | 25 | class BackupRestoreFlow(private val partitionId: Int?, private val partFile: File?): WizardFlow() { 26 | override fun get(vm: WizardState): List { 27 | val c = CreateBackupDataHolder(vm, partitionId, partFile) 28 | return listOf(WizardPage("start", 29 | NavButton(vm.activity.getString(R.string.cancel)) { it.finish() }, 30 | NavButton("") {}) 31 | { 32 | ChooseAction(c) 33 | }, WizardPage("select", 34 | NavButton(vm.activity.getString(R.string.prev)) { it.navigate("start") }, 35 | NavButton("") {} 36 | ) { 37 | SelectDroidBoot(c) 38 | }, WizardPage("go", 39 | NavButton("") {}, 40 | NavButton("") {} 41 | ) { 42 | Flash(c) 43 | }) 44 | } 45 | } 46 | 47 | private class CreateBackupDataHolder(val vm: WizardState, val pi: Int?, val partFile: File?) { 48 | var action: Int = 0 49 | var path: Uri? = null 50 | var meta: SDUtils.SDPartitionMeta? = null 51 | } 52 | 53 | @Composable 54 | private fun ChooseAction(c: CreateBackupDataHolder) { 55 | if (c.vm.deviceInfo.metaonsd) { 56 | LaunchedEffect(Unit) { 57 | c.meta = SDUtils.generateMeta(c.vm.deviceInfo.asMetaOnSdDeviceInfo()) 58 | } 59 | } 60 | 61 | Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, 62 | modifier = Modifier.fillMaxSize() 63 | ) { 64 | val name = if (c.vm.deviceInfo.metaonsd) 65 | c.meta!!.dumpKernelPartition(c.pi!!).name else c.partFile!!.name 66 | Text(stringResource(id = R.string.backup_msg, name), textAlign = TextAlign.Center) 67 | Button(onClick = { c.action=1; c.vm.navigate("select") }) { 68 | Text(stringResource(R.string.backup)) 69 | } 70 | Button(onClick = { c.action=2; c.vm.navigate("select") }) { 71 | Text(stringResource(R.string.restore)) 72 | } 73 | Button(onClick = { c.action=3; c.vm.navigate("select") }) { 74 | Text(stringResource(R.string.flash_sparse)) 75 | } 76 | } 77 | } 78 | 79 | @Composable 80 | private fun SelectDroidBoot(c: CreateBackupDataHolder) { 81 | var nextButtonAvailable by remember { mutableStateOf(false) } 82 | 83 | Column( 84 | horizontalAlignment = Alignment.CenterHorizontally, 85 | verticalArrangement = Arrangement.Center, 86 | modifier = Modifier.fillMaxSize() 87 | ) { 88 | if (nextButtonAvailable) { 89 | Text(stringResource(R.string.successfully_selected)) 90 | } else { 91 | Text( 92 | when (c.action) { 93 | 1 -> stringResource(R.string.make_backup) 94 | 2 -> stringResource(R.string.restore_backup) 95 | 3 -> stringResource(R.string.restore_backup_sparse) 96 | else -> "" 97 | } 98 | ) 99 | Button(onClick = { 100 | val name = if (c.vm.deviceInfo.metaonsd) 101 | c.meta!!.dumpKernelPartition(c.pi!!).name else c.partFile!!.nameWithoutExtension 102 | if (c.action != 1) { 103 | c.vm.activity.chooseFile("*/*") { 104 | c.vm.chosen["file"] = WizardState.DownloadedFile(it, null) 105 | nextButtonAvailable = true 106 | c.vm.nextText = c.vm.activity.getString(R.string.next) 107 | c.vm.onNext = { i -> i.navigate("go") } 108 | } 109 | } else { 110 | c.vm.activity.createFile("${name}.img") { 111 | c.path = it 112 | nextButtonAvailable = true 113 | c.vm.nextText = c.vm.activity.getString(R.string.next) 114 | c.vm.onNext = { i -> i.navigate("go") } 115 | } 116 | } 117 | }) { 118 | Text(stringResource(if (c.action != 1) R.string.choose_file else R.string.create_file)) 119 | } 120 | } 121 | } 122 | } 123 | 124 | @Composable 125 | private fun Flash(c: CreateBackupDataHolder) { 126 | WizardTerminalWork(c.vm, logFile = "flash_${System.currentTimeMillis()}.txt") { terminal -> 127 | c.vm.logic.extractToolkit(terminal) 128 | terminal.add(c.vm.activity.getString(R.string.term_starting)) 129 | val path = if (c.vm.deviceInfo.metaonsd) { 130 | val p = c.meta!!.dumpKernelPartition(c.pi!!) 131 | if (!c.vm.logic.unmount(p).to(terminal).exec().isSuccess) 132 | throw IOException(c.vm.activity.getString(R.string.term_cant_umount)) 133 | p.path 134 | } else c.partFile!!.absolutePath 135 | if (c.action == 1) { 136 | c.vm.copy( 137 | SuFileInputStream.open(path), 138 | c.vm.activity.contentResolver.openOutputStream(c.path!!)!! 139 | ) 140 | } else if (c.action == 2) { 141 | c.vm.copyPriv( 142 | c.vm.chosen["file"]!!.openInputStream(c.vm), 143 | File(path) 144 | ) 145 | } else if (c.action == 3) { 146 | val result2 = Shell.cmd( 147 | File( 148 | c.vm.logic.toolkitDir, 149 | "simg2img" 150 | ).absolutePath + " ${c.vm.chosen["file"]!!.toFile(c.vm).absolutePath} $path" 151 | ).to(terminal).exec() 152 | if (!result2.isSuccess) { 153 | terminal.add(c.vm.activity.getString(R.string.term_failure)) 154 | return@WizardTerminalWork 155 | } 156 | } else { 157 | throw IOException(c.vm.activity.getString(R.string.term_invalid_action)) 158 | } 159 | terminal.add(c.vm.activity.getString(R.string.term_success)) 160 | } 161 | } -------------------------------------------------------------------------------- /app/src/main/java/org/andbootmgr/app/DeviceInfo.kt: -------------------------------------------------------------------------------- 1 | package org.andbootmgr.app 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.util.Log 6 | import com.topjohnwu.superuser.Shell 7 | import com.topjohnwu.superuser.io.SuFile 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.async 10 | import kotlinx.coroutines.launch 11 | import kotlinx.coroutines.withContext 12 | import org.andbootmgr.app.util.SDUtils 13 | import org.json.JSONObject 14 | import org.json.JSONTokener 15 | import java.io.File 16 | import java.io.FileNotFoundException 17 | import java.lang.reflect.Method 18 | import java.net.URL 19 | 20 | interface DeviceInfo { 21 | val codename: String 22 | val blBlock: String 23 | val metaonsd: Boolean 24 | val realEntryHasKernel: Boolean 25 | /* Environment variables: 26 | * - BOOTED=true SETUP=false BL_BACKUP= for droidboot update 27 | * - BOOTED=false SETUP=false BL_BACKUP= for droidboot fix 28 | * - BOOTED=false SETUP=true BL_BACKUP= for droidboot install + sd creation 29 | * - BOOTED=true SETUP=true BL_BACKUP= for sd creation with already installed droidboot 30 | */ 31 | val postInstallScript: Boolean 32 | val havedtbo: Boolean 33 | fun isInstalled(logic: DeviceLogic): Boolean 34 | @SuppressLint("PrivateApi") 35 | fun isBooted(logic: DeviceLogic): Boolean { 36 | // TODO migrate all modern BL to one of these three variants 37 | try { 38 | val c = Class.forName("android.os.SystemProperties") 39 | val getBoolean: Method = c.getMethod( 40 | "getBoolean", 41 | String::class.java, 42 | Boolean::class.javaPrimitiveType 43 | ) 44 | if (getBoolean.invoke(c, "ro.boot.has_dualboot", false) as Boolean 45 | || getBoolean.invoke(c, "ro.boot.hasdualboot", false) as Boolean) 46 | return true 47 | } catch (e: Exception) { 48 | e.printStackTrace() 49 | } 50 | val result = Shell.cmd("grep ABM.bootloader=1 /proc/cmdline").exec() 51 | return result.isSuccess && result.out.joinToString("\n").contains("ABM.bootloader=1") 52 | } 53 | fun isCorrupt(logic: DeviceLogic): Boolean 54 | fun getAbmSettings(logic: DeviceLogic): String? 55 | } 56 | 57 | fun DeviceInfo.asMetaOnSdDeviceInfo() = this as MetaOnSdDeviceInfo 58 | 59 | abstract class MetaOnSdDeviceInfo : DeviceInfo { 60 | abstract val bdev: String 61 | abstract val pbdev: String 62 | override val metaonsd = true 63 | override fun isInstalled(logic: DeviceLogic): Boolean { 64 | return SuFile.open(bdev).exists() && SDUtils.generateMeta(this)?.let { meta -> 65 | meta.p.find { it.id == 1 && it.type == SDUtils.PartitionType.RESERVED } != null 66 | } == true 67 | } 68 | override fun isCorrupt(logic: DeviceLogic): Boolean { 69 | return !SuFile.open(logic.abmDb, "db.conf").exists() 70 | } 71 | override fun getAbmSettings(logic: DeviceLogic): String? { 72 | if (SuFile.open(bdev).exists()) 73 | SDUtils.generateMeta(this)?.let { meta -> 74 | if (meta.p.isNotEmpty()) { 75 | val part = meta.dumpKernelPartition(1) 76 | if (part.type == SDUtils.PartitionType.RESERVED) 77 | return part.path 78 | } 79 | } 80 | return null 81 | } 82 | } 83 | 84 | abstract class SdLessDeviceInfo : DeviceInfo { 85 | override val metaonsd = false 86 | override fun isInstalled(logic: DeviceLogic): Boolean { 87 | return SuFile.open(logic.abmSdLessBootsetImg.toURI()).exists() 88 | } 89 | override fun isCorrupt(logic: DeviceLogic): Boolean { 90 | return !SuFile.open(logic.abmDb, "db.conf").exists() 91 | } 92 | override fun getAbmSettings(logic: DeviceLogic): String? { 93 | return File(logic.dmBase, logic.dmName).absolutePath 94 | } 95 | } 96 | 97 | class JsonMetaOnSdDeviceInfo( 98 | override val codename: String, 99 | override val blBlock: String, 100 | override val bdev: String, 101 | override val pbdev: String, 102 | override val postInstallScript: Boolean, 103 | override val havedtbo: Boolean, 104 | override val realEntryHasKernel: Boolean 105 | ) : MetaOnSdDeviceInfo() 106 | 107 | class JsonSdLessDeviceInfo( 108 | override val codename: String, 109 | override val blBlock: String, 110 | override val postInstallScript: Boolean, 111 | override val havedtbo: Boolean, 112 | override val realEntryHasKernel: Boolean 113 | ) : SdLessDeviceInfo() 114 | 115 | class JsonDeviceInfoFactory(private val ctx: Context) { 116 | suspend fun get(codename: String): DeviceInfo? { 117 | return try { 118 | withContext(Dispatchers.IO) { 119 | var fromNet = true 120 | val jsonText = try { 121 | try { 122 | ctx.assets.open("abm.json").readBytes().toString(Charsets.UTF_8) 123 | } catch (_: FileNotFoundException) { 124 | URL("https://raw.githubusercontent.com/Android-Boot-Manager/ABM-json/master/devices/$codename.json").readText() 125 | } 126 | } catch (e: Exception) { 127 | fromNet = false 128 | Log.e("ABM device info", Log.getStackTraceString(e)) 129 | val f = File(ctx.filesDir, "abm_dd_cache.json") 130 | if (f.exists()) f.readText() else 131 | ctx.assets.open("abm_fallback/$codename.json").readBytes() 132 | .toString(Charsets.UTF_8) 133 | } 134 | val jsonRoot = JSONTokener(jsonText).nextValue() as JSONObject? ?: return@withContext null 135 | val json = jsonRoot.getJSONObject("deviceInfo") 136 | if (BuildConfig.VERSION_CODE < jsonRoot.getInt("minAppVersion")) 137 | throw IllegalStateException("please upgrade app") 138 | if (fromNet) { 139 | launch { 140 | val newRoot = JSONObject() 141 | newRoot.put("deviceInfo", json) 142 | newRoot.put("minAppVersion", jsonRoot.getInt("minAppVersion")) 143 | File(ctx.filesDir, "abm_dd_cache.json").writeText(newRoot.toString()) 144 | } 145 | } 146 | if (json.getBoolean("metaOnSd")) { 147 | JsonMetaOnSdDeviceInfo( 148 | json.getString("codename"), 149 | json.getString("blBlock"), 150 | json.getString("sdBlock"), 151 | json.getString("sdBlockP"), 152 | json.getBoolean("postInstallScript"), 153 | json.getBoolean("haveDtbo"), 154 | json.optBoolean("realEntryHasKernel", false) 155 | ) 156 | } else { 157 | JsonSdLessDeviceInfo( 158 | json.getString("codename"), 159 | json.getString("blBlock"), 160 | json.getBoolean("postInstallScript"), 161 | json.getBoolean("haveDtbo"), 162 | json.optBoolean("realEntryHasKernel", false) 163 | ) 164 | } 165 | } 166 | } catch (e: Exception) { 167 | Log.e("ABM device info", Log.getStackTraceString(e)) 168 | null 169 | } 170 | } 171 | } -------------------------------------------------------------------------------- /app/src/main/java/org/andbootmgr/app/DeviceLogic.kt: -------------------------------------------------------------------------------- 1 | package org.andbootmgr.app 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import com.topjohnwu.superuser.Shell 6 | import com.topjohnwu.superuser.io.SuFile 7 | import org.andbootmgr.app.util.SDLessUtils 8 | import org.andbootmgr.app.util.SDUtils 9 | import org.andbootmgr.app.util.Toolkit 10 | import java.io.File 11 | 12 | class DeviceLogic(private val ctx: Context) { 13 | private val rootDir = ctx.filesDir.parentFile!! 14 | private val toolkit = Toolkit(ctx) 15 | val fileDir = File(rootDir, "files") 16 | val cacheDir = File(rootDir, "cache") 17 | val toolkitDir = File(toolkit.targetPath, "Toolkit") // will occasionally be pruned by OS, but it's fine 18 | private val rootTmpDir = File("/data/local/tmp") 19 | val abmBootset = File(rootTmpDir, ".abm_bootset") 20 | val abmSdLessBootset = File("/data/abm") 21 | val abmSdLessBootsetImg = File(abmSdLessBootset, "bootset.img") 22 | private val metadata = File("/metadata") 23 | val metadataMap = File(metadata, "abm_settings.map") 24 | val dmBase = File("/dev/block/mapper") 25 | val dmName = "abmbootset" 26 | val abmDb = File(abmBootset, "db") 27 | val abmEntries = File(abmDb, "entries") 28 | val abmDbConf = File(abmDb, "db.conf") 29 | val lkBackupPrimary = File(fileDir, "backup_lk1.img") 30 | val lkBackupSecondary = File(fileDir, "backup_lk.img") 31 | var mounted = false 32 | fun mountBootset(d: DeviceInfo): Boolean { 33 | if (checkMounted()) return true 34 | val ast = d.getAbmSettings(this) ?: return false 35 | val bootsetSu = SuFile.open(abmBootset.toURI()) 36 | if (!bootsetSu.exists()) bootsetSu.mkdir() 37 | if (!d.metaonsd && !mapBootset()) return false 38 | val result = Shell 39 | .cmd("mount $ast ${abmBootset.absolutePath}") 40 | .exec() 41 | if (!result.isSuccess) { 42 | val out = result.out.joinToString("\n") + result.err.joinToString("\n") 43 | if (out.contains("Device or resource busy")) { 44 | mounted = false 45 | } 46 | if (out.contains("Invalid argument")) { 47 | mounted = false 48 | } 49 | Log.e("ABM_MOUNT", out) 50 | return mounted 51 | } 52 | mounted = true 53 | return true 54 | } 55 | fun unmountBootset(d: DeviceInfo): Boolean { 56 | if (!checkMounted()) return true 57 | val result = Shell.cmd("umount ${abmBootset.absolutePath}").exec() 58 | if (!result.isSuccess) { 59 | val out = result.out.joinToString("\n") + result.err.joinToString("\n") 60 | if (out.contains("Device or resource busy")) { 61 | mounted = true 62 | } else if (out.contains("Invalid argument")) { 63 | mounted = false 64 | } 65 | Log.e("ABM_UMOUNT", out) 66 | return !mounted 67 | } 68 | mounted = false 69 | if (!d.metaonsd && !unmapBootset()) return false 70 | return true 71 | } 72 | fun checkMounted(): Boolean { 73 | mounted = when (val code = Shell.cmd("mountpoint -q ${abmBootset.absolutePath}").exec().code) { 74 | 0 -> true 75 | 1 -> false 76 | else -> throw IllegalStateException("mountpoint returned exit code $code, expected 0 or 1") 77 | } 78 | return mounted 79 | } 80 | fun mapBootset(terminal: MutableList? = null): Boolean { 81 | return SDLessUtils.map(this, dmName, metadataMap, terminal) 82 | } 83 | private fun unmapBootset(): Boolean { 84 | return SDLessUtils.unmap(this, dmName, true) 85 | } 86 | fun getDmName(fn: String, id: Int) = "abm_${fn}_$id" 87 | fun getDmFile(fn: String, id: Int) = File(dmBase, getDmName(fn, id)) 88 | fun map(fn: String, id: Int, terminal: MutableList? = null): Boolean { 89 | if (!unmap(fn, id, terminal)) 90 | throw IllegalStateException("failed to unmap part which shouldn't even exist?") 91 | val map = File(File(abmBootset, fn), "$id.map") 92 | return SDLessUtils.map(this, getDmName(fn, id), map, terminal) 93 | } 94 | fun unmap(fn: String, id: Int, terminal: MutableList? = null): Boolean { 95 | return SDLessUtils.unmap(this, getDmName(fn, id), false, terminal) 96 | } 97 | fun mount(p: SDUtils.Partition): Shell.Job { 98 | return Shell.cmd(p.mount()) 99 | } 100 | fun unmount(p: SDUtils.Partition): Shell.Job { 101 | return Shell.cmd(p.unmount()) 102 | } 103 | fun delete(p: SDUtils.Partition): Shell.Job { 104 | return Shell.cmd(SDUtils.umsd(p.meta) + " && " + p.delete()) 105 | } 106 | fun rename(p: SDUtils.Partition, name: String): Shell.Job { 107 | return Shell.cmd(SDUtils.umsd(p.meta) + " && " + p.rename(name)) 108 | } 109 | fun create(p: SDUtils.Partition.FreeSpace, start: Long, end: Long, typeCode: String, name: String): Shell.Job { 110 | return Shell.cmd(SDUtils.umsd(p.meta) + " && " + p.create(start, end, typeCode, name)) 111 | } 112 | fun runShFileWithArgs(cmd: String): Shell.Job { 113 | return Shell.cmd("export PATH=\"${toolkitDir.absolutePath}:\$PATH\" " + 114 | "TMPDIR=\"${cacheDir.absolutePath}\" BOOTSET=\"${abmBootset.absolutePath}\" " + 115 | "TK=\"${toolkitDir.absolutePath}\" && cd \"\$TK\" && $cmd") 116 | } 117 | suspend fun extractToolkit(terminal: MutableList) { 118 | try { 119 | toolkit.copyAssets { 120 | terminal.add(ctx.getString(R.string.toolkit_extracting)) 121 | } 122 | } catch (e: Exception) { 123 | terminal.add(ctx.getString(R.string.toolkit_error)) 124 | throw e 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /app/src/main/java/org/andbootmgr/app/DroidBootFlow.kt: -------------------------------------------------------------------------------- 1 | package org.andbootmgr.app 2 | 3 | import android.os.Handler 4 | import android.os.Looper 5 | import android.util.Log 6 | import android.widget.Toast 7 | import androidx.compose.foundation.layout.Arrangement 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Text 13 | import androidx.compose.material3.TextField 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.LaunchedEffect 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.runtime.mutableStateOf 18 | import androidx.compose.runtime.remember 19 | import androidx.compose.runtime.setValue 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.platform.LocalContext 23 | import androidx.compose.ui.res.stringResource 24 | import androidx.compose.ui.text.style.TextAlign 25 | import androidx.compose.ui.unit.dp 26 | import com.topjohnwu.superuser.Shell 27 | import com.topjohnwu.superuser.io.SuFile 28 | import com.topjohnwu.superuser.io.SuFileInputStream 29 | import kotlinx.coroutines.CoroutineScope 30 | import kotlinx.coroutines.Dispatchers 31 | import kotlinx.coroutines.launch 32 | import org.andbootmgr.app.util.ConfigFile 33 | import org.andbootmgr.app.util.SDLessUtils 34 | import org.andbootmgr.app.util.SDUtils 35 | import org.json.JSONObject 36 | import org.json.JSONTokener 37 | import java.io.File 38 | import java.io.FileNotFoundException 39 | import java.io.IOException 40 | import java.net.URL 41 | 42 | class DroidBootFlow : WizardFlow() { 43 | override fun get(vm: WizardState): List { 44 | val d = DroidBootFlowDataHolder(vm) 45 | return listOf(WizardPage("start", 46 | NavButton(vm.activity.getString(R.string.cancel)) { it.finish() }, 47 | NavButton(vm.activity.getString(R.string.next)) { it.navigate("input") }) 48 | { 49 | Start(vm) 50 | }, WizardPage("input", 51 | NavButton(vm.activity.getString(R.string.prev)) { it.navigate("start") }, 52 | NavButton("") {} 53 | ) { 54 | Input(d) 55 | }, WizardPage("dload", 56 | NavButton(vm.activity.getString(R.string.cancel)) { it.finish() }, 57 | NavButton("") {} 58 | ) { 59 | WizardDownloader(vm, "flash") 60 | }, WizardPage("flash", 61 | NavButton("") {}, 62 | NavButton("") {} 63 | ) { 64 | Flash(d) 65 | }) 66 | } 67 | } 68 | 69 | class DroidBootFlowDataHolder(val vm: WizardState) { 70 | var osName by mutableStateOf(vm.activity.getString(R.string.android)) 71 | } 72 | 73 | @Composable 74 | private fun Start(vm: WizardState) { 75 | Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, 76 | modifier = Modifier.fillMaxSize() 77 | ) { 78 | Text(stringResource(R.string.welcome_text)) 79 | Text( 80 | if (remember { vm.deviceInfo.isBooted(vm.logic) }) { 81 | stringResource(R.string.install_abm) 82 | } else { 83 | stringResource(R.string.install_abm_dboot) 84 | } 85 | ) 86 | if (vm.deviceInfo.metaonsd) { 87 | Text(stringResource(R.string.sd_erase1)) 88 | Text(stringResource(R.string.sd_erase2)) 89 | } 90 | } 91 | } 92 | 93 | // shared across DroidBootFlow, UpdateDroidBootFlow, FixDroidBootFlow 94 | @Composable 95 | fun LoadDroidBootJson(vm: WizardState, update: Boolean, content: @Composable () -> Unit) { 96 | var loading by remember { mutableStateOf(!vm.deviceInfo.isBooted(vm.logic) || vm.deviceInfo.postInstallScript || update) } 97 | var error by remember { mutableStateOf(false) } 98 | val ctx = LocalContext.current 99 | LaunchedEffect(Unit) { 100 | if (!loading) return@LaunchedEffect 101 | CoroutineScope(Dispatchers.IO).launch { 102 | try { 103 | val jsonText = try { 104 | ctx.assets.open("abm.json").readBytes().toString(Charsets.UTF_8) 105 | } catch (e: FileNotFoundException) { 106 | URL("https://raw.githubusercontent.com/Android-Boot-Manager/ABM-json/master/devices/" + vm.codename + ".json").readText() 107 | } 108 | 109 | val json = JSONTokener(jsonText).nextValue() as JSONObject 110 | if (BuildConfig.VERSION_CODE < json.getInt("minAppVersion")) 111 | throw IllegalStateException("please upgrade app") 112 | if ((!vm.deviceInfo.isBooted(vm.logic) || update) && json.has("bootloader")) { 113 | val bl = json.getJSONObject("bootloader") 114 | if (!bl.optBoolean("updateOnly", false) || update) { 115 | val url = bl.getString("url") 116 | val sha = bl.getStringOrNull("sha256") 117 | vm.inetAvailable["droidboot"] = WizardState.Downloadable( 118 | url, sha, vm.activity.getString(R.string.droidboot_online) 119 | ) 120 | vm.idNeeded.add("droidboot") 121 | } 122 | } 123 | if (vm.deviceInfo.postInstallScript) { 124 | val i = json.getJSONObject("installScript") 125 | val url = i.getString("url") 126 | val sha = i.getStringOrNull("sha256") 127 | vm.inetAvailable["_install.sh_"] = WizardState.Downloadable( 128 | url, sha, vm.activity.getString(R.string.installer_sh) 129 | ) 130 | vm.idNeeded.add("_install.sh_") 131 | } 132 | loading = false 133 | } catch (e: Exception) { 134 | Handler(Looper.getMainLooper()).post { 135 | Toast.makeText(vm.activity, R.string.dl_error, Toast.LENGTH_LONG).show() 136 | } 137 | Log.e("ABM droidboot json", Log.getStackTraceString(e)) 138 | error = true 139 | } 140 | } 141 | } 142 | if (loading) { 143 | if (error) { 144 | Text(stringResource(R.string.dl_error)) 145 | } else { 146 | LoadingCircle(stringResource(R.string.loading), modifier = Modifier.fillMaxSize()) 147 | } 148 | } else content() 149 | } 150 | 151 | @Composable 152 | private fun Input(d: DroidBootFlowDataHolder) { 153 | LoadDroidBootJson(d.vm, false) { 154 | if (!d.vm.deviceInfo.isBooted(d.vm.logic) && !d.vm.idNeeded.contains("droidboot")) { 155 | Text(stringResource(R.string.install_bl_first)) 156 | return@LoadDroidBootJson 157 | } 158 | Column( 159 | horizontalAlignment = Alignment.CenterHorizontally, 160 | verticalArrangement = Arrangement.Center, 161 | modifier = Modifier.fillMaxSize() 162 | ) { 163 | val e = d.osName.isBlank() || !d.osName.matches(Regex("[\\dA-Za-z]+")) 164 | 165 | Text( 166 | stringResource(R.string.enter_name_for_current), 167 | textAlign = TextAlign.Center, 168 | modifier = Modifier.padding(vertical = 5.dp) 169 | ) 170 | TextField( 171 | value = d.osName, 172 | onValueChange = { 173 | d.osName = it 174 | }, 175 | label = { Text(stringResource(R.string.os_name)) }, 176 | isError = e 177 | ) 178 | if (e) { 179 | Text(stringResource(R.string.invalid_in), color = MaterialTheme.colorScheme.error) 180 | } else { 181 | Text("") // Budget spacer 182 | } 183 | LaunchedEffect(e) { 184 | if (e) { 185 | d.vm.nextText = "" 186 | d.vm.onNext = {} 187 | } else { 188 | d.vm.nextText = d.vm.activity.getString(R.string.next) 189 | d.vm.onNext = { it.navigate(if (d.vm.idNeeded.isNotEmpty()) "dload" else "flash") } 190 | } 191 | } 192 | } 193 | } 194 | } 195 | 196 | @Composable 197 | private fun Flash(d: DroidBootFlowDataHolder) { 198 | val vm = d.vm 199 | WizardTerminalWork(d.vm, logFile = "blflash_${System.currentTimeMillis()}.txt") { terminal -> 200 | vm.logic.extractToolkit(terminal) 201 | vm.downloadRemainingFiles(terminal) 202 | terminal.add(vm.activity.getString(R.string.term_preparing_fs)) 203 | if (vm.logic.checkMounted()) { 204 | terminal.add(vm.activity.getString(R.string.term_mount_state_bad)) 205 | return@WizardTerminalWork 206 | } 207 | if (!SuFile.open(vm.logic.abmBootset.toURI()).exists()) { 208 | if (!SuFile.open(vm.logic.abmBootset.toURI()).mkdir()) { 209 | terminal.add(vm.activity.getString(R.string.term_cant_create_mount_point)) 210 | return@WizardTerminalWork 211 | } 212 | } 213 | if (!SuFile.open(File(vm.logic.abmBootset, ".NOT_MOUNTED").toURI()).exists()) { 214 | if (!SuFile.open(File(vm.logic.abmBootset, ".NOT_MOUNTED").toURI()).createNewFile()) { 215 | terminal.add(vm.activity.getString(R.string.term_cant_create_placeholder)) 216 | return@WizardTerminalWork 217 | } 218 | } 219 | 220 | if (vm.deviceInfo.metaonsd) { 221 | var meta = SDUtils.generateMeta(vm.deviceInfo.asMetaOnSdDeviceInfo()) 222 | if (meta == null) { 223 | terminal.add(vm.activity.getString(R.string.term_cant_get_meta)) 224 | return@WizardTerminalWork 225 | } 226 | if (!Shell.cmd(SDUtils.umsd(meta)).to(terminal).exec().isSuccess) { 227 | terminal.add(vm.activity.getString(R.string.term_failed_umount_drive)) 228 | } 229 | if (!Shell.cmd("sgdisk --mbrtogpt --clear ${vm.deviceInfo.asMetaOnSdDeviceInfo().bdev}").to(terminal) 230 | .exec().isSuccess 231 | ) { 232 | terminal.add(vm.activity.getString(R.string.term_failed_create_pt)) 233 | return@WizardTerminalWork 234 | } 235 | meta = SDUtils.generateMeta(vm.deviceInfo.asMetaOnSdDeviceInfo()) 236 | if (meta == null) { 237 | terminal.add(vm.activity.getString(R.string.term_cant_get_meta)) 238 | return@WizardTerminalWork 239 | } 240 | val r = vm.logic.create(meta.s[0] as SDUtils.Partition.FreeSpace, 241 | 0, 242 | ((meta.sectors - 2048) / 41 + 2048) // create meta partition proportional to sd size 243 | .coerceAtLeast((512L * 1024L * 1024L) / meta.logicalSectorSizeBytes) // but never less than 512mb 244 | .coerceAtMost((4L * 1024L * 1024L * 1024L) / meta.logicalSectorSizeBytes), // and never more than 4gb 245 | "8301", 246 | "abm_settings" 247 | ).to(terminal).exec() 248 | if (r.out.joinToString("\n").contains("kpartx")) { 249 | terminal.add(vm.activity.getString(R.string.term_reboot_asap)) 250 | } 251 | if (r.isSuccess) { 252 | terminal.add(vm.activity.getString(R.string.term_done)) 253 | } else { 254 | terminal.add(vm.activity.getString(R.string.term_failed_create_meta)) 255 | return@WizardTerminalWork 256 | } 257 | } else { 258 | if (!SuFile.open(vm.logic.abmSdLessBootset.toURI()).exists()) { 259 | if (!SuFile.open(vm.logic.abmSdLessBootset.toURI()).mkdir()) { 260 | terminal.add(vm.activity.getString(R.string.term_cant_create_bootset)) 261 | return@WizardTerminalWork 262 | } 263 | } 264 | val kilobytes = 4L * 1024L * 1024L // 4 GB for now 265 | if (!Shell.cmd("dd if=/dev/zero bs=1024 count=$kilobytes of=${vm.logic.abmSdLessBootsetImg.absolutePath}").to(terminal).exec().isSuccess) { 266 | terminal.add(vm.activity.getString(R.string.term_failed_fallocate)) 267 | return@WizardTerminalWork 268 | } 269 | if (!Shell.cmd("uncrypt ${vm.logic.abmSdLessBootsetImg.absolutePath} " + 270 | vm.logic.metadataMap.absolutePath).to(terminal).exec().isSuccess) { 271 | terminal.add(vm.activity.getString(R.string.term_failed_uncrypt)) 272 | return@WizardTerminalWork 273 | } 274 | val ast = vm.deviceInfo.getAbmSettings(vm.logic) 275 | if (ast == null) { 276 | terminal.add(vm.activity.getString(R.string.term_failed_prepare_map)) 277 | return@WizardTerminalWork 278 | } 279 | 280 | if (!SDLessUtils.unmap(vm.logic, vm.logic.dmName, false, terminal)) { 281 | terminal.add(vm.activity.getString(R.string.term_failed_unmap)) 282 | return@WizardTerminalWork 283 | 284 | } 285 | if (!vm.logic.mapBootset(terminal)) { 286 | terminal.add(vm.activity.getString(R.string.term_failed_map)) 287 | return@WizardTerminalWork 288 | } 289 | 290 | if (!Shell.cmd("mkfs.ext4 $ast").to(terminal).exec().isSuccess) { 291 | terminal.add(vm.activity.getString(R.string.term_failed_bootset_mkfs)) 292 | return@WizardTerminalWork 293 | } 294 | } 295 | 296 | if (!vm.logic.mountBootset(vm.deviceInfo)) { 297 | terminal.add(vm.activity.getString(R.string.term_failed_mount)) 298 | return@WizardTerminalWork 299 | } 300 | if (SuFile.open(File(vm.logic.abmBootset, ".NOT_MOUNTED").toURI()).exists()) { 301 | terminal.add(vm.activity.getString(R.string.term_mount_failure_inconsist)) 302 | return@WizardTerminalWork 303 | } 304 | 305 | if (!SuFile.open(vm.logic.abmDb.toURI()).exists()) { 306 | if (!SuFile.open(vm.logic.abmDb.toURI()).mkdir()) { 307 | terminal.add(vm.activity.getString(R.string.term_failed_create_db_dir)) 308 | vm.logic.unmountBootset(vm.deviceInfo) 309 | return@WizardTerminalWork 310 | } 311 | } 312 | if (!SuFile.open(vm.logic.abmEntries.toURI()).exists()) { 313 | if (!SuFile.open(vm.logic.abmEntries.toURI()).mkdir()) { 314 | terminal.add(vm.activity.getString(R.string.term_failed_create_entries_dir)) 315 | vm.logic.unmountBootset(vm.deviceInfo) 316 | return@WizardTerminalWork 317 | } 318 | } 319 | val tmpFile = if (vm.deviceInfo.postInstallScript) { 320 | vm.chosen["_install.sh_"]!!.toFile(vm).also { 321 | it.setExecutable(true) 322 | } 323 | } else null 324 | 325 | terminal.add(vm.activity.getString(R.string.term_building_cfg)) 326 | val db = ConfigFile() 327 | db["default"] = "Entry 01" 328 | db["timeout"] = "5" 329 | db.exportToFile(File(vm.logic.abmDb, "db.conf")) 330 | val entry = ConfigFile() 331 | entry["title"] = d.osName.trim() 332 | if (vm.deviceInfo.realEntryHasKernel) { 333 | entry["linux"] = "real/kernel" 334 | entry["initrd"] = "real/initrd.cpio.gz" 335 | entry["dtb"] = "real/dtb.dtb" 336 | if (vm.deviceInfo.havedtbo) 337 | entry["dtbo"] = "real/dtbo.dtbo" 338 | entry["options"] = "REPLACECMDLINE" 339 | } else { 340 | entry["linux"] = "null" 341 | entry["initrd"] = "null" 342 | entry["dtb"] = "null" 343 | if (vm.deviceInfo.havedtbo) 344 | entry["dtbo"] = "null" 345 | entry["options"] = "null" 346 | } 347 | entry["xtype"] = "droid" 348 | entry["xpart"] = "real" 349 | entry.exportToFile(File(vm.logic.abmEntries, "real.conf")) 350 | if (!vm.deviceInfo.isBooted(vm.logic)) { 351 | terminal.add(vm.activity.getString(R.string.term_flashing_droidboot)) 352 | val f = SuFile.open(vm.deviceInfo.blBlock) 353 | if (!f.canWrite()) 354 | terminal.add(vm.activity.getString(R.string.term_cant_write_bl)) 355 | vm.copyPriv(SuFileInputStream.open(vm.deviceInfo.blBlock), vm.logic.lkBackupPrimary) 356 | try { 357 | vm.copyPriv(vm.chosen["droidboot"]!!.openInputStream(vm), File(vm.deviceInfo.blBlock)) 358 | } catch (e: IOException) { 359 | terminal.add(vm.activity.getString(R.string.term_bl_failed)) 360 | terminal.add(e.message ?: "(null)") 361 | terminal.add(vm.activity.getString(R.string.term_consult_doc)) 362 | return@WizardTerminalWork 363 | } 364 | } 365 | if (vm.deviceInfo.postInstallScript) { 366 | terminal.add(vm.activity.getString(R.string.term_device_setup)) 367 | vm.logic.runShFileWithArgs( 368 | "BOOTED=${vm.deviceInfo.isBooted(vm.logic)} SETUP=true " + 369 | (if (vm.deviceInfo.isBooted(vm.logic)) "" else 370 | "BL_BACKUP=${vm.logic.lkBackupPrimary.absolutePath} ") + 371 | "${tmpFile!!.absolutePath} real" 372 | ).to(terminal).exec() 373 | tmpFile.delete() 374 | } 375 | terminal.add(vm.activity.getString(R.string.term_success)) 376 | vm.logic.unmountBootset(vm.deviceInfo) 377 | // TODO prompt user to reboot? 378 | } 379 | } -------------------------------------------------------------------------------- /app/src/main/java/org/andbootmgr/app/FixDroidBootFlow.kt: -------------------------------------------------------------------------------- 1 | package org.andbootmgr.app 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.LaunchedEffect 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.res.stringResource 12 | import com.topjohnwu.superuser.io.SuFile 13 | import com.topjohnwu.superuser.io.SuFileInputStream 14 | import java.io.File 15 | import java.io.IOException 16 | 17 | class FixDroidBootFlow: WizardFlow() { 18 | override fun get(vm: WizardState): List { 19 | return listOf(WizardPage("start", 20 | NavButton(vm.activity.getString(R.string.cancel)) { it.finish() }, 21 | NavButton("") {}) 22 | { 23 | Start(vm) 24 | }, WizardPage("dload", 25 | NavButton(vm.activity.getString(R.string.cancel)) { it.finish() }, 26 | NavButton("") {} 27 | ) { 28 | WizardDownloader(vm, "flash") 29 | }, WizardPage("flash", 30 | NavButton("") {}, 31 | NavButton("") {} 32 | ) { 33 | Flash(vm) 34 | }) 35 | } 36 | } 37 | 38 | @Composable 39 | private fun Start(vm: WizardState) { 40 | LoadDroidBootJson(vm, false) { 41 | if (!vm.deviceInfo.isBooted(vm.logic) && !vm.idNeeded.contains("droidboot")) { 42 | Text(stringResource(R.string.install_bl_first)) 43 | return@LoadDroidBootJson 44 | } 45 | LaunchedEffect(Unit) { 46 | vm.nextText = vm.activity.getString(R.string.next) 47 | vm.onNext = { it.navigate(if (vm.idNeeded.isNotEmpty()) "dload" else "flash") } 48 | } 49 | Column( 50 | horizontalAlignment = Alignment.CenterHorizontally, 51 | verticalArrangement = Arrangement.Center, 52 | modifier = Modifier.fillMaxSize() 53 | ) { 54 | Text(stringResource(id = R.string.welcome_text)) 55 | Text(stringResource(R.string.reinstall_dboot)) 56 | } 57 | } 58 | } 59 | 60 | @Composable 61 | private fun Flash(vm: WizardState) { 62 | WizardTerminalWork(vm, logFile = "blfix_${System.currentTimeMillis()}.txt") { terminal -> 63 | vm.logic.extractToolkit(terminal) 64 | vm.downloadRemainingFiles(terminal) 65 | val tmpFile = if (vm.deviceInfo.postInstallScript) { 66 | vm.chosen["_install.sh_"]!!.toFile(vm).also { 67 | it.setExecutable(true) 68 | } 69 | } else null 70 | terminal.add(vm.activity.getString(R.string.term_flashing_droidboot)) 71 | val f = SuFile.open(vm.deviceInfo.blBlock) 72 | if (!f.canWrite()) 73 | terminal.add(vm.activity.getString(R.string.term_cant_write_bl)) 74 | vm.copyPriv(SuFileInputStream.open(vm.deviceInfo.blBlock), vm.logic.lkBackupSecondary) 75 | try { 76 | vm.copyPriv(vm.chosen["droidboot"]!!.openInputStream(vm), File(vm.deviceInfo.blBlock)) 77 | } catch (e: IOException) { 78 | terminal.add(vm.activity.getString(R.string.term_bl_failed)) 79 | terminal.add(e.message ?: "(null)") 80 | terminal.add(vm.activity.getString(R.string.term_consult_doc)) 81 | return@WizardTerminalWork 82 | } 83 | if (vm.deviceInfo.postInstallScript) { 84 | terminal.add(vm.activity.getString(R.string.term_device_setup)) 85 | vm.logic.runShFileWithArgs( 86 | "BOOTED=${vm.deviceInfo.isBooted(vm.logic)} SETUP=false " + 87 | "BL_BACKUP=${vm.logic.lkBackupSecondary.absolutePath} " + 88 | "${tmpFile!!.absolutePath} real" 89 | ).to(terminal).exec() 90 | tmpFile.delete() 91 | } 92 | terminal.add(vm.activity.getString(R.string.term_success)) 93 | } 94 | } -------------------------------------------------------------------------------- /app/src/main/java/org/andbootmgr/app/Settings.kt: -------------------------------------------------------------------------------- 1 | package org.andbootmgr.app 2 | 3 | import android.widget.Toast 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.text.KeyboardOptions 10 | import androidx.compose.material3.Button 11 | import androidx.compose.material3.OutlinedButton 12 | import androidx.compose.material3.OutlinedTextField 13 | import androidx.compose.material3.Switch 14 | import androidx.compose.material3.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.derivedStateOf 17 | import androidx.compose.runtime.getValue 18 | import androidx.compose.runtime.mutableStateMapOf 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.platform.LocalContext 23 | import androidx.compose.ui.platform.LocalDensity 24 | import androidx.compose.ui.res.stringResource 25 | import androidx.compose.ui.text.input.ImeAction 26 | import androidx.compose.ui.text.input.KeyboardCapitalization 27 | import androidx.compose.ui.text.input.KeyboardType 28 | import androidx.compose.ui.unit.dp 29 | import androidx.compose.ui.unit.sp 30 | import kotlinx.coroutines.CoroutineScope 31 | import kotlinx.coroutines.Dispatchers 32 | import kotlinx.coroutines.launch 33 | 34 | 35 | @Composable 36 | fun Settings(vm: MainActivityState) { 37 | val ctx = LocalContext.current 38 | val changes = remember { mutableStateMapOf() } 39 | val defaultErr by remember { derivedStateOf { 40 | val defaultText = changes["default"] ?: vm.defaultCfg["default"] ?: "Entry 01" 41 | defaultText.isBlank() || !defaultText.matches(Regex("[\\dA-Za-z ]+")) } } 42 | val timeoutErr by remember { derivedStateOf { 43 | val timeoutText = changes["timeout"] ?: vm.defaultCfg["timeout"] ?: "20" 44 | timeoutText.isBlank() || !timeoutText.matches(Regex("\\d+")) } } 45 | Column(modifier = Modifier.padding(horizontal = 5.dp)) { 46 | OutlinedTextField( 47 | value = changes["default"] ?: vm.defaultCfg["default"] ?: "Entry 01", 48 | onValueChange = { 49 | changes["default"] = it 50 | }, 51 | label = { Text(stringResource(R.string.default_entry)) }, 52 | isError = defaultErr, 53 | modifier = Modifier.fillMaxWidth() 54 | .padding(vertical = with(LocalDensity.current) { 8.sp.toDp() }), 55 | keyboardOptions = KeyboardOptions( 56 | keyboardType = KeyboardType.Text, 57 | capitalization = KeyboardCapitalization.None, 58 | imeAction = ImeAction.Next, 59 | autoCorrect = false 60 | ) 61 | ) 62 | OutlinedTextField( 63 | value = changes["timeout"] ?: vm.defaultCfg["timeout"] ?: "20", 64 | onValueChange = { 65 | changes["timeout"] = it 66 | }, 67 | label = { Text(stringResource(R.string.timeout_secs)) }, 68 | isError = timeoutErr, 69 | modifier = Modifier.fillMaxWidth() 70 | .padding(vertical = with(LocalDensity.current) { 8.sp.toDp() }), 71 | keyboardOptions = KeyboardOptions( 72 | keyboardType = KeyboardType.Decimal, 73 | capitalization = KeyboardCapitalization.None, 74 | imeAction = ImeAction.Next, 75 | autoCorrect = false 76 | ) 77 | ) 78 | Button(onClick = { 79 | if (defaultErr || timeoutErr) 80 | Toast.makeText(vm.activity!!, vm.activity.getString(R.string.invalid_in), Toast.LENGTH_LONG).show() 81 | else CoroutineScope(Dispatchers.Main).launch { 82 | vm.editDefaultCfg(changes) 83 | changes.clear() 84 | } 85 | }, enabled = !(defaultErr || timeoutErr)) { 86 | Text(stringResource(R.string.save_changes)) 87 | } 88 | Button(onClick = { 89 | vm.currentWizardFlow = UpdateDroidBootFlow() 90 | }) { 91 | Text(stringResource(R.string.update_droidboot)) 92 | } 93 | OutlinedButton(onClick = { 94 | vm.unmountBootset() 95 | vm.activity!!.finish() 96 | }) { 97 | Text(stringResource(R.string.umount)) 98 | } 99 | Row(horizontalArrangement = Arrangement.SpaceBetween, 100 | verticalAlignment = Alignment.CenterVertically, 101 | modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp)) { 102 | Text(stringResource(R.string.noob_mode)) 103 | Switch(checked = vm.noobMode, onCheckedChange = { 104 | ctx.getSharedPreferences("abm", 0).edit().putBoolean("noob_mode", it).apply() 105 | vm.noobMode = it 106 | }) 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /app/src/main/java/org/andbootmgr/app/UpdateDroidBootFlow.kt: -------------------------------------------------------------------------------- 1 | package org.andbootmgr.app 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.LaunchedEffect 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.res.stringResource 12 | import com.topjohnwu.superuser.io.SuFile 13 | import com.topjohnwu.superuser.io.SuFileInputStream 14 | import java.io.File 15 | import java.io.IOException 16 | 17 | class UpdateDroidBootFlow: WizardFlow() { 18 | override fun get(vm: WizardState): List { 19 | return listOf(WizardPage("start", 20 | NavButton(vm.activity.getString(R.string.cancel)) { it.finish() }, 21 | NavButton("") {}) 22 | { 23 | Start(vm) 24 | }, WizardPage("dload", 25 | NavButton(vm.activity.getString(R.string.cancel)) { it.finish() }, 26 | NavButton("") {} 27 | ) { 28 | WizardDownloader(vm, "flash") 29 | }, WizardPage("flash", 30 | NavButton("") {}, 31 | NavButton("") {} 32 | ) { 33 | Flash(vm) 34 | }) 35 | } 36 | } 37 | 38 | @Composable 39 | private fun Start(vm: WizardState) { 40 | LoadDroidBootJson(vm, true) { 41 | LaunchedEffect(Unit) { 42 | vm.nextText = vm.activity.getString(R.string.next) 43 | vm.onNext = { it.navigate(if (vm.idNeeded.isNotEmpty()) "dload" else "flash") } 44 | } 45 | Column( 46 | horizontalAlignment = Alignment.CenterHorizontally, 47 | verticalArrangement = Arrangement.Center, 48 | modifier = Modifier.fillMaxSize() 49 | ) { 50 | Text(stringResource(id = R.string.welcome_text)) 51 | Text(stringResource(R.string.update_droidboot_text)) 52 | } 53 | } 54 | } 55 | 56 | @Composable 57 | private fun Flash(vm: WizardState) { 58 | WizardTerminalWork(vm, logFile = "blup_${System.currentTimeMillis()}.txt") { terminal -> 59 | vm.logic.extractToolkit(terminal) 60 | vm.downloadRemainingFiles(terminal) 61 | val tmpFile = if (vm.deviceInfo.postInstallScript) { 62 | vm.chosen["_install.sh_"]!!.toFile(vm).also { 63 | it.setExecutable(true) 64 | } 65 | } else null 66 | terminal.add(vm.activity.getString(R.string.term_flashing_droidboot)) 67 | val backupLk = File(vm.logic.fileDir, "backup2_lk.img") 68 | val f = SuFile.open(vm.deviceInfo.blBlock) 69 | if (!f.canWrite()) 70 | terminal.add(vm.activity.getString(R.string.term_cant_write_bl)) 71 | vm.copyPriv(SuFileInputStream.open(vm.deviceInfo.blBlock), backupLk) 72 | try { 73 | vm.copyPriv(vm.chosen["droidboot"]!!.openInputStream(vm), File(vm.deviceInfo.blBlock)) 74 | } catch (e: IOException) { 75 | terminal.add(vm.activity.getString(R.string.term_bl_failed)) 76 | terminal.add(e.message ?: "(null)") 77 | terminal.add(vm.activity.getString(R.string.term_consult_doc)) 78 | return@WizardTerminalWork 79 | } 80 | if (vm.deviceInfo.postInstallScript) { 81 | terminal.add(vm.activity.getString(R.string.term_device_setup)) 82 | vm.logic.runShFileWithArgs( 83 | "BOOTED=${vm.deviceInfo.isBooted(vm.logic)} SETUP=false " + 84 | "${tmpFile!!.absolutePath} real" 85 | ).to(terminal).exec() 86 | tmpFile.delete() 87 | } 88 | terminal.add(vm.activity.getString(R.string.term_success)) 89 | } 90 | } -------------------------------------------------------------------------------- /app/src/main/java/org/andbootmgr/app/UpdateFlow.kt: -------------------------------------------------------------------------------- 1 | package org.andbootmgr.app 2 | 3 | import android.util.Log 4 | import android.widget.Toast 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.width 12 | import androidx.compose.material3.Button 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.LaunchedEffect 16 | import androidx.compose.runtime.getValue 17 | import androidx.compose.runtime.mutableIntStateOf 18 | import androidx.compose.runtime.mutableStateOf 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.runtime.rememberCoroutineScope 21 | import androidx.compose.runtime.setValue 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.res.stringResource 24 | import androidx.compose.ui.unit.dp 25 | import com.topjohnwu.superuser.Shell 26 | import com.topjohnwu.superuser.io.SuFile 27 | import kotlinx.coroutines.Dispatchers 28 | import kotlinx.coroutines.launch 29 | import org.andbootmgr.app.util.ConfigFile 30 | import org.andbootmgr.app.util.SDLessUtils 31 | import org.andbootmgr.app.util.SDUtils 32 | import org.json.JSONObject 33 | import org.json.JSONTokener 34 | import java.io.File 35 | import java.net.URL 36 | 37 | class UpdateFlow(private val entryName: String): WizardFlow() { 38 | override fun get(vm: WizardState): List { 39 | val c = UpdateFlowDataHolder(vm, entryName) 40 | return listOf(WizardPage("start", 41 | NavButton(vm.activity.getString(R.string.cancel)) { 42 | it.finish() 43 | }, 44 | if (c.vm.mvm.noobMode) NavButton("") {} else NavButton(vm.activity.getString(R.string.local_update)) { vm.navigate("local") } 45 | ) { 46 | Start(c) 47 | }, WizardPage("local", 48 | NavButton(vm.activity.getString(R.string.cancel)) { it.finish() }, 49 | NavButton(vm.activity.getString(R.string.online_update)) { vm.navigate("start") } 50 | ) { 51 | Local(c) 52 | }, WizardPage("dload", 53 | NavButton(vm.activity.getString(R.string.cancel)) { it.finish() }, 54 | NavButton("") {} 55 | ) { 56 | WizardDownloader(c.vm, "flash") 57 | }, WizardPage("flash", 58 | NavButton("") {}, 59 | NavButton("") {} 60 | ) { 61 | Flash(c) 62 | }) 63 | } 64 | } 65 | 66 | private class UpdateFlowDataHolder(val vm: WizardState, val entryFilename: String) { 67 | var json: JSONObject? = null 68 | var e: ConfigFile? = null 69 | var ef: File? = null 70 | var updateJson: String? = null 71 | val sparse = ArrayList() 72 | } 73 | 74 | @Composable 75 | private fun Start(u: UpdateFlowDataHolder) { 76 | var hasChecked by remember { mutableStateOf(false) } 77 | var hasUpdate by remember { mutableStateOf(false) } 78 | val ioDispatcher = rememberCoroutineScope { Dispatchers.IO } 79 | LaunchedEffect(Unit) { 80 | ioDispatcher.launch { 81 | u.e = ConfigFile.importFromFile( 82 | SuFile.open( 83 | u.vm.logic.abmEntries.absolutePath, 84 | u.entryFilename 85 | ) 86 | ) 87 | u.ef = u.vm.logic.abmEntries.resolve(u.entryFilename) 88 | try { 89 | val jsonText = 90 | URL(u.e!!["xupdate"]).readText() 91 | u.json = JSONTokener(jsonText).nextValue() as JSONObject 92 | } catch (e: Exception) { 93 | launch(Dispatchers.Main) { 94 | Toast.makeText(u.vm.activity, e.message, Toast.LENGTH_LONG).show() 95 | } 96 | Log.e("ABM", Log.getStackTraceString(e)) 97 | } 98 | if (u.json != null) { 99 | hasUpdate = u.json!!.optBoolean("hasUpdate", false) 100 | } 101 | hasChecked = true 102 | } 103 | } 104 | if (hasChecked) { 105 | Column(modifier = Modifier.fillMaxSize()) { 106 | if (u.json == null) { 107 | Text(stringResource(R.string.update_check_failed)) 108 | } else { 109 | if (hasUpdate) { 110 | Text(stringResource(id = R.string.found_update)) 111 | Button(onClick = { 112 | try { 113 | u.run { 114 | val j = json!! 115 | if (j.has("extraIds") && j.has("script")) { 116 | val extraIdNeeded = j.getJSONArray("extraIds") 117 | var i = 0 118 | while (i < extraIdNeeded.length()) { 119 | vm.inetAvailable["boot$i"] = WizardState.Downloadable( 120 | extraIdNeeded.get(i) as String, 121 | null, 122 | "" 123 | ) 124 | vm.idNeeded.add("boot$i") 125 | i++ 126 | } 127 | vm.inetAvailable["_install.sh_"] = WizardState.Downloadable( 128 | j.getString("script"), 129 | j.getStringOrNull("scriptSha256"), 130 | vm.activity.getString(R.string.installer_sh) 131 | ) 132 | vm.idNeeded.add("_install.sh_") 133 | } 134 | if (j.has("parts")) { 135 | val p = j.getJSONObject("parts") 136 | for (k in p.keys()) { 137 | vm.inetAvailable["part$k"] = WizardState.Downloadable( 138 | p.getString(k), 139 | null, 140 | "" 141 | ) 142 | vm.idNeeded.add("part$k") 143 | } 144 | } 145 | updateJson = j.getStringOrNull("updateJson") 146 | val a = j.optJSONArray("sparse") 147 | if (a != null) { 148 | for (i in 0 until a.length()) { 149 | val v = a.getInt(i) 150 | sparse.add(v) 151 | } 152 | } 153 | } 154 | u.vm.navigate("dload") 155 | } catch (e: Exception) { 156 | Log.e("ABM", u.json?.toString() ?: "(null json)") 157 | Log.e("ABM", Log.getStackTraceString(e)) 158 | u.json = null 159 | } 160 | }) { 161 | Text(stringResource(R.string.install_update)) 162 | } 163 | } else { 164 | Text(stringResource(R.string.up2date)) 165 | } 166 | } 167 | } 168 | } else { 169 | LoadingCircle(stringResource(R.string.checking_for_update), modifier = Modifier.fillMaxSize()) 170 | } 171 | 172 | } 173 | 174 | @Composable 175 | private fun Local(u: UpdateFlowDataHolder) { 176 | Column(verticalArrangement = Arrangement.SpaceEvenly) { 177 | Column { 178 | Text(stringResource(R.string.local_updater_1)) 179 | Text(stringResource(R.string.local_updater_2)) 180 | Text(stringResource(R.string.local_updater_3)) 181 | } 182 | Column { 183 | var i by remember { mutableIntStateOf(0) } 184 | Text(stringResource(R.string.how_many_extras, i)) 185 | Row { 186 | Button({ i++ }) { 187 | Text("+") 188 | } 189 | Spacer(Modifier.width(5.dp)) 190 | Button({ i-- }) { 191 | Text("-") 192 | } 193 | } 194 | Spacer(Modifier.height(5.dp)) 195 | Button({ 196 | u.vm.idNeeded.add("_install.sh_") 197 | for (j in 1..i) { 198 | u.vm.idNeeded.add("boot${j - 1}") 199 | } 200 | u.vm.navigate("dload") 201 | }) { 202 | Text(stringResource(R.string.install_update)) 203 | } 204 | } 205 | } 206 | } 207 | 208 | @Composable 209 | private fun Flash(u: UpdateFlowDataHolder) { 210 | WizardTerminalWork(u.vm, logFile = "update_${System.currentTimeMillis()}.txt") { terminal -> 211 | u.vm.logic.extractToolkit(terminal) 212 | u.vm.downloadRemainingFiles(terminal) 213 | val sp = u.e!!["xpart"]!!.split(":") 214 | val meta = if (u.vm.deviceInfo.metaonsd) 215 | SDUtils.generateMeta(u.vm.deviceInfo.asMetaOnSdDeviceInfo())!! else null 216 | meta?.let { Shell.cmd(SDUtils.umsd(it)).exec() } 217 | val tmpFile = if (u.vm.idNeeded.contains("_install.sh_")) { 218 | u.vm.chosen["_install.sh_"]!!.toFile(u.vm).also { 219 | it.setExecutable(true) 220 | } 221 | } else null 222 | for (p in u.vm.idNeeded.filter { it.startsWith("part") }.map { it.substring(4) }) { 223 | val physicalId = sp[p.toInt()].toInt() 224 | terminal.add(u.vm.activity.getString(R.string.term_flashing_p, p)) 225 | val f2 = u.vm.chosen["part$p"]!! 226 | val tp = if (u.vm.deviceInfo.metaonsd) 227 | File(meta!!.dumpKernelPartition(physicalId).path) 228 | else { 229 | if (!u.vm.logic.map(u.ef!!.nameWithoutExtension, physicalId, terminal)) 230 | throw IllegalStateException("failed to map $physicalId") 231 | u.vm.logic.getDmFile(u.ef!!.nameWithoutExtension, physicalId) 232 | } 233 | if (u.sparse.contains(p.toInt())) { 234 | val result2 = Shell.cmd( 235 | File( 236 | u.vm.logic.toolkitDir, 237 | "simg2img" 238 | ).absolutePath + " ${f2.toFile(u.vm)} ${tp.absolutePath}" 239 | ).to(terminal).exec() 240 | if (!result2.isSuccess) { 241 | throw IllegalStateException(u.vm.activity.getString(R.string.term_simg2img_fail)) 242 | } 243 | } else { 244 | u.vm.copyPriv(f2.openInputStream(u.vm), tp) 245 | } 246 | terminal.add(u.vm.activity.getString(R.string.term_done)) 247 | } 248 | val bootFiles = u.vm.idNeeded.filter { it.startsWith("boot") } 249 | if (bootFiles.isNotEmpty()) { 250 | terminal.add(u.vm.activity.getString(R.string.term_patch_update)) 251 | var cmd = 252 | "FORMATDATA=false " + tmpFile!!.absolutePath + " ${u.ef!!.nameWithoutExtension}" 253 | for (i in bootFiles) { 254 | cmd += " " + u.vm.chosen[i]!!.toFile(u.vm) 255 | } 256 | for (i in sp) { 257 | cmd += " $i" 258 | } 259 | val r = u.vm.logic.runShFileWithArgs(cmd).to(terminal).exec() 260 | if (!r.isSuccess) { 261 | throw IllegalStateException(u.vm.activity.getString(R.string.term_script_fail)) 262 | } 263 | } 264 | u.e!!["xupdate"] = u.updateJson ?: "" 265 | u.e!!.exportToFile(u.ef!!) 266 | terminal.add(u.vm.activity.getString(R.string.term_success)) 267 | tmpFile?.delete() 268 | } 269 | } -------------------------------------------------------------------------------- /app/src/main/java/org/andbootmgr/app/Wizard.kt: -------------------------------------------------------------------------------- 1 | package org.andbootmgr.app 2 | 3 | import android.net.Uri 4 | import android.view.WindowManager 5 | import androidx.activity.compose.BackHandler 6 | import androidx.compose.foundation.layout.Arrangement 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.foundation.layout.Row 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.foundation.layout.fillMaxWidth 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.material3.Button 14 | import androidx.compose.material3.Card 15 | import androidx.compose.material3.Icon 16 | import androidx.compose.material3.MaterialTheme 17 | import androidx.compose.material3.Text 18 | import androidx.compose.material3.TextButton 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.DisposableEffect 21 | import androidx.compose.runtime.LaunchedEffect 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.runtime.mutableStateListOf 24 | import androidx.compose.runtime.mutableStateMapOf 25 | import androidx.compose.runtime.mutableStateOf 26 | import androidx.compose.runtime.remember 27 | import androidx.compose.runtime.setValue 28 | import androidx.compose.ui.Alignment 29 | import androidx.compose.ui.Modifier 30 | import androidx.compose.ui.res.painterResource 31 | import androidx.compose.ui.res.stringResource 32 | import androidx.compose.ui.unit.dp 33 | import androidx.navigation.NavHostController 34 | import androidx.navigation.compose.NavHost 35 | import androidx.navigation.compose.composable 36 | import androidx.navigation.compose.rememberNavController 37 | import com.topjohnwu.superuser.io.SuFileOutputStream 38 | import org.andbootmgr.app.util.AbmOkHttp 39 | import org.andbootmgr.app.util.TerminalCancelException 40 | import org.andbootmgr.app.util.TerminalList 41 | import org.andbootmgr.app.util.TerminalWork 42 | import java.io.File 43 | import java.io.FileInputStream 44 | import java.io.IOException 45 | import java.io.InputStream 46 | import java.io.OutputStream 47 | import java.nio.file.Files 48 | import java.nio.file.StandardCopyOption 49 | 50 | abstract class WizardFlow { 51 | abstract fun get(vm: WizardState): List 52 | } 53 | 54 | @Composable 55 | fun WizardCompat(mvm: MainActivityState, flow: WizardFlow) { 56 | DisposableEffect(Unit) { 57 | mvm.activity!!.window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) 58 | onDispose { mvm.activity.window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) } 59 | } 60 | val vm = remember { WizardState(mvm) } 61 | vm.navController = rememberNavController() 62 | val wizardPages = remember(flow) { flow.get(vm) } 63 | NavHost( 64 | navController = vm.navController, 65 | startDestination = "start", 66 | modifier = Modifier 67 | .fillMaxWidth() 68 | ) { 69 | for (i in wizardPages) { 70 | composable(i.name) { 71 | Column(modifier = Modifier.fillMaxSize()) { 72 | BackHandler { 73 | (vm.onPrev ?: i.prev.onClick)(vm) 74 | } 75 | Box(Modifier.fillMaxWidth().weight(1f)) { 76 | i.run() 77 | } 78 | Box(Modifier.fillMaxWidth()) { 79 | BasicButtonRow(vm.prevText ?: i.prev.text, 80 | { (vm.onPrev ?: i.prev.onClick)(vm) }, 81 | vm.nextText ?: i.next.text, 82 | { (vm.onNext ?: i.next.onClick)(vm) }) 83 | } 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | class HashMismatchException(message: String) : Exception(message) 91 | 92 | class WizardState(val mvm: MainActivityState) { 93 | val codename = mvm.deviceInfo!!.codename 94 | val activity = mvm.activity!! 95 | lateinit var navController: NavHostController 96 | val logic = mvm.logic!! 97 | val deviceInfo = mvm.deviceInfo!! 98 | var prevText by mutableStateOf(null) 99 | var nextText by mutableStateOf(null) 100 | var onPrev by mutableStateOf<((WizardState) -> Unit)?>(null) 101 | var onNext by mutableStateOf<((WizardState) -> Unit)?>(null) 102 | 103 | val inetAvailable = mutableStateMapOf() 104 | val idNeeded = mutableStateListOf() 105 | val chosen = mutableStateMapOf() 106 | class Downloadable(val url: String, val hash: String?, val desc: String) 107 | class DownloadedFile(private val safFile: Uri?, private val netFile: File?) { 108 | fun delete() { 109 | netFile?.delete() 110 | } 111 | 112 | fun openInputStream(vm: WizardState): InputStream { 113 | netFile?.let { 114 | return FileInputStream(it) 115 | } 116 | safFile?.let { 117 | val istr = vm.activity.contentResolver.openInputStream(it) 118 | if (istr != null) { 119 | return istr 120 | } 121 | } 122 | throw IllegalStateException("invalid DledFile OR failure") 123 | } 124 | 125 | fun toFile(vm: WizardState): File { 126 | netFile?.let { return it } 127 | safFile?.let { 128 | val istr = vm.activity.contentResolver.openInputStream(it) 129 | if (istr != null) { 130 | val f = File(vm.logic.cacheDir, System.currentTimeMillis().toString()) 131 | vm.copyUnpriv(istr, f) 132 | istr.close() 133 | return f 134 | } 135 | } 136 | throw IllegalStateException("invalid DledFile OR safFile failure") 137 | } 138 | } 139 | suspend fun downloadRemainingFiles(terminal: TerminalList) { 140 | terminal.isCancelled = false 141 | for (id in idNeeded.filter { !chosen.containsKey(it) }) { 142 | if (!inetAvailable.containsKey(id)) 143 | throw IllegalStateException("$id not chosen and not available from inet") 144 | terminal.add(activity.getString(R.string.downloading_s, id)) 145 | val inet = inetAvailable[id]!! 146 | val f = File(logic.cacheDir, System.currentTimeMillis().toString()) 147 | terminal.add(activity.getString(R.string.connecting_text)) 148 | val client = AbmOkHttp(inet.url, f, inet.hash) { readBytes, total, done -> 149 | terminal[terminal.size - 1] = if (done) activity.getString(R.string.done) else 150 | activity.getString( 151 | R.string.download_progress, 152 | "${readBytes / (1024 * 1024)} MiB", "${total / (1024 * 1024)} MiB" 153 | ) 154 | } 155 | terminal.cancel = { terminal.isCancelled = true; client.cancel() } 156 | try { 157 | client.run() 158 | } catch (e: IOException) { 159 | if (terminal.isCancelled == true) { 160 | throw TerminalCancelException() 161 | } 162 | throw e 163 | } 164 | if (terminal.isCancelled == true) { 165 | throw TerminalCancelException() 166 | } 167 | chosen[id] = DownloadedFile(null, f) 168 | } 169 | if (terminal.isCancelled == true) { 170 | throw TerminalCancelException() 171 | } else { 172 | terminal.isCancelled = null 173 | } 174 | } 175 | 176 | fun navigate(next: String) { 177 | prevText = null 178 | nextText = null 179 | onPrev = null 180 | onNext = null 181 | navController.navigate(next) { 182 | launchSingleTop = true 183 | } 184 | } 185 | fun finish() { 186 | mvm.init() 187 | mvm.currentWizardFlow = null 188 | } 189 | 190 | fun copy(inputStream: InputStream, outputStream: OutputStream): Long { 191 | var nread = 0L 192 | val buf = ByteArray(8192) 193 | var n: Int 194 | while (inputStream.read(buf).also { n = it } > 0) { 195 | outputStream.write(buf, 0, n) 196 | nread += n.toLong() 197 | } 198 | inputStream.close() 199 | outputStream.flush() 200 | outputStream.close() 201 | return nread 202 | } 203 | 204 | fun copyUnpriv(inputStream: InputStream, output: File) { 205 | Files.copy(inputStream, output.toPath(), StandardCopyOption.REPLACE_EXISTING) 206 | inputStream.close() 207 | } 208 | 209 | fun copyPriv(inputStream: InputStream, output: File) { 210 | val outStream = SuFileOutputStream.open(output) 211 | copy(inputStream, outStream) 212 | } 213 | } 214 | 215 | 216 | class NavButton(val text: String, val onClick: (WizardState) -> Unit) 217 | class WizardPage(override val name: String, override val prev: NavButton, 218 | override val next: NavButton, override val run: @Composable () -> Unit 219 | ) : IWizardPage 220 | 221 | interface IWizardPage { 222 | val name: String 223 | val prev: NavButton 224 | val next: NavButton 225 | val run: @Composable () -> Unit 226 | } 227 | 228 | @Composable 229 | fun BasicButtonRow(prev: String, onPrev: () -> Unit, 230 | next: String, onNext: () -> Unit) { 231 | Row { 232 | TextButton(onClick = { 233 | onPrev() 234 | }, modifier = Modifier.weight(1f, true)) { 235 | Text(prev) 236 | } 237 | TextButton(onClick = { 238 | onNext() 239 | }, modifier = Modifier.weight(1f, true)) { 240 | Text(next) 241 | } 242 | } 243 | } 244 | 245 | @Composable 246 | fun WizardDownloader(vm: WizardState, next: String) { 247 | Column(Modifier.fillMaxSize()) { 248 | MyInfoCard(stringResource(id = R.string.provide_images)) 249 | for (i in vm.idNeeded) { 250 | Row( 251 | verticalAlignment = Alignment.CenterVertically, 252 | horizontalArrangement = Arrangement.SpaceBetween, 253 | modifier = Modifier.fillMaxWidth() 254 | ) { 255 | Column { 256 | Text(i) 257 | Text( 258 | vm.inetAvailable[i]?.desc ?: stringResource(R.string.user_selected), 259 | color = MaterialTheme.colorScheme.onSurfaceVariant 260 | ) 261 | } 262 | Column { 263 | if (vm.chosen.containsKey(i)) { 264 | Button(onClick = { 265 | vm.chosen[i]!!.delete() 266 | vm.chosen.remove(i) 267 | }) { 268 | Text(stringResource(R.string.undo)) 269 | } 270 | } else { 271 | Button(onClick = { 272 | vm.activity.chooseFile("*/*") { 273 | vm.chosen[i] = WizardState.DownloadedFile(it, null) 274 | } 275 | }) { 276 | Text(stringResource(R.string.choose)) 277 | } 278 | } 279 | } 280 | } 281 | } 282 | val isOk = vm.idNeeded.find { !vm.chosen.containsKey(it) && 283 | !vm.inetAvailable.containsKey(it) } == null 284 | LaunchedEffect(isOk) { 285 | if (isOk) { 286 | vm.onNext = { it.navigate(next) } 287 | vm.nextText = vm.activity.getString(R.string.install) 288 | } else { 289 | vm.onNext = {} 290 | vm.nextText = "" 291 | } 292 | } 293 | } 294 | } 295 | 296 | @Composable 297 | fun WizardTerminalWork(vm: WizardState, logFile: String? = null, 298 | action: suspend (TerminalList) -> Unit) { 299 | TerminalWork(logFile) { 300 | vm.mvm.currentWizardFlow = null 301 | action(it) 302 | } 303 | } -------------------------------------------------------------------------------- /app/src/main/java/org/andbootmgr/app/ext.kt: -------------------------------------------------------------------------------- 1 | package org.andbootmgr.app 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.material.icons.Icons 9 | import androidx.compose.material.icons.filled.Done 10 | import androidx.compose.material3.Card 11 | import androidx.compose.material3.CircularProgressIndicator 12 | import androidx.compose.material3.FilterChip 13 | import androidx.compose.material3.FilterChipDefaults 14 | import androidx.compose.material3.Icon 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.res.painterResource 20 | import androidx.compose.ui.res.stringResource 21 | import androidx.compose.ui.unit.Dp 22 | import androidx.compose.ui.unit.dp 23 | import org.json.JSONObject 24 | 25 | open class ActionAbortedError(e: Exception?) : Exception(e) 26 | class ActionAbortedCleanlyError(e: Exception?) : ActionAbortedError(e) 27 | 28 | @Composable 29 | fun LoadingCircle(text: String, modifier: Modifier = Modifier, paddingBetween: Dp = 20.dp) { 30 | Row( 31 | verticalAlignment = Alignment.CenterVertically, 32 | horizontalArrangement = Arrangement.SpaceAround, 33 | modifier = modifier 34 | ) { 35 | CircularProgressIndicator(Modifier.padding(end = paddingBetween)) 36 | Text(text) 37 | } 38 | } 39 | 40 | @Composable 41 | fun MyFilterChipBar(selected: Int, values: List, onSelectionChanged: (Int) -> Unit) { 42 | Row { 43 | values.forEachIndexed { i, text -> 44 | FilterChip( 45 | selected = selected == i, 46 | onClick = { 47 | onSelectionChanged(i) 48 | }, 49 | label = { Text(text) }, 50 | Modifier.padding(start = 5.dp), 51 | leadingIcon = if (selected == i) { 52 | { 53 | Icon( 54 | imageVector = Icons.Filled.Done, 55 | contentDescription = stringResource(id = R.string.enabled_content_desc), 56 | modifier = Modifier.size(FilterChipDefaults.IconSize) 57 | ) 58 | } 59 | } else { 60 | null 61 | } 62 | ) 63 | } 64 | } 65 | } 66 | 67 | @Composable 68 | fun MyInfoCard(text: String, padding: Dp = 0.dp) { 69 | Card( 70 | modifier = Modifier 71 | .fillMaxWidth() 72 | .padding(padding) 73 | ) { 74 | Row( 75 | Modifier 76 | .fillMaxWidth() 77 | .padding(20.dp) 78 | ) { 79 | Icon( 80 | painterResource(id = R.drawable.ic_about), 81 | stringResource(id = R.string.icon_content_desc) 82 | ) 83 | Text(text) 84 | } 85 | } 86 | } 87 | 88 | fun JSONObject.getStringOrNull(key: String) = if (has(key)) getString(key) else null 89 | 90 | val safeFsRegex = Regex("\\A[A-Za-z0-9_-]+\\z") 91 | val asciiNonEmptyRegex = Regex("\\A\\p{ASCII}+\\z") 92 | val numberRegex = Regex("\\A\\d+\\z") 93 | val partitionTypeCodes = listOf( 94 | Pair("0700", R.string.portable_part), 95 | Pair("8302", R.string.os_userdata), 96 | Pair("8305", R.string.os_system)) -------------------------------------------------------------------------------- /app/src/main/java/org/andbootmgr/app/themes/Simulator.kt: -------------------------------------------------------------------------------- 1 | package org.andbootmgr.app.themes 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.Canvas 5 | import android.graphics.Color 6 | import android.os.Bundle 7 | import android.os.Handler 8 | import android.os.Looper 9 | import android.util.Log 10 | import android.view.KeyEvent 11 | import android.view.MotionEvent 12 | import android.view.View 13 | import android.widget.LinearLayout 14 | import android.widget.Toast 15 | import androidx.activity.OnBackPressedCallback 16 | import androidx.appcompat.app.AppCompatActivity 17 | import androidx.core.view.WindowCompat 18 | import androidx.core.view.WindowInsetsCompat 19 | import androidx.core.view.WindowInsetsControllerCompat 20 | import com.topjohnwu.superuser.Shell 21 | import com.topjohnwu.superuser.io.SuRandomAccessFile 22 | import java.io.File 23 | import kotlin.math.min 24 | import kotlin.system.exitProcess 25 | 26 | 27 | class Simulator : AppCompatActivity() { 28 | init { 29 | Log.i("Simulator","going to load library") 30 | System.loadLibrary("app") 31 | } 32 | external fun start(bitmap: Bitmap, w: Int, h: Int) 33 | external fun stop() 34 | external fun key(key: Int) 35 | private lateinit var v: View 36 | private lateinit var bitmap: Bitmap 37 | private var w: Int = 0 38 | private var h: Int = 0 39 | private lateinit var f: File 40 | private val handler = Handler(Looper.getMainLooper()) 41 | 42 | override fun onCreate(savedInstanceState: Bundle?) { 43 | super.onCreate(savedInstanceState) 44 | Log.i("Simulator", "welcome") 45 | onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { 46 | override fun handleOnBackPressed() { 47 | stop() 48 | } 49 | }) 50 | f = File(intent.getStringExtra("sdCardBlock")!!) // TODO support sd-less with "bootsetBlock" 51 | val l = LinearLayout(this) 52 | v = object : View(this) { 53 | private var firstTime = true 54 | override fun onDraw(canvas: Canvas) { 55 | super.onDraw(canvas) 56 | if (firstTime) { 57 | w = width 58 | h = height 59 | bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888) 60 | Thread { 61 | Log.i("Simulator","going to call start()") 62 | start(bitmap, w, h) 63 | }.run { 64 | name = "droidboot0" 65 | start() 66 | } 67 | firstTime = false 68 | } 69 | canvas.drawColor(Color.BLACK) 70 | canvas.drawBitmap(this@Simulator.bitmap, 0f, 0f, null) 71 | } 72 | 73 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 74 | setMeasuredDimension(when (MeasureSpec.getMode(widthMeasureSpec)) { 75 | MeasureSpec.EXACTLY -> MeasureSpec.getSize(widthMeasureSpec) 76 | MeasureSpec.AT_MOST -> min(Int.MAX_VALUE, MeasureSpec.getSize(widthMeasureSpec)) 77 | else -> Int.MAX_VALUE 78 | }, when (MeasureSpec.getMode(heightMeasureSpec)) { 79 | MeasureSpec.EXACTLY -> MeasureSpec.getSize(heightMeasureSpec) 80 | MeasureSpec.AT_MOST -> min(Int.MAX_VALUE, MeasureSpec.getSize(heightMeasureSpec)) 81 | else -> Int.MAX_VALUE 82 | }) 83 | } 84 | } 85 | l.addView(v, LinearLayout.LayoutParams( 86 | LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT)) 87 | setContentView(l) 88 | WindowCompat.setDecorFitsSystemWindows(window, true) 89 | WindowInsetsControllerCompat(window, l).apply { 90 | systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE 91 | hide(WindowInsetsCompat.Type.systemBars()) 92 | } 93 | } 94 | 95 | @Suppress("unused") // jni 96 | private fun blockCount(): Long { 97 | return Shell.cmd("blockdev --getsz ${f.absolutePath}").exec().out.joinToString("\n").toLong().also { 98 | Log.i("Simulator", "block count: $it") 99 | } 100 | } 101 | 102 | @Suppress("unused") // jni 103 | private fun readBlocks(offset: Long, count: Int): ByteArray { 104 | Log.i("Simulator", "read $count bytes at $offset") 105 | val fo = SuRandomAccessFile.open(f, "r") 106 | fo.seek(offset) 107 | val b = ByteArray(count) 108 | fo.read(b) 109 | fo.close() 110 | return b 111 | } 112 | 113 | @Suppress("unused") // jni 114 | private fun redraw() { 115 | Log.i("Simulator", "redrawing") 116 | v.invalidate() 117 | } 118 | 119 | @Suppress("unused") // jni 120 | private fun screenPrint(str: String) { 121 | handler.post { 122 | Toast.makeText(this, str.trim(), Toast.LENGTH_SHORT).show() 123 | } 124 | } 125 | 126 | override fun onPause() { 127 | stop() 128 | super.onPause() 129 | } 130 | 131 | override fun onStop() { 132 | Log.i("Simulator", "goodbye") 133 | super.onStop() 134 | // droidboot cannot cope with starting twice in same process due to static variables 135 | exitProcess(0) 136 | } 137 | 138 | override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { 139 | return if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP) { 140 | Log.i("Simulator", "key down: $keyCode") 141 | key(if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) 1 else 2) 142 | true 143 | } else { 144 | super.onKeyDown(keyCode, event) 145 | } 146 | } 147 | 148 | override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { 149 | return if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP) { 150 | Log.i("Simulator", "key up: $keyCode") 151 | key(if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) 8 else 16) 152 | true 153 | } else { 154 | super.onKeyUp(keyCode, event) 155 | } 156 | } 157 | 158 | override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { 159 | when (ev?.action) { 160 | MotionEvent.ACTION_UP -> { 161 | Log.i("Simulator", "key up: power") 162 | key(32) 163 | } 164 | MotionEvent.ACTION_DOWN -> { 165 | Log.i("Simulator", "key down: power") 166 | key(4) 167 | } 168 | else -> return false 169 | } 170 | return true 171 | } 172 | } -------------------------------------------------------------------------------- /app/src/main/java/org/andbootmgr/app/themes/Themes.kt: -------------------------------------------------------------------------------- 1 | package org.andbootmgr.app.themes 2 | 3 | import android.content.Intent 4 | import android.widget.Toast 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.Row 9 | import androidx.compose.foundation.layout.Spacer 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.foundation.layout.fillMaxWidth 12 | import androidx.compose.foundation.layout.height 13 | import androidx.compose.foundation.layout.padding 14 | import androidx.compose.foundation.layout.width 15 | import androidx.compose.foundation.rememberScrollState 16 | import androidx.compose.foundation.text.KeyboardOptions 17 | import androidx.compose.foundation.verticalScroll 18 | import androidx.compose.material3.AlertDialog 19 | import androidx.compose.material3.Button 20 | import androidx.compose.material3.Card 21 | import androidx.compose.material3.Checkbox 22 | import androidx.compose.material3.OutlinedButton 23 | import androidx.compose.material3.OutlinedTextField 24 | import androidx.compose.material3.Text 25 | import androidx.compose.runtime.Composable 26 | import androidx.compose.runtime.derivedStateOf 27 | import androidx.compose.runtime.getValue 28 | import androidx.compose.runtime.mutableStateMapOf 29 | import androidx.compose.runtime.mutableStateOf 30 | import androidx.compose.runtime.remember 31 | import androidx.compose.runtime.setValue 32 | import androidx.compose.ui.Alignment 33 | import androidx.compose.ui.Modifier 34 | import androidx.compose.ui.draw.drawWithContent 35 | import androidx.compose.ui.graphics.Color 36 | import androidx.compose.ui.graphics.toArgb 37 | import androidx.compose.ui.platform.LocalDensity 38 | import androidx.compose.ui.res.stringResource 39 | import androidx.compose.ui.text.input.ImeAction 40 | import androidx.compose.ui.text.input.KeyboardCapitalization 41 | import androidx.compose.ui.text.input.KeyboardType 42 | import androidx.compose.ui.tooling.preview.Preview 43 | import androidx.compose.ui.unit.dp 44 | import androidx.compose.ui.unit.sp 45 | import androidx.lifecycle.ViewModel 46 | import androidx.navigation.compose.rememberNavController 47 | import com.github.skydoves.colorpicker.compose.BrightnessSlider 48 | import com.github.skydoves.colorpicker.compose.HsvColorPicker 49 | import com.github.skydoves.colorpicker.compose.rememberColorPickerController 50 | import kotlinx.coroutines.CoroutineScope 51 | import kotlinx.coroutines.Dispatchers 52 | import kotlinx.coroutines.launch 53 | import org.andbootmgr.app.AppContent 54 | import org.andbootmgr.app.MainActivityState 55 | import org.andbootmgr.app.R 56 | import org.andbootmgr.app.asMetaOnSdDeviceInfo 57 | import org.andbootmgr.app.util.AbmTheme 58 | 59 | /* 60 | uint32_t win_bg_color; 61 | uint8_t win_radius; 62 | uint8_t win_border_size; 63 | uint32_t win_border_color; 64 | uint32_t list_bg_color; 65 | uint8_t list_radius; 66 | uint8_t list_border_size; 67 | uint32_t list_border_color; 68 | uint32_t global_font_size; 69 | char* global_font_name; 70 | uint32_t button_unselected_color; 71 | uint32_t button_unselected_text_color; 72 | uint32_t button_selected_color; 73 | uint32_t button_selected_text_color; 74 | uint8_t button_unselected_radius; 75 | uint8_t button_selected_radius; 76 | bool button_grow_default; 77 | uint8_t button_border_unselected_size; 78 | uint32_t button_border_unselected_color; 79 | uint8_t button_border_selected_size; 80 | uint32_t button_border_selected_color; 81 | */ 82 | @OptIn(ExperimentalStdlibApi::class) 83 | @Composable 84 | fun Themes(vm: ThemeViewModel) { 85 | val changes = remember { mutableStateMapOf() } 86 | val state = rememberScrollState() 87 | val errors = remember { 88 | vm.configs.map { cfg -> 89 | derivedStateOf { 90 | val value = 91 | changes[cfg.configKey] ?: vm.mvm.defaultCfg[cfg.configKey] ?: cfg.default 92 | !cfg.validate(value) 93 | } 94 | } 95 | } 96 | Column(modifier = Modifier 97 | .verticalScroll(state) 98 | .fillMaxSize() 99 | .padding(horizontal = 5.dp)) { 100 | val saveChanges = remember { { 101 | if (errors.find { it.value } != null) 102 | Toast.makeText( 103 | vm.mvm.activity!!, 104 | vm.mvm.activity.getString(R.string.invalid_in), 105 | Toast.LENGTH_LONG 106 | ).show() 107 | else CoroutineScope(Dispatchers.Main).launch { 108 | vm.mvm.editDefaultCfg(changes) 109 | changes.clear() 110 | }; Unit 111 | } } 112 | Card(modifier = Modifier 113 | .fillMaxWidth() 114 | .padding(horizontal = 5.dp, vertical = with(LocalDensity.current) { 8.sp.toDp() })) { 115 | Column(modifier = Modifier.padding(10.dp)) { 116 | Text(stringResource(id = R.string.simulator_info)) 117 | Button(onClick = { 118 | if (errors.find { it.value } != null) 119 | Toast.makeText( 120 | vm.mvm.activity!!, 121 | vm.mvm.activity.getString(R.string.invalid_in), 122 | Toast.LENGTH_LONG 123 | ).show() 124 | else CoroutineScope(Dispatchers.Main).launch { 125 | vm.mvm.editDefaultCfg(changes) 126 | changes.clear() 127 | // sync does not work :/ 128 | vm.mvm.remountBootset() 129 | vm.mvm.activity!!.startActivity( 130 | Intent( 131 | vm.mvm.activity, 132 | Simulator::class.java 133 | ).apply { 134 | if (vm.mvm.deviceInfo!!.metaonsd) 135 | putExtra("sdCardBlock", 136 | vm.mvm.deviceInfo!!.asMetaOnSdDeviceInfo().bdev) 137 | else 138 | putExtra("bootsetBlock", vm.mvm.logic!!.abmSdLessBootsetImg) 139 | } 140 | ) 141 | } 142 | }, modifier = Modifier.align(Alignment.End).padding(top = 5.dp), enabled = errors.find { it.value } == null) { 143 | Text(text = stringResource(id = R.string.simulator)) 144 | } 145 | } 146 | } 147 | Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth() 148 | .padding(vertical = with(LocalDensity.current) { 8.sp.toDp() })) { 149 | Button(onClick = saveChanges, enabled = errors.find { it.value } == null) { 150 | Text(stringResource(R.string.save_changes)) 151 | } 152 | Button(onClick = { 153 | for (cfg in vm.configs) { 154 | changes[cfg.configKey] = cfg.default 155 | } 156 | }) { 157 | Text(stringResource(R.string.reset)) 158 | } 159 | } 160 | vm.configs.forEachIndexed { i, cfg -> 161 | val value = changes[cfg.configKey] ?: vm.mvm.defaultCfg[cfg.configKey] ?: cfg.default 162 | val error = errors[i].value 163 | if (cfg is ColorConfig) { 164 | var edit by remember { mutableStateOf(false) } 165 | Row(modifier = Modifier.fillMaxWidth().padding(vertical = with(LocalDensity.current) { 8.sp.toDp() }), 166 | verticalAlignment = Alignment.CenterVertically) { 167 | Text(stringResource(cfg.text)) 168 | Spacer(modifier = Modifier.weight(1f)) 169 | Box( 170 | modifier = Modifier 171 | .drawWithContent { 172 | drawRect( 173 | Color( 174 | (value 175 | .substring(2) 176 | .toIntOrNull(16) ?: 0) or (0xff shl 24) 177 | ) 178 | ) 179 | } 180 | .width(16.dp) 181 | .height(16.dp) 182 | ) 183 | OutlinedButton( 184 | onClick = { edit = true }, 185 | modifier = Modifier.padding(start = 12.5.dp) 186 | ) { 187 | Text(stringResource(id = R.string.edit)) 188 | } 189 | } 190 | if (edit) { 191 | val color = rememberColorPickerController() 192 | AlertDialog(onDismissRequest = { edit = false }, confirmButton = { 193 | Button(onClick = { 194 | changes[cfg.configKey] = 195 | "0x" + (color.selectedColor.value.toArgb() and (0xff shl 24).inv()) 196 | .toHexString().substring(2, 8) 197 | edit = false 198 | }) { 199 | Text(stringResource(id = R.string.ok)) 200 | } 201 | }, dismissButton = { 202 | Button(onClick = { edit = false }) { 203 | Text(stringResource(id = R.string.cancel)) 204 | } 205 | }, title = { Text(stringResource(id = cfg.text)) }, text = { 206 | Column { 207 | HsvColorPicker( 208 | modifier = Modifier.height(200.dp).padding(bottom = 10.dp), 209 | controller = color, 210 | initialColor = Color( 211 | (value.substring(2).toIntOrNull(16) ?: 0) or (0xff shl 24) 212 | ) 213 | ) 214 | BrightnessSlider(modifier = Modifier.height(35.dp), controller = color) 215 | } 216 | }) 217 | } 218 | } else if (cfg is BoolConfig) { 219 | Row(verticalAlignment = Alignment.CenterVertically) { 220 | Text(stringResource(id = cfg.text)) 221 | Spacer(modifier = Modifier.weight(1f)) 222 | Checkbox(checked = value.toBooleanStrictOrNull() == true, onCheckedChange = { 223 | changes[cfg.configKey] = it.toString() 224 | }) 225 | } 226 | } else { 227 | OutlinedTextField( 228 | value = value, 229 | modifier = Modifier.fillMaxWidth() 230 | .let { 231 | if (i > 0 && vm.configs[i - 1] is ColorConfig) 232 | it.padding(top = with(LocalDensity.current) { 4.sp.toDp() } ,bottom = with(LocalDensity.current) { 8.sp.toDp() }) 233 | else 234 | it.padding(vertical = with(LocalDensity.current) { 8.sp.toDp() }) 235 | }, 236 | onValueChange = { 237 | changes[cfg.configKey] = it.trim() 238 | }, 239 | label = { Text(stringResource(id = cfg.text)) }, 240 | isError = error, 241 | keyboardOptions = KeyboardOptions( 242 | keyboardType = if (cfg is IntConfig || cfg is ShortConfig) 243 | KeyboardType.Decimal else KeyboardType.Text, 244 | capitalization = KeyboardCapitalization.None, 245 | imeAction = ImeAction.Next, 246 | autoCorrect = false 247 | ) 248 | ) 249 | } 250 | } 251 | } 252 | } 253 | 254 | open class Config(val text: Int, val configKey: String, val default: String, 255 | val validate: (String) -> Boolean) 256 | class ColorConfig(text: Int, configKey: String, default: String) : Config(text, configKey, 257 | default, { it.startsWith("0x") && it.length == 8 && 258 | (it.substring(2).toIntOrNull(16) ?: -1) in 0..0xffffff }) 259 | class BoolConfig(text: Int, configKey: String, default: String) : Config(text, configKey, 260 | default, { it.toBooleanStrictOrNull() != null}) 261 | class IntConfig(text: Int, configKey: String, default: String) 262 | : Config(text, configKey, default, { it.toIntOrNull() != null }) 263 | class ShortConfig(text: Int, configKey: String, default: String) 264 | : Config(text, configKey, default, { it.toShortOrNull() != null }) 265 | 266 | class ThemeViewModel(val mvm: MainActivityState) : ViewModel() { 267 | val configs = listOf( 268 | ColorConfig(R.string.win_bg_color, "win_bg_color", "0x000000"), 269 | ShortConfig(R.string.win_radius, "win_radius", "0"), 270 | ShortConfig(R.string.win_border_size, "win_border_size", "0"), 271 | ColorConfig(R.string.win_border_color, "win_border_color", "0xffffff"), 272 | ColorConfig(R.string.list_bg_color, "list_bg_color", "0x000000"), 273 | ShortConfig(R.string.list_radius, "list_radius", "0"), 274 | ShortConfig(R.string.list_border_size, "list_border_size", "0"), 275 | ColorConfig(R.string.list_border_color, "list_border_color", "0xffffff"), 276 | IntConfig(R.string.global_font_size, "global_font_size", "0"), 277 | Config( 278 | R.string.global_font_name, 279 | "global_font_name", 280 | "" 281 | ) { true /* should check if exists later */ }, 282 | ColorConfig(R.string.button_unselected_color, "button_unselected_color", "0x000000"), 283 | ColorConfig( 284 | R.string.button_unselected_text_color, 285 | "button_unselected_text_color", 286 | "0xffffff" 287 | ), 288 | ShortConfig( 289 | R.string.button_unselected_radius, 290 | "button_unselected_radius", 291 | "0" 292 | ), 293 | ColorConfig(R.string.button_selected_color, "button_selected_color", "0xff9800"), 294 | ColorConfig(R.string.button_selected_text_color, "button_selected_text_color", "0x000000"), 295 | ShortConfig( 296 | R.string.button_selected_radius, 297 | "button_selected_radius", 298 | "0" 299 | ), 300 | BoolConfig( 301 | R.string.button_grow_default, 302 | "button_grow_default", 303 | "true" 304 | ), 305 | ColorConfig( 306 | R.string.button_border_unselected_color, 307 | "button_border_unselected_color", 308 | "0xffffff" 309 | ), 310 | IntConfig( 311 | R.string.button_border_unselected_size, 312 | "button_border_unselected_size", 313 | "1" 314 | ), 315 | ColorConfig( 316 | R.string.button_border_selected_color, 317 | "button_border_selected_color", 318 | "0xffffff" 319 | ), 320 | IntConfig( 321 | R.string.button_border_selected_size, 322 | "button_border_selected_size", 323 | "1" 324 | ) 325 | ) 326 | } 327 | 328 | @Preview 329 | @Composable 330 | fun ThemePreview() { 331 | val vm = MainActivityState(null) 332 | AbmTheme { 333 | AppContent(vm, rememberNavController()) { 334 | Box(modifier = Modifier.padding(it)) { 335 | Themes(vm.theme) 336 | } 337 | } 338 | } 339 | } -------------------------------------------------------------------------------- /app/src/main/java/org/andbootmgr/app/util/AbmOkHttp.kt: -------------------------------------------------------------------------------- 1 | package org.andbootmgr.app.util 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.withContext 5 | import okhttp3.Call 6 | import okhttp3.MediaType 7 | import okhttp3.OkHttpClient 8 | import okhttp3.Request 9 | import okhttp3.Response 10 | import okhttp3.ResponseBody 11 | import okhttp3.internal.http2.StreamResetException 12 | import okio.Buffer 13 | import okio.BufferedSource 14 | import okio.ForwardingSource 15 | import okio.HashingSink 16 | import okio.Source 17 | import okio.buffer 18 | import okio.sink 19 | import org.andbootmgr.app.HashMismatchException 20 | import java.io.File 21 | import java.io.IOException 22 | import java.util.concurrent.TimeUnit 23 | 24 | class AbmOkHttp(private val url: String, private val f: File, private val hash: String?, 25 | pl: ((bytesRead: Long, contentLength: Long, done: Boolean) -> Unit)?) { 26 | private val client by lazy { OkHttpClient().newBuilder().readTimeout(1L, TimeUnit.HOURS) 27 | .let { c -> 28 | if (pl != null) 29 | c.addNetworkInterceptor { 30 | val originalResponse: Response = it.proceed(it.request()) 31 | return@addNetworkInterceptor originalResponse.newBuilder() 32 | .body(ProgressResponseBody(originalResponse.body!!, pl)) 33 | .build() 34 | } 35 | else c 36 | }.build() } 37 | private var call: Call? = null 38 | 39 | suspend fun run(): Boolean { 40 | return withContext(Dispatchers.IO) { 41 | val request = 42 | Request.Builder().url(url).build() 43 | val call = client.newCall(request) 44 | this@AbmOkHttp.call = call 45 | val response = call.execute() 46 | val rawSink = f.sink() 47 | val sink = if (hash != null) HashingSink.sha256(rawSink) else rawSink 48 | val buffer = sink.buffer() 49 | try { 50 | buffer.writeAll(response.body!!.source()) 51 | } catch (e: StreamResetException) { 52 | if (e.message != "stream was reset: CANCEL") 53 | throw e 54 | buffer.close() 55 | f.delete() 56 | return@withContext false 57 | } 58 | buffer.close() 59 | if (!call.isCanceled()) { 60 | val realHash = if (hash != null) (sink as HashingSink).hash.hex() else null 61 | if (hash != null && realHash != hash) { 62 | try { 63 | f.delete() 64 | } catch (_: Exception) {} 65 | throw HashMismatchException("hash $realHash does not match expected hash $hash") 66 | } 67 | return@withContext true 68 | } 69 | return@withContext false 70 | } 71 | } 72 | 73 | fun cancel() { 74 | call?.cancel() 75 | f.delete() 76 | } 77 | 78 | private class ProgressResponseBody( 79 | private val responseBody: ResponseBody, 80 | private val progressListener: (bytesRead: Long, contentLength: Long, done: Boolean) -> Unit 81 | ) : 82 | ResponseBody() { 83 | private var bufferedSource: BufferedSource? = null 84 | override fun contentType(): MediaType? { 85 | return responseBody.contentType() 86 | } 87 | 88 | override fun contentLength(): Long { 89 | return responseBody.contentLength() 90 | } 91 | 92 | override fun source(): BufferedSource { 93 | if (bufferedSource == null) { 94 | bufferedSource = source(responseBody.source()).buffer() 95 | } 96 | return bufferedSource!! 97 | } 98 | 99 | private fun source(source: Source): Source { 100 | return object : ForwardingSource(source) { 101 | var totalBytesRead = 0L 102 | 103 | @Throws(IOException::class) 104 | override fun read(sink: Buffer, byteCount: Long): Long { 105 | val bytesRead = super.read(sink, byteCount) 106 | // read() returns the number of bytes read, or -1 if this source is exhausted. 107 | totalBytesRead += if (bytesRead != -1L) bytesRead else 0 108 | progressListener( 109 | totalBytesRead, 110 | responseBody.contentLength(), 111 | bytesRead == -1L 112 | ) 113 | return bytesRead 114 | } 115 | } 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /app/src/main/java/org/andbootmgr/app/util/ConfigFile.kt: -------------------------------------------------------------------------------- 1 | package org.andbootmgr.app.util 2 | 3 | import android.util.Log 4 | import androidx.compose.runtime.mutableStateMapOf 5 | import com.topjohnwu.superuser.io.SuFile 6 | import com.topjohnwu.superuser.io.SuFileInputStream 7 | import com.topjohnwu.superuser.io.SuFileOutputStream 8 | import org.andbootmgr.app.ActionAbortedCleanlyError 9 | import org.andbootmgr.app.ActionAbortedError 10 | import java.io.* 11 | import kotlin.collections.set 12 | 13 | 14 | class ConfigFile(private val data: MutableMap = HashMap()) { 15 | operator fun get(name: String): String? { 16 | return data[name] 17 | } 18 | 19 | operator fun set(name: String, value: String) { 20 | data[name] = value 21 | } 22 | 23 | fun toMap(): Map { 24 | return data 25 | } 26 | 27 | private fun exportToString(): String { 28 | val out = StringBuilder() 29 | for (key in data.keys) { 30 | out.append(key).append(" ").append(get(key)).append("\n") 31 | } 32 | return out.toString() 33 | } 34 | 35 | @Throws(ActionAbortedError::class) 36 | fun exportToFile(file: File) { 37 | try { 38 | val outStream = SuFileOutputStream.open(file) 39 | outStream.write(exportToString().toByteArray()) 40 | outStream.close() 41 | } catch (e: IOException) { 42 | throw ActionAbortedError(e) 43 | } 44 | } 45 | 46 | fun has(s: String): Boolean { 47 | return data.containsKey(s) 48 | } 49 | 50 | companion object { 51 | private fun importFromString(s: String): ConfigFile { 52 | val out = ConfigFile() 53 | var line: String 54 | for (lline in s.split("\n").toTypedArray()) { 55 | line = lline.trim() 56 | val delim = line.indexOf(" ") 57 | if (delim != -1) 58 | out[line.substring(0, delim)] = 59 | line.substring(delim).trim() 60 | } 61 | return out 62 | } 63 | 64 | @Throws(ActionAbortedCleanlyError::class) 65 | fun importFromFile(f: File): ConfigFile { 66 | val s = ByteArrayOutputStream() 67 | val b = ByteArray(1024) 68 | var o: Int 69 | val i: InputStream = try { 70 | SuFileInputStream.open(f) 71 | } catch (e: FileNotFoundException) { 72 | throw ActionAbortedCleanlyError(e) 73 | } 74 | while (true) { 75 | try { 76 | if (i.read(b).also { o = it } <= 1) break 77 | s.write(b, 0, o) 78 | } catch (e: IOException) { 79 | throw ActionAbortedCleanlyError(e) 80 | } 81 | } 82 | return importFromString(s.toString()) 83 | } 84 | 85 | fun importFromFolder(f: File): Map { 86 | val out = hashMapOf() 87 | for (i in SuFile.open(f.absolutePath).listFiles()!!) { 88 | try { 89 | out[importFromFile(i)] = i 90 | } catch (e: ActionAbortedCleanlyError) { 91 | Log.e("ABM", Log.getStackTraceString(e)) 92 | } 93 | } 94 | return out 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/org/andbootmgr/app/util/SDLessUtils.kt: -------------------------------------------------------------------------------- 1 | package org.andbootmgr.app.util 2 | 3 | import com.topjohnwu.superuser.Shell 4 | import com.topjohnwu.superuser.io.SuFile 5 | import org.andbootmgr.app.DeviceLogic 6 | import java.io.File 7 | import kotlin.math.max 8 | 9 | object SDLessUtils { 10 | fun getFreeSpaceBytes(logic: DeviceLogic): Long { 11 | val raw = Shell.cmd("stat -f ${logic.abmSdLessBootset} -c '%f:%S'").exec().out.joinToString("\n").split(":").map { it.trim().toLong() } 12 | return max(raw[0] * raw[1] - 1024L * 1024L * 1024L, 0) 13 | } 14 | 15 | fun getSpaceUsageBytes(logic: DeviceLogic, fn: String): Long? { 16 | return SuFile.open(logic.abmSdLessBootset, fn).listFiles()?.let { 17 | it.sumOf { it.length() } 18 | } 19 | } 20 | 21 | fun map(logic: DeviceLogic, name: String, mapFile: File, terminal: MutableList? = null): Boolean { 22 | val dmPath = File(logic.dmBase, name) 23 | if (SuFile.open(dmPath.toURI()).exists()) 24 | return true 25 | val tempFile = File(logic.cacheDir, "${System.currentTimeMillis()}.txt") 26 | if (!Shell.cmd( 27 | File(logic.toolkitDir, "droidboot_map_to_dm") 28 | .absolutePath + " " + mapFile.absolutePath + " " + tempFile.absolutePath 29 | ).let { 30 | if (terminal != null) 31 | it.to(terminal) 32 | else it 33 | }.exec().isSuccess 34 | ) { 35 | return false 36 | } 37 | return Shell.cmd("dmsetup create $name ${tempFile.absolutePath}").let { 38 | if (terminal != null) 39 | it.to(terminal) 40 | else it 41 | }.exec().isSuccess 42 | } 43 | 44 | fun unmap(logic: DeviceLogic, name: String, force: Boolean, terminal: MutableList? = null): Boolean { 45 | val dmPath = File(logic.dmBase, name) 46 | if (SuFile.open(dmPath.toURI()).exists()) 47 | return Shell.cmd( 48 | "dmsetup remove " + (if (force) "-f " else "") + name 49 | ).let { 50 | if (terminal != null) 51 | it.to(terminal) 52 | else it 53 | }.exec().isSuccess && !SuFile.open(dmPath.toURI()).exists() 54 | return true 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/org/andbootmgr/app/util/SDUtils.kt: -------------------------------------------------------------------------------- 1 | package org.andbootmgr.app.util 2 | 3 | import android.util.Log 4 | import com.topjohnwu.superuser.Shell 5 | import org.andbootmgr.app.MetaOnSdDeviceInfo 6 | import java.util.* 7 | import java.util.stream.Collectors 8 | import kotlin.jvm.optionals.getOrElse 9 | 10 | object SDUtils { 11 | 12 | // Unmount drive 13 | fun umsd(meta: SDPartitionMeta): String { 14 | val s = StringBuilder() 15 | for (p in meta.p) s.append(p.unmount()).append(" && ") 16 | val e = s.toString() 17 | return if (e.isEmpty()) "true" else e.substring(0, e.length - 4) 18 | } 19 | 20 | fun generateMeta(deviceInfo: MetaOnSdDeviceInfo): SDPartitionMeta? { 21 | val meta: SDPartitionMeta 22 | val r = 23 | Shell.cmd("printf \"mm:%d:%d\\n\" `stat -c '0x%t 0x%T' ${deviceInfo.bdev}` && sgdisk ${deviceInfo.bdev} --print") 24 | .exec() 25 | meta = if (r.isSuccess) SDPartitionMeta() else return null 26 | try { 27 | meta.path = deviceInfo.bdev 28 | meta.ppath = deviceInfo.pbdev 29 | meta.nid = 1 30 | var temp: Long = -1 31 | var o: String 32 | for (oo in r.out) { 33 | o = oo 34 | if (o.startsWith("Disk ") && !o.contains("GUID")) { 35 | val t = o.split(": ").toTypedArray()[1].split(", ").toTypedArray() 36 | meta.sectors = t[0].replace(" sectors", "").toLong() 37 | meta.friendlySize = t[1] 38 | } else if (o.startsWith("Logical sector size: ") && o.endsWith(" bytes")) { 39 | meta.logicalSectorSizeBytes = 40 | o.replace("Logical sector size: ", "").replace(" bytes", "").toInt() 41 | } else if (o.startsWith("Sector size (logical/physical): ") && o.endsWith(" bytes")) { 42 | meta.logicalSectorSizeBytes = 43 | o.replace("Sector size (logical/physical): ", "").replace(" bytes", "") 44 | .split("/").toTypedArray()[0].toInt() 45 | } else if (o.startsWith("Disk identifier (GUID): ")) { 46 | meta.guid = o.replace("Disk identifier (GUID): ", "") 47 | } else if (o.startsWith("Partition table holds up to ")) { 48 | meta.maxEntries = 49 | o.replace("Partition table holds up to ", "").replace(" entries", "").toInt() 50 | } else if (o.startsWith("First usable sector is ")) { 51 | meta.firstUsableSector = o.replace("First usable sector is ", "") 52 | .replaceFirst(", last usable sector is .*$".toRegex(), "").toLong() 53 | meta.lastUsableSector = o.replace( 54 | "First usable sector is " + meta.firstUsableSector + ", last usable sector is ", 55 | "" 56 | ).toLong() 57 | meta.usableSectors = meta.lastUsableSector - meta.firstUsableSector 58 | temp = meta.firstUsableSector 59 | } else if (o.startsWith("Partitions will be aligned on ") && o.endsWith("-sector boundaries")) { 60 | meta.alignSector = o.replace("Partitions will be aligned on ", "") 61 | .replace("-sector boundaries", "").toLong() 62 | } else if (o.startsWith("Total free space is ") && o.contains("sectors")) { 63 | val t = o.replace(")", "").split("(").toTypedArray() 64 | meta.totalFreeSectors = 65 | t[0].replace("Total free space is ", "").replace(" sectors ", "").toLong() 66 | meta.totalFreeFancy = t[1] 67 | } else if (o.startsWith("mm:")) { 68 | val t = o.trim().split(":").toTypedArray() 69 | meta.major = t[1].toInt() 70 | meta.minor = t[2].toInt() 71 | } else if (o == "" || o.startsWith("Number Start (sector) End (sector) Size Code Name") || o.startsWith( 72 | "Main partition table begins at" 73 | ) 74 | ) { 75 | assert(true) //avoid empty statement warning but do nothing 76 | } else if (o.startsWith(" ") && o.contains("iB")) { 77 | while (o.contains(" ")) o = o.trim { it <= ' ' }.replace(" ", " ") 78 | val ocut = o.split(" ").toTypedArray() 79 | val id = ocut[0].toInt() 80 | val code = ocut[5] 81 | val type = when (code) { 82 | "8301" -> 83 | if (id == 1) PartitionType.RESERVED else PartitionType.UNKNOWN 84 | "0700" -> PartitionType.PORTABLE 85 | "FFFF" -> 86 | if (id == 1) PartitionType.RESERVED else PartitionType.ADOPTED 87 | "8302" -> PartitionType.DATA 88 | "8305" -> PartitionType.SYSTEM 89 | "8300" -> PartitionType.UNKNOWN 90 | else -> PartitionType.UNKNOWN 91 | } 92 | val p = Partition( 93 | meta, 94 | type, 95 | id, 96 | meta.ppath + id, 97 | ocut[1].toLong() /* startSector */, 98 | ocut[2].toLong()/* endSector */, 99 | meta.logicalSectorSizeBytes, 100 | code, 101 | ocut.copyOfRange(6, ocut.size).toList().joinToString(" ") /* label/name */, 102 | meta.major, 103 | meta.minor + id 104 | ) 105 | if (meta.nid == p.id) meta.nid++ 106 | meta.p.add(p) 107 | meta.u.add(p) 108 | } else if (o=="Found invalid GPT and valid MBR; converting MBR to GPT format"){ 109 | meta.ismbr=true 110 | } else if(o=="***************************************************************" || o=="in memory. ") { 111 | assert(true) 112 | } else { 113 | Log.e("ABM SDUtils", "can't handle $o") 114 | return null 115 | } 116 | } 117 | val l = meta.u.stream() 118 | .collect(Collectors.toList()) // this actually copies, therefore, we need this. 119 | l.sortWith { o1: Partition, o2: Partition -> 120 | // this is like this because startSector is long and I don't want overflows due to casting 121 | if (o1.startSector - o2.startSector < -1) return@sortWith -1 else if (o1.startSector == o2.startSector) return@sortWith 0 else return@sortWith 1 122 | } 123 | for (p in l) { 124 | if (p.startSector > temp + meta.alignSector) meta.u.add( 125 | Partition.FreeSpace( 126 | meta, 127 | temp, 128 | p.startSector - 1, 129 | meta.logicalSectorSizeBytes 130 | ) 131 | ) 132 | temp = p.endSector 133 | } 134 | if (meta.lastUsableSector > temp + meta.alignSector) meta.u.add( 135 | Partition.FreeSpace( 136 | meta, 137 | temp, 138 | meta.lastUsableSector - 1, 139 | meta.logicalSectorSizeBytes 140 | ) 141 | ) 142 | meta.s = meta.u.subList(0, meta.u.size) 143 | meta.s.sortWith { o1: Partition, o2: Partition -> 144 | // this is like this because startSector is long and I don't want overflows due to casting 145 | if (o1.startSector - o2.startSector < -1) return@sortWith -1 else if (o1.startSector == o2.startSector) return@sortWith 0 else return@sortWith 1 146 | } 147 | Log.i("ABM", meta.toString()) 148 | return meta 149 | } catch (e: Exception) { 150 | Log.e("ABM", Log.getStackTraceString(e)) 151 | return null 152 | } 153 | } 154 | 155 | enum class PartitionType { 156 | RESERVED, ADOPTED, PORTABLE, UNKNOWN, FREE, SYSTEM, DATA 157 | } 158 | 159 | open class Partition( 160 | var meta: SDPartitionMeta, 161 | val type: PartitionType, 162 | val id: Int, 163 | open val path: String, 164 | val startSector: Long, 165 | val endSector: Long, 166 | bytes: Int, 167 | val code: String, 168 | val name: String, 169 | val major: Int, 170 | val minor: Int 171 | ) { 172 | val size: Long = endSector - startSector 173 | val sizeFancy: String = SOUtils.humanReadableByteCountBin(size * bytes) 174 | override fun toString(): String { 175 | return "Partition{" + 176 | "type=" + type + 177 | ", id=" + id + 178 | ", startSector=" + startSector + 179 | ", endSector=" + endSector + 180 | ", size=" + size + 181 | ", sizeFancy='" + sizeFancy + '\'' + 182 | ", code='" + code + '\'' + 183 | ", name='" + name + '\'' + 184 | ", minor=" + minor + 185 | '}' 186 | } 187 | 188 | fun mount(): String { 189 | return when (this.type) { 190 | PartitionType.FREE -> "echo 'Why are you trying to mount free space?!'" 191 | PartitionType.PORTABLE -> "sm mount public:${this.major},${this.minor}" 192 | PartitionType.ADOPTED -> "sm mount private:${this.major},${this.minor}" 193 | else -> "echo 'Warning: Unsure on how to mount this partition.'" 194 | } 195 | } 196 | fun unmount(): String { 197 | return when (type) { 198 | PartitionType.FREE -> "true" 199 | PartitionType.PORTABLE -> "sm unmount public:${this.major},${this.minor}" 200 | PartitionType.ADOPTED -> "sm unmount private:${this.major},${this.minor}" 201 | PartitionType.RESERVED -> { 202 | if (name == "abm_settings") { 203 | // Unmounting bootset is handled by DeviceLogic, don't forget to do that 204 | "true" 205 | } else { 206 | "echo 'Warning: Unsure on how to unmount this partition.'" 207 | } 208 | } 209 | PartitionType.SYSTEM, PartitionType.DATA -> { 210 | // TODO rework this when dual android is supported by looking at current os' xpart 211 | // but for that we need to know the current os entry :D 212 | "true" 213 | } 214 | else -> "echo 'Warning: Unsure on how to unmount this partition.'" 215 | } 216 | } 217 | 218 | fun delete(): String { 219 | if (type == PartitionType.FREE) 220 | return "echo 'Tried to delete free space'" 221 | return "sgdisk " + meta.path + " --delete " + id 222 | } 223 | fun rename(newName: String): String { 224 | if (type == PartitionType.FREE) 225 | return "echo 'Tried to rename free space'; exit 1" 226 | return "sgdisk " + meta.path + " --change-name " + id + ":'" + newName.replace("'", "") + "'" 227 | } 228 | 229 | class FreeSpace(meta: SDPartitionMeta, start: Long, end: Long, bytes: Int) : 230 | Partition(meta, 231 | PartitionType.FREE, 0, "", (start / 2048 + 1) * 2048, end, bytes, "", "", 0, 0) { 232 | 233 | override val path 234 | get() = throw IllegalArgumentException() 235 | 236 | override fun toString(): String { 237 | return "FreeSpace{" + 238 | "startSector=" + startSector + 239 | ", endSector=" + endSector + 240 | ", size=" + size + 241 | ", sizeFancy='" + sizeFancy + '\'' + 242 | '}' 243 | } 244 | 245 | // start & end are RELATIVE to startSector of this instance 246 | fun create(start: Long, end: Long, typecode: String, name: String): String { 247 | val rstart = startSector + start 248 | val rend = startSector + end 249 | val a = rstart > endSector 250 | val b = start < 0 251 | val c = rend > endSector 252 | val d = end < start 253 | if (a || b || c || d) { 254 | return "echo 'Invalid values ($start:$end - $rstart>$startSector:$endSector>$rend - $a $b $c $d). Aborting...'; exit 1" 255 | } 256 | return "sgdisk ${meta.path} --new ${meta.nid}:$rstart:$rend --typecode ${meta.nid}:$typecode --change-name ${meta.nid}:'${name.replace("'", "")}' && sleep 1 && ls ${meta.ppath}${meta.nid}" + when(typecode) { 257 | "0700" -> " && sm format public:${meta.major},${meta.minor+meta.nid}" 258 | "8301" -> " && mkfs.ext4 ${meta.ppath}${meta.nid}" 259 | else -> "" 260 | } 261 | } 262 | } 263 | } 264 | 265 | class SDPartitionMeta { 266 | // List of partitions sorted by partition number 267 | var p: MutableList = ArrayList() 268 | // List of partitions with free space entries, unsorted 269 | var u: MutableList = ArrayList() 270 | // List of partitions with free space entries, sorted by physical order 271 | var s: MutableList = ArrayList() 272 | var friendlySize: String? = null 273 | var guid: String? = null 274 | var sectors: Long = 0 275 | var logicalSectorSizeBytes = 0 276 | var maxEntries = 0 277 | var firstUsableSector: Long = 0 278 | var lastUsableSector: Long = 0 279 | var ismbr : Boolean = false 280 | var alignSector: Long = 0 281 | var totalFreeSectors: Long = 0 282 | var totalFreeFancy: String? = null 283 | var usableSectors: Long = 0 284 | // First free partition number 285 | var nid = 0 286 | var major = 0 287 | var minor = 0 288 | var path: String? = null 289 | var ppath: String? = null 290 | override fun toString(): String { 291 | return "SDPartitionMeta{" + 292 | "p=" + p + 293 | ", s=" + s + 294 | ", friendlySize='" + friendlySize + '\'' + 295 | ", guid='" + guid + '\'' + 296 | ", sectors=" + sectors + 297 | ", logicalSectorSizeBytes=" + logicalSectorSizeBytes + 298 | ", maxEntries=" + maxEntries + 299 | ", firstUsableSector=" + firstUsableSector + 300 | ", lastUsableSector=" + lastUsableSector + 301 | ", alignSector=" + alignSector + 302 | ", totalFreeSectors=" + totalFreeSectors + 303 | ", totalFreeFancy='" + totalFreeFancy + '\'' + 304 | ", usableSectors=" + usableSectors + 305 | '}' 306 | } 307 | 308 | // Get partition by kernel id 309 | fun dumpKernelPartition(id: Int): Partition { 310 | return p.stream().filter { it.id == id }.findFirst().getOrElse { 311 | throw IllegalArgumentException("no such partition with id $id, have: ${this.p}") } 312 | } 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /app/src/main/java/org/andbootmgr/app/util/SOUtils.java: -------------------------------------------------------------------------------- 1 | package org.andbootmgr.app.util; 2 | 3 | import java.text.CharacterIterator; 4 | import java.text.StringCharacterIterator; 5 | import java.util.Locale; 6 | 7 | public class SOUtils { 8 | 9 | // https://stackoverflow.com/questions/3758606/how-can-i-convert-byte-size-into-a-human-readable-format-in-java 10 | public static String humanReadableByteCountBin(long bytes) { 11 | long absB = bytes == Long.MIN_VALUE ? Long.MAX_VALUE : Math.abs(bytes); 12 | if (absB < 1024) { 13 | return bytes + " B"; 14 | } 15 | long value = absB; 16 | CharacterIterator ci = new StringCharacterIterator("KMGTPE"); 17 | for (int i = 40; i >= 0 && absB > 0xfffccccccccccccL >> i; i -= 10) { 18 | value >>= 10; 19 | ci.next(); 20 | } 21 | value *= Long.signum(bytes); 22 | return String.format(Locale.ENGLISH,"%.1f %ciB", value / 1024.0, ci.current()); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/org/andbootmgr/app/util/StayAliveService.kt: -------------------------------------------------------------------------------- 1 | package org.andbootmgr.app.util 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.ComponentName 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.ServiceConnection 8 | import android.os.Binder 9 | import android.os.IBinder 10 | import android.os.PowerManager 11 | import android.os.PowerManager.WakeLock 12 | import android.util.Log 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.runtime.mutableStateOf 15 | import androidx.compose.runtime.setValue 16 | import androidx.core.app.NotificationChannelCompat 17 | import androidx.core.app.NotificationCompat 18 | import androidx.core.app.NotificationManagerCompat 19 | import androidx.core.util.Supplier 20 | import androidx.lifecycle.LifecycleService 21 | import androidx.lifecycle.lifecycleScope 22 | import kotlinx.coroutines.delay 23 | import kotlinx.coroutines.launch 24 | import org.andbootmgr.app.R 25 | 26 | class StayAliveService : LifecycleService() { 27 | companion object { 28 | private const val TAG = "ABM_StayAlive" 29 | private const val SERVICE_CHANNEL = "service" 30 | private const val FG_SERVICE_ID = 1001 31 | var instance by mutableStateOf(null) 32 | private set 33 | } 34 | private lateinit var wakeLock: WakeLock 35 | private var work: (suspend (Context) -> Unit)? = null 36 | var isWorkDone by mutableStateOf(false) 37 | private set 38 | private var extra: Any? = null 39 | val workExtra: Any 40 | get() { 41 | if (destroyed) { 42 | throw IllegalStateException("This StayAliveService was leaked. It is already destroyed.") 43 | } 44 | if (work == null) { 45 | throw IllegalStateException("Tried to access work extra before work was set.") 46 | } 47 | return extra!! 48 | } 49 | private var destroyed = false 50 | private var onDone: (() -> Unit)? = null 51 | @SuppressLint("WakelockTimeout") 52 | fun startWork(work: suspend (Context) -> Unit, extra: Any) { 53 | if (destroyed) { 54 | throw IllegalStateException("This StayAliveService was leaked. It is already destroyed.") 55 | } 56 | if (this.work != null) { 57 | throw IllegalStateException("Work already set on this StayAliveService.") 58 | } 59 | // make sure we get promoted to started service 60 | startService(Intent(this, this::class.java)) 61 | this.work = work 62 | this.extra = extra 63 | if (instance != null) { 64 | throw IllegalStateException("expected instance to be null for non-running service") 65 | } 66 | instance = this 67 | lifecycleScope.launch { 68 | wakeLock.acquire() 69 | try { 70 | Log.i(TAG, "Starting work...") 71 | this@StayAliveService.work!!.invoke(this@StayAliveService) 72 | Log.i(TAG, "Done working!") 73 | isWorkDone = true 74 | onDone?.invoke() 75 | } finally { 76 | wakeLock.release() 77 | } 78 | } 79 | } 80 | 81 | override fun onCreate() { 82 | super.onCreate() 83 | NotificationManagerCompat.from(this).createNotificationChannel( 84 | NotificationChannelCompat.Builder(SERVICE_CHANNEL, 85 | NotificationManagerCompat.IMPORTANCE_HIGH) 86 | .setName(getString(R.string.service_notifications)) 87 | .setShowBadge(true) 88 | .setLightsEnabled(false) 89 | .setVibrationEnabled(false) 90 | .setSound(null, null) 91 | .build() 92 | ) 93 | wakeLock = getSystemService(PowerManager::class.java) 94 | .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "ABM::StayAlive(user_task)") 95 | lifecycleScope.launch { 96 | delay(10000) 97 | // If there was nothing started after 10 seconds, there's a bug. 98 | if (work == null) { 99 | throw IllegalStateException("No work was submitted to StayAliveService after 10 seconds.") 100 | } 101 | } 102 | } 103 | 104 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 105 | super.onStartCommand(intent, flags, startId) 106 | startForeground(FG_SERVICE_ID, NotificationCompat.Builder(this, SERVICE_CHANNEL) 107 | .setSmallIcon(R.drawable.abm_notif) 108 | .setContentTitle(getString(R.string.abm_processing_title)) 109 | .setContentText(getString(R.string.abm_processing_text)) 110 | .setOngoing(true) 111 | .setOnlyAlertOnce(true) 112 | .setLocalOnly(true) 113 | .build()) 114 | return START_NOT_STICKY 115 | } 116 | 117 | override fun onBind(intent: Intent): IBinder { 118 | super.onBind(intent) 119 | if (work != null) { 120 | throw IllegalStateException("Work was already set on this StayAliveService.") 121 | } 122 | return object : Binder(), Supplier { 123 | override fun get(): StayAliveService { 124 | return this@StayAliveService 125 | } 126 | } 127 | } 128 | 129 | override fun onDestroy() { 130 | Log.i(TAG, "Goodbye!") 131 | if (!isWorkDone) 132 | throw IllegalStateException("work isn't done but destroying?") 133 | super.onDestroy() 134 | if (!destroyed) { 135 | if (instance != this) 136 | throw IllegalStateException("excepted instance to be this for non-destroyed service") 137 | instance = null 138 | destroyed = true 139 | } 140 | } 141 | } 142 | 143 | class StayAliveConnection(inContext: Context, 144 | private val work: suspend (Context) -> Unit, 145 | private val extra: Any) 146 | : ServiceConnection { 147 | private val context = inContext.applicationContext 148 | 149 | init { 150 | context.bindService( 151 | Intent(context, StayAliveService::class.java), 152 | this, 153 | Context.BIND_IMPORTANT or Context.BIND_AUTO_CREATE 154 | ) 155 | } 156 | 157 | override fun onServiceConnected(name: ComponentName?, inService: IBinder?) { 158 | val provider = inService as Supplier<*> 159 | val service = provider.get() as StayAliveService 160 | service.startWork(work, extra) 161 | context.unbindService(this) 162 | } 163 | 164 | override fun onServiceDisconnected(name: ComponentName?) { 165 | // do nothing 166 | } 167 | } -------------------------------------------------------------------------------- /app/src/main/java/org/andbootmgr/app/util/Terminal.kt: -------------------------------------------------------------------------------- 1 | package org.andbootmgr.app.util 2 | 3 | import android.os.Environment 4 | import android.util.Log 5 | import androidx.compose.foundation.horizontalScroll 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.rememberScrollState 10 | import androidx.compose.foundation.verticalScroll 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.LaunchedEffect 14 | import androidx.compose.runtime.getValue 15 | import androidx.compose.runtime.mutableStateOf 16 | import androidx.compose.runtime.setValue 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.platform.LocalContext 19 | import androidx.compose.ui.text.font.FontFamily 20 | import androidx.compose.ui.unit.dp 21 | import com.topjohnwu.superuser.io.SuFile 22 | import com.topjohnwu.superuser.io.SuFileOutputStream 23 | import kotlinx.coroutines.CoroutineScope 24 | import kotlinx.coroutines.Dispatchers 25 | import kotlinx.coroutines.ExperimentalCoroutinesApi 26 | import kotlinx.coroutines.delay 27 | import kotlinx.coroutines.launch 28 | import kotlinx.coroutines.withContext 29 | import org.andbootmgr.app.R 30 | import java.io.File 31 | import java.io.OutputStream 32 | 33 | private class BudgetCallbackList(private val scope: CoroutineScope, 34 | private val log: OutputStream?) 35 | : MutableList, TerminalList { 36 | override var isCancelled by mutableStateOf(null) 37 | override var cancel: (() -> Unit)? = null 38 | private val internalList = ArrayList() 39 | private var textMinusLastLine = "" 40 | override var text by mutableStateOf("") 41 | private set 42 | override val size: Int 43 | get() = internalList.size 44 | 45 | override fun contains(element: String): Boolean { 46 | return internalList.contains(element) 47 | } 48 | 49 | override fun containsAll(elements: Collection): Boolean { 50 | return internalList.containsAll(elements) 51 | } 52 | 53 | override fun get(index: Int): String { 54 | return internalList[index] 55 | } 56 | 57 | override fun indexOf(element: String): Int { 58 | return internalList.indexOf(element) 59 | } 60 | 61 | override fun isEmpty(): Boolean { 62 | return internalList.isEmpty() 63 | } 64 | 65 | override fun iterator(): MutableIterator { 66 | return internalList.iterator() 67 | } 68 | 69 | override fun lastIndexOf(element: String): Int { 70 | return internalList.lastIndexOf(element) 71 | } 72 | 73 | override fun add(element: String): Boolean { 74 | onAdd(element) 75 | return internalList.add(element) 76 | } 77 | 78 | override fun add(index: Int, element: String) { 79 | onAdd(element) 80 | return internalList.add(index, element) 81 | } 82 | 83 | override fun addAll(index: Int, elements: Collection): Boolean { 84 | for (i in elements) { 85 | onAdd(i) 86 | } 87 | return internalList.addAll(index, elements) 88 | } 89 | 90 | override fun addAll(elements: Collection): Boolean { 91 | for (i in elements) { 92 | onAdd(i) 93 | } 94 | return internalList.addAll(elements) 95 | } 96 | 97 | override fun clear() { 98 | return internalList.clear() 99 | } 100 | 101 | override fun listIterator(): MutableListIterator { 102 | return internalList.listIterator() 103 | } 104 | 105 | override fun listIterator(index: Int): MutableListIterator { 106 | return internalList.listIterator(index) 107 | } 108 | 109 | override fun remove(element: String): Boolean { 110 | return internalList.remove(element) 111 | } 112 | 113 | override fun removeAll(elements: Collection): Boolean { 114 | return internalList.removeAll(elements.toSet()) 115 | } 116 | 117 | override fun removeAt(index: Int): String { 118 | return internalList.removeAt(index) 119 | } 120 | 121 | override fun retainAll(elements: Collection): Boolean { 122 | return internalList.retainAll(elements.toSet()) 123 | } 124 | 125 | override fun set(index: Int, element: String): String { 126 | return internalList.set(index, element).also { 127 | text = textMinusLastLine + element + "\n" 128 | } 129 | } 130 | 131 | override fun subList(fromIndex: Int, toIndex: Int): MutableList { 132 | return internalList.subList(fromIndex, toIndex) 133 | } 134 | 135 | fun onAdd(element: String) { 136 | textMinusLastLine = text 137 | text += element + "\n" 138 | scope.launch { 139 | log?.write((element + "\n").encodeToByteArray()) 140 | } 141 | } 142 | } 143 | 144 | interface TerminalList : MutableList { 145 | val text: String 146 | var isCancelled: Boolean? 147 | var cancel: (() -> Unit)? 148 | } 149 | class TerminalCancelException : RuntimeException() 150 | 151 | @Composable 152 | fun Terminal(list: TerminalList) { 153 | val scrollH = rememberScrollState() 154 | val scrollV = rememberScrollState() 155 | LaunchedEffect(list.text) { 156 | delay(200) // Give it time to re-measure 157 | scrollV.animateScrollTo(scrollV.maxValue) 158 | scrollH.animateScrollTo(0) 159 | } 160 | Column(modifier = Modifier.fillMaxSize()) { 161 | Text(list.text, modifier = Modifier 162 | .fillMaxSize() 163 | .weight(1f) 164 | .horizontalScroll(scrollH) 165 | .verticalScroll(scrollV) 166 | .padding(10.dp), fontFamily = FontFamily.Monospace 167 | ) 168 | } 169 | } 170 | 171 | @OptIn(ExperimentalCoroutinesApi::class) 172 | @Composable 173 | fun TerminalWork(logFile: String? = null, action: suspend (TerminalList) -> Unit) { 174 | val ctx = LocalContext.current.applicationContext 175 | LaunchedEffect(Unit) { 176 | val logDispatcher = Dispatchers.IO.limitedParallelism(1) 177 | val log = logFile?.let { 178 | val logDir = ctx.externalCacheDirs.firstOrNull() ?: run { 179 | SuFile.open(Environment.getExternalStorageDirectory(), "AbmLogs").also { it.mkdir() } 180 | } 181 | SuFileOutputStream.open(File(logDir, it)) 182 | } 183 | val s = BudgetCallbackList(CoroutineScope(logDispatcher), log) 184 | StayAliveConnection(ctx, { 185 | withContext(Dispatchers.Default) { 186 | try { 187 | action(s) 188 | } catch (_: TerminalCancelException) { 189 | s.add(ctx.getString(R.string.install_canceled)) 190 | } catch (e: Throwable) { 191 | s.add(ctx.getString(R.string.term_failure)) 192 | s.add(ctx.getString(R.string.dev_details)) 193 | s.add(Log.getStackTraceString(e)) 194 | } 195 | withContext(logDispatcher) { 196 | log?.close() 197 | } 198 | } 199 | }, s) 200 | } 201 | } -------------------------------------------------------------------------------- /app/src/main/java/org/andbootmgr/app/util/Theme.kt: -------------------------------------------------------------------------------- 1 | package org.andbootmgr.app.util 2 | 3 | import android.annotation.TargetApi 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.material3.* 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.platform.LocalContext 11 | 12 | @Composable 13 | fun AbmTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { 14 | val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S 15 | val colorScheme = when { 16 | dynamicColor && darkTheme -> @TargetApi(Build.VERSION_CODES.S) { 17 | dynamicDarkColorScheme(LocalContext.current) 18 | } 19 | 20 | dynamicColor && !darkTheme -> @TargetApi(Build.VERSION_CODES.S) { 21 | dynamicLightColorScheme(LocalContext.current) 22 | } 23 | 24 | darkTheme -> darkColorScheme() 25 | else -> lightColorScheme() 26 | } 27 | 28 | MaterialTheme( 29 | colorScheme = colorScheme, 30 | typography = Typography(), 31 | content = { 32 | // A surface container using the 'background' color from the theme 33 | Surface( 34 | modifier = Modifier.fillMaxSize(), 35 | color = MaterialTheme.colorScheme.background, 36 | content = content 37 | ) 38 | } 39 | ) 40 | } -------------------------------------------------------------------------------- /app/src/main/java/org/andbootmgr/app/util/Toolkit.kt: -------------------------------------------------------------------------------- 1 | package org.andbootmgr.app.util 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import com.topjohnwu.superuser.Shell 6 | import com.topjohnwu.superuser.Shell.FLAG_NON_ROOT_SHELL 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.withContext 9 | import java.io.* 10 | 11 | // Manage & extract Toolkit 12 | class Toolkit(private val ctx: Context) { 13 | companion object { 14 | private const val TAG = "ABM_AssetCopy" 15 | private const val DEBUG = false 16 | } 17 | 18 | val targetPath = File(ctx.cacheDir, "tools") 19 | 20 | suspend fun copyAssets(extractingNotification: suspend () -> Unit) { 21 | val shell = withContext(Dispatchers.IO) { 22 | Shell.Builder.create().setFlags(FLAG_NON_ROOT_SHELL).setTimeout(90).setContext(ctx).build() 23 | } 24 | var b = withContext(Dispatchers.IO) { 25 | ctx.assets.open("cp/_ts").use { it.readBytes() } 26 | } 27 | val s = String(b).trim() 28 | b = try { 29 | withContext(Dispatchers.IO) { 30 | FileInputStream(File(targetPath, "_ts")).use { it.readBytes() } 31 | } 32 | } catch (e: IOException) { 33 | ByteArray(0) 34 | } 35 | val s2 = String(b).trim() 36 | if (s != s2) { 37 | extractingNotification.invoke() 38 | withContext(Dispatchers.IO) { 39 | targetPath.deleteRecursively() 40 | if (!ctx.filesDir.exists() && !ctx.filesDir.mkdir()) 41 | throw IOException("mkdir failed ${ctx.filesDir}") 42 | if (!targetPath.exists() && !targetPath.mkdir()) 43 | throw IOException("mkdir failed $targetPath") 44 | if (!ctx.cacheDir.exists() && !ctx.cacheDir.mkdir()) 45 | throw IOException("mkdir failed ${ctx.cacheDir}") 46 | copyAssets("Toolkit", "Toolkit") 47 | copyAssets("cp", "") 48 | } 49 | } 50 | withContext(Dispatchers.IO) { 51 | shell.newJob().add("chmod -R +x " + targetPath.absolutePath).exec() 52 | } 53 | } 54 | 55 | private fun copyAssets(src: String, outp: String) { 56 | for (filename in ctx.assets.list(src)!!) { 57 | copyAssets(src, outp, filename) 58 | } 59 | } 60 | 61 | private fun copyAssets( 62 | src: String, 63 | outPath: String, 64 | filename: String 65 | ) { 66 | val `in`: InputStream 67 | val out: OutputStream 68 | try { 69 | `in` = ctx.assets.open("$src/$filename") 70 | val outFile = File(File(targetPath, outPath), filename) 71 | out = FileOutputStream(outFile) 72 | copyFile(`in`, out) 73 | `in`.close() 74 | out.close() 75 | } catch (e: FileNotFoundException) { 76 | val r = File(targetPath, outPath).mkdir() 77 | if (DEBUG) Log.d(TAG, "Result of mkdir #1: $r") 78 | if (DEBUG) Log.d(TAG, Log.getStackTraceString(e)) 79 | try { 80 | ctx.assets.open(src + File.separator + filename).close() 81 | copyAssets(src, outPath, filename) 82 | } catch (e2: FileNotFoundException) { 83 | val r2 = File(File(targetPath, outPath), filename).mkdir() 84 | if (DEBUG) Log.d(TAG, "Result of mkdir #2: $r2") 85 | if (DEBUG) Log.d(TAG, Log.getStackTraceString(e2)) 86 | copyAssets(src + File.separator + filename, outPath + File.separator + filename) 87 | } 88 | } 89 | } 90 | 91 | @Throws(IOException::class) 92 | fun copyFile(`in`: InputStream, out: OutputStream) { 93 | val buffer = ByteArray(1024) 94 | var read: Int 95 | while (`in`.read(buffer).also { read = it } != -1) { 96 | out.write(buffer, 0, read) 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-anydpi/ic_about.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-anydpi/ic_droidbooticon.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/abm_notif.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_check_circle_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_error_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_keyboard_arrow_down_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_keyboard_arrow_up_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_roms.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_sailfish_os_logo.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_sd.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ut_logo.xml: -------------------------------------------------------------------------------- 1 | 8 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values-ar/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | هاتف واحد لتشغيل كل منهم! 4 | مرحبا بك في أداة النسخ الاحتياطي والاستعادة البسيطة. ماذا تريد أن تفعل بهذا البارتيشن %1$s؟ 5 | القائمة 6 | نسخ احتياطي 7 | إستعادة 8 | -------------------------------------------------------------------------------- /app/src/main/res/values-ja/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | メニュー 4 | 復元 5 | sparse イメージをフラッシュ 6 | 正常に選択されました。 7 | 8 | 復元するファイルを選択 9 | ファイル選択 10 | ファイル作成 11 | 完了 12 | キャンセル 13 | 14 | 開始セクタ (相対的) 15 | 終了セクタ (相対的) 16 | 利用可能領域: %s (%d セクタ) 17 | 推定サイズ: %s 18 | 以下のオプションから一つ選択してください。 19 | ポータブルデータパーティション 20 | パーティション名 21 | 作成 22 | 続ける 23 | アイコン 24 | OS ロゴ 25 | ROM ロゴ 26 | ブートローダオプション 27 | 閉じる 28 | 拡張 29 | ROM 内部 ID (よく分からない場合は変更しないでください) 30 | ブートメニューで利用する ROM 名 31 | パーティションレイアウト (高度なユーザー向け) 32 | OS ユーザデータ 33 | OS システムデータ 34 | 選択された値: %d %s (%d を %d セクタで使用) 35 | サイズ 36 | 種類 37 | ID 38 | Sparsed 39 | 残り %d (%d) 40 | インストール 41 | (接続中...) 42 | ダウンロード中… 43 | シンプルバックアップ&復元ツールへようこそ。パーティション %1$s に対して何を行いますか? 44 | バックアップ 45 | バックアップファイルを作成 46 | パーティションに書き込む sparse ファイルを選択 47 | 一般設定 48 | (無効な入力) 49 | OS インストール 50 | インストールするオペレーティングシステムを選択してください。 51 | すでにこの ROM がインストールされています!あと少しのステップで完了です。いくつかのオプションを選択してカスタマイズしてください… 52 | -------------------------------------------------------------------------------- /app/src/main/res/values-ko/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/values-pl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Jeden telefon do uruchomienia wszystkiego! 4 | Zainstaluj OS 5 | Wybierz system operacyjny który chcesz zainstalować. 6 | Prawie zainstalowałeś ten ROM! Zostało jeszcze tylko kilka kroków. Poniżej znajdują się opcjonalne ustawienia do spersonalizowania instalacji… 7 | Podaj obrazy do wszystkich wymaganych ID. Możesz użyć rekomendowanych obrazów używając przycisku \"Pobierz\"! 8 | To zainstaluje DroidBoot ponownie 9 | Menu 10 | Kopia zapasowa 11 | Przywróć 12 | Wgraj obraz sparse 13 | Poprawnie wybrano. 14 | Dalej 15 | Stwórz plik kopii zapasowej 16 | Wybierz plik do przywrócenia 17 | Wybierz plik sparse do wgrania na partycję 18 | Wybierz plik 19 | Utwórz plik 20 | Koniec 21 | Anuluj 22 | Cofnij 23 | Sektor początkowy (relatywny) 24 | Wybierz jedną z poniższych opcji. 25 | Partycja przenośnych danych 26 | Nazwa partycji 27 | Utwórz 28 | Dalej 29 | Ikona 30 | Logo OS 31 | Logo ROM 32 | Rozmiar 33 | Typ 34 | ID 35 | Zostało %d z %d 36 | Zainstaluj 37 | (Łączenie...) 38 | Pobieranie… 39 | Sektor końcowy (relatywny) 40 | Dostępne miejsce: %s (%d sektorów) 41 | Szacowany rozmiar: %s 42 | Opcje bootloadera 43 | Zamknij 44 | Rozszerz 45 | Wewnętrzne ID ROM (nie dotykaj jeżeli nie jesteś pewien) 46 | Nazwa ROM w menu uruchamiania 47 | Układ partycji (użytkownicy zaawansowani) 48 | Dane systemowe OS 49 | Wybrana wartość: %d %s (używając %d z %d sektorów) 50 | Wybór użytkownika 51 | Cofnij 52 | %s z %s pobrane 53 | Pobierz 54 | Wybierz 55 | Witaj w ABM! 56 | To zainstaluje ABM 57 | To zainstaluje ABM + DroidBoot 58 | UWAGA: Twoja karta SD zostanie w pełni wymazana. 59 | Android 60 | Nazwa OS 61 | Niepoprawne dane 62 | Ikona DroidBoot 63 | Plik niedostępny, spróbuj ponownie później! 64 | Aktualizacja lokalna 65 | Aktualizacja online 66 | Nie udało się sprawdzić aktualizacji! Spróbuj ponownie później. 67 | Zainstaluj aktualizację 68 | Nie znaleziono aktualizacji, twój system operacyjny jest zaaktualizowany! 69 | Witaj w aktualizatorze lokalnym. 70 | To umożliwia ci zaaktualizowanie obrazu rozruchowego systemu operacyjnego ręcznie. 71 | Kontynuuj tylko jeżeli wiesz co robisz. 72 | Nazwa skryptu 73 | Nie wybrano pliku 74 | Plik wybrany 75 | To zaaktualizuje DroidBoot 76 | Wypakowywanie narzędzi, proszę czekać… 77 | Witaj w tym prostym narzędziu do kopii zapasowen i przywracania\? Co chcesz zrobić z tą partycją %1$s\? 78 | Upewnij się że posiadasz kopię zapasową! 79 | Podaj nazwę obecnie uruchomionego systemu operacyjnego. Możesz wybrać własną. 80 | -------------------------------------------------------------------------------- /app/src/main/res/values-pt-rBR/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/values-tr/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Menü 4 | Yedekle 5 | Geri Yükle 6 | Başarıyla seçildi. 7 | İleri 8 | Yedek dosyası oluştur 9 | Geri Yükleme için dosya seçin 10 | Dosya seçin 11 | Dosya oluştur 12 | Bitiş 13 | İptal 14 | Önceki 15 | Genel ayarlar 16 | Kullanılabilir alan: %s (%d tane sektör) 17 | Yaklaşık alam: %s 18 | (geçersiz giriş) 19 | Lütfen aşağıdaki seçeneklerden birini seçin. 20 | Bölüm adı 21 | Oluştur 22 | OS Yükle 23 | Devam et 24 | İkon 25 | Yüklemek istediğiniz işletim sistemini seçiniz. 26 | İS logo 27 | ROM logosu 28 | Ön yükleyici ayarları 29 | Kapat 30 | Genişlet 31 | Açılış menüsündeki ROM ismi 32 | Bölüm dizilimi (gelişmiş kullanıcılar için) 33 | Başlangıç sektörü (baştan) 34 | Bitiş sektör (baştan) 35 | ROM dahili ID (emin değilseniz dokunmayın) 36 | İS sistem verileri 37 | Seçilen değer: %d %s (%d sektörden %d kullanılıyor) 38 | Boyut 39 | ID 40 | %d\'den kalan %d 41 | Yükle 42 | (Bağlanıyor...) 43 | İndiriliyor… 44 | Yaz 45 | Kullanıcı-seçimi 46 | Geri Al 47 | %ste %s indirildi 48 | İndir 49 | Seç 50 | ABM\'ye hoşgeldiniz! 51 | Bu ABM\'yi yükleyecektir 52 | Bu ABM + Drodboot\'u yükleyecektir 53 | UYARI: SD kardınız tamamıyla silinecektir. 54 | Android 55 | Geçerli işletim sisteminiz için lütfen bir isim girin. İstediğinizi seçebilirsiniz. 56 | İS adı 57 | Geçersiz giriş 58 | DroidBoot İkonu 59 | Dosya kullanılamıyor, lütfen daha sonra tekrar deneyin! 60 | Yerel güncelleme 61 | Online güncelleme 62 | Güncellemeyi yükle 63 | Hiç bir güncelleme yok, işletim sistemiz en güncel sürümde! 64 | Yerel yükleyiciye hoş geldiniz. 65 | Burası işletim sisteminin boot imajını el ile güncellemeye izin vermektedir. 66 | Lütfen ne yaptığınızdan emin iseniz devam edin. 67 | Script ismi 68 | Dosya seçilmedi 69 | Dosya seçildi 70 | Bu DroidBoot\'u güncelleyecektir 71 | Toolkit çıkartılıyor, lütfen sabırlı olun… 72 | Toolkit çıkartılması başarısız oldu. Lütfen hatayı geliştiricilere bildirin! 73 | Çıkış 74 | İkon ekle 75 | Ayarlar 76 | Hepsini başlatmak için tek bir telefon! 77 | Basit Yedekle & Geri Yükle aracına hoşgeldiniz. Bu %1$s bölüm ile ne yapmak istiyorsunuz\? 78 | Taşınabilir data bölümü 79 | ROM yükleme neredeyse tamamlandı! Sadece bir kaç adım kaldı. Yüklemenizi özelleştirmek için aşağıdaki seçenekleri kullanabilirsiniz… 80 | İS kişisel veriler 81 | Gerekli IDler için lütfen imajları sağlayın. \"İndir\" tuşunu kullanarak önerilenleri kullanabilirsiniz! 82 | Yedeğiniz olduğundan lütfen emin olun! 83 | Bu DroidBoot\'u tekrar yükleyecektir 84 | Güncellemeler denetlenemedi! Lütfen daha sonra tekrar deneyin. 85 | Yükleme için bir güncelleme mevcut. 86 | \n 87 | \n 88 | \n 89 | \n\t\tBu işleme devam etmek, imajları internet üzerinden indirerek 4 gigabayt üzerinde bir veri kullanımı olabilir. Lütfen tarifesiz bir bağlantı kullandığınızdan emin olun. 90 | \n 91 | \n\t\tLütfen devam etmeden önce bir yedeğiniz olduğundan emin olun. 92 | \n 93 | \n\t\tGüncelleme otomatik yapılırken, lütfen güncelleme gerçekleşirken uygulamadan çıkmayın. 94 | \n 95 | Ev 96 | Sparse imajını yükleyin 97 | Bölüme yazmak için sparse dosyasını seçin 98 | Sparselı 99 | OK 100 | Yüklendi 101 | Yüklendi ama aktif edilmedi 102 | Hasarlı ve aktif değil 103 | Yüklenmedi 104 | Evet 105 | Hayır 106 | Aktif: %s 107 | Bağlı önyükleme seti: %s 108 | SD Kart bağlandı: %s 109 | Yüklendi: %s 110 | Önyükleme seti hasarlı: %s 111 | Cihaz: %s 112 | (desteklenmeyen) 113 | Root erişimi verilmedi, ama bu uygulama için gerekli. 114 | Ayarlar dosyası bulunamadı. Daha çok bilgi için, lütfen dokümanlara uyunuz. 115 | Önyükleyici seti bağlanamadı, lütfen dokümanlara uyunuz. 116 | Bilinmeyen hata, geçersiz bölüm 117 | Bölümler 118 | Etkinleştirildi 119 | Birleşik 120 | Girdiler 121 | Bölüm yöneticisi yüklenemedi 122 | (Geçersiz girdi) 123 | Geçersiz girdi 124 | Girdi \"%s\" 125 | Boş alan (%s) 126 | Bölüm %d \"%s\" 127 | (Yeni girdi oluştur) 128 | Yeni girdi 129 | Boş alan 130 | Bölüm %d 131 | Bilinmeyen 132 | Uyarlanabilir Hafıza Metadata 133 | Rezerve Edilmiş 134 | Yeniden Adlandır 135 | Bağla 136 | Çıkar 137 | İd: %d (%d:%d) 138 | Tür: %s %s 139 | (kod %s) 140 | Boyut: %s %s 141 | (%d sektörü) 142 | Pozisyon: %d -> %d 143 | Yedekle & Geri Yükle 144 | GERÇEKTEN bu bölümü silip üstündeki TÜM verileri kaybetmek istiyor musunuz\? 145 | Dosya adı 146 | Başlık 147 | Linux 148 | Initrd 149 | Dtb 150 | Ayarlar 151 | ROM türü 152 | Güncelleyici Bağlantısı 153 | Dosya zaten mevcut, başka isim seçin 154 | Güncelle 155 | Lütfen bekleyin… 156 | Yükleniyor… 157 | Tamam 158 | Varsayılan girdi 159 | Zaman aşımı (saniye) 160 | Değişiklikler kaydedilemedi… 161 | Değişiklikleri kaydet 162 | DroidBoot\'u Güncelle 163 | Basitleştirilmiş mod 164 | Dosya indirilirken hata :( 165 | Hata 166 | SD Kardı Kur 167 | Önyükleme setini onar 168 | %s silinemiyor 169 | db.conf hasarlı - yeniden oluşturuluyor 170 | --- Hata --- 171 | Geliştiriciler için detaylar: 172 | --- Başlatılıyor… 173 | Çıkarılamıyor. Hiç bir değişiklik yapılmadı 174 | Geçersiz giriş, hiç bir değişiklik yapılmadı 175 | -- Başarılı! 176 | -- Yedekleme/geri yükleme hatası, sebep: 177 | DroidBoot Yükleniyor… 178 | -- Önyükleyiciyi yüklerken hata, sebep: 179 | -- Lütfen yüklemeyi bitirene kadar dokümanlara uyunuz. 180 | Dosya sistemi hazırlanıyor… 181 | -- yer tutucu oluşturulamadı, iptal ediliyor 182 | -- geçersiz bağlama durumu, iptal ediliyor 183 | -- bölüm tablosu oluşturulamadı, iptal ediliyor 184 | -- Lütfen EN YAKIN ZAMANDA yeniden başlatın!!! 185 | -- Tamamlandı. 186 | -- metadata bölümü oluşturulamadı. 187 | -- geçersiz bağlama hatası, iptal ediliyor 188 | -- db klasörü oluşturulamadı, iptal ediliyor 189 | Ayarlar oluşturuluyor… 190 | Cihaz kurulumu… 191 | -- Güncellenmiş boot imajı indiriliyor… 192 | İndirme başarısız, tekrar deneyin… 193 | İndirme başarısız, vazgeçiliyor. 194 | Güncellenmiş bölüm tablosu indiriliyor… 195 | simg2img hata ile döndü 196 | -- Güncelleme yamalanıyor… 197 | Script hata ile döndü 198 | -- Güncelleme hazırlanırken hata oluştu. Hiç bir değişiklik yapılmadı. 199 | Bölüm yükleniyor %s… 200 | -- Klasör adı: %s 201 | -- GUI adı: %s 202 | -- Bölüm düzeni oluşturuluyor… 203 | -- mkdir esnasında hata oluştu 204 | -- İmajlar yükleniyor… 205 | %s Yükleniyor… 206 | -- İşletim sistemi yamalanıyor… 207 | Bölüm oluşturuluyor… 208 | Bölüm oluşturuldu. 209 | -- Bölüm tablosu oluşturuldu. 210 | Hasarlı ama aktif 211 | SD Kart dualboot için biçimlendirildi: %s 212 | ABM\'nin çalışması için SD Kart gerekli, ama hiç algılanamadı. 213 | Cihaz DroidBoot kullanarak açılmadı, ama ayarlar dosyası oluşturulmuş. ABM\'yi henüz yüklediyseniz, yeniden başlatma gerekiyor. Eğer önyükleyici yüklemede sürekli hata veriyorsa, lütfen dokümanlara uyunuz. 214 | Aşağıdaki seçeneklerden birini incelemek için seçiniz. 215 | Sil 216 | Atanmış bölümler 217 | GERÇEKTEN bu işletim sistemini silmek ve TÜM verilerini kaybetmek istiyor musunuz\? 218 | DroidBoot\'u onar 219 | Yükleme iptal edildi. Hiç bir değişiklik yapılmadı. 220 | -- Lütfen desteğe başvurun 221 | Not: yüksek ihtimal önyükleyiciye yazamıyor 222 | -- bağlama noktası eklenemedi, iptal ediliyor 223 | -- meta alınamadı, iptal ediliyor 224 | -- uyarı: disk çıkarılamadı 225 | -- bağlanırken hata, iptal ediliyor 226 | -- girdiler klasörü oluşturulamadı, iptal ediliyor 227 | İşletim sistemini veya taşınabilir bölümü eklemeye başlamak için boş alan seçin. 228 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FF9F11 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 22 | 25 | 26 | #FE9200 27 | #FE9200 28 | #FE9F11 29 | #ff0000 30 | 31 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | plugins { 3 | id("com.android.application") version "8.8.0" apply false 4 | val kotlinVersion = "2.0.0" 5 | id("org.jetbrains.kotlin.android") version kotlinVersion apply false 6 | id("org.jetbrains.kotlin.plugin.compose") version kotlinVersion apply false 7 | id("com.mikepenz.aboutlibraries.plugin") version "11.2.1" apply false 8 | } -------------------------------------------------------------------------------- /buildutils/clone-submodules.sh: -------------------------------------------------------------------------------- 1 | git submodule update --init --recursive 2 | -------------------------------------------------------------------------------- /buildutils/screenshot/abm1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Android-Boot-Manager/App/06c7e0b1a8fdac505d595fb96b11f2dfba338da7/buildutils/screenshot/abm1.png -------------------------------------------------------------------------------- /buildutils/screenshot/abm2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Android-Boot-Manager/App/06c7e0b1a8fdac505d595fb96b11f2dfba338da7/buildutils/screenshot/abm2.png -------------------------------------------------------------------------------- /buildutils/screenshot/abm3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Android-Boot-Manager/App/06c7e0b1a8fdac505d595fb96b11f2dfba338da7/buildutils/screenshot/abm3.png -------------------------------------------------------------------------------- /buildutils/screenshot/abm4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Android-Boot-Manager/App/06c7e0b1a8fdac505d595fb96b11f2dfba338da7/buildutils/screenshot/abm4.png -------------------------------------------------------------------------------- /buildutils/screenshot/abm5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Android-Boot-Manager/App/06c7e0b1a8fdac505d595fb96b11f2dfba338da7/buildutils/screenshot/abm5.png -------------------------------------------------------------------------------- /buildutils/screenshot/abm6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Android-Boot-Manager/App/06c7e0b1a8fdac505d595fb96b11f2dfba338da7/buildutils/screenshot/abm6.png -------------------------------------------------------------------------------- /buildutils/update-submodules.sh: -------------------------------------------------------------------------------- 1 | git pull --recurse-submodules 2 | git submodule update --remote --recursive 3 | -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /app/src/main/res/values/strings*.xml 3 | translation: /app/src/main/res/values-%android_code%/%original_file_name% 4 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -XX:+UseParallelGC 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | # Generate compile-time only R class for app modules 25 | android.enableAppCompileTimeRClass=true 26 | # Only keep the single relevant constructor for types mentioned in XML files 27 | # instead of using a parameter wildcard which keeps them all 28 | android.useMinimalKeepRules=true 29 | # Enable resource optimizations for release build 30 | android.enableResourceOptimizations=true 31 | # Gradle 32 | org.gradle.caching=true 33 | org.gradle.configureondemand=true 34 | # https://github.com/mikepenz/AboutLibraries/issues/857 35 | #org.gradle.configuration-cache=true 36 | # Use R8 in full mode instead of ProGuard compatibility mode. 37 | android.enableR8.fullMode=true 38 | # Build BuildConfig as Bytecode 39 | android.enableBuildConfigAsBytecode=true 40 | # https://github.com/Kotlin/kotlinx-atomicfu?tab=readme-ov-file#atomicfu-compiler-plugin 41 | kotlinx.atomicfu.enableJvmIrTransformation=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Android-Boot-Manager/App/06c7e0b1a8fdac505d595fb96b11f2dfba338da7/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=31c55713e40233a8303827ceb42ca48a47267a0ad4bab9177123121e71524c26 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | 88 | @rem Execute Gradle 89 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 90 | 91 | :end 92 | @rem End local scope for the variables with windows NT shell 93 | if "%ERRORLEVEL%"=="0" goto mainEnd 94 | 95 | :fail 96 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 97 | rem the _cmd.exe /c_ return code! 98 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 99 | exit /b 1 100 | 101 | :mainEnd 102 | if "%OS%"=="Windows_NT" endlocal 103 | 104 | :omega 105 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | maven("https://jitpack.io") 14 | } 15 | } 16 | 17 | rootProject.name = "Abm" 18 | include(":app") 19 | -------------------------------------------------------------------------------- /web_hi_res_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Android-Boot-Manager/App/06c7e0b1a8fdac505d595fb96b11f2dfba338da7/web_hi_res_512.png --------------------------------------------------------------------------------