diff --git a/README.md b/README.md
index 3ead6639..8fc28133 100644
--- a/README.md
+++ b/README.md
@@ -171,6 +171,13 @@ OnePlus | Nord N200 | [dre](https://wiki.lineageos.org/devices/dre) | | tested
OnePlus | 9 | lemonade | | under development
+Xiaomi
+
+Vendor | Device Name | CodeName | Models | Status
+---|---|---|---|---
+Xiaomi | Redmi 7A / 8 / 8A / 8A Dual | [Mi439](https://wiki.lineageos.org/devices/Mi439) : pine / olive / olivelite / olivewood | | tested
+
+
And more to come!
@@ -197,7 +204,6 @@ Please have a look before opening an issue or starting to contribute.
A detailed list can be found [here](https://openandroidinstaller.org/#contribute).
-
## Tools
- The [Android SDK Platform Tools](https://developer.android.com/studio/releases/platform-tools) (such as adb and fastboot) are [Apache](https://android.googlesource.com/platform/system/adb/+/refs/heads/master/NOTICE)-licensed universal Android utilities
diff --git a/docs/how_to_contribute_your_own_installation_configurations.md b/docs/how_to_contribute_your_own_installation_configurations.md
index 1e481e70..d9063776 100644
--- a/docs/how_to_contribute_your_own_installation_configurations.md
+++ b/docs/how_to_contribute_your_own_installation_configurations.md
@@ -12,10 +12,14 @@ A config file consists of two parts. The first part are some metadata about the
Every config file should have `metadata` with the following fields:
- `maintainer`: str; Maintainer and author of the config file.
- `device_name`: str; Name of the device.
+- `brand`: [OPTIONAL] str; Name of the brand. Can be used to make brand specific actions.
- `is_ab_device`: bool; A boolean to determine if the device is a/b-partitioned or not.
- `device_code`: str; The official device code.
- `supported_device_codes`: List[str]; A list of supported device codes for the config. The config will be loaded based on this field.
- `twrp-link`: [OPTIONAL] str; name of the corresponding twrp page.
+- `additional_steps` : [OPTIONAL] List[str]; A list of additional steps. Can be `dtbo`, `vbmeta`, `vendor_boot` or `super_empty`.
+- `notes`: [OPTIONAL] List[str]; specific phone information, showed before choosing ROM / recovery
+- `untested`: [OPTIONAL] bool; If `true`, a warning message is showed during installation process.
In addition to these metadata, every config can have optional `requirements`. If these are set, the user is asked to check if they are meet.
- `android`: [OPTIONAL] int|str; Android version to install prior to installing a custom ROM.
@@ -32,7 +36,7 @@ Every step in the config file corresponds to one view in the application. These
- `img`: [OPTIONAL] Display an image on the left pane of the step view. Images are loaded from `openandroidinstaller/assets/imgs/`.
- `content`: str; The content text displayed alongside the action of the step. Used to inform the user about what's going on. For consistency and better readability the text should be moved into the next line using `>`.
- `link`: [OPTIONAL] Link to use for the link button if type is `link_button_with_confirm`.
-- `command`: [ONLY for call_button* steps] str; The command to run. One of `adb_reboot`, `adb_reboot_bootloader`, `adb_reboot_download`, `adb_sideload`, `adb_twrp_wipe_and_install`, `adb_twrp_copy_partitions`, `fastboot_boot_recovery`, `fastboot_unlock_with_code`, `fastboot_unlock`, `fastboot_oem_unlock`, `fastboot_get_unlock_data`, `fastboot_reboot`, `heimdall_flash_recovery`.
+- `command`: [ONLY for call_button* steps] str; The command to run. One of `adb_reboot`, `adb_reboot_bootloader`, `adb_reboot_download`, `adb_sideload`, `adb_twrp_wipe_and_install`, `adb_twrp_copy_partitions`, `fastboot_boot_recovery`, `fastboot_flash_recovery`, `fastboot_reboot_recovery`, `fastboot_flash_additional_partitions`, `fastboot_unlock_with_code`, `fastboot_unlock`, `fastboot_unlock_critical`, `fastboot_oem_unlock`, `fastboot_get_unlock_data`, `fastboot_reboot`, `heimdall_flash_recovery`.
- `allow_skip`: [OPTIONAL] boolean; If a skip button should be displayed to allow skipping this step. Can be useful when the bootloader is already unlocked.
**Please try to retain this order of these fields in your config to ensure consistency.**
diff --git a/openandroidinstaller/app_state.py b/openandroidinstaller/app_state.py
index eee468a9..30f68b08 100644
--- a/openandroidinstaller/app_state.py
+++ b/openandroidinstaller/app_state.py
@@ -16,8 +16,9 @@
import copy
from pathlib import Path
from typing import List, Optional
+from loguru import logger
-from installer_config import _load_config
+from installer_config import _load_config, Step
class AppState:
@@ -37,6 +38,10 @@ def __init__(
self.test = test
self.test_config = test_config
+ # store state
+ self.unlock_bootloader = True
+ self.flash_recovery = True
+
# placeholders
self.advanced = False
self.install_addons = False
@@ -44,6 +49,9 @@ def __init__(
self.config = None
self.image_path = None
self.recovery_path = None
+ self.dtbo_path = None
+ self.vbmeta_path = None
+ self.super_empty_path = None
# store views
self.default_views: List = []
@@ -69,3 +77,48 @@ def load_config(self, device_code: str):
self.steps = copy.deepcopy(self.config.unlock_bootloader) + copy.deepcopy(
self.config.boot_recovery
)
+
+ def toggle_flash_unlock_bootloader(self):
+ """Toggle flashing of unlock bootloader."""
+ self.unlock_bootloader = not self.unlock_bootloader
+ if self.unlock_bootloader:
+ logger.info("Enabled unlocking the bootloader again.")
+ self.steps = copy.deepcopy(self.config.unlock_bootloader)
+ else:
+ logger.info("Skipping bootloader unlocking.")
+ self.steps = []
+ # if the recovery is already flashed, skip flashing it again
+ if self.flash_recovery:
+ self.steps += copy.deepcopy(self.config.boot_recovery)
+ else:
+ self.steps = [
+ Step(
+ title="Boot custom recovery",
+ type="confirm_button",
+ content="If you already flashed TWRP, boot into it by pressing 'Confirm and run'. Otherwise restart the process. Once your phone screen looks like the picture on the left, continue.",
+ command="adb_reboot_recovery",
+ img="twrp-start.jpeg",
+ )
+ ]
+
+ def toggle_flash_recovery(self):
+ """Toggle flashing of recovery."""
+ self.flash_recovery = not self.flash_recovery
+ if self.unlock_bootloader:
+ self.steps = copy.deepcopy(self.config.unlock_bootloader)
+ else:
+ self.steps = []
+ if self.flash_recovery:
+ logger.info("Enabled flashing recovery again.")
+ self.steps += copy.deepcopy(self.config.boot_recovery)
+ else:
+ logger.info("Skipping flashing recovery.")
+ self.steps = [
+ Step(
+ title="Boot custom recovery",
+ type="call_button",
+ content="If you already flashed TWRP, boot into it by pressing 'Confirm and run'. Otherwise restart the process. Once your phone screen looks like the picture on the left, continue.",
+ command="adb_reboot_recovery",
+ img="twrp-start.jpeg",
+ )
+ ]
diff --git a/openandroidinstaller/assets/configs/Mi439.yaml b/openandroidinstaller/assets/configs/Mi439.yaml
new file mode 100644
index 00000000..a5362a32
--- /dev/null
+++ b/openandroidinstaller/assets/configs/Mi439.yaml
@@ -0,0 +1,68 @@
+metadata:
+ maintainer: A non (anon)
+ brand: xiaomi
+ device_name: Xiaomi Redmi 7A / 8 / 8A / 8A Dual
+ is_ab_device: false
+ device_code: Mi439
+ additional_steps:
+ - dtbo
+ - vbmeta
+ - super_empty
+ supported_device_codes:
+ - Mi439
+ - mi439
+ - pine
+ - olive
+ - olivelite
+ - olivewood
+ notes:
+ - Be careful when choosing OrangeFox version, Android 12 & 13 ROM needs OrangeFox version code with `A12`, for example `R11.1_5_A12`. Android 10 & 11 ROM needs OrangeFox version code without `A12` (bellow on the page)
+requirements:
+ firmware: MiUI 12.5 (Q)
+steps:
+ unlock_bootloader:
+ - type: confirm_button
+ content: >
+ As a first step, you need to unlock the bootloader. A bootloader is the piece of software, that tells your phone
+ how to start and run an operating system (like Android). Your device should be turned on. This will reset your phone.
+ - type: link_button_with_confirm
+ content: >
+ - Create a Mi account on Xiaomi’s website. Beware that one account is only allowed to unlock one unique device every 30 days.
+
+ - Add a phone number to your Mi account, insert a SIM into your phone.
+
+ - Enable developer options in `Settings` > `About Phone` by repeatedly tapping MIUI Version.
+
+ - Link the device to your Mi account in `Settings` > `Additional settings` > `Developer options` > `Mi Unlock status`.
+
+ - Download the Mi Unlock app with the link bellow (Windows is required to run the app), and follow the instructions provided by the app. It may tell you that you have to wait, usually 7 days. If it does so, please wait the quoted amount of time before continuing to the next step!
+
+ - After device and Mi account are successfully verified, the bootloader should be unlocked.
+
+ - Since the device resets completely, you will need to re-enable USB debugging to continue : `Settings` > `Additional settings` > `Developer options` > `USB debugging`
+ link: https://en.miui.com/unlock/download_en.html
+ boot_recovery:
+ - type: call_button
+ content: >
+ Now you need to install a custom recovery system on the phone. A recovery is a small subsystem on your phone,
+ that manages updating, adapting and repairing of the operating system.
+
+ Once the device is fully booted, you need to reboot into the bootloader again by pressing 'Confirm and run' here. Then continue.
+ command: adb_reboot_bootloader
+ - type: call_button
+ content: >
+ Install additional partitions selected before by pressing 'Confirm and run'. Once it's done continue.
+
+ Note : If you have not selected this partition, it will do nothing.
+ command: fastboot_flash_additional_partitions
+ - type: call_button
+ content: >
+ Install the recovery you chosen before by pressing 'Confirm and run'. Once it's done continue.
+ command: fastboot_flash_recovery
+ - type: call_button
+ img: ofox.png
+ content: >
+ Reboot to recovery by pressing 'Confirm and run', and hold the Vol+ button of your phone UNTIL you see the recovery.
+ If MiUI starts, you have to start the process again, since MiUI delete the recovery you just flashed.
+ Once it's done continue.
+ command: fastboot_reboot_recovery
\ No newline at end of file
diff --git a/openandroidinstaller/installer_config.py b/openandroidinstaller/installer_config.py
index 8d0c65e3..e5be79c7 100644
--- a/openandroidinstaller/installer_config.py
+++ b/openandroidinstaller/installer_config.py
@@ -15,6 +15,7 @@
from pathlib import Path
from typing import List, Optional
+from typing_extensions import Self
import schema
import yaml
@@ -62,11 +63,12 @@ def __init__(
self.requirements = requirements
self.device_code = metadata.get("device_code")
self.is_ab = metadata.get("is_ab_device", False)
+ self.additional_steps = metadata.get("additional_steps", [])
self.supported_device_codes = metadata.get("supported_device_codes")
self.twrp_link = metadata.get("twrp-link")
@classmethod
- def from_file(cls, path):
+ def from_file(cls, path) -> Self:
with open(path, "r", encoding="utf-8") as stream:
try:
raw_config = yaml.safe_load(stream)
@@ -150,7 +152,8 @@ def validate_config(config: str) -> bool:
),
"content": str,
schema.Optional("command"): Regex(
- r"adb_reboot|adb_reboot_bootloader|adb_reboot_download|adb_sideload|adb_twrp_wipe_and_install|adb_twrp_copy_partitions|fastboot_boot_recovery|fastboot_flash_boot|fastboot_unlock_critical|fastboot_unlock_with_code|fastboot_get_unlock_data|fastboot_unlock|fastboot_oem_unlock|fastboot_reboot|heimdall_flash_recovery"
+ r"""adb_reboot|adb_reboot_bootloader|adb_reboot_download|adb_sideload|adb_twrp_wipe_and_install|adb_twrp_copy_partitions|fastboot_boot_recovery|fastboot_flash_boot|fastboot_flash_recovery|
+ fastboot_unlock_critical|fastboot_unlock_with_code|fastboot_get_unlock_data|fastboot_unlock|fastboot_oem_unlock|fastboot_reboot|fastboot_reboot_recovery|heimdall_flash_recovery|fastboot_flash_additional_partitions"""
),
schema.Optional("allow_skip"): bool,
schema.Optional("img"): str,
@@ -166,6 +169,11 @@ def validate_config(config: str) -> bool:
"device_code": str,
"supported_device_codes": [str],
schema.Optional("twrp-link"): str,
+ schema.Optional("additional_steps"): [
+ Regex(r"dtbo|vbmeta|vendor_boot|super_empty")
+ ],
+ schema.Optional("notes"): [str],
+ schema.Optional("brand"): str,
},
schema.Optional("requirements"): {
schema.Optional("android"): schema.Or(str, int),
diff --git a/openandroidinstaller/tooling.py b/openandroidinstaller/tooling.py
index d3a551be..03dcff5a 100644
--- a/openandroidinstaller/tooling.py
+++ b/openandroidinstaller/tooling.py
@@ -161,6 +161,15 @@ def adb_wait_for_sideload(bin_path: Path) -> TerminalResponse:
yield line
+@add_logging("Reboot to recovery with adb")
+def adb_reboot_recovery(bin_path: Path) -> TerminalResponse:
+ """Reboot to recovery with adb."""
+ for line in run_command("adb reboot recovery", bin_path):
+ yield line
+ for line in adb_wait_for_recovery(bin_path=bin_path):
+ yield line
+
+
def adb_twrp_copy_partitions(bin_path: Path, config_path: Path) -> TerminalResponse:
# some devices like one plus 6t or motorola moto g7 power need the partitions copied to prevent a hard brick
logger.info("Sideload copy_partitions script with adb.")
@@ -416,7 +425,7 @@ def fastboot_boot_recovery(
def fastboot_flash_boot(bin_path: Path, recovery: str) -> TerminalResponse:
- """Temporarily, flash custom recovery with fastboot to boot partition."""
+ """Flash custom recovery with fastboot to boot partition."""
logger.info("Flash custom recovery with fastboot.")
for line in run_command(
"fastboot flash boot", target=f"{recovery}", bin_path=bin_path
@@ -440,6 +449,85 @@ def fastboot_flash_boot(bin_path: Path, recovery: str) -> TerminalResponse:
yield True
+@add_logging("Flash custom recovery with fastboot.")
+def fastboot_flash_recovery(
+ bin_path: Path, recovery: str, is_ab: bool = True
+) -> TerminalResponse:
+ """Flash custom recovery with fastboot."""
+ for line in run_command(
+ "fastboot flash recovery ", target=f"{recovery}", bin_path=bin_path
+ ):
+ yield line
+ if not is_ab:
+ if (type(line) == bool) and not line:
+ logger.error("Flashing recovery failed.")
+ yield False
+ else:
+ yield True
+
+
+@add_logging("Rebooting device to recovery.")
+def fastboot_reboot_recovery(bin_path: Path) -> TerminalResponse:
+ """Reboot to recovery with fastboot.
+
+ WARNING: On some devices, users need to press a specific key combo to make it work.
+ """
+ for line in run_command("fastboot reboot recovery", bin_path):
+ yield line
+
+
+@add_logging("Flash additional partitions with fastboot")
+def fastboot_flash_additional_partitions(
+ bin_path: Path,
+ dtbo: Optional[str],
+ vbmeta: Optional[str],
+ super_empty: Optional[str],
+ is_ab: bool = True,
+) -> TerminalResponse:
+ """Flash additional partitions (dtbo, vbmeta, super_empty) with fastboot."""
+ logger.info("Flash additional partitions with fastboot.")
+ if dtbo:
+ logger.info("dtbo selected. Flashing dtbo partition.")
+ for line in run_command(
+ "fastboot flash dtbo ", target=f"{dtbo}", bin_path=bin_path
+ ):
+ yield line
+ if not is_ab:
+ if (type(line) == bool) and not line:
+ logger.error("Flashing dtbo failed.")
+ yield False
+ else:
+ yield True
+ else:
+ yield True
+
+ if vbmeta:
+ logger.info("vbmeta selected. Flashing vbmeta partition.")
+ for line in run_command(
+ "fastboot flash vbmeta ", target=f"{vbmeta}", bin_path=bin_path
+ ):
+ yield line
+ if not is_ab:
+ if (type(line) == bool) and not line:
+ logger.error("Flashing vbmeta failed.")
+ yield False
+ else:
+ yield True
+
+ if super_empty:
+ logger.info("super_empty selected. Wiping super partition.")
+ for line in run_command(
+ "fastboot wipe-super ", target=f"{super_empty}", bin_path=bin_path
+ ):
+ yield line
+ if not is_ab:
+ if (type(line) == bool) and not line:
+ logger.error("Wiping super failed.")
+ yield False
+ else:
+ yield True
+
+
def heimdall_wait_for_download_available(bin_path: Path) -> bool:
"""Use heimdall detect to wait for download mode to become available on the device."""
logger.info("Wait for download mode to become available.")
diff --git a/openandroidinstaller/utils.py b/openandroidinstaller/utils.py
index 0c17baf5..a8511611 100644
--- a/openandroidinstaller/utils.py
+++ b/openandroidinstaller/utils.py
@@ -41,7 +41,15 @@ def get_download_link(devicecode: str) -> Optional[str]:
def image_works_with_device(supported_device_codes: List[str], image_path: str) -> bool:
- """Determine if an image works for the given device."""
+ """Determine if an image works for the given device.
+
+ Args:
+ supported_device_codes: List of supported device codes from the config file.
+ image_path: Path to the image file.
+
+ Returns:
+ True if the image works with the device, False otherwise.
+ """
with zipfile.ZipFile(image_path) as image_zip:
with image_zip.open(
"META-INF/com/android/metadata", mode="r"
@@ -60,13 +68,41 @@ def image_works_with_device(supported_device_codes: List[str], image_path: str)
return False
-def recovery_works_with_device(device_code: str, recovery_path: str) -> bool:
+def image_sdk_level(image_path: str) -> int:
+ """Determine Android version of the selected image.
+
+ Example:
+ Android 13: 33
+ """
+ with zipfile.ZipFile(image_path) as image_zip:
+ with image_zip.open(
+ "META-INF/com/android/metadata", mode="r"
+ ) as image_metadata:
+ metadata = image_metadata.readlines()
+ for line in metadata:
+ if b"sdk-level" in line:
+ return int(line[line.find(b"=") + 1 : -1].decode("utf-8"))
+ return 0
+
+
+def recovery_works_with_device(
+ supported_device_codes: List[str], recovery_path: str
+) -> bool:
"""Determine if a recovery works for the given device.
BEWARE: THE RECOVERY PART IS STILL VERY BASIC!
+
+ Args:
+ supported_device_codes: List of supported device codes from the config file.
+ recovery_path: Path to the recovery file.
+
+ Returns:
+ True if the recovery works with the device, False otherwise.
"""
recovery_file_name = recovery_path.split("/")[-1]
- if (device_code in recovery_file_name) and ("twrp" in recovery_file_name):
+ if any(code in recovery_file_name for code in supported_device_codes) and (
+ "twrp" in recovery_file_name
+ ):
logger.success("Device supported by the selected recovery.")
return True
else:
diff --git a/openandroidinstaller/views/base.py b/openandroidinstaller/views/base.py
index f32b9d31..e40dd31b 100644
--- a/openandroidinstaller/views/base.py
+++ b/openandroidinstaller/views/base.py
@@ -34,7 +34,7 @@ def __init__(self, state: AppState, image: str = "placeholder.png"):
# right part of the display, add content here.
self.right_view_header = Column(width=self.column_width, height=120, spacing=30)
self.right_view = Column(
- alignment="center", width=self.column_width, height=650, scroll="auto"
+ alignment="center", width=self.column_width, height=650, scroll="adaptive"
)
# left part of the display: used for displaying the images
self.left_view = Column(
diff --git a/openandroidinstaller/views/install_view.py b/openandroidinstaller/views/install_view.py
index 4b2b7021..6e8c7b4d 100644
--- a/openandroidinstaller/views/install_view.py
+++ b/openandroidinstaller/views/install_view.py
@@ -54,7 +54,7 @@ def __init__(
def build(self):
"""Create the content of the view."""
# error text
- self.error_text = Text("", color=colors.RED)
+ self.error_text = Text("", color=colors.GREEN)
# switch to enable advanced output - here it means show terminal input/output in tool
def check_advanced_switch(e):
@@ -178,7 +178,8 @@ def run_install(self, e):
# disable the call button while the command is running
self.install_button.disabled = True
self.install_addons_switch.disabled = True
- self.error_text.value = ""
+ self.error_text.value = "Please be patient, it may take a few minutes."
+ self.error_text.color = colors.GREEN
# reset the progress indicators
self.progress_indicator.clear()
# reset terminal output
@@ -208,6 +209,7 @@ def run_install(self, e):
self.install_button.disabled = False
# also remove the last error text if it happened
self.error_text.value = "Installation failed! Try again or make sure everything is setup correctly."
+ self.error_text.color = colors.RED
else:
sleep(5) # wait to make sure everything is fine
self.progress_indicator.set_progress_bar(100)
diff --git a/openandroidinstaller/views/select_view.py b/openandroidinstaller/views/select_view.py
index 442fa98b..1e3e98f0 100644
--- a/openandroidinstaller/views/select_view.py
+++ b/openandroidinstaller/views/select_view.py
@@ -30,6 +30,7 @@
AlertDialog,
FilePicker,
FilePickerResultEvent,
+ Checkbox,
)
from flet_core.buttons import CountinuosRectangleBorder
@@ -40,7 +41,12 @@
from views import BaseView
from app_state import AppState
from widgets import get_title, confirm_button
-from utils import get_download_link, image_works_with_device, recovery_works_with_device
+from utils import (
+ get_download_link,
+ image_works_with_device,
+ recovery_works_with_device,
+ image_sdk_level,
+)
class SelectFilesView(BaseView):
@@ -95,14 +101,36 @@ def init_visuals(
# initialize file pickers
self.pick_image_dialog = FilePicker(on_result=self.pick_image_result)
self.pick_recovery_dialog = FilePicker(on_result=self.pick_recovery_result)
+ self.pick_dtbo_dialog = FilePicker(on_result=self.pick_dtbo_result)
+ self.pick_vbmeta_dialog = FilePicker(on_result=self.pick_vbmeta_result)
+ self.pick_super_empty_dialog = FilePicker(
+ on_result=self.pick_super_empty_result
+ )
+
self.selected_image = Text("Selected image: ")
self.selected_recovery = Text("Selected recovery: ")
+ self.selected_dtbo = Checkbox(
+ fill_color=colors.RED, value=None, disabled=True, tristate=True
+ )
+ self.selected_vbmeta = Checkbox(
+ fill_color=colors.RED, value=None, disabled=True, tristate=True
+ )
+ self.selected_super_empty = Checkbox(
+ fill_color=colors.RED, value=None, disabled=True, tristate=True
+ )
# initialize and manage button state.
self.confirm_button = confirm_button(self.on_confirm)
self.confirm_button.disabled = True
+ self.continue_eitherway_button = confirm_button(
+ self.on_confirm, "Continue without additional images"
+ )
+ self.continue_eitherway_button.disabled = True
self.pick_recovery_dialog.on_result = self.enable_button_if_ready
self.pick_image_dialog.on_result = self.enable_button_if_ready
+ self.pick_dtbo_dialog.on_result = self.enable_button_if_ready
+ self.pick_vbmeta_dialog.on_result = self.enable_button_if_ready
+ self.pick_super_empty_dialog.on_result = self.enable_button_if_ready
# back button
self.back_button = ElevatedButton(
"Back",
@@ -120,11 +148,18 @@ def build(self):
)
# attach hidden dialogues
- self.right_view.controls.append(self.pick_image_dialog)
- self.right_view.controls.append(self.pick_recovery_dialog)
+ self.right_view.controls.extend(
+ [
+ self.pick_image_dialog,
+ self.pick_recovery_dialog,
+ self.pick_dtbo_dialog,
+ self.pick_vbmeta_dialog,
+ self.pick_super_empty_dialog,
+ ]
+ )
- # create help/info button to show the help dialog
- info_button = OutlinedButton(
+ # create help/info button to show the help dialog for the image and recovery selection
+ explain_images_button = OutlinedButton(
"What is this?",
on_click=self.open_explain_images_dlg,
expand=True,
@@ -137,13 +172,31 @@ def build(self):
self.right_view_header.controls.append(
get_title(
"Now pick an OS image and a recovery file:",
- info_button=info_button,
+ info_button=explain_images_button,
step_indicator_img="steps-header-select.png",
)
)
# text row to show infos during the process
self.info_field = Row()
+ # column to insert the additional image selection controls if needed
+ self.additional_image_selection = Column()
+
+ # Device specific notes
+ notes = self.get_notes()
+ if notes:
+ self.right_view.controls.extend(
+ [
+ Text(
+ "Important notes for your device",
+ style="titleSmall",
+ color=colors.RED,
+ weight="bold",
+ ),
+ Markdown(notes),
+ ]
+ )
+
# if there is an available download, show the button to the page
if self.download_link:
twrp_download_link = f"https://dl.twrp.me/{self.state.config.twrp_link if self.state.config.twrp_link else self.state.config.device_code}"
@@ -202,36 +255,219 @@ def build(self):
),
self.selected_image,
Divider(),
- Text("Select a TWRP recovery image:", style="titleSmall"),
- Markdown(
- f"""
+ ]
+ )
+ if self.state.flash_recovery:
+ self.right_view.controls.extend(
+ [
+ Text("Select a TWRP recovery image:", style="titleSmall"),
+ Markdown(
+ f"""
The recovery image should look something like `twrp-3.7.0_12-0-{self.state.config.device_code}.img`.
**Note:** This tool **only supports TWRP recoveries**.""",
- extension_set="gitHubFlavored",
- ),
- Row(
- [
- FilledButton(
- "Pick TWRP recovery file",
- icon=icons.UPLOAD_FILE,
- on_click=lambda _: self.pick_recovery_dialog.pick_files(
- allow_multiple=False,
- file_type="custom",
- allowed_extensions=["img"],
+ extension_set="gitHubFlavored",
+ ),
+ Row(
+ [
+ FilledButton(
+ "Pick TWRP recovery file",
+ icon=icons.UPLOAD_FILE,
+ on_click=lambda _: self.pick_recovery_dialog.pick_files(
+ allow_multiple=False,
+ file_type="custom",
+ allowed_extensions=["img"],
+ ),
+ expand=True,
),
- expand=True,
- ),
- ]
- ),
- self.selected_recovery,
- Divider(),
+ ]
+ ),
+ self.selected_recovery,
+ Divider(),
+ self.additional_image_selection,
+ ]
+ )
+
+ # attach the bottom buttons
+ if self.state.config.additional_steps:
+ bottom_buttons = [
+ self.back_button,
+ self.continue_eitherway_button,
+ self.confirm_button,
+ ]
+
+ else:
+ bottom_buttons = [self.back_button, self.confirm_button]
+ self.right_view.controls.extend(
+ [
self.info_field,
- Row([self.back_button, self.confirm_button]),
+ Row(bottom_buttons),
]
)
return self.view
+ def get_notes(self) -> str:
+ """Prepare and get notes for the specific device from config.
+
+ These notes should be displayed to the user.
+ """
+ notes = []
+
+ brand = self.state.config.metadata.get("brand", "")
+ if brand in ["xiaomi", "poco"]:
+ notes.append(
+ "- If something goes wrong, you can reinstall MiUI here:\n\n"
+ )
+
+ # this should be used as little as possible!
+ if self.state.config.metadata.get("untested", False):
+ notes.append(
+ "- **This device has not been tested with OpenAndroidInstaller yet.** The installation can go wrong. You may have to finish the installation process with command line. If you test, please report the result on GitHub."
+ )
+
+ notes.extend(
+ f"- {note}" for note in self.state.config.metadata.get("notes", [])
+ )
+ return "\n\n".join(notes)
+
+ def toggle_additional_image_selection(self):
+ """Toggle the visibility of the additional image selection controls."""
+ # dialogue box to explain additional required images
+ self.dlg_explain_additional_images = AlertDialog(
+ modal=True,
+ title=Text("Why do I need additional images and where do I get them?"),
+ content=Markdown(
+ f"""## About additional images
+Some devices require additional images to be flashed before the recovery and OS image can be flashed.
+Not all images explained below are required for all devices. The installer will tell you which images are required for your device.
+
+### dtbo.img
+The `dtbo.img` is a partition image that contains the device tree overlay.
+
+### vbmeta.img
+The `vbmeta.img` is a partition image that contains the verified boot metadata.
+This is required to prevent issues with the verified boot process.
+
+### super_empty.img
+The `super_empty.img` is used to wipe the super partition. This is required to
+prevent issues with the super partition when flashing a new ROM.
+
+### vendor_boot.img
+The `vendor_boot.img` is a partition image that contains the vendor boot image.
+
+## Where do I get these images?
+You can download the required images for your device from the [LineageOS downloads page](https://download.lineageos.org/devices/{self.state.config.device_code}/builds).
+If this download page does not contain the required images, you can try to find them on the [XDA developers forum](https://forum.xda-developers.com/).
+ """,
+ auto_follow_links=True,
+ ),
+ actions=[
+ TextButton(
+ "Close", on_click=self.close_close_explain_additional_images_dlg
+ ),
+ ],
+ actions_alignment="end",
+ shape=CountinuosRectangleBorder(radius=0),
+ )
+
+ # create help/info button to show the help dialog for the image and recovery selection
+ explain_additional_images_button = OutlinedButton(
+ "Why do I need this and where do I get it?",
+ on_click=self.open_explain_additional_images_dlg,
+ expand=True,
+ icon=icons.HELP_OUTLINE_OUTLINED,
+ icon_color=colors.DEEP_ORANGE_500,
+ tooltip="Get more details on additional images and download links.",
+ )
+
+ # attach the controls for uploading others partitions, like dtbo, vbmeta & super_empty
+ additional_image_selection = []
+ if self.state.config.additional_steps:
+ additional_image_selection.extend(
+ [
+ Row(
+ [
+ Text(
+ "Select required additional images:", style="titleSmall"
+ ),
+ explain_additional_images_button,
+ ]
+ ),
+ Markdown(
+ """
+Your selected device and ROM requires flashing of additional partitions. Please select the required images below.
+
+Make sure the file is for **your exact phone model!**""",
+ ),
+ ]
+ )
+ if "dtbo" in self.state.config.additional_steps:
+ self.selected_dtbo.value = False
+ additional_image_selection.extend(
+ [
+ Row(
+ [
+ FilledButton(
+ "Pick `dtbo.img` image",
+ icon=icons.UPLOAD_FILE,
+ on_click=lambda _: self.pick_dtbo_dialog.pick_files(
+ allow_multiple=False,
+ file_type="custom",
+ allowed_extensions=["img"],
+ ),
+ expand=True,
+ ),
+ self.selected_dtbo,
+ ]
+ ),
+ ]
+ )
+ if "vbmeta" in self.state.config.additional_steps:
+ self.selected_vbmeta.value = False
+ additional_image_selection.extend(
+ [
+ Row(
+ [
+ FilledButton(
+ "Pick `vbmeta.img` image",
+ icon=icons.UPLOAD_FILE,
+ on_click=lambda _: self.pick_vbmeta_dialog.pick_files(
+ allow_multiple=False,
+ file_type="custom",
+ allowed_extensions=["img"],
+ ),
+ expand=True,
+ ),
+ self.selected_vbmeta,
+ ]
+ ),
+ ]
+ )
+ if "super_empty" in self.state.config.additional_steps:
+ self.selected_super_empty.value = False
+ additional_image_selection.extend(
+ [
+ Row(
+ [
+ FilledButton(
+ "Pick `super_empty.img` image",
+ icon=icons.UPLOAD_FILE,
+ on_click=lambda _: self.pick_super_empty_dialog.pick_files(
+ allow_multiple=False,
+ file_type="custom",
+ allowed_extensions=["img"],
+ ),
+ expand=True,
+ ),
+ self.selected_super_empty,
+ ]
+ ),
+ Divider(),
+ ]
+ )
+ self.additional_image_selection.controls = additional_image_selection
+ self.additional_image_selection.update()
+
def open_explain_images_dlg(self, e):
"""Open the dialog to explain OS and recovery image."""
self.page.dialog = self.dlg_explain_images
@@ -243,6 +479,17 @@ def close_close_explain_images_dlg(self, e):
self.dlg_explain_images.open = False
self.page.update()
+ def open_explain_additional_images_dlg(self, e):
+ """Open the dialog to explain additional images."""
+ self.page.dialog = self.dlg_explain_additional_images
+ self.dlg_explain_additional_images.open = True
+ self.page.update()
+
+ def close_close_explain_additional_images_dlg(self, e):
+ """Close the dialog to explain additional images."""
+ self.dlg_explain_additional_images.open = False
+ self.page.update()
+
def pick_image_result(self, e: FilePickerResultEvent):
path = ", ".join(map(lambda f: f.name, e.files)) if e.files else "Cancelled!"
# update the textfield with the name of the file
@@ -264,6 +511,16 @@ def pick_image_result(self, e: FilePickerResultEvent):
self.selected_image.color = colors.GREEN
else:
self.selected_image.color = colors.RED
+ # if the image works and the sdk level is 33 or higher, show the additional image selection
+ if self.state.flash_recovery:
+ if (
+ self.selected_image.color == colors.GREEN
+ and image_sdk_level(self.state.image_path) >= 33
+ ):
+ self.toggle_additional_image_selection()
+ else:
+ self.additional_image_selection.controls = []
+ self.additional_image_selection.update()
# update
self.selected_image.update()
@@ -281,9 +538,9 @@ def pick_recovery_result(self, e: FilePickerResultEvent):
logger.info("No image selected.")
# check if the recovery works with the device and show the filename in different colors accordingly
if e.files:
- device_code = self.state.config.device_code
if recovery_works_with_device(
- device_code=device_code, recovery_path=self.state.recovery_path
+ supported_device_codes=self.state.config.supported_device_codes,
+ recovery_path=self.state.recovery_path,
):
self.selected_recovery.color = colors.GREEN
else:
@@ -291,19 +548,71 @@ def pick_recovery_result(self, e: FilePickerResultEvent):
# update
self.selected_recovery.update()
+ def pick_dtbo_result(self, e: FilePickerResultEvent):
+ path = ", ".join(map(lambda f: f.name, e.files)) if e.files else "Cancelled!"
+ if e.files:
+ # check if the dtbo works with the device and show the filename in different colors accordingly
+ if path == "dtbo.img":
+ self.selected_dtbo.fill_color = colors.GREEN
+ self.selected_dtbo.value = True
+ self.state.dtbo_path = e.files[0].path
+ logger.info(f"Selected dtbo from {self.state.dtbo_path}")
+ else:
+ self.selected_dtbo.fill_color = colors.RED
+ self.selected_dtbo.value = False
+ else:
+ logger.info("No image selected.")
+ # update
+ self.selected_dtbo.update()
+
+ def pick_vbmeta_result(self, e: FilePickerResultEvent):
+ path = ", ".join(map(lambda f: f.name, e.files)) if e.files else "Cancelled!"
+ if e.files:
+ # check if the vbmeta works with the device and show the filename in different colors accordingly
+ if path == "vbmeta.img":
+ self.selected_vbmeta.fill_color = colors.GREEN
+ self.selected_vbmeta.value = True
+ self.state.vbmeta_path = e.files[0].path
+ logger.info(f"Selected vbmeta from {self.state.vbmeta_path}")
+ else:
+ self.selected_vbmeta.fill_color = colors.RED
+ self.selected_vbmeta.value = False
+ else:
+ logger.info("No image selected.")
+ # update
+ self.selected_vbmeta.update()
+
+ def pick_super_empty_result(self, e: FilePickerResultEvent):
+ path = ", ".join(map(lambda f: f.name, e.files)) if e.files else "Cancelled!"
+ # update the textfield with the name of the file
+ if e.files:
+ # check if the super_empty works with the device and show the filename in different colors accordingly
+ if path == "super_empty.img":
+ self.selected_super_empty.fill_color = colors.GREEN
+ self.selected_super_empty.value = True
+ self.state.super_empty_path = e.files[0].path
+ logger.info(f"Selected super_empty from {self.state.super_empty_path}")
+ else:
+ self.selected_super_empty.fill_color = colors.RED
+ self.selected_super_empty.value = False
+ else:
+ logger.info("No image selected.")
+ # update
+ self.selected_super_empty.update()
+
def enable_button_if_ready(self, e):
"""Enable the confirm button if both files have been selected."""
if (".zip" in self.selected_image.value) and (
".img" in self.selected_recovery.value
):
- device_code = self.state.config.device_code
if not (
image_works_with_device(
supported_device_codes=self.state.config.supported_device_codes,
image_path=self.state.image_path,
)
and recovery_works_with_device(
- device_code=device_code, recovery_path=self.state.recovery_path
+ supported_device_codes=self.state.config.supported_device_codes,
+ recovery_path=self.state.recovery_path,
)
):
# if image and recovery work for device allow to move on, otherwise display message
@@ -318,11 +627,67 @@ def enable_button_if_ready(self, e):
)
]
self.confirm_button.disabled = True
+ self.continue_eitherway_button.disabled = True
self.right_view.update()
return
+
+ self.continue_eitherway_button.disabled = False
+
+ # check if the additional images are there
+ if self.state.config.additional_steps and not all(
+ [
+ self.selected_dtbo.value or "dtbo" not in self.state.config.additional_steps,
+ self.selected_vbmeta.value or "vbmeta" not in self.state.config.additional_steps,
+ self.selected_super_empty.value or "super_empty" not in self.state.config.additional_steps,
+ ]
+ ):
+ logger.error(
+ "Some additional images don't match or are missing. Please select different ones."
+ )
+ self.info_field.controls = [
+ Text(
+ "Some additional images don't match or are missing. Please select the right ones.",
+ color=colors.RED,
+ weight="bold",
+ )
+ ]
+ self.confirm_button.disabled = True
+ self.right_view.update()
+ return
+
logger.info("Image and recovery work with the device. You can continue.")
self.info_field.controls = []
self.confirm_button.disabled = False
+ self.continue_eitherway_button.disabled = True
+ self.right_view.update()
+ elif (".zip" in self.selected_image.value) and (not self.state.flash_recovery):
+ if not (
+ image_works_with_device(
+ supported_device_codes=self.state.config.supported_device_codes,
+ image_path=self.state.image_path,
+ )
+ ):
+ # if image works for device allow to move on, otherwise display message
+ logger.error(
+ "Image doesn't work with the device. Please select a different one."
+ )
+ self.info_field.controls = [
+ Text(
+ "Image doesn't work with the device.",
+ color=colors.RED,
+ weight="bold",
+ )
+ ]
+ self.confirm_button.disabled = True
+ self.continue_eitherway_button.disabled = True
+ self.right_view.update()
+ return
+
+ logger.info("Image works with the device. You can continue.")
+ self.info_field.controls = []
+ self.confirm_button.disabled = False
+ self.continue_eitherway_button.disabled = True
self.right_view.update()
else:
self.confirm_button.disabled = True
+ # self.continue_eitherway_button.disabled = True
diff --git a/openandroidinstaller/views/start_view.py b/openandroidinstaller/views/start_view.py
index 1f30490d..f7333429 100644
--- a/openandroidinstaller/views/start_view.py
+++ b/openandroidinstaller/views/start_view.py
@@ -13,7 +13,6 @@
# If not, see ."""
# Author: Tobias Sterbak
-import copy
import webbrowser
from loguru import logger
from typing import Callable
@@ -99,15 +98,7 @@ def init_visuals(
# toggleswitch to allow skipping unlocking the bootloader
def check_bootloader_unlocked(e):
"""Enable skipping unlocking the bootloader if selected."""
- if self.bootloader_switch.value:
- logger.info("Skipping bootloader unlocking.")
- self.state.steps = []
- else:
- logger.info("Enabled unlocking the bootloader again.")
- self.state.steps = copy.deepcopy(self.state.config.unlock_bootloader)
- # if the recovery is already flashed, skip flashing it again
- if self.recovery_switch.value == False:
- self.state.steps += copy.deepcopy(self.state.config.boot_recovery)
+ self.state.toggle_flash_unlock_bootloader()
self.bootloader_switch = Switch(
label="Bootloader is already unlocked.",
@@ -120,18 +111,7 @@ def check_bootloader_unlocked(e):
# toggleswitch to allow skipping flashing recovery
def check_recovery_already_flashed(e):
"""Enable skipping flashing recovery if selected."""
-
- # manage the bootloader unlocking switch
- if self.bootloader_switch.value == False:
- self.state.steps = copy.deepcopy(self.state.config.unlock_bootloader)
- else:
- self.state.steps = []
-
- if self.recovery_switch.value:
- logger.info("Skipping flashing recovery.")
- else:
- logger.info("Enabled flashing recovery again.")
- self.state.steps += copy.deepcopy(self.state.config.boot_recovery)
+ self.state.toggle_flash_recovery()
self.recovery_switch = Switch(
label="Custom recovery is already flashed.",
@@ -206,9 +186,6 @@ def build(self):
If you don't know what this means, you most likely don't need to do anything and you can just continue.
"""
),
- Row([self.bootloader_switch]),
- Row([self.recovery_switch]),
- Divider(),
self.device_infobox,
Row(
[
@@ -224,6 +201,8 @@ def build(self):
],
alignment="center",
),
+ Divider(),
+ Row([self.bootloader_switch, self.recovery_switch]),
]
)
return self.view
diff --git a/openandroidinstaller/views/step_view.py b/openandroidinstaller/views/step_view.py
index 3ab89f23..3fbe2af3 100644
--- a/openandroidinstaller/views/step_view.py
+++ b/openandroidinstaller/views/step_view.py
@@ -41,10 +41,14 @@
adb_reboot,
adb_reboot_bootloader,
adb_reboot_download,
+ adb_reboot_recovery,
adb_sideload,
adb_twrp_copy_partitions,
fastboot_boot_recovery,
fastboot_flash_boot,
+ fastboot_flash_recovery,
+ fastboot_reboot_recovery,
+ fastboot_flash_additional_partitions,
fastboot_oem_unlock,
fastboot_reboot,
fastboot_unlock,
@@ -213,6 +217,7 @@ def call_to_phone(self, e, command: str):
"adb_reboot": adb_reboot,
"adb_reboot_bootloader": adb_reboot_bootloader,
"adb_reboot_download": adb_reboot_download,
+ "adb_reboot_recovery": adb_reboot_recovery,
"adb_sideload": partial(adb_sideload, target=self.state.image_path),
"adb_twrp_copy_partitions": partial(
adb_twrp_copy_partitions, config_path=self.state.config_path
@@ -233,6 +238,19 @@ def call_to_phone(self, e, command: str):
fastboot_flash_boot,
recovery=self.state.recovery_path,
),
+ "fastboot_flash_recovery": partial(
+ fastboot_flash_recovery,
+ recovery=self.state.recovery_path,
+ is_ab=self.state.config.is_ab,
+ ),
+ "fastboot_reboot_recovery": fastboot_reboot_recovery,
+ "fastboot_flash_additional_partitions": partial(
+ fastboot_flash_additional_partitions,
+ dtbo=self.state.dtbo_path,
+ vbmeta=self.state.vbmeta_path,
+ super_empty=self.state.super_empty_path,
+ is_ab=self.state.config.is_ab,
+ ),
"fastboot_reboot": fastboot_reboot,
"heimdall_flash_recovery": partial(
heimdall_flash_recovery, recovery=self.state.recovery_path
diff --git a/poetry.lock b/poetry.lock
index 63bc8e14..143175c4 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -903,7 +903,7 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"]
[metadata]
lock-version = "1.1"
python-versions = "<3.11,>=3.9"
-content-hash = "f5462dcfd8695f093ffdc7e70d7a90909ab0982f96c3de1564b831223221faeb"
+content-hash = "5eb8a128bbc3eb03bb8dd70b45c9e546d7ef88c16b9b562e9c769d1ccc7c0f52"
[metadata.files]
altgraph = [
diff --git a/pyproject.toml b/pyproject.toml
index 61b0fd15..9e20594a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -29,6 +29,7 @@ pytest-mock = "^3.10.0"
bandit = "^1.7.4"
pytest-subprocess = "^1.5.0"
mypy = "^1.0.0"
+typing-extensions = "^4.7.1"
[tool.poetry.dev-dependencies]
diff --git a/tests/test_progress_bar.py b/tests/test_progress_bar.py
index 6d79afe5..31700450 100644
--- a/tests/test_progress_bar.py
+++ b/tests/test_progress_bar.py
@@ -39,7 +39,7 @@ def test_update_progress_bar():
assert progress_indicator.progress_bar
# test if percentages are parsed correctly and update is performed
- for percentage in range(0, 47):
+ for percentage in range(1, 47):
line = f"serving: '/home/tobias/Repositories/openandroidinstaller/images/google-pixel3a/lineage-19.1-20221004-nightly-sargo-signed.zip' (~{percentage}%)\n"
progress_indicator.display_progress_bar(line)
assert progress_indicator.progress_bar.value == percentage / 100
@@ -47,3 +47,7 @@ def test_update_progress_bar():
# test if the finishing print is detected and updated correctly.
progress_indicator.display_progress_bar(line="Total xfer: 1.00x\n")
assert progress_indicator.progress_bar.value == 0.99
+
+ # test if the final set_progress_bar is working correctly
+ progress_indicator.set_progress_bar(100)
+ assert progress_indicator.progress_bar.value == 1.0