├── .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 |
4 |
5 |
Android Boot Manager
6 |
7 |
8 |
9 |
10 |
11 |
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 | |  |  |  |
75 | |-------------------------------------------------|-------------------------------------------------|-------------------------------------------------|
76 | |  |  |  |
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
--------------------------------------------------------------------------------