Skip to content

Commit bf01053

Browse files
authored
Improve error message for loading images (#399)
This PR mainly addresses #394 and #354. It also adds: - improved Makefile
2 parents 6859a5e + cbea2e9 commit bf01053

File tree

4 files changed

+154
-70
lines changed

4 files changed

+154
-70
lines changed

Makefile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
.phoney: install export format lint typing test app test-app build-app clean-build
2+
3+
help:
4+
@echo "install - install dependencies"
5+
@echo "export - export dependencies to requirements.txt"
6+
@echo "format - format code with black"
7+
@echo "lint - lint code with ruff"
8+
@echo "typing - type check code with mypy"
9+
@echo "test - run tests"
10+
@echo "app - run app"
11+
@echo "test-app - run app in test mode with test config for sargo"
12+
@echo "build-app - build app"
13+
@echo "clean-build - clean build"
14+
115
poetry:
216
curl -sSL https://install.python-poetry.org | python3 -
317

openandroidinstaller/utils.py

Lines changed: 109 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,35 @@
1414
# Author: Tobias Sterbak
1515

1616
import zipfile
17+
from dataclasses import dataclass
18+
from enum import Enum
1719
from typing import Optional, List
1820

1921
import requests
2022
from loguru import logger
2123

2224

25+
class CompatibilityStatus(Enum):
26+
"""Enum for the compatibility status of a device."""
27+
28+
UNKNOWN = 0
29+
COMPATIBLE = 1
30+
INCOMPATIBLE = 2
31+
32+
33+
@dataclass
34+
class CheckResult:
35+
"""Dataclass for the result of a check.
36+
37+
Attributes:
38+
status: Compatibility status of the device.
39+
message: Message to be displayed to the user.
40+
"""
41+
42+
status: CompatibilityStatus
43+
message: str
44+
45+
2346
def get_download_link(devicecode: str) -> Optional[str]:
2447
"""Check if a lineageOS version for this device exists on download.lineageos.com and return the respective download link."""
2548
url = f"https://download.lineageos.org/api/v2/devices/{devicecode}"
@@ -40,54 +63,101 @@ def get_download_link(devicecode: str) -> Optional[str]:
4063
return None
4164

4265

43-
def image_works_with_device(supported_device_codes: List[str], image_path: str) -> bool:
44-
"""Determine if an image works for the given device.
66+
def retrieve_image_metadata(image_path: str) -> dict:
67+
"""Retrieve metadata from the selected image.
4568
4669
Args:
47-
supported_device_codes: List of supported device codes from the config file.
4870
image_path: Path to the image file.
4971
5072
Returns:
51-
True if the image works with the device, False otherwise.
73+
Dictionary containing the metadata.
5274
"""
53-
with zipfile.ZipFile(image_path) as image_zip:
54-
with image_zip.open(
55-
"META-INF/com/android/metadata", mode="r"
56-
) as image_metadata:
57-
metadata = image_metadata.readlines()
58-
supported_devices = str(metadata[-1]).split("=")[-1][:-3].split(",")
59-
logger.info(f"Image works with device: {supported_devices}")
60-
61-
if any(code in supported_devices for code in supported_device_codes):
62-
logger.success("Device supported by the selected image.")
63-
return True
64-
else:
65-
logger.error(
66-
f"Image file {image_path.split('/')[-1]} is not supported."
67-
)
68-
return False
75+
metapath = "META-INF/com/android/metadata"
76+
try:
77+
with zipfile.ZipFile(image_path) as image_zip:
78+
with image_zip.open(metapath, mode="r") as image_metadata:
79+
metadata = image_metadata.readlines()
80+
metadata_dict = {}
81+
for line in metadata:
82+
metadata_dict[line[: line.find(b"=")].decode("utf-8")] = line[
83+
line.find(b"=") + 1 : -1
84+
].decode("utf-8")
85+
logger.info(f"Metadata retrieved from image {image_path.split('/')[-1]}.")
86+
return metadata_dict
87+
except (FileNotFoundError, KeyError):
88+
logger.error(
89+
f"Metadata file {metapath} not found in {image_path.split('/')[-1]}."
90+
)
91+
return dict()
6992

7093

7194
def image_sdk_level(image_path: str) -> int:
7295
"""Determine Android version of the selected image.
7396
74-
Example:
97+
Examples:
98+
Android 10: 29
99+
Android 11: 30
100+
Android 12: 31
101+
Android 12.1: 32
75102
Android 13: 33
103+
104+
Args:
105+
image_path: Path to the image file.
106+
107+
Returns:
108+
Android version as integer.
76109
"""
77-
with zipfile.ZipFile(image_path) as image_zip:
78-
with image_zip.open(
79-
"META-INF/com/android/metadata", mode="r"
80-
) as image_metadata:
81-
metadata = image_metadata.readlines()
82-
for line in metadata:
83-
if b"sdk-level" in line:
84-
return int(line[line.find(b"=") + 1 : -1].decode("utf-8"))
85-
return 0
110+
metadata = retrieve_image_metadata(image_path)
111+
try:
112+
sdk_level = metadata["post-sdk-level"]
113+
logger.info(f"Android version of {image_path}: {sdk_level}")
114+
return int(sdk_level)
115+
except (ValueError, TypeError, KeyError) as e:
116+
logger.error(f"Could not determine Android version of {image_path}. Error: {e}")
117+
return -1
118+
119+
120+
def image_works_with_device(
121+
supported_device_codes: List[str], image_path: str
122+
) -> CheckResult:
123+
"""Determine if an image works for the given device.
124+
125+
Args:
126+
supported_device_codes: List of supported device codes from the config file.
127+
image_path: Path to the image file.
128+
129+
Returns:
130+
CheckResult object containing the compatibility status and a message.
131+
"""
132+
metadata = retrieve_image_metadata(image_path)
133+
try:
134+
supported_devices = metadata["pre-device"].split(",")
135+
logger.info(f"Image works with the following device(s): {supported_devices}")
136+
if any(code in supported_devices for code in supported_device_codes):
137+
logger.success("Device supported by the selected image.")
138+
return CheckResult(
139+
CompatibilityStatus.COMPATIBLE,
140+
"Device supported by the selected image.",
141+
)
142+
else:
143+
logger.error(f"Image file {image_path.split('/')[-1]} is not supported.")
144+
return CheckResult(
145+
CompatibilityStatus.INCOMPATIBLE,
146+
f"Image file {image_path.split('/')[-1]} is not supported by device code.",
147+
)
148+
except KeyError:
149+
logger.error(
150+
f"Could not determine supported devices for {image_path.split('/')[-1]}."
151+
)
152+
return CheckResult(
153+
CompatibilityStatus.UNKNOWN,
154+
f"Could not determine supported devices for {image_path.split('/')[-1]}. Missing metadata file? You may try to flash the image anyway.",
155+
)
86156

87157

88158
def recovery_works_with_device(
89159
supported_device_codes: List[str], recovery_path: str
90-
) -> bool:
160+
) -> CheckResult:
91161
"""Determine if a recovery works for the given device.
92162
93163
BEWARE: THE RECOVERY PART IS STILL VERY BASIC!
@@ -97,14 +167,19 @@ def recovery_works_with_device(
97167
recovery_path: Path to the recovery file.
98168
99169
Returns:
100-
True if the recovery works with the device, False otherwise.
170+
CheckResult object containing the compatibility status and a message.
101171
"""
102172
recovery_file_name = recovery_path.split("/")[-1]
103173
if any(code in recovery_file_name for code in supported_device_codes) and (
104174
"twrp" in recovery_file_name
105175
):
106176
logger.success("Device supported by the selected recovery.")
107-
return True
177+
return CheckResult(
178+
CompatibilityStatus.COMPATIBLE, "Device supported by the selected recovery."
179+
)
108180
else:
109181
logger.error(f"Recovery file {recovery_file_name} is not supported.")
110-
return False
182+
return CheckResult(
183+
CompatibilityStatus.INCOMPATIBLE,
184+
f"Recovery file {recovery_file_name} is not supported by device code in file name.",
185+
)

openandroidinstaller/views/select_view.py

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
image_works_with_device,
4747
recovery_works_with_device,
4848
image_sdk_level,
49+
CheckResult,
50+
CompatibilityStatus,
4951
)
5052

5153

@@ -145,6 +147,9 @@ def init_visuals(
145147
icon=icons.ARROW_BACK,
146148
expand=True,
147149
)
150+
# store image and recovery compatibility
151+
self.image_compatibility: CheckResult | None = None
152+
self.recovery_compatibility: CheckResult | None = None
148153

149154
def build(self):
150155
self.clear()
@@ -533,17 +538,21 @@ def pick_image_result(self, e: FilePickerResultEvent):
533538
logger.info("No image selected.")
534539
# check if the image works with the device and show the filename in different colors accordingly
535540
if e.files:
536-
if image_works_with_device(
541+
self.image_compatibility = image_works_with_device(
537542
supported_device_codes=self.state.config.supported_device_codes,
538543
image_path=self.state.image_path,
539-
):
544+
)
545+
if self.image_compatibility.status == CompatibilityStatus.COMPATIBLE:
540546
self.selected_image.color = colors.GREEN
547+
elif self.image_compatibility.status == CompatibilityStatus.UNKNOWN:
548+
self.selected_image.color = colors.ORANGE
541549
else:
542550
self.selected_image.color = colors.RED
551+
self.selected_image.value += f"\n> {self.image_compatibility.message}"
543552
# if the image works and the sdk level is 33 or higher, show the additional image selection
544553
if self.state.flash_recovery:
545554
if (
546-
self.selected_image.color == colors.GREEN
555+
self.image_compatibility
547556
and image_sdk_level(self.state.image_path) >= 33
548557
):
549558
self.toggle_additional_image_selection()
@@ -567,13 +576,17 @@ def pick_recovery_result(self, e: FilePickerResultEvent):
567576
logger.info("No image selected.")
568577
# check if the recovery works with the device and show the filename in different colors accordingly
569578
if e.files:
570-
if recovery_works_with_device(
579+
self.recovery_compatibility = recovery_works_with_device(
571580
supported_device_codes=self.state.config.supported_device_codes,
572581
recovery_path=self.state.recovery_path,
573-
):
582+
)
583+
if self.recovery_compatibility.status == CompatibilityStatus.COMPATIBLE:
574584
self.selected_recovery.color = colors.GREEN
585+
elif self.recovery_compatibility.status == CompatibilityStatus.UNKNOWN:
586+
self.selected_recovery.color = colors.ORANGE
575587
else:
576588
self.selected_recovery.color = colors.RED
589+
self.selected_recovery.value += f"\n> {self.recovery_compatibility.message}"
577590
# update
578591
self.selected_recovery.update()
579592

@@ -654,23 +667,18 @@ def enable_button_if_ready(self, e):
654667
if (".zip" in self.selected_image.value) and (
655668
".img" in self.selected_recovery.value
656669
):
657-
if not (
658-
image_works_with_device(
659-
supported_device_codes=self.state.config.supported_device_codes,
660-
image_path=self.state.image_path,
661-
)
662-
and recovery_works_with_device(
663-
supported_device_codes=self.state.config.supported_device_codes,
664-
recovery_path=self.state.recovery_path,
665-
)
670+
if (
671+
self.image_compatibility.status == CompatibilityStatus.INCOMPATIBLE
672+
) or (
673+
self.recovery_compatibility.status == CompatibilityStatus.INCOMPATIBLE
666674
):
667675
# if image and recovery work for device allow to move on, otherwise display message
668676
logger.error(
669677
"Image and recovery don't work with the device. Please select different ones."
670678
)
671679
self.info_field.controls = [
672680
Text(
673-
"Image and/or recovery don't work with the device. Make sure you use a TWRP-based recovery.",
681+
"Something is wrong with the selected files.",
674682
color=colors.RED,
675683
weight="bold",
676684
)
@@ -695,12 +703,10 @@ def enable_button_if_ready(self, e):
695703
or "vendor_boot" not in self.state.config.additional_steps,
696704
]
697705
):
698-
logger.error(
699-
"Some additional images don't match or are missing. Please select different ones."
700-
)
706+
logger.error("Some additional images don't match or are missing.")
701707
self.info_field.controls = [
702708
Text(
703-
"Some additional images don't match or are missing. Please select the right ones.",
709+
"Some additional images don't match or are missing.",
704710
color=colors.RED,
705711
weight="bold",
706712
)
@@ -715,16 +721,9 @@ def enable_button_if_ready(self, e):
715721
self.continue_eitherway_button.disabled = True
716722
self.right_view.update()
717723
elif (".zip" in self.selected_image.value) and (not self.state.flash_recovery):
718-
if not (
719-
image_works_with_device(
720-
supported_device_codes=self.state.config.supported_device_codes,
721-
image_path=self.state.image_path,
722-
)
723-
):
724+
if self.image_compatibility.status != CompatibilityStatus.COMPATIBLE:
724725
# if image works for device allow to move on, otherwise display message
725-
logger.error(
726-
"Image doesn't work with the device. Please select a different one."
727-
)
726+
logger.error("Image doesn't work with the device.")
728727
self.info_field.controls = [
729728
Text(
730729
"Image doesn't work with the device.",

openandroidinstaller/widgets.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def write_line(self, line: str):
6868
6969
Ignores empty lines.
7070
"""
71-
if (type(line) == str) and line.strip():
71+
if isinstance(line, str) and line.strip():
7272
self._box.content.controls[0].value += f"\n>{line.strip()}"
7373
self._box.content.controls[0].value = self._box.content.controls[
7474
0
@@ -115,7 +115,7 @@ def display_progress_bar(self, line: str):
115115
percentage_done = None
116116
result = None
117117
# create the progress bar
118-
if self.progress_bar == None:
118+
if not self.progress_bar:
119119
self.progress_bar = ProgressBar(
120120
value=1 / 100,
121121
width=500,
@@ -129,7 +129,7 @@ def display_progress_bar(self, line: str):
129129
Row([self.percentage_text, self.progress_bar])
130130
)
131131
# get the progress numbers from the output lines
132-
if (type(line) == str) and line.strip():
132+
if isinstance(line, str) and line.strip():
133133
result = re.search(
134134
r"\(\~(\d{1,3})\%\)|(Total xfer:|adb: failed to read command: Success)",
135135
line.strip(),
@@ -139,11 +139,7 @@ def display_progress_bar(self, line: str):
139139
percentage_done = 99
140140
elif result.group(1):
141141
percentage_done = int(result.group(1))
142-
if percentage_done == 0:
143-
percentage_done = 1
144-
elif percentage_done >= 100:
145-
percentage_done = 99
146-
142+
percentage_done = max(1, min(99, percentage_done))
147143
# update the progress bar
148144
self.set_progress_bar(percentage_done)
149145

0 commit comments

Comments
 (0)