From 6f61999b3db06c9c980bd2705e9a6f2a99c980ed Mon Sep 17 00:00:00 2001 From: rudu <40572253+anon1892@users.noreply.github.com> Date: Thu, 27 Jul 2023 15:08:22 +0200 Subject: [PATCH 1/6] Add support for Xiaomi with OrangeFox. For now, lavender, ginkgo and biloba --- README.md | 10 +++- .../assets/configs/ginkgo.yaml | 49 +++++++++++++++++ .../assets/configs/lavender.yaml | 48 +++++++++++++++++ openandroidinstaller/installer_config.py | 7 ++- openandroidinstaller/tooling.py | 52 +++++++++++++++++-- openandroidinstaller/utils.py | 3 ++ openandroidinstaller/views/install_view.py | 1 + openandroidinstaller/views/select_view.py | 17 +++--- openandroidinstaller/views/step_view.py | 8 +++ 9 files changed, 182 insertions(+), 13 deletions(-) create mode 100644 openandroidinstaller/assets/configs/ginkgo.yaml create mode 100644 openandroidinstaller/assets/configs/lavender.yaml diff --git a/README.md b/README.md index a49b1a4a..8139f3c9 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Linux is currently the best supported platform (tested with Ubuntu 20.04/22.04 L ## Officially supported devices -Currently, the **we support 62 devices** by various vendors and working on adding more soon! +Currently, the **we support 65 devices** by various vendors and working on adding more soon! Support for these devices is provided as best effort, but things might still go wrong. @@ -169,6 +169,14 @@ 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 Note 7 | [lavender](https://wiki.lineageos.org/devices/lavender) | lavender | tested +Xiaomi | Redmi Note 8 / 8T | [ginkgo](https://wiki.lineageos.org/devices/ginkgo) | ginkgo | untested +
+ And more to come! diff --git a/openandroidinstaller/assets/configs/ginkgo.yaml b/openandroidinstaller/assets/configs/ginkgo.yaml new file mode 100644 index 00000000..103b4425 --- /dev/null +++ b/openandroidinstaller/assets/configs/ginkgo.yaml @@ -0,0 +1,49 @@ +metadata: + maintainer: A non (anon) + device_name: Xiaomi Redmi Note 8 / 8T + is_ab_device: false + device_code: ginkgo + recovery: orangefox + supported_device_codes: + - ginkgo + - biloba +requirements: + android: 10 (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. + allow_skip: true + - type: link_button_with_confirm + content: > + Please download official Xiaomi unlock tool, and follow instructions. + https://en.miui.com/unlock/download_en.html + You may have to wait 7 days before being able to unlock the bootloader. + Do not attempt to bypass this limitation / use non officials tools, you may brick your phone. + link: https://en.miui.com/unlock/download_en.html + allow_skip: true + - type: confirm_button + content: > + The bootloader is now unlocked. Since the device resets completely, you will need to re-enable USB debugging to continue. + boot_recovery: + - type: confirm_button + content: > + Now you need to boot 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. + - type: call_button + content: > + 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 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 diff --git a/openandroidinstaller/assets/configs/lavender.yaml b/openandroidinstaller/assets/configs/lavender.yaml new file mode 100644 index 00000000..9ca75cc2 --- /dev/null +++ b/openandroidinstaller/assets/configs/lavender.yaml @@ -0,0 +1,48 @@ +metadata: + maintainer: A non (anon) + device_name: Xiaomi Redmi Note 7 + is_ab_device: false + device_code: lavender + recovery: orangefox + supported_device_codes: + - lavender +requirements: + android: 10 (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. + allow_skip: true + - type: link_button_with_confirm + content: > + Please download official Xiaomi unlock tool, and follow instructions. + https://en.miui.com/unlock/download_en.html + You may have to wait 7 days before being able to unlock the bootloader. + Do not attempt to bypass this limitation / use non officials tools, you may brick your phone. + link: https://en.miui.com/unlock/download_en.html + allow_skip: true + - type: confirm_button + content: > + The bootloader is now unlocked. Since the device resets completely, you will need to re-enable USB debugging to continue. + boot_recovery: + - type: confirm_button + content: > + Now you need to boot 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. + - type: call_button + content: > + 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 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 diff --git a/openandroidinstaller/installer_config.py b/openandroidinstaller/installer_config.py index 2a857d83..d0b63dd9 100644 --- a/openandroidinstaller/installer_config.py +++ b/openandroidinstaller/installer_config.py @@ -124,6 +124,8 @@ def _load_config(device_code: str, config_path: Path) -> Optional[InstallerConfi if custom_path: config = InstallerConfig.from_file(custom_path) logger.info(f"Loaded custom device config from {custom_path}.") + if 'recovery' not in config.metadata: + config.metadata.update({'recovery': 'twrp'}) logger.info(f"Config metadata: {config.metadata}.") return config else: @@ -133,6 +135,8 @@ def _load_config(device_code: str, config_path: Path) -> Optional[InstallerConfi if path: config = InstallerConfig.from_file(path) logger.info(f"Loaded device config from {path}.") + if 'recovery' not in config.metadata: + config.metadata.update({'recovery': 'twrp'}) if config: logger.info(f"Config metadata: {config.metadata}.") return config @@ -150,7 +154,7 @@ 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_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_unlock_with_code|fastboot_get_unlock_data|fastboot_unlock|fastboot_oem_unlock|fastboot_reboot|heimdall_flash_recovery|fastboot_reboot_recovery|fastboot_flash_recovery" ), schema.Optional("allow_skip"): bool, schema.Optional("img"): str, @@ -166,6 +170,7 @@ def validate_config(config: str) -> bool: "device_code": str, "supported_device_codes": [str], schema.Optional("twrp-link"): str, + schema.Optional("recovery"): str, }, schema.Optional("requirements"): { schema.Optional("android"): schema.Or(str, int), diff --git a/openandroidinstaller/tooling.py b/openandroidinstaller/tooling.py index d3a551be..0fe86219 100644 --- a/openandroidinstaller/tooling.py +++ b/openandroidinstaller/tooling.py @@ -139,6 +139,11 @@ def activate_sideload(bin_path: Path) -> TerminalResponse: for line in adb_wait_for_sideload(bin_path=bin_path): yield line +@add_logging("Activate sideloading in OranfeFox.", return_if_fail=True) +def activate_sideload_ofox(bin_path: Path) -> TerminalResponse: + """Activate sideload with adb shell in OrangeFox.""" + for line in run_command("adb shell twrp sideload help", bin_path): # Why help ? Don't know, but it works + yield line @add_logging("Wait for device") def adb_wait_for_device(bin_path: Path) -> TerminalResponse: @@ -216,12 +221,13 @@ def adb_twrp_wipe_and_install( target: str, config_path: Path, is_ab: bool, + which_recovery: str, install_addons=True, recovery: Optional[str] = None, ) -> TerminalResponse: """Wipe and format data with twrp, then flash os image with adb. - Only works for twrp recovery. + Only works for twrp and orangefox recovery. """ logger.info("Wipe and format data with twrp, then install os image.") for line in adb_wait_for_recovery(bin_path): @@ -239,15 +245,30 @@ def adb_twrp_wipe_and_install( sleep(1) # activate sideload - logger.info("Wiping is done, now activate sideload.") - for line in activate_sideload(bin_path=bin_path): - yield line + if which_recovery == 'twrp': + logger.info("Wiping is done, now activate sideload.") + for line in activate_sideload(bin_path=bin_path): + yield line + else: + logger.info("Wiping is done, now activate sideload (ofox)") + for line in activate_sideload_ofox(bin_path=bin_path): + yield line + sleep(5) + # now flash os image logger.info("Sideload and install os image.") for line in adb_sideload(bin_path=bin_path, target=target): yield line + # wipe some cache partitions - sleep(7) + if which_recovery == 'orangefox': + logger.info("Waiting for OrangeFox to restart...") + for line in adb_wait_for_recovery(bin_path): + yield line + else: + sleep(7) + + logger.info("Wiping cache and dalvik...") for partition in ["dalvik", "cache"]: for line in run_command(f"adb shell twrp wipe {partition}", bin_path): yield line @@ -497,3 +518,24 @@ def search_device(platform: str, bin_path: Path) -> Optional[str]: except CalledProcessError: logger.error("Failed to detect a device.") return None + +@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.""" + logger.info("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""" + for line in run_command("fastboot reboot recovery", bin_path): + yield line \ No newline at end of file diff --git a/openandroidinstaller/utils.py b/openandroidinstaller/utils.py index 0c17baf5..49eecca1 100644 --- a/openandroidinstaller/utils.py +++ b/openandroidinstaller/utils.py @@ -69,6 +69,9 @@ def recovery_works_with_device(device_code: str, recovery_path: str) -> bool: if (device_code in recovery_file_name) and ("twrp" in recovery_file_name): logger.success("Device supported by the selected recovery.") return True + elif recovery_file_name == "recovery.img": + logger.error("Cannot check recovery. Supposing it is OrangeFox.") + return True else: logger.error(f"Recovery file {recovery_file_name} is not supported.") return False diff --git a/openandroidinstaller/views/install_view.py b/openandroidinstaller/views/install_view.py index de6c3858..69514b63 100644 --- a/openandroidinstaller/views/install_view.py +++ b/openandroidinstaller/views/install_view.py @@ -193,6 +193,7 @@ def run_install(self, e): bin_path=self.state.bin_path, install_addons=self.state.install_addons, is_ab=self.state.config.is_ab, + which_recovery=self.state.config.metadata['recovery'], recovery=self.state.recovery_path, ): # write the line to advanced output terminal diff --git a/openandroidinstaller/views/select_view.py b/openandroidinstaller/views/select_view.py index 442fa98b..ada3b314 100644 --- a/openandroidinstaller/views/select_view.py +++ b/openandroidinstaller/views/select_view.py @@ -83,7 +83,7 @@ def init_visuals( This custom software can include smaller modifications like rooting your device or even replacing the firmware of the device with a completely custom ROM. -OpenAndroidInstaller works with the [TWRP recovery project](https://twrp.me/about).""", +OpenAndroidInstaller works with the [TWRP recovery project](https://twrp.me/about) or [OrangeFox recovery](https://wiki.orangefox.tech/en/home), depending of your device.""", ), actions=[ TextButton("Close", on_click=self.close_close_explain_images_dlg), @@ -91,7 +91,6 @@ def init_visuals( actions_alignment="end", shape=CountinuosRectangleBorder(radius=0), ) - # 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) @@ -179,6 +178,12 @@ def build(self): ) ) # attach the controls for uploading image and recovery + if(self.state.config.metadata['recovery'] == "orangefox"): + recovery = "OrangeFox" + recoveryFile = "`recovery.img`" + else: + recovery = "TWRP" + recoveryFile = f"`twrp-3.7.0_12-0-{self.state.config.device_code}.img`" self.right_view.controls.extend( [ Text("Select an OS image:", style="titleSmall"), @@ -202,18 +207,18 @@ def build(self): ), self.selected_image, Divider(), - Text("Select a TWRP recovery image:", style="titleSmall"), + Text(f"Select a {recovery} 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`. +The recovery image should look something like {recoveryFile}. -**Note:** This tool **only supports TWRP recoveries**.""", +**Note:** This tool **only supports {recovery} recoveries**.""", extension_set="gitHubFlavored", ), Row( [ FilledButton( - "Pick TWRP recovery file", + f"Pick {recovery} recovery file", icon=icons.UPLOAD_FILE, on_click=lambda _: self.pick_recovery_dialog.pick_files( allow_multiple=False, diff --git a/openandroidinstaller/views/step_view.py b/openandroidinstaller/views/step_view.py index 1e4dffbf..01a2c6ed 100644 --- a/openandroidinstaller/views/step_view.py +++ b/openandroidinstaller/views/step_view.py @@ -51,6 +51,8 @@ fastboot_unlock_with_code, fastboot_get_unlock_data, heimdall_flash_recovery, + fastboot_flash_recovery, + fastboot_reboot_recovery, ) from widgets import ( call_button, @@ -235,6 +237,12 @@ def call_to_phone(self, e, command: str): "heimdall_flash_recovery": partial( heimdall_flash_recovery, 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, } # run the right command From a400b04a2de39de13c13d06dc347e3f600e1675a Mon Sep 17 00:00:00 2001 From: rudu <40572253+anon1892@users.noreply.github.com> Date: Thu, 27 Jul 2023 16:08:54 +0200 Subject: [PATCH 2/6] Some adjustments, and add of `notes` metadata --- README.md | 4 +++- openandroidinstaller/assets/configs/ginkgo.yaml | 8 +++++++- openandroidinstaller/assets/configs/lavender.yaml | 8 +++++++- openandroidinstaller/installer_config.py | 1 + openandroidinstaller/views/select_view.py | 13 ++++++++++++- 5 files changed, 30 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8139f3c9..05c1c707 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,8 @@ Every config file should have `metadata` with the following fields: - `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. +- `recovery`: [OPTIONAL] str; can be twrp or orangefox (twrp by default) +- `notes`: [OPTIONAL] str; specific phone information, showed before choosing ROM / recovery 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. @@ -239,7 +241,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_reboot_recovery`, `fastboot_flash_recovery` `fastboot_unlock_with_code`, `fastboot_unlock`, `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/assets/configs/ginkgo.yaml b/openandroidinstaller/assets/configs/ginkgo.yaml index 103b4425..62c2c723 100644 --- a/openandroidinstaller/assets/configs/ginkgo.yaml +++ b/openandroidinstaller/assets/configs/ginkgo.yaml @@ -7,6 +7,12 @@ metadata: supported_device_codes: - ginkgo - biloba + notes: > + - If something goes wrong, you can reinstall MiUI here : https://xiaomifirmwareupdater.com/miui/lavender/ + + - You should install Android 10 or newer ROM. + + - Be careful when choosing OrangeFox version, Android 12 & 13 ROM needs OrangeFox version code with `A12`, for example `R11.1_5_A12`. requirements: android: 10 (Q) steps: @@ -14,7 +20,7 @@ steps: - 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. + how to start and run an operating system (like Android). Your device should be turned on. This will reset your phone. allow_skip: true - type: link_button_with_confirm content: > diff --git a/openandroidinstaller/assets/configs/lavender.yaml b/openandroidinstaller/assets/configs/lavender.yaml index 9ca75cc2..e09d6d68 100644 --- a/openandroidinstaller/assets/configs/lavender.yaml +++ b/openandroidinstaller/assets/configs/lavender.yaml @@ -6,6 +6,12 @@ metadata: recovery: orangefox supported_device_codes: - lavender + notes: > + - If something goes wrong, you can reinstall MiUI here : https://xiaomifirmwareupdater.com/miui/lavender/ + + - You should install Android 10 or newer ROM. + + - Be careful when choosing OrangeFox version, Android 12 & 13 ROM needs OrangeFox version code with `A12`, for example `R11.1_5_A12`. requirements: android: 10 (Q) steps: @@ -13,7 +19,7 @@ steps: - 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. + how to start and run an operating system (like Android). Your device should be turned on. This will reset your phone. allow_skip: true - type: link_button_with_confirm content: > diff --git a/openandroidinstaller/installer_config.py b/openandroidinstaller/installer_config.py index d0b63dd9..4c7e4d42 100644 --- a/openandroidinstaller/installer_config.py +++ b/openandroidinstaller/installer_config.py @@ -171,6 +171,7 @@ def validate_config(config: str) -> bool: "supported_device_codes": [str], schema.Optional("twrp-link"): str, schema.Optional("recovery"): str, + schema.Optional("notes"): str, }, schema.Optional("requirements"): { schema.Optional("android"): schema.Or(str, int), diff --git a/openandroidinstaller/views/select_view.py b/openandroidinstaller/views/select_view.py index ada3b314..927ef829 100644 --- a/openandroidinstaller/views/select_view.py +++ b/openandroidinstaller/views/select_view.py @@ -184,6 +184,17 @@ def build(self): else: recovery = "TWRP" recoveryFile = f"`twrp-3.7.0_12-0-{self.state.config.device_code}.img`" + + if "notes" in self.state.config.metadata: + self.right_view.controls.extend( + [ + Text("Important notes for your device", style="titleSmall", color=colors.RED, weight="bold", ), + Markdown( + f"""{self.state.config.metadata['notes']}""" + ) + ] + ) + self.right_view.controls.extend( [ Text("Select an OS image:", style="titleSmall"), @@ -212,7 +223,7 @@ def build(self): f""" The recovery image should look something like {recoveryFile}. -**Note:** This tool **only supports {recovery} recoveries**.""", +**Note:** This tool **only supports {recovery} recoveries** for this phone.""", extension_set="gitHubFlavored", ), Row( From caee8b84cd22750155be486d7b0f0035a044e0f9 Mon Sep 17 00:00:00 2001 From: rudu <40572253+anon1892@users.noreply.github.com> Date: Thu, 27 Jul 2023 18:09:53 +0200 Subject: [PATCH 3/6] Little changes, huge consequences (wrong codename for RN 8T) --- README.md | 8 ++++---- openandroidinstaller/assets/configs/ginkgo.yaml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 05c1c707..a412e147 100644 --- a/README.md +++ b/README.md @@ -171,10 +171,10 @@ OnePlus | 9 | lemonade | | under development
Xiaomi -Vendor | Device Name | CodeName | Models | Status ----|-------------------|---------------------------------------------------------|----------|--- -Xiaomi | Redmi Note 7 | [lavender](https://wiki.lineageos.org/devices/lavender) | lavender | tested -Xiaomi | Redmi Note 8 / 8T | [ginkgo](https://wiki.lineageos.org/devices/ginkgo) | ginkgo | untested +Vendor | Device Name | CodeName | Models | Status +---|-------------------|---------------------------------------------------------|-----------------|--- +Xiaomi | Redmi Note 7 | [lavender](https://wiki.lineageos.org/devices/lavender) | lavender | tested +Xiaomi | Redmi Note 8 / 8T | [ginkgo](https://wiki.lineageos.org/devices/ginkgo) | ginkgo / willow | untested
And more to come! diff --git a/openandroidinstaller/assets/configs/ginkgo.yaml b/openandroidinstaller/assets/configs/ginkgo.yaml index 62c2c723..bc2b1936 100644 --- a/openandroidinstaller/assets/configs/ginkgo.yaml +++ b/openandroidinstaller/assets/configs/ginkgo.yaml @@ -6,9 +6,9 @@ metadata: recovery: orangefox supported_device_codes: - ginkgo - - biloba + - willow notes: > - - If something goes wrong, you can reinstall MiUI here : https://xiaomifirmwareupdater.com/miui/lavender/ + - If something goes wrong, you can reinstall MiUI here : https://xiaomifirmwareupdater.com/miui/ginkgo/ - You should install Android 10 or newer ROM. From 0fd5f3524210061a360aeac830758d91b851bcbb Mon Sep 17 00:00:00 2001 From: rudu <40572253+anon1892@users.noreply.github.com> Date: Sat, 29 Jul 2023 19:54:13 +0200 Subject: [PATCH 4/6] Added garden & rosemary. Changes in handling recoveries --- README.md | 19 +++--- openandroidinstaller/app_state.py | 1 + .../assets/configs/garden.yaml | 59 ++++++++++++++++++ .../assets/configs/ginkgo.yaml | 8 ++- .../assets/configs/lavender.yaml | 6 +- .../assets/configs/rosemary.yaml | 51 +++++++++++++++ openandroidinstaller/assets/imgs/ofox.png | Bin 0 -> 82333 bytes openandroidinstaller/installer_config.py | 11 ++-- openandroidinstaller/tooling.py | 15 +++-- openandroidinstaller/utils.py | 36 ++++++++--- openandroidinstaller/views/install_view.py | 2 +- openandroidinstaller/views/select_view.py | 28 ++++++--- 12 files changed, 193 insertions(+), 43 deletions(-) create mode 100644 openandroidinstaller/assets/configs/garden.yaml create mode 100644 openandroidinstaller/assets/configs/rosemary.yaml create mode 100644 openandroidinstaller/assets/imgs/ofox.png diff --git a/README.md b/README.md index a412e147..01ae4142 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,12 @@ Linux is currently the best supported platform (tested with Ubuntu 20.04/22.04 L - [LineageOS for microg](https://download.lineage.microg.org) - [BlissRoms](https://blissroms.org) - [PixelExperience](https://download.pixelexperience.org) + - [crDroid](https://crdroid.net/) + - [ArrowOS](https://arrowos.net/) + - [DivestOS](https://divestos.org/) - TWRP Recovery: - [TWRP recovery](https://twrp.me/Devices) + - [OrangeFox](https://wiki.orangefox.tech) - Optional Addons: - There are different packages of *Google Apps* available. - [MindTheGapps](https://wiki.lineageos.org/gapps#downloads) @@ -63,7 +67,7 @@ Linux is currently the best supported platform (tested with Ubuntu 20.04/22.04 L ## Officially supported devices -Currently, the **we support 65 devices** by various vendors and working on adding more soon! +Currently, the **we support 68 devices** by various vendors and working on adding more soon! Support for these devices is provided as best effort, but things might still go wrong. @@ -171,15 +175,16 @@ OnePlus | 9 | lemonade | | under development
Xiaomi -Vendor | Device Name | CodeName | Models | Status ----|-------------------|---------------------------------------------------------|-----------------|--- -Xiaomi | Redmi Note 7 | [lavender](https://wiki.lineageos.org/devices/lavender) | lavender | tested -Xiaomi | Redmi Note 8 / 8T | [ginkgo](https://wiki.lineageos.org/devices/ginkgo) | ginkgo / willow | untested +Vendor | Device Name | CodeName | Models | Status +---|----------------------------------|--------------------------------------------------------|------------------------------------------|--- +Xiaomi | Redmi Note 7 | [lavender](https://wiki.lineageos.org/devices/lavender) | lavender | tested +Xiaomi | Redmi Note 8 / 8T | [ginkgo](https://wiki.lineageos.org/devices/ginkgo) | ginkgo / willow | untested +Xiaomi | Redmi Note 10S / 11SE / Poco M5S | [rosemary](https://wiki.lineageos.org/devices/rosemary) | rosemary / maltose / secret / rosemary_p | untested +Xiaomi | Redmi 9A / 9C / 9AT / 9i / 9A Sport / 10A / 10A Sport | [garden](https://wiki.lineageos.org/devices/garden) | garden / dandelion / blossom / angelican | tested
And more to come! - ## Run OpenAndroidInstaller for development Currently development is only supported on Ubuntu Linux. MacOS and Windows should also work fine. You might need to install additional USB-drivers on Windows. @@ -223,7 +228,7 @@ Every config file should have `metadata` with the following fields: - `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. -- `recovery`: [OPTIONAL] str; can be twrp or orangefox (twrp by default) +- `supported_recovery`: [OPTIONAL] List[str]; A lit of supported recoveries. For the moment, can be twrp and/or orangefox (twrp by default) - `notes`: [OPTIONAL] str; specific phone information, showed before choosing ROM / recovery 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. diff --git a/openandroidinstaller/app_state.py b/openandroidinstaller/app_state.py index eee468a9..55ff526d 100644 --- a/openandroidinstaller/app_state.py +++ b/openandroidinstaller/app_state.py @@ -44,6 +44,7 @@ def __init__( self.config = None self.image_path = None self.recovery_path = None + self.chosen_recovery = None # store views self.default_views: List = [] diff --git a/openandroidinstaller/assets/configs/garden.yaml b/openandroidinstaller/assets/configs/garden.yaml new file mode 100644 index 00000000..5dec4008 --- /dev/null +++ b/openandroidinstaller/assets/configs/garden.yaml @@ -0,0 +1,59 @@ +metadata: + maintainer: A non (anon) + device_name: Xiaomi Redmi 9A / 9C / 9AT / 9i / 9A Sport / 10A / 10A Sport + is_ab_device: false + device_code: garden + supported_recovery: + - orangefox + - twrp + supported_device_codes: + - dandelion + - garden + - blossom + - angelican + notes: > + - If something goes wrong, you can reinstall MiUI here : https://xiaomifirmwareupdater.com + + - OAI don't support ROM that requires permissiver. + + - 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. + allow_skip: true + - type: link_button_with_confirm + content: > + Please download official Xiaomi unlock tool, and follow instructions. + https://en.miui.com/unlock/download_en.html + You may have to wait 7 days before being able to unlock the bootloader. + Do not attempt to bypass this limitation / use non officials tools, you may brick your phone. + link: https://en.miui.com/unlock/download_en.html + allow_skip: true + - type: confirm_button + content: > + The bootloader is now unlocked. Since the device resets completely, you will need to re-enable USB debugging to continue. + boot_recovery: + - type: confirm_button + content: > + Now you need to boot 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. + - type: call_button + content: > + 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 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 diff --git a/openandroidinstaller/assets/configs/ginkgo.yaml b/openandroidinstaller/assets/configs/ginkgo.yaml index bc2b1936..2de8f0be 100644 --- a/openandroidinstaller/assets/configs/ginkgo.yaml +++ b/openandroidinstaller/assets/configs/ginkgo.yaml @@ -3,16 +3,18 @@ metadata: device_name: Xiaomi Redmi Note 8 / 8T is_ab_device: false device_code: ginkgo - recovery: orangefox + supported_recovery: + - orangefox + - twrp supported_device_codes: - ginkgo - willow notes: > - - If something goes wrong, you can reinstall MiUI here : https://xiaomifirmwareupdater.com/miui/ginkgo/ + - If something goes wrong, you can reinstall MiUI here : https://xiaomifirmwareupdater.com - You should install Android 10 or newer ROM. - - Be careful when choosing OrangeFox version, Android 12 & 13 ROM needs OrangeFox version code with `A12`, for example `R11.1_5_A12`. + - 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: android: 10 (Q) steps: diff --git a/openandroidinstaller/assets/configs/lavender.yaml b/openandroidinstaller/assets/configs/lavender.yaml index e09d6d68..02b777ca 100644 --- a/openandroidinstaller/assets/configs/lavender.yaml +++ b/openandroidinstaller/assets/configs/lavender.yaml @@ -3,7 +3,9 @@ metadata: device_name: Xiaomi Redmi Note 7 is_ab_device: false device_code: lavender - recovery: orangefox + supported_recovery: + - orangefox + - twrp supported_device_codes: - lavender notes: > @@ -11,7 +13,7 @@ metadata: - You should install Android 10 or newer ROM. - - Be careful when choosing OrangeFox version, Android 12 & 13 ROM needs OrangeFox version code with `A12`, for example `R11.1_5_A12`. + - 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: android: 10 (Q) steps: diff --git a/openandroidinstaller/assets/configs/rosemary.yaml b/openandroidinstaller/assets/configs/rosemary.yaml new file mode 100644 index 00000000..ff2a23c6 --- /dev/null +++ b/openandroidinstaller/assets/configs/rosemary.yaml @@ -0,0 +1,51 @@ +metadata: + maintainer: A non (anon) + device_name: Xiaomi Redmi Note 10S / 11SE / Poco M5S + is_ab_device: false + device_code: rosemary + supported_recovery: + - orangefox + - twrp + supported_device_codes: + - rosemary + - secret + - maltose + - rosemary_p + notes: > + - If something goes wrong, you can reinstall MiUI here : https://xiaomifirmwareupdater.com + + - Please respect the requested by the ROM developers version for MIUI and Android + + - 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) +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. + allow_skip: true + - type: link_button_with_confirm + content: > + Please download official Xiaomi unlock tool, and follow instructions. + https://en.miui.com/unlock/download_en.html + You may have to wait 7 days before being able to unlock the bootloader. + Do not attempt to bypass this limitation / use non officials tools, you may brick your phone. + link: https://en.miui.com/unlock/download_en.html + allow_skip: true + - type: confirm_button + content: > + The bootloader is now unlocked. Since the device resets completely, you will need to re-enable USB debugging to continue. + boot_recovery: + - type: confirm_button + content: > + Now you need to boot 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. + - type: call_button + content: > + 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 the recovery you chosen before by pressing 'Confirm and run'. Once it's done continue. + UNTESTED : Need to hold Vol+ or no ? + command: fastboot_flash_boot diff --git a/openandroidinstaller/assets/imgs/ofox.png b/openandroidinstaller/assets/imgs/ofox.png new file mode 100644 index 0000000000000000000000000000000000000000..2d8197a0e361200585eb48316878871312a203fc GIT binary patch literal 82333 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 Optional[InstallerConfi if custom_path: config = InstallerConfig.from_file(custom_path) logger.info(f"Loaded custom device config from {custom_path}.") - if 'recovery' not in config.metadata: - config.metadata.update({'recovery': 'twrp'}) + if 'supported_recovery' not in config.metadata: + config.metadata.update({"supported_recovery": "['twrp']"}) logger.info(f"Config metadata: {config.metadata}.") return config else: @@ -135,9 +136,9 @@ def _load_config(device_code: str, config_path: Path) -> Optional[InstallerConfi if path: config = InstallerConfig.from_file(path) logger.info(f"Loaded device config from {path}.") - if 'recovery' not in config.metadata: - config.metadata.update({'recovery': 'twrp'}) if config: + if 'supported_recovery' not in config.metadata: + config.metadata.update({"supported_recovery": "['twrp']"}) logger.info(f"Config metadata: {config.metadata}.") return config else: @@ -170,7 +171,7 @@ def validate_config(config: str) -> bool: "device_code": str, "supported_device_codes": [str], schema.Optional("twrp-link"): str, - schema.Optional("recovery"): str, + schema.Optional("supported_recovery"): [str], schema.Optional("notes"): str, }, schema.Optional("requirements"): { diff --git a/openandroidinstaller/tooling.py b/openandroidinstaller/tooling.py index 0fe86219..ac1fae8b 100644 --- a/openandroidinstaller/tooling.py +++ b/openandroidinstaller/tooling.py @@ -221,7 +221,7 @@ def adb_twrp_wipe_and_install( target: str, config_path: Path, is_ab: bool, - which_recovery: str, + chosen_recovery: str, install_addons=True, recovery: Optional[str] = None, ) -> TerminalResponse: @@ -245,7 +245,7 @@ def adb_twrp_wipe_and_install( sleep(1) # activate sideload - if which_recovery == 'twrp': + if chosen_recovery == 'twrp': logger.info("Wiping is done, now activate sideload.") for line in activate_sideload(bin_path=bin_path): yield line @@ -261,12 +261,11 @@ def adb_twrp_wipe_and_install( yield line # wipe some cache partitions - if which_recovery == 'orangefox': + if chosen_recovery == 'orangefox': logger.info("Waiting for OrangeFox to restart...") for line in adb_wait_for_recovery(bin_path): yield line - else: - sleep(7) + sleep(7) logger.info("Wiping cache and dalvik...") for partition in ["dalvik", "cache"]: @@ -536,6 +535,10 @@ def fastboot_flash_recovery( @add_logging("Rebooting device to recovery.") def fastboot_reboot_recovery(bin_path: Path) -> TerminalResponse: - """Reboot to recovery with fastboot""" + """ + Reboot to recovery with fastboot + + WARNING : On some devices, users should perform a key combo + """ for line in run_command("fastboot reboot recovery", bin_path): yield line \ No newline at end of file diff --git a/openandroidinstaller/utils.py b/openandroidinstaller/utils.py index 49eecca1..8b5ac61b 100644 --- a/openandroidinstaller/utils.py +++ b/openandroidinstaller/utils.py @@ -60,18 +60,36 @@ 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 recovery_works_with_device(supported_device_codes: [str], supported_recovery: [str], recovery_path: str) -> bool: """Determine if a recovery works for the given device. BEWARE: THE RECOVERY PART IS STILL VERY BASIC! """ recovery_file_name = recovery_path.split("/")[-1] - if (device_code in recovery_file_name) and ("twrp" in recovery_file_name): - logger.success("Device supported by the selected recovery.") - return True - elif recovery_file_name == "recovery.img": - logger.error("Cannot check recovery. Supposing it is OrangeFox.") - return True - else: - logger.error(f"Recovery file {recovery_file_name} is not supported.") + if recovery_file_name[-4:] != ".img": + logger.error(f"The file {recovery_file_name} is not a recovery file") return False + for codename in supported_device_codes: + if (codename in recovery_file_name) and ("twrp" in recovery_file_name or "TWRP" in recovery_file_name)\ + and ("twrp" in supported_recovery or not supported_recovery): + logger.success("Selected recovery supported for this device.") + return True + elif recovery_file_name == "recovery.img" and ("orangefox" in supported_recovery): + logger.error("Cannot check recovery. Supposing it is OrangeFox.") + return True + logger.error(f"Recovery file {recovery_file_name} is not supported.") + return False + +def which_recovery(recovery_path: str) -> str: + """Determine which recovery was selected + This does not replace recovery_works_with_device. + + BEWARE: THE RECOVERY PART IS STILL VERY BASIC! + """ + recovery_file_name = recovery_path.split("/")[-1] + if "twrp" in recovery_file_name or "TWRP" in recovery_file_name: + return "twrp" + if recovery_file_name == "recovery.img": + return "orangefox" + logger.error("Unable to determine which recovery was selected !") + return None diff --git a/openandroidinstaller/views/install_view.py b/openandroidinstaller/views/install_view.py index 69514b63..89c30c25 100644 --- a/openandroidinstaller/views/install_view.py +++ b/openandroidinstaller/views/install_view.py @@ -193,7 +193,7 @@ def run_install(self, e): bin_path=self.state.bin_path, install_addons=self.state.install_addons, is_ab=self.state.config.is_ab, - which_recovery=self.state.config.metadata['recovery'], + chosen_recovery=self.state.chosen_recovery, recovery=self.state.recovery_path, ): # write the line to advanced output terminal diff --git a/openandroidinstaller/views/select_view.py b/openandroidinstaller/views/select_view.py index 927ef829..2cd33e02 100644 --- a/openandroidinstaller/views/select_view.py +++ b/openandroidinstaller/views/select_view.py @@ -40,7 +40,7 @@ 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, which_recovery class SelectFilesView(BaseView): @@ -178,12 +178,16 @@ def build(self): ) ) # attach the controls for uploading image and recovery - if(self.state.config.metadata['recovery'] == "orangefox"): - recovery = "OrangeFox" - recoveryFile = "`recovery.img`" - else: + recovery, recoveryFile = "", "" + if "twrp" in self.state.config.supported_recovery: recovery = "TWRP" recoveryFile = f"`twrp-3.7.0_12-0-{self.state.config.device_code}.img`" + if "orangefox" in self.state.config.supported_recovery: + if recovery != "": + recovery += " or " + recoveryFile += " or " + recovery += "OrangeFox" + recoveryFile += "`recovery.img`" if "notes" in self.state.config.metadata: self.right_view.controls.extend( @@ -223,7 +227,7 @@ def build(self): f""" The recovery image should look something like {recoveryFile}. -**Note:** This tool **only supports {recovery} recoveries** for this phone.""", +**Note:** This tool **only supports {recovery} recovery** for this phone.""", extension_set="gitHubFlavored", ), Row( @@ -297,11 +301,14 @@ 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, + supported_recovery=self.state.config.supported_recovery, + recovery_path=self.state.recovery_path ): self.selected_recovery.color = colors.GREEN + self.state.chosen_recovery = which_recovery(self.state.recovery_path) + logger.info(f"Chosen recovery : {self.state.chosen_recovery}") else: self.selected_recovery.color = colors.RED # update @@ -312,14 +319,15 @@ def enable_button_if_ready(self, e): 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, + supported_recovery=self.state.config.supported_recovery, + recovery_path=self.state.recovery_path ) ): # if image and recovery work for device allow to move on, otherwise display message From 09b002a7214fe8e83e8e905c12eddc357e2576e7 Mon Sep 17 00:00:00 2001 From: rudu <40572253+anon1892@users.noreply.github.com> Date: Sat, 29 Jul 2023 19:55:04 +0200 Subject: [PATCH 5/6] Added garden & rosemary. Changes in handling recoveries --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 01ae4142..b057942e 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Linux is currently the best supported platform (tested with Ubuntu 20.04/22.04 L ## Officially supported devices -Currently, the **we support 68 devices** by various vendors and working on adding more soon! +Currently, the **we support 68+ devices** by various vendors and working on adding more soon! Support for these devices is provided as best effort, but things might still go wrong. From c303024f156fa919f9fb00609155af26619228de Mon Sep 17 00:00:00 2001 From: rudu <40572253+anon1892@users.noreply.github.com> Date: Mon, 31 Jul 2023 17:20:10 +0200 Subject: [PATCH 6/6] Added Mi439, flashing vbmeta, dtbo, super_empty, and some modifications to recovery handling --- README.md | 18 +- openandroidinstaller/app_state.py | 3 + .../assets/configs/Mi439.yaml | 73 ++++++++ openandroidinstaller/installer_config.py | 6 +- openandroidinstaller/tooling.py | 45 ++++- openandroidinstaller/utils.py | 1 + openandroidinstaller/views/install_view.py | 2 +- openandroidinstaller/views/select_view.py | 164 +++++++++++++++++- openandroidinstaller/views/step_view.py | 10 +- 9 files changed, 308 insertions(+), 14 deletions(-) create mode 100644 openandroidinstaller/assets/configs/Mi439.yaml diff --git a/README.md b/README.md index b057942e..66d42b4c 100644 --- a/README.md +++ b/README.md @@ -175,12 +175,13 @@ OnePlus | 9 | lemonade | | under development

Xiaomi -Vendor | Device Name | CodeName | Models | Status ----|----------------------------------|--------------------------------------------------------|------------------------------------------|--- -Xiaomi | Redmi Note 7 | [lavender](https://wiki.lineageos.org/devices/lavender) | lavender | tested -Xiaomi | Redmi Note 8 / 8T | [ginkgo](https://wiki.lineageos.org/devices/ginkgo) | ginkgo / willow | untested -Xiaomi | Redmi Note 10S / 11SE / Poco M5S | [rosemary](https://wiki.lineageos.org/devices/rosemary) | rosemary / maltose / secret / rosemary_p | untested -Xiaomi | Redmi 9A / 9C / 9AT / 9i / 9A Sport / 10A / 10A Sport | [garden](https://wiki.lineageos.org/devices/garden) | garden / dandelion / blossom / angelican | tested +Vendor | Device Name | CodeName | Models | Status +---|-----------------------------------------------------|------------------------------------------------------------------------------------------|-------|--- +Xiaomi | Redmi Note 7 | [lavender](https://wiki.lineageos.org/devices/lavender) | | tested +Xiaomi | Redmi Note 8 / 8T | [ginkgo / willow](https://wiki.lineageos.org/devices/ginkgo) | | untested +Xiaomi | Redmi Note 10S / 11SE / Poco M5S | [rosemary](https://wiki.lineageos.org/devices/rosemary) / maltose / secret /rosemary_p | | untested +Xiaomi | Redmi 7A / 8 / 8A / 8A Dual | [Mi439](https://wiki.lineageos.org/devices/Mi439) : pine / olive / olivelite / olivewood | | tested +Xiaomi | Redmi 9A / 9C / 9AT / 9i / 9A Sport / 10A / 10A Sport | garden / dandelion / blossom / angelican | | tested
And more to come! @@ -228,7 +229,8 @@ Every config file should have `metadata` with the following fields: - `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. -- `supported_recovery`: [OPTIONAL] List[str]; A lit of supported recoveries. For the moment, can be twrp and/or orangefox (twrp by default) +- `additional_steps` : [OPTIONAL] List[str]; A list of additional steps. Could be `dtbo`, `vbmeta`, `super_empty`. +- `supported_recovery`: [OPTIONAL] List[str]; A list of supported recoveries. For the moment, can be twrp and/or orangefox (twrp by default) - `notes`: [OPTIONAL] str; specific phone information, showed before choosing ROM / recovery 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. @@ -246,7 +248,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_reboot_recovery`, `fastboot_flash_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_reboot_recovery`, `fastboot_flash_recovery` `fastboot_unlock_with_code`, `fastboot_flash_additional_partitions`, `fastboot_unlock`, `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 55ff526d..31d9d226 100644 --- a/openandroidinstaller/app_state.py +++ b/openandroidinstaller/app_state.py @@ -44,6 +44,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 self.chosen_recovery = None # store views diff --git a/openandroidinstaller/assets/configs/Mi439.yaml b/openandroidinstaller/assets/configs/Mi439.yaml new file mode 100644 index 00000000..19cc6a46 --- /dev/null +++ b/openandroidinstaller/assets/configs/Mi439.yaml @@ -0,0 +1,73 @@ +metadata: + maintainer: A non (anon) + device_name: Xiaomi Redmi 7A / 8 / 8A / 8A Dual + is_ab_device: false + device_code: Mi439 + supported_recovery: + - orangefox + - twrp + additional_steps: + - dtbo + - vbmeta + - super_empty + supported_device_codes: + - Mi439 + - pine + - olive + - olivelite + - olivewood + notes: > + - If something goes wrong, you can reinstall MiUI here : https://xiaomifirmwareupdater.com + + - 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. + allow_skip: true + - 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 + allow_skip: true + 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 diff --git a/openandroidinstaller/installer_config.py b/openandroidinstaller/installer_config.py index 15209178..795dc52c 100644 --- a/openandroidinstaller/installer_config.py +++ b/openandroidinstaller/installer_config.py @@ -63,6 +63,7 @@ def __init__( self.device_code = metadata.get("device_code") self.is_ab = metadata.get("is_ab_device", False) self.supported_recovery = metadata.get("supported_recovery") + self.additional_steps = metadata.get("additional_steps") self.supported_device_codes = metadata.get("supported_device_codes") self.twrp_link = metadata.get("twrp-link") @@ -139,6 +140,8 @@ def _load_config(device_code: str, config_path: Path) -> Optional[InstallerConfi if config: if 'supported_recovery' not in config.metadata: config.metadata.update({"supported_recovery": "['twrp']"}) + if 'additional_steps' not in config.metadata: + config.metadata.update({"additional_steps": "[]"}) logger.info(f"Config metadata: {config.metadata}.") return config else: @@ -155,7 +158,7 @@ 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_with_code|fastboot_get_unlock_data|fastboot_unlock|fastboot_oem_unlock|fastboot_reboot|heimdall_flash_recovery|fastboot_reboot_recovery|fastboot_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_with_code|fastboot_get_unlock_data|fastboot_unlock|fastboot_oem_unlock|fastboot_reboot|heimdall_flash_recovery|fastboot_reboot_recovery|fastboot_flash_additional_partitions" ), schema.Optional("allow_skip"): bool, schema.Optional("img"): str, @@ -172,6 +175,7 @@ def validate_config(config: str) -> bool: "supported_device_codes": [str], schema.Optional("twrp-link"): str, schema.Optional("supported_recovery"): [str], + schema.Optional("additional_steps"): [str], schema.Optional("notes"): str, }, schema.Optional("requirements"): { diff --git a/openandroidinstaller/tooling.py b/openandroidinstaller/tooling.py index ac1fae8b..96b5aa8e 100644 --- a/openandroidinstaller/tooling.py +++ b/openandroidinstaller/tooling.py @@ -523,7 +523,6 @@ def fastboot_flash_recovery( bin_path: Path, recovery: str, is_ab: bool = True ) -> TerminalResponse: """Flash custom recovery with fastboot.""" - logger.info("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: @@ -533,6 +532,50 @@ def fastboot_flash_recovery( else: yield True +@add_logging("Flash additional partitions with fastboot") +def fastboot_flash_additional_partitions( + bin_path: Path, dtbo: str, vbmeta: str, super_empty: str, is_ab: bool = True +) -> TerminalResponse: + """Flash additional partitions (dtbo, vbmeta, super_empty) with fastboot.""" + if dtbo: + 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: + logger.info("No dtbo selected. Skipping") + yield True + + if vbmeta: + 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 + else: + logger.info("No vbmeta selected. Skipping") + yield True + + if super_empty: + 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 + else: + logger.info("No super_empty selected. Skipping") + yield True + @add_logging("Rebooting device to recovery.") def fastboot_reboot_recovery(bin_path: Path) -> TerminalResponse: """ diff --git a/openandroidinstaller/utils.py b/openandroidinstaller/utils.py index 8b5ac61b..2d42928e 100644 --- a/openandroidinstaller/utils.py +++ b/openandroidinstaller/utils.py @@ -93,3 +93,4 @@ def which_recovery(recovery_path: str) -> str: return "orangefox" logger.error("Unable to determine which recovery was selected !") return None + diff --git a/openandroidinstaller/views/install_view.py b/openandroidinstaller/views/install_view.py index 89c30c25..04210e74 100644 --- a/openandroidinstaller/views/install_view.py +++ b/openandroidinstaller/views/install_view.py @@ -171,7 +171,7 @@ def check_addons_switch(e): def run_install(self, e): """ - Run the installation process through twrp. + Run the installation process through recovery. Some parts of the command are changed by placeholders. """ diff --git a/openandroidinstaller/views/select_view.py b/openandroidinstaller/views/select_view.py index 2cd33e02..65bac82b 100644 --- a/openandroidinstaller/views/select_view.py +++ b/openandroidinstaller/views/select_view.py @@ -94,14 +94,24 @@ 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 = Text("Selected dtbo: ") + self.selected_vbmeta = Text("Selected vbmeta: ") + self.selected_super_empty = Text("Selected super_empty: ") # initialize and manage button state. self.confirm_button = confirm_button(self.on_confirm) self.confirm_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", @@ -121,6 +131,9 @@ 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.append(self.pick_dtbo_dialog) + self.right_view.controls.append(self.pick_vbmeta_dialog) + self.right_view.controls.append(self.pick_super_empty_dialog) # create help/info button to show the help dialog info_button = OutlinedButton( @@ -151,7 +164,7 @@ def build(self): Column( [ Text( - "You can bring your own image and recovery or you download the officially supported image file for your device here:" + "You can bring your own image and recovery or you download the officially supported LineageOS image file for your device here:" ), Row( [ @@ -246,6 +259,85 @@ def build(self): ), self.selected_recovery, Divider(), + ] + ) + + # attach the controls for uploading others partitions, like dtbo, vbmeta & super_empty + if "dtbo" in self.state.config.metadata['additional_steps']: + self.right_view.controls.extend( + [ + Text("Select other specific images:", style="titleSmall"), + Markdown( + f""" +Depending of the ROM, OpenAndroidInstaller may have to install additional images. + +These images are usually needed for Android 13 ROM. + +Make sure the file is for **your exact phone model!** + """ + ), + 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.metadata['additional_steps']: + self.right_view.controls.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.metadata['additional_steps']: + self.right_view.controls.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() + ] + ) + + # attach the bottom buttons + self.right_view.controls.extend( + [ self.info_field, Row([self.back_button, self.confirm_button]), ] @@ -270,6 +362,7 @@ def pick_image_result(self, e: FilePickerResultEvent): self.selected_image.value.split(":")[0] + f": {path}" ) if e.files: + # Why two variables needed? self.image_path = e.files[0].path self.state.image_path = e.files[0].path logger.info(f"Selected image from {self.image_path}") @@ -314,6 +407,57 @@ 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!" + # update the textfield with the name of the file + self.selected_dtbo.value = (self.selected_dtbo.value.split(":")[0] + f": {path}") + 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.color = colors.GREEN + self.state.dtbo_path = e.files[0].path + logger.info(f"Selected dtbo from {self.state.dtbo_path}") + else: + self.selected_dtbo.color = colors.RED + 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!" + # update the textfield with the name of the file + self.selected_vbmeta.value = (self.selected_vbmeta.value.split(":")[0] + f": {path}") + 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.color = colors.GREEN + self.state.vbmeta_path = e.files[0].path + logger.info(f"Selected vbmeta from {self.state.vbmeta_path}") + else: + self.selected_vbmeta.color = colors.RED + 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 + self.selected_super_empty.value = (self.selected_super_empty.value.split(":")[0] + f": {path}") + 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.color = colors.GREEN + 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.color = colors.RED + 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 ( @@ -336,7 +480,7 @@ def enable_button_if_ready(self, e): ) self.info_field.controls = [ Text( - "Image and/or recovery don't work with the device. Make sure you use a TWRP-based recovery.", + "Image and/or recovery don't work with the device. Make sure you use a supported recovery.", color=colors.RED, weight="bold", ) @@ -344,6 +488,22 @@ def enable_button_if_ready(self, e): self.confirm_button.disabled = True self.right_view.update() return + + if (self.selected_dtbo.color and self.selected_dtbo.color == "red") or\ + (self.selected_vbmeta.color and self.selected_vbmeta.color == "red") or\ + (self.selected_super_empty.color and self.selected_super_empty.color == "red"): + logger.error("Some additional images don't match. Please select different ones.") + self.info_field.controls = [ + Text( + "Some additional images don't match. Select right ones or unselect them.", + 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 diff --git a/openandroidinstaller/views/step_view.py b/openandroidinstaller/views/step_view.py index 01a2c6ed..9e2957d0 100644 --- a/openandroidinstaller/views/step_view.py +++ b/openandroidinstaller/views/step_view.py @@ -45,13 +45,14 @@ adb_twrp_copy_partitions, fastboot_boot_recovery, fastboot_flash_boot, + fastboot_flash_recovery, fastboot_oem_unlock, fastboot_reboot, fastboot_unlock, fastboot_unlock_with_code, fastboot_get_unlock_data, heimdall_flash_recovery, - fastboot_flash_recovery, + fastboot_flash_additional_partitions, fastboot_reboot_recovery, ) from widgets import ( @@ -242,6 +243,13 @@ def call_to_phone(self, e, command: str): recovery=self.state.recovery_path, is_ab=self.state.config.is_ab, ), + "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_recovery": fastboot_reboot_recovery, }