Skip to content

Commit

Permalink
Improve error message for loading images (#399)
Browse files Browse the repository at this point in the history
This PR mainly addresses #394 and #354.

It also adds:
- improved Makefile
  • Loading branch information
tsterbak committed Jan 14, 2024
2 parents 6859a5e + cbea2e9 commit bf01053
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 70 deletions.
14 changes: 14 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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 -

Expand Down
143 changes: 109 additions & 34 deletions openandroidinstaller/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand All @@ -40,54 +63,101 @@ def get_download_link(devicecode: str) -> Optional[str]:
return None


def image_works_with_device(supported_device_codes: List[str], image_path: str) -> bool:
"""Determine if an image works for the given device.
def retrieve_image_metadata(image_path: str) -> dict:
"""Retrieve metadata from the selected image.
Args:
supported_device_codes: List of supported device codes from the config file.
image_path: Path to the image file.
Returns:
True if the image works with the device, False otherwise.
Dictionary containing the metadata.
"""
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
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, KeyError):
logger.error(
f"Metadata file {metapath} not found in {image_path.split('/')[-1]}."
)
return dict()


def image_sdk_level(image_path: str) -> int:
"""Determine Android version of the selected image.
Example:
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.
"""
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 -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:
supported_device_codes: List of supported device codes from the config file.
image_path: Path to the image file.
Returns:
CheckResult object containing the compatibility status and a message.
"""
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 CheckResult(
CompatibilityStatus.COMPATIBLE,
"Device supported by the selected image.",
)
else:
logger.error(f"Image file {image_path.split('/')[-1]} is not supported.")
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 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!
Expand All @@ -97,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.",
)
55 changes: 27 additions & 28 deletions openandroidinstaller/views/select_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
image_works_with_device,
recovery_works_with_device,
image_sdk_level,
CheckResult,
CompatibilityStatus,
)


Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand All @@ -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()

Expand Down Expand Up @@ -654,23 +667,18 @@ 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(
"Image and recovery don't work with the device. Please select different ones."
)
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",
)
Expand All @@ -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",
)
Expand All @@ -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.",
Expand Down
12 changes: 4 additions & 8 deletions openandroidinstaller/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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(),
Expand All @@ -139,11 +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)

Expand Down

0 comments on commit bf01053

Please sign in to comment.