From cc7444ce69e22dee0508137f610ec636d1b3a145 Mon Sep 17 00:00:00 2001 From: Tobias Sterbak Date: Sun, 24 Dec 2023 11:29:29 +0000 Subject: [PATCH 1/3] Refactor metadata loading from ROM image; improve Makefile --- Makefile | 14 ++++++ openandroidinstaller/utils.py | 82 ++++++++++++++++++++++++----------- 2 files changed, 71 insertions(+), 25 deletions(-) diff --git a/Makefile b/Makefile index 4b7aeae7..93f0e1a5 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,17 @@ +.phoney: install export format lint typing test app test-app build-app clean-build + +help: + @echo "install - install dependencies" + @echo "export - export dependencies to requirements.txt" + @echo "format - format code with black" + @echo "lint - lint code with ruff" + @echo "typing - type check code with mypy" + @echo "test - run tests" + @echo "app - run app" + @echo "test-app - run app in test mode with test config for sargo" + @echo "build-app - build app" + @echo "clean-build - clean build" + poetry: curl -sSL https://install.python-poetry.org | python3 - diff --git a/openandroidinstaller/utils.py b/openandroidinstaller/utils.py index a8511611..b29745df 100644 --- a/openandroidinstaller/utils.py +++ b/openandroidinstaller/utils.py @@ -40,6 +40,34 @@ def get_download_link(devicecode: str) -> Optional[str]: return None +def retrieve_image_metadata(image_path: str) -> dict: + """Retrieve metadata from the selected image. + + Args: + image_path: Path to the image file. + + Returns: + Dictionary containing the metadata. + """ + metapath = "META-INF/com/android/metadata" + try: + with zipfile.ZipFile(image_path) as image_zip: + with image_zip.open(metapath, mode="r") as image_metadata: + metadata = image_metadata.readlines() + metadata_dict = {} + for line in metadata: + metadata_dict[line[: line.find(b"=")].decode("utf-8")] = line[ + line.find(b"=") + 1 : -1 + ].decode("utf-8") + logger.info(f"Metadata retrieved from image {image_path.split('/')[-1]}.") + return metadata_dict + except FileNotFoundError: + logger.error( + f"Metadata file {metapath} not found in {image_path.split('/')[-1]}." + ) + return dict() + + def image_works_with_device(supported_device_codes: List[str], image_path: str) -> bool: """Determine if an image works for the given device. @@ -50,22 +78,21 @@ def image_works_with_device(supported_device_codes: List[str], image_path: str) 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" - ) as image_metadata: - metadata = image_metadata.readlines() - supported_devices = str(metadata[-1]).split("=")[-1][:-3].split(",") - logger.info(f"Image works with device: {supported_devices}") - - if any(code in supported_devices for code in supported_device_codes): - logger.success("Device supported by the selected image.") - return True - else: - logger.error( - f"Image file {image_path.split('/')[-1]} is not supported." - ) - return False + metadata = retrieve_image_metadata(image_path) + try: + supported_devices = metadata["pre-device"].split(",") + logger.info(f"Image works with the following device(s): {supported_devices}") + if any(code in supported_devices for code in supported_device_codes): + logger.success("Device supported by the selected image.") + return True + else: + logger.error(f"Image file {image_path.split('/')[-1]} is not supported.") + return False + except KeyError: + logger.error( + f"Could not determine supported devices for {image_path.split('/')[-1]}." + ) + return False def image_sdk_level(image_path: str) -> int: @@ -73,16 +100,21 @@ def image_sdk_level(image_path: str) -> int: Example: Android 13: 33 + + Args: + image_path: Path to the image file. + + Returns: + Android version as integer. """ - 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 + metadata = retrieve_image_metadata(image_path) + try: + sdk_level = metadata["post-sdk-level"] + logger.info(f"Android version of {image_path}: {sdk_level}") + return int(sdk_level) + except (ValueError, TypeError, KeyError) as e: + logger.error(f"Could not determine Android version of {image_path}. Error: {e}") + return 0 def recovery_works_with_device( From d0b6c6fc5be3afab169e113c025a8472d352aa10 Mon Sep 17 00:00:00 2001 From: Tobias Sterbak Date: Sun, 24 Dec 2023 12:19:25 +0000 Subject: [PATCH 2/3] Rework the user messages of the image/recovery selection and the validation process --- openandroidinstaller/utils.py | 107 +++++++++++++++------- openandroidinstaller/views/select_view.py | 55 ++++++----- 2 files changed, 102 insertions(+), 60 deletions(-) diff --git a/openandroidinstaller/utils.py b/openandroidinstaller/utils.py index b29745df..4a16062f 100644 --- a/openandroidinstaller/utils.py +++ b/openandroidinstaller/utils.py @@ -14,12 +14,35 @@ # Author: Tobias Sterbak import zipfile +from dataclasses import dataclass +from enum import Enum from typing import Optional, List import requests from loguru import logger +class CompatibilityStatus(Enum): + """Enum for the compatibility status of a device.""" + + UNKNOWN = 0 + COMPATIBLE = 1 + INCOMPATIBLE = 2 + + +@dataclass +class CheckResult: + """Dataclass for the result of a check. + + Attributes: + status: Compatibility status of the device. + message: Message to be displayed to the user. + """ + + status: CompatibilityStatus + message: str + + def get_download_link(devicecode: str) -> Optional[str]: """Check if a lineageOS version for this device exists on download.lineageos.com and return the respective download link.""" url = f"https://download.lineageos.org/api/v2/devices/{devicecode}" @@ -61,14 +84,42 @@ def retrieve_image_metadata(image_path: str) -> dict: ].decode("utf-8") logger.info(f"Metadata retrieved from image {image_path.split('/')[-1]}.") return metadata_dict - except FileNotFoundError: + except (FileNotFoundError, KeyError): logger.error( f"Metadata file {metapath} not found in {image_path.split('/')[-1]}." ) return dict() -def image_works_with_device(supported_device_codes: List[str], image_path: str) -> bool: +def image_sdk_level(image_path: str) -> int: + """Determine Android version of the selected image. + + Examples: + Android 10: 29 + Android 11: 30 + Android 12: 31 + Android 12.1: 32 + Android 13: 33 + + Args: + image_path: Path to the image file. + + Returns: + Android version as integer. + """ + metadata = retrieve_image_metadata(image_path) + try: + sdk_level = metadata["post-sdk-level"] + logger.info(f"Android version of {image_path}: {sdk_level}") + return int(sdk_level) + except (ValueError, TypeError, KeyError) as e: + logger.error(f"Could not determine Android version of {image_path}. Error: {e}") + return -1 + + +def image_works_with_device( + supported_device_codes: List[str], image_path: str +) -> CheckResult: """Determine if an image works for the given device. Args: @@ -76,7 +127,7 @@ def image_works_with_device(supported_device_codes: List[str], image_path: str) image_path: Path to the image file. Returns: - True if the image works with the device, False otherwise. + CheckResult object containing the compatibility status and a message. """ metadata = retrieve_image_metadata(image_path) try: @@ -84,42 +135,29 @@ def image_works_with_device(supported_device_codes: List[str], image_path: str) logger.info(f"Image works with the following device(s): {supported_devices}") if any(code in supported_devices for code in supported_device_codes): logger.success("Device supported by the selected image.") - return True + return CheckResult( + CompatibilityStatus.COMPATIBLE, + "Device supported by the selected image.", + ) else: logger.error(f"Image file {image_path.split('/')[-1]} is not supported.") - return False + return CheckResult( + CompatibilityStatus.INCOMPATIBLE, + f"Image file {image_path.split('/')[-1]} is not supported by device code.", + ) except KeyError: logger.error( f"Could not determine supported devices for {image_path.split('/')[-1]}." ) - return False - - -def image_sdk_level(image_path: str) -> int: - """Determine Android version of the selected image. - - Example: - Android 13: 33 - - Args: - image_path: Path to the image file. - - Returns: - Android version as integer. - """ - metadata = retrieve_image_metadata(image_path) - try: - sdk_level = metadata["post-sdk-level"] - logger.info(f"Android version of {image_path}: {sdk_level}") - return int(sdk_level) - except (ValueError, TypeError, KeyError) as e: - logger.error(f"Could not determine Android version of {image_path}. Error: {e}") - return 0 + return CheckResult( + CompatibilityStatus.UNKNOWN, + f"Could not determine supported devices for {image_path.split('/')[-1]}. Missing metadata file? You may try to flash the image anyway.", + ) def recovery_works_with_device( supported_device_codes: List[str], recovery_path: str -) -> bool: +) -> CheckResult: """Determine if a recovery works for the given device. BEWARE: THE RECOVERY PART IS STILL VERY BASIC! @@ -129,14 +167,19 @@ def recovery_works_with_device( recovery_path: Path to the recovery file. Returns: - True if the recovery works with the device, False otherwise. + CheckResult object containing the compatibility status and a message. """ recovery_file_name = recovery_path.split("/")[-1] 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 + return CheckResult( + CompatibilityStatus.COMPATIBLE, "Device supported by the selected recovery." + ) else: logger.error(f"Recovery file {recovery_file_name} is not supported.") - return False + return CheckResult( + CompatibilityStatus.INCOMPATIBLE, + f"Recovery file {recovery_file_name} is not supported by device code in file name.", + ) diff --git a/openandroidinstaller/views/select_view.py b/openandroidinstaller/views/select_view.py index d5937870..f0921abb 100644 --- a/openandroidinstaller/views/select_view.py +++ b/openandroidinstaller/views/select_view.py @@ -46,6 +46,8 @@ image_works_with_device, recovery_works_with_device, image_sdk_level, + CheckResult, + CompatibilityStatus, ) @@ -145,6 +147,9 @@ def init_visuals( icon=icons.ARROW_BACK, expand=True, ) + # store image and recovery compatibility + self.image_compatibility: CheckResult | None = None + self.recovery_compatibility: CheckResult | None = None def build(self): self.clear() @@ -533,17 +538,21 @@ def pick_image_result(self, e: FilePickerResultEvent): logger.info("No image selected.") # check if the image works with the device and show the filename in different colors accordingly if e.files: - if image_works_with_device( + self.image_compatibility = image_works_with_device( supported_device_codes=self.state.config.supported_device_codes, image_path=self.state.image_path, - ): + ) + if self.image_compatibility.status == CompatibilityStatus.COMPATIBLE: self.selected_image.color = colors.GREEN + elif self.image_compatibility.status == CompatibilityStatus.UNKNOWN: + self.selected_image.color = colors.ORANGE else: self.selected_image.color = colors.RED + self.selected_image.value += f"\n> {self.image_compatibility.message}" # 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 + self.image_compatibility and image_sdk_level(self.state.image_path) >= 33 ): self.toggle_additional_image_selection() @@ -567,13 +576,17 @@ 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: - if recovery_works_with_device( + self.recovery_compatibility = recovery_works_with_device( supported_device_codes=self.state.config.supported_device_codes, recovery_path=self.state.recovery_path, - ): + ) + if self.recovery_compatibility.status == CompatibilityStatus.COMPATIBLE: self.selected_recovery.color = colors.GREEN + elif self.recovery_compatibility.status == CompatibilityStatus.UNKNOWN: + self.selected_recovery.color = colors.ORANGE else: self.selected_recovery.color = colors.RED + self.selected_recovery.value += f"\n> {self.recovery_compatibility.message}" # update self.selected_recovery.update() @@ -654,15 +667,10 @@ def enable_button_if_ready(self, e): if (".zip" in self.selected_image.value) and ( ".img" in self.selected_recovery.value ): - 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( - supported_device_codes=self.state.config.supported_device_codes, - recovery_path=self.state.recovery_path, - ) + if ( + self.image_compatibility.status == CompatibilityStatus.INCOMPATIBLE + ) or ( + self.recovery_compatibility.status == CompatibilityStatus.INCOMPATIBLE ): # if image and recovery work for device allow to move on, otherwise display message logger.error( @@ -670,7 +678,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.", + "Something is wrong with the selected files.", color=colors.RED, weight="bold", ) @@ -695,12 +703,10 @@ def enable_button_if_ready(self, e): or "vendor_boot" not in self.state.config.additional_steps, ] ): - logger.error( - "Some additional images don't match or are missing. Please select different ones." - ) + logger.error("Some additional images don't match or are missing.") self.info_field.controls = [ Text( - "Some additional images don't match or are missing. Please select the right ones.", + "Some additional images don't match or are missing.", color=colors.RED, weight="bold", ) @@ -715,16 +721,9 @@ def enable_button_if_ready(self, e): 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 self.image_compatibility.status != CompatibilityStatus.COMPATIBLE: # 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." - ) + logger.error("Image doesn't work with the device.") self.info_field.controls = [ Text( "Image doesn't work with the device.", From bb1cd2046d18fa6f4449cfb4b10901d08b2bb398 Mon Sep 17 00:00:00 2001 From: Tobias Sterbak Date: Tue, 26 Dec 2023 09:41:21 +0000 Subject: [PATCH 3/3] Minor improvements to progressbar --- openandroidinstaller/widgets.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/openandroidinstaller/widgets.py b/openandroidinstaller/widgets.py index e4400c5a..49227912 100644 --- a/openandroidinstaller/widgets.py +++ b/openandroidinstaller/widgets.py @@ -68,7 +68,7 @@ def write_line(self, line: str): Ignores empty lines. """ - if (type(line) == str) and line.strip(): + if isinstance(line, str) and line.strip(): self._box.content.controls[0].value += f"\n>{line.strip()}" self._box.content.controls[0].value = self._box.content.controls[ 0 @@ -115,7 +115,7 @@ def display_progress_bar(self, line: str): percentage_done = None result = None # create the progress bar - if self.progress_bar == None: + if not self.progress_bar: self.progress_bar = ProgressBar( value=1 / 100, width=500, @@ -129,7 +129,7 @@ def display_progress_bar(self, line: str): Row([self.percentage_text, self.progress_bar]) ) # get the progress numbers from the output lines - if (type(line) == str) and line.strip(): + if isinstance(line, str) and line.strip(): result = re.search( r"\(\~(\d{1,3})\%\)|(Total xfer:|adb: failed to read command: Success)", line.strip(), @@ -139,10 +139,7 @@ def display_progress_bar(self, line: str): percentage_done = 99 elif result.group(1): percentage_done = int(result.group(1)) - if percentage_done == 0: - percentage_done = 1 - elif percentage_done == 100: - percentage_done = 99 + percentage_done = max(1, min(99, percentage_done)) # update the progress bar self.set_progress_bar(percentage_done)