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.",