Skip to content

Commit a45eeaf

Browse files
authored
Merge pull request #4 from openandroidinstaller-dev/gui
Gui
2 parents b13c0b5 + 2ce8c60 commit a45eeaf

File tree

12 files changed

+805
-6
lines changed

12 files changed

+805
-6
lines changed

Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
install:
22
sudo apt update && sudo apt install android-tools-adb android-tools-fastboot
33
poetry install
4+
5+
app:
6+
poetry run python openandroidinstaller/openandroidinstaller.py
7+
8+
build-app:
9+
poetry run pyinstaller openandroidinstaller/openandroidinstaller.py --noconsole --noconfirm --onefile --icon "/assets/favicon.ico" --add-data "openandroidinstaller/assets:assets"

README.md

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,52 @@
1-
# openandroidinstaller
1+
<br />
2+
<div align="center">
3+
<h1>OpenAndroidInstaller</h1>
4+
<a href="https://github.com/openandroidinstaller-dev/openandroidinstaller">
5+
<img src="openandroidinstaller/assets/logo-192x192.png" alt="OpenAndroidInstaller" height="80">
6+
</a>
7+
8+
<p align="center">
9+
<br />
10+
Makes installing alternative Android distributions nice and easy.
11+
<br />
12+
The OpenAndroidInstaller project helps Android users to keep their smartphone's operating system up to date with free software and to continue using the device even though the manufacturer no longer offers updates. With a graphical installation software, users are easily guided through the installation process of free Android operating systems like LineageOS.
13+
<br />
14+
<br />
15+
<a href="https://github.com/openandroidinstaller-dev/openandroidinstaller/issues">Report Bug</a>
16+
·
17+
<a href="https://openandroidinstaller.org">Website</a>
18+
·
19+
<a href="mailto: [email protected]">Request Feature</a>
20+
<br />
21+
</p>
22+
</div>
23+
24+
## Installation
25+
26+
1. Download the AppImage, .exe or appropriate file for your OS.
27+
2. Install `adb` and `fastboot` by running `sudo apt install android-tools-adb android-tools-fastboot`
28+
3. OPTIONAL: Install `heimdall` for Samsung Devices:
29+
- download heimdall: https://androidfilehost.com/?w=files&flid=304516
30+
- install heimdall:
31+
$ unzip /path/to/heimdall_ubuntu.zip -d /tmp
32+
$ cp /tmp/bin/heimdall* /usr/bin/
33+
$ rm -rf /tmp/bin
34+
35+
## Usage
36+
37+
Download the lineageOS image and the custom recovery image.
38+
Start the desktop app and follow the instructions.
39+
40+
## Run OpenAndroidInstaller for development
41+
42+
1. Clone the main branch of this repository
43+
2. Run `make poetry` and `make install` to setup poetry and the relevant requirements
44+
3. Clone this Run `make app` to start the desktop app from the source code.
45+
46+
## Add your own installation configurations
47+
48+
## Contributing
49+
50+
## Acknowledgements
51+
52+
* Funded from September 2022 until February 2023 by ![logos of the "Bundesministerium für Bildung und Forschung", Prodotype Fund and OKFN-Deutschland](resources/pf_funding_logos.svg)

openandroidinstaller/__init__.py

Whitespace-only changes.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
steps:
2+
- title: "Unlock the bootloader"
3+
type: confirm_button
4+
content: "Turn on developer options and OEM Unlock on your phone."
5+
- title: "Boot into recovery"
6+
type: confirm_button
7+
content: "Turn on your device and wait until its fully booted."
8+
- title: "Boot into recovery"
9+
type: call_button
10+
content: "Reboot into bootloader"
11+
command: "adb reboot download"
12+
- title: "Boot into recovery"
13+
type: call_button
14+
content: "Flash custom recovery"
15+
command: "heimdall flash --no-reboot --RECOVERY recovery"
16+
- title: "Boot into recovery"
17+
type: confirm_button
18+
content: "Unplug the USB cable from your device. Manually reboot into recovery. Press the Volume Down + Power buttons for 8~10 seconds until the screen turns black & release the buttons immediately when it does, then boot to recovery with the device powered off, hold Volume Up + Home + Power."
19+
- title: "Flash LineageOS"
20+
type: confirm_button
21+
content: "Now tap 'Wipe'. Then tap 'Format Data' and continue with the formatting process. This will remove encryption and delete all files stored in the internal storage."
22+
- title: "Flash LineageOS"
23+
type: confirm_button
24+
content: "Return to the previous menu and tap 'Advanced Wipe', then select the 'Cache' and 'System' partitions and then 'Swipe to Wipe'."
25+
- title: "Flash LineageOS"
26+
type: confirm_button
27+
content: "On the device, go back and select “Advanced”, “ADB Sideload”, then swipe to begin sideload. Then confirm here"
28+
- title: "Flash LineageOS"
29+
type: call_button
30+
content: "Flash lineageOS image. Don't remove the USB-Cable!"
31+
command: "adb sideload image"
32+
- title: "Boot into recovery"
33+
type: call_button
34+
content: "Reboot into OS"
35+
command: "adb reboot"
36+
- title: "Successfully finished flashing"
37+
type: text
38+
content: "Have fun with LineageOS!"
15 KB
Binary file not shown.
20 KB
Loading
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""Class to load config files for the install procedure."""
2+
import yaml
3+
from typing import List
4+
5+
6+
class Step():
7+
8+
def __init__(self, title: str, type: str, content: str, command: str=None):
9+
self.title = title
10+
self.type = type
11+
self.content = content
12+
self.command = command
13+
14+
15+
class InstallerConfig():
16+
17+
def __init__(self, steps: List[Step]):
18+
self.steps = steps
19+
20+
@classmethod
21+
def from_file(cls, path):
22+
with open(path, "r") as stream:
23+
try:
24+
raw_steps = yaml.safe_load(stream)
25+
raw_steps = dict(raw_steps)["steps"]
26+
except yaml.YAMLError as exc:
27+
print(exc)
28+
29+
steps = [Step(**raw_step) for raw_step in raw_steps]
30+
return cls(steps)
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import chunk
2+
from time import sleep
3+
import flet
4+
from flet import (AppBar, ElevatedButton, Page, Text, View, Row, ProgressRing, Column, FilePicker, FilePickerResultEvent, icons)
5+
from typing import List
6+
from subprocess import check_output, STDOUT, call, CalledProcessError
7+
from functools import partial
8+
9+
from installer_config import InstallerConfig
10+
11+
12+
recovery_path = None
13+
image_path = None
14+
15+
16+
def main(page: Page):
17+
page.title = "OpenAndroidInstaller"
18+
views = []
19+
20+
# Click-event handlers
21+
22+
def confirm(e):
23+
view_num = int(page.views[-1].route) + 1
24+
page.views.clear()
25+
page.views.append(views[view_num])
26+
page.update()
27+
28+
def go_back(e):
29+
view_num = int(page.views[-1].route) - 1
30+
if view_num < 0:
31+
view_num = 0
32+
page.views.clear()
33+
page.views.append(views[view_num])
34+
page.update()
35+
36+
def search_devices(e):
37+
config_path = "openandroidinstaller/assets/configs/"
38+
try:
39+
# read device properties
40+
output = check_output(["adb", "shell", "dumpsys", "bluetooth_manager", "|", "grep", "\'name:\'", "|", "cut", "-c9-"], stderr=STDOUT).decode()
41+
page.views[-1].controls.append(Text(f"Detected: {output}"))
42+
# load config from file
43+
config = InstallerConfig.from_file(config_path + output.strip() + ".yaml")
44+
page.views[-1].controls.append(Text(f"Installer configuration found."))
45+
page.views[-1].controls.append(ElevatedButton("Confirm and continue", on_click=confirm))
46+
new_views = views_from_config(config)
47+
views.extend(new_views)
48+
except CalledProcessError:
49+
output = "No device detected!"
50+
page.views[-1].controls.append(Text(f"{output}"))
51+
page.update()
52+
53+
54+
def views_from_config(config: InstallerConfig) -> List[View]:
55+
new_views = []
56+
for num_step, step in enumerate(config.steps):
57+
if step.type == "confirm_button":
58+
new_views.append(
59+
get_new_view(title=step.title, content=[confirm_button(step.content)], index=2+num_step)
60+
)
61+
elif step.type == "call_button":
62+
new_views.append(
63+
get_new_view(title=step.title, content=[call_button(step.content, command=step.command)], index=2+num_step)
64+
)
65+
elif step.type == "text":
66+
new_views.append(
67+
get_new_view(title=step.title, content=[Text(step.content)], index=2+num_step)
68+
)
69+
else:
70+
raise Exception(f"Unknown step type: {step.type}")
71+
return new_views
72+
73+
74+
def call_to_phone(e, command: str):
75+
command = command.replace("recovery", recovery_path)
76+
command = command.replace("image", image_path)
77+
page.views[-1].controls.append(ProgressRing())
78+
page.update()
79+
res = call(f'{command}', shell=True)
80+
if res != 0:
81+
page.views[-1].controls.pop()
82+
page.views[-1].controls.append(Text("Command {command} failed!"))
83+
else:
84+
sleep(5)
85+
page.views[-1].controls.pop()
86+
page.views[-1].controls.append(ElevatedButton("Confirm and continue", on_click=confirm))
87+
page.update()
88+
89+
# file picker setup
90+
91+
def pick_image_result(e: FilePickerResultEvent):
92+
selected_image.value = (
93+
", ".join(map(lambda f: f.name, e.files)) if e.files else "Cancelled!"
94+
)
95+
global image_path
96+
image_path = e.files[0].path
97+
selected_image.update()
98+
99+
def pick_recovery_result(e: FilePickerResultEvent):
100+
selected_recovery.value = (
101+
", ".join(map(lambda f: f.name, e.files)) if e.files else "Cancelled!"
102+
)
103+
global recovery_path
104+
recovery_path = e.files[0].path
105+
selected_recovery.update()
106+
107+
pick_image_dialog = FilePicker(on_result=pick_image_result)
108+
pick_recovery_dialog = FilePicker(on_result=pick_recovery_result)
109+
selected_image = Text()
110+
selected_recovery = Text()
111+
page.overlay.append(pick_image_dialog)
112+
page.overlay.append(pick_recovery_dialog)
113+
114+
# Generate the Views for the different steps
115+
116+
def confirm_button(text: str, confirm_text: str = "Confirm and continue") -> Row:
117+
words = text.split(" ")
118+
chunk_size = 10
119+
if len(words) > chunk_size:
120+
n_chunks = len(words) // chunk_size
121+
text_field = [Text(f"{' '.join(words[i*chunk_size:(i+1)*chunk_size])}") for i in range(n_chunks)]
122+
return Column(text_field + [ElevatedButton(f"{confirm_text}", on_click=confirm)])
123+
else:
124+
text_field = Text(f"{text}")
125+
return Row([text_field, ElevatedButton(f"{confirm_text}", on_click=confirm)])
126+
127+
def call_button(text: str, command: str, confirm_text: str = "Confirm and run") -> Row:
128+
return Row([
129+
Text(f"{text}"),
130+
ElevatedButton(f"{confirm_text}", on_click=partial(call_to_phone, command=command))
131+
])
132+
133+
def get_new_view(title: str, index: int, content: List = []) -> View:
134+
return View(
135+
f"{index}", [AppBar(title=Text(f"{title}"))] + content
136+
)
137+
138+
# main part
139+
140+
views = [
141+
get_new_view(title="Welcome to OpenAndroidInstaller!", content=[ElevatedButton("Search device", on_click=search_devices)], index=0),
142+
get_new_view(title="Pick image and recovery", content=[Row(
143+
[
144+
ElevatedButton(
145+
"Pick image file",
146+
icon=icons.UPLOAD_FILE,
147+
on_click=lambda _: pick_image_dialog.pick_files(
148+
allow_multiple=False
149+
),
150+
),
151+
selected_image,
152+
]), Row(
153+
[
154+
ElevatedButton(
155+
"Pick recovery file",
156+
icon=icons.UPLOAD_FILE,
157+
on_click=lambda _: pick_recovery_dialog.pick_files(
158+
allow_multiple=False
159+
),
160+
),
161+
selected_recovery,
162+
]
163+
164+
), confirm_button("Done?")], index=1),
165+
]
166+
167+
page.views.append(views[0])
168+
page.update()
169+
170+
171+
flet.app(target=main, assets_dir="assets")

0 commit comments

Comments
 (0)