Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve error message for loading images #399

Merged
merged 4 commits into from
Jan 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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