flutter-dev 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- common_utils.py +261 -0
- core/__init__.py +7 -0
- core/constants.py +57 -0
- core/state.py +25 -0
- create_page.py +288 -0
- fdev.py +258 -0
- flutter_dev-0.1.0.dist-info/METADATA +411 -0
- flutter_dev-0.1.0.dist-info/RECORD +30 -0
- flutter_dev-0.1.0.dist-info/WHEEL +5 -0
- flutter_dev-0.1.0.dist-info/entry_points.txt +5 -0
- flutter_dev-0.1.0.dist-info/licenses/LICENSE +21 -0
- flutter_dev-0.1.0.dist-info/top_level.txt +9 -0
- gemini_api.py +395 -0
- git_diff_output_editor.py +34 -0
- install_legacy.py +467 -0
- managers/__init__.py +69 -0
- managers/ai.py +113 -0
- managers/app.py +541 -0
- managers/brew.py +477 -0
- managers/build.py +436 -0
- managers/datetime.py +49 -0
- managers/device.py +207 -0
- managers/doctor.py +286 -0
- managers/git.py +981 -0
- managers/git_account.py +542 -0
- managers/merge.py +165 -0
- managers/mirror.py +205 -0
- managers/project.py +138 -0
- managers/web_deploy.py +43 -0
- switch_ai.py +181 -0
managers/build.py
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Build Manager - APK/AAB build functions
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import time
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
|
|
12
|
+
from common_utils import (
|
|
13
|
+
RED, GREEN, YELLOW, BLUE, NC, MAGENTA, CHECKMARK, CROSS,
|
|
14
|
+
timer_decorator,
|
|
15
|
+
is_windows,
|
|
16
|
+
open_directory,
|
|
17
|
+
)
|
|
18
|
+
from core.constants import PATTERNS, PATHS
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def show_loading(description, process):
|
|
22
|
+
"""
|
|
23
|
+
Displays a loading spinner with a custom message while a process is running
|
|
24
|
+
Parameters:
|
|
25
|
+
description: Description message to display
|
|
26
|
+
process: Process object to monitor
|
|
27
|
+
"""
|
|
28
|
+
spinner_index = 0
|
|
29
|
+
braille_spinner_list = '⡿⣟⣯⣷⣾⣽⣻⢿'
|
|
30
|
+
print(description, end='', flush=True)
|
|
31
|
+
# Continue spinning while the process is running
|
|
32
|
+
while process.poll() is None:
|
|
33
|
+
print(f"\b{MAGENTA}{braille_spinner_list[spinner_index]}{NC}", end='', flush=True)
|
|
34
|
+
spinner_index = (spinner_index + 1) % len(braille_spinner_list)
|
|
35
|
+
time.sleep(0.025)
|
|
36
|
+
stdout, stderr = process.communicate()
|
|
37
|
+
# Display success or failure icon based on the process exit status
|
|
38
|
+
if process.returncode == 0:
|
|
39
|
+
print(f"\b{CHECKMARK} ", flush=True)
|
|
40
|
+
return True
|
|
41
|
+
else:
|
|
42
|
+
print(f"\b{CROSS} ", flush=True)
|
|
43
|
+
if stdout:
|
|
44
|
+
print(f"\n{GREEN}Output:\n{stdout}{NC}")
|
|
45
|
+
if stderr:
|
|
46
|
+
print(f"\n{RED}Error Output:\n{stderr}{NC}")
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run_flutter_command(cmd_list, description, env=None):
|
|
51
|
+
"""
|
|
52
|
+
Runs a flutter/dart command with a loading spinner.
|
|
53
|
+
Parameters:
|
|
54
|
+
cmd_list: List of command arguments
|
|
55
|
+
description: Description to show with spinner
|
|
56
|
+
env: Optional environment variables dict (default: inherits current env)
|
|
57
|
+
"""
|
|
58
|
+
# Windows compatibility for shell commands
|
|
59
|
+
shell_needed = (is_windows() and cmd_list[0] in ['timeout', 'start', 'flutter', 'dart']) or cmd_list[0] == 'pod'
|
|
60
|
+
|
|
61
|
+
process = subprocess.Popen(
|
|
62
|
+
cmd_list,
|
|
63
|
+
stdout=subprocess.PIPE,
|
|
64
|
+
stderr=subprocess.PIPE,
|
|
65
|
+
shell=shell_needed,
|
|
66
|
+
encoding='utf-8',
|
|
67
|
+
errors='replace',
|
|
68
|
+
env=env,
|
|
69
|
+
)
|
|
70
|
+
return show_loading(description, process)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_package_name():
|
|
74
|
+
"""
|
|
75
|
+
Dynamically extract package name (applicationId) from Android build files.
|
|
76
|
+
Checks both build.gradle.kts and build.gradle files.
|
|
77
|
+
Returns the package name or None if not found.
|
|
78
|
+
"""
|
|
79
|
+
gradle_kts_path = PATHS['gradle_kts']
|
|
80
|
+
gradle_path = PATHS['gradle']
|
|
81
|
+
|
|
82
|
+
# Try build.gradle.kts first
|
|
83
|
+
if gradle_kts_path.exists():
|
|
84
|
+
try:
|
|
85
|
+
with open(gradle_kts_path, 'r', encoding='utf-8') as file:
|
|
86
|
+
content = file.read()
|
|
87
|
+
match = PATTERNS['package_name_kts'].search(content)
|
|
88
|
+
if match:
|
|
89
|
+
return match.group(1)
|
|
90
|
+
except Exception as e:
|
|
91
|
+
print(f"{YELLOW}Warning: Could not read {gradle_kts_path}: {e}{NC}")
|
|
92
|
+
|
|
93
|
+
# Try build.gradle (Groovy format)
|
|
94
|
+
if gradle_path.exists():
|
|
95
|
+
try:
|
|
96
|
+
with open(gradle_path, 'r', encoding='utf-8') as file:
|
|
97
|
+
content = file.read()
|
|
98
|
+
match = PATTERNS['package_name_groovy'].search(content)
|
|
99
|
+
if match:
|
|
100
|
+
return match.group(1)
|
|
101
|
+
except Exception as e:
|
|
102
|
+
print(f"{YELLOW}Warning: Could not read {gradle_path}: {e}{NC}")
|
|
103
|
+
|
|
104
|
+
print(f"{RED}Error: Could not find package name in build.gradle or build.gradle.kts{NC}")
|
|
105
|
+
print(f"{YELLOW}Searched in: android/app/build.gradle and android/app/build.gradle.kts{NC}")
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_app_label_from_manifest():
|
|
110
|
+
"""
|
|
111
|
+
Extract app label from AndroidManifest.xml
|
|
112
|
+
Returns the app label or None if not found
|
|
113
|
+
"""
|
|
114
|
+
manifest_path = PATHS['manifest']
|
|
115
|
+
|
|
116
|
+
if not manifest_path.exists():
|
|
117
|
+
print(f"{YELLOW}Warning: AndroidManifest.xml not found at {manifest_path}{NC}")
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
with open(manifest_path, 'r', encoding='utf-8') as file:
|
|
122
|
+
content = file.read()
|
|
123
|
+
match = PATTERNS['app_label'].search(content)
|
|
124
|
+
if match:
|
|
125
|
+
label = match.group(1)
|
|
126
|
+
# If it's a string resource reference, try to get actual value
|
|
127
|
+
if label.startswith('@string/'):
|
|
128
|
+
return None
|
|
129
|
+
# Decode HTML entities like & to &
|
|
130
|
+
label = label.replace('&', '&').replace('<', '<').replace('>', '>')
|
|
131
|
+
return label
|
|
132
|
+
else:
|
|
133
|
+
print(f"{YELLOW}Warning: Could not find android:label in AndroidManifest.xml{NC}")
|
|
134
|
+
return None
|
|
135
|
+
except Exception as e:
|
|
136
|
+
print(f"{YELLOW}Warning: Could not read AndroidManifest.xml: {e}{NC}")
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def sanitize_filename(name):
|
|
141
|
+
"""
|
|
142
|
+
Sanitize app name for use in filename (cross-platform compatible)
|
|
143
|
+
Removes/replaces characters not allowed in Windows/macOS/Linux filenames
|
|
144
|
+
"""
|
|
145
|
+
# Replace & with 'and'
|
|
146
|
+
name = name.replace('&', 'and')
|
|
147
|
+
# Remove Windows forbidden characters: < > : " / \ | ? *
|
|
148
|
+
name = PATTERNS['sanitize_special'].sub('', name)
|
|
149
|
+
# Remove any remaining special characters except alphanumeric, spaces, hyphens, underscores
|
|
150
|
+
name = PATTERNS['sanitize_non_word'].sub('', name)
|
|
151
|
+
# Replace multiple spaces/hyphens with single underscore
|
|
152
|
+
name = PATTERNS['sanitize_spaces'].sub('_', name)
|
|
153
|
+
# Remove leading/trailing underscores
|
|
154
|
+
name = name.strip('_')
|
|
155
|
+
return name
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def get_formatted_date():
|
|
159
|
+
"""
|
|
160
|
+
Get current date formatted as 'DD_MMM' (e.g., '07_Jan')
|
|
161
|
+
"""
|
|
162
|
+
return datetime.now().strftime('%d_%b')
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def rename_build_files(output_dir, file_extension, app_label=None):
|
|
166
|
+
"""
|
|
167
|
+
Rename APK/AAB files with app label and date
|
|
168
|
+
Parameters:
|
|
169
|
+
output_dir: Directory containing the build files (Path object)
|
|
170
|
+
file_extension: File extension to search for ('apk' or 'aab')
|
|
171
|
+
app_label: Optional app label (if None, will extract from AndroidManifest.xml)
|
|
172
|
+
"""
|
|
173
|
+
if not output_dir.exists():
|
|
174
|
+
print(f"{YELLOW}Warning: Output directory {output_dir} does not exist{NC}")
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
# Get app label from AndroidManifest.xml if not provided
|
|
178
|
+
if app_label is None:
|
|
179
|
+
app_label = get_app_label_from_manifest()
|
|
180
|
+
|
|
181
|
+
if not app_label:
|
|
182
|
+
print(f"{YELLOW}Warning: Could not get app label, files will not be renamed{NC}")
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
# Sanitize app name for filename
|
|
186
|
+
sanitized_name = sanitize_filename(app_label)
|
|
187
|
+
|
|
188
|
+
# Get formatted date
|
|
189
|
+
date_str = get_formatted_date()
|
|
190
|
+
|
|
191
|
+
# Find all files with the specified extension
|
|
192
|
+
build_files = list(output_dir.glob(f"*.{file_extension}"))
|
|
193
|
+
|
|
194
|
+
if not build_files:
|
|
195
|
+
print(f"{YELLOW}Warning: No {file_extension.upper()} files found in {output_dir}{NC}")
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
print(f"\n{BLUE}Renaming {file_extension.upper()} files...{NC}")
|
|
199
|
+
|
|
200
|
+
for file_path in build_files:
|
|
201
|
+
# Check if file contains architecture info (e.g., arm64-v8a)
|
|
202
|
+
arch_match = PATTERNS['architecture'].search(file_path.name)
|
|
203
|
+
|
|
204
|
+
if arch_match:
|
|
205
|
+
# Include architecture in filename
|
|
206
|
+
arch = arch_match.group(1)
|
|
207
|
+
new_name = f"{sanitized_name}_{arch}_{date_str}.{file_extension}"
|
|
208
|
+
else:
|
|
209
|
+
# No architecture info
|
|
210
|
+
new_name = f"{sanitized_name}_{date_str}.{file_extension}"
|
|
211
|
+
|
|
212
|
+
new_path = output_dir / new_name
|
|
213
|
+
|
|
214
|
+
# Rename the file
|
|
215
|
+
try:
|
|
216
|
+
shutil.move(str(file_path), str(new_path))
|
|
217
|
+
print(f"{GREEN} ✓ Renamed: {file_path.name} → {new_name}{NC}")
|
|
218
|
+
except Exception as e:
|
|
219
|
+
print(f"{RED} ✗ Failed to rename {file_path.name}: {e}{NC}")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def display_build_size(file_type, directory):
|
|
223
|
+
"""
|
|
224
|
+
Display build file size (works for both APK and AAB)
|
|
225
|
+
|
|
226
|
+
Parameters:
|
|
227
|
+
file_type: "apk" or "aab"
|
|
228
|
+
directory: Path object or string of the directory containing build files
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
None
|
|
232
|
+
"""
|
|
233
|
+
# Convert to Path object if string
|
|
234
|
+
build_dir = Path(directory) if isinstance(directory, str) else directory
|
|
235
|
+
|
|
236
|
+
# Get files with the specified extension
|
|
237
|
+
build_files = list(build_dir.glob(f"*.{file_type}")) if build_dir.exists() else []
|
|
238
|
+
|
|
239
|
+
if build_files:
|
|
240
|
+
for build_file in build_files:
|
|
241
|
+
size_bytes = build_file.stat().st_size
|
|
242
|
+
size_mb = round(size_bytes / 1048576, 2)
|
|
243
|
+
# Display with uppercase file type (APK, AAB)
|
|
244
|
+
print(f"{BLUE}{file_type.upper()}: {build_file.name} | Size: {size_mb} MB{NC}")
|
|
245
|
+
else:
|
|
246
|
+
print(f"{RED}{file_type.upper()} file not found in {build_dir}{NC}")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def common_build_process(
|
|
250
|
+
build_name,
|
|
251
|
+
build_command,
|
|
252
|
+
build_description,
|
|
253
|
+
output_dir,
|
|
254
|
+
file_extension,
|
|
255
|
+
install_after=False
|
|
256
|
+
):
|
|
257
|
+
"""
|
|
258
|
+
Common build process for all build types (APK, APK-split, AAB)
|
|
259
|
+
|
|
260
|
+
Parameters:
|
|
261
|
+
build_name: Display name for the build (e.g., "APK", "AAB")
|
|
262
|
+
build_command: List of flutter build command arguments
|
|
263
|
+
build_description: Loading text for the build step
|
|
264
|
+
output_dir: Path object where build output is located
|
|
265
|
+
file_extension: "apk" or "aab"
|
|
266
|
+
install_after: Boolean to install APK after build (default: False)
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Boolean indicating success
|
|
270
|
+
"""
|
|
271
|
+
# Initial message
|
|
272
|
+
print(f"{YELLOW}Building {build_name}...{NC}\n")
|
|
273
|
+
|
|
274
|
+
# Step 1: Clean the project
|
|
275
|
+
run_flutter_command(["flutter", "clean"], "Cleaning project... ")
|
|
276
|
+
|
|
277
|
+
# Step 2: Get dependencies
|
|
278
|
+
run_flutter_command(["flutter", "pub", "get"], "Getting dependencies... ")
|
|
279
|
+
|
|
280
|
+
# Step 3: Generate localizations
|
|
281
|
+
run_flutter_command(["flutter", "gen-l10n"], "Generating localizations... ")
|
|
282
|
+
|
|
283
|
+
# Step 4: Generate build files
|
|
284
|
+
run_flutter_command(["dart", "run", "build_runner", "build", "--delete-conflicting-outputs"], "Generating build files... ")
|
|
285
|
+
|
|
286
|
+
# Step 5: Build (APK/AAB)
|
|
287
|
+
run_flutter_command(build_command, build_description)
|
|
288
|
+
|
|
289
|
+
# Step 6: Rename build files with app label and date
|
|
290
|
+
rename_build_files(output_dir, file_extension)
|
|
291
|
+
|
|
292
|
+
# Step 7: Display file size
|
|
293
|
+
display_build_size(file_extension, output_dir)
|
|
294
|
+
|
|
295
|
+
# Step 8: Install (if requested) or open directory
|
|
296
|
+
if install_after:
|
|
297
|
+
# Import here to avoid circular imports
|
|
298
|
+
from managers.app import install_apk
|
|
299
|
+
install_result = install_apk()
|
|
300
|
+
if install_result:
|
|
301
|
+
print(f"\n{GREEN}✓ {build_name} built and installed successfully!{NC}")
|
|
302
|
+
else:
|
|
303
|
+
print(f"\n{RED}✗ {build_name} built but install failed!{NC}")
|
|
304
|
+
return install_result
|
|
305
|
+
else:
|
|
306
|
+
# Success message
|
|
307
|
+
print(f"\n{GREEN}✓ {build_name} built successfully!{NC}")
|
|
308
|
+
# Open the directory containing the build
|
|
309
|
+
open_directory(str(output_dir))
|
|
310
|
+
return True
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@timer_decorator
|
|
314
|
+
def build_apk():
|
|
315
|
+
"""Build APK (Full Process)"""
|
|
316
|
+
return common_build_process(
|
|
317
|
+
build_name="APK (Full Process)",
|
|
318
|
+
build_command=[
|
|
319
|
+
"flutter", "build", "apk", "--release", "--obfuscate",
|
|
320
|
+
"--target-platform", "android-arm64", "--split-debug-info=./"
|
|
321
|
+
],
|
|
322
|
+
build_description="Building APK... ",
|
|
323
|
+
output_dir=PATHS['apk_output'],
|
|
324
|
+
file_extension="apk",
|
|
325
|
+
install_after=False
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@timer_decorator
|
|
330
|
+
def build_apk_split_per_abi():
|
|
331
|
+
"""Build APK with --split-per-abi"""
|
|
332
|
+
return common_build_process(
|
|
333
|
+
build_name="APK (split-per-abi)",
|
|
334
|
+
build_command=[
|
|
335
|
+
"flutter", "build", "apk", "--release", "--split-per-abi",
|
|
336
|
+
"--obfuscate", "--split-debug-info=./"
|
|
337
|
+
],
|
|
338
|
+
build_description="Building APK (split-per-abi)... ",
|
|
339
|
+
output_dir=PATHS['apk_output'],
|
|
340
|
+
file_extension="apk",
|
|
341
|
+
install_after=False
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@timer_decorator
|
|
346
|
+
def build_aab():
|
|
347
|
+
"""Build AAB"""
|
|
348
|
+
return common_build_process(
|
|
349
|
+
build_name="AAB",
|
|
350
|
+
build_command=[
|
|
351
|
+
"flutter", "build", "appbundle", "--release",
|
|
352
|
+
"--obfuscate", "--split-debug-info=./"
|
|
353
|
+
],
|
|
354
|
+
build_description="Building AAB... ",
|
|
355
|
+
output_dir=PATHS['aab_output'],
|
|
356
|
+
file_extension="aab",
|
|
357
|
+
install_after=False
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@timer_decorator
|
|
362
|
+
def build_ipa():
|
|
363
|
+
"""Build IPA for iOS App Store"""
|
|
364
|
+
if is_windows():
|
|
365
|
+
print(f"{RED}Error: iOS builds are not supported on Windows. macOS required.{NC}")
|
|
366
|
+
return False
|
|
367
|
+
|
|
368
|
+
print(f"{YELLOW}Building IPA (App Store)...{NC}\n")
|
|
369
|
+
|
|
370
|
+
# Step 1: Clean the project
|
|
371
|
+
run_flutter_command(["flutter", "clean"], "Cleaning project... ")
|
|
372
|
+
|
|
373
|
+
# Step 2: Get dependencies
|
|
374
|
+
run_flutter_command(["flutter", "pub", "get"], "Getting dependencies... ")
|
|
375
|
+
|
|
376
|
+
# Step 3: Generate localizations
|
|
377
|
+
run_flutter_command(["flutter", "gen-l10n"], "Generating localizations... ")
|
|
378
|
+
|
|
379
|
+
# Step 4: Generate build files
|
|
380
|
+
run_flutter_command(["dart", "run", "build_runner", "build", "--delete-conflicting-outputs"], "Generating build files... ")
|
|
381
|
+
|
|
382
|
+
# Step 5: Update iOS pods
|
|
383
|
+
import os
|
|
384
|
+
current_dir = os.getcwd()
|
|
385
|
+
ios_dir = Path("ios")
|
|
386
|
+
if ios_dir.exists():
|
|
387
|
+
# Set UTF-8 encoding to prevent CocoaPods locale issues
|
|
388
|
+
pod_env = os.environ.copy()
|
|
389
|
+
pod_env['LANG'] = 'en_US.UTF-8'
|
|
390
|
+
pod_env['LC_ALL'] = 'en_US.UTF-8'
|
|
391
|
+
os.chdir("ios")
|
|
392
|
+
run_flutter_command(["pod", "deintegrate"], "Deintegrating pods... ", env=pod_env)
|
|
393
|
+
run_flutter_command(["pod", "install"], "Installing pods... ", env=pod_env)
|
|
394
|
+
os.chdir(current_dir)
|
|
395
|
+
|
|
396
|
+
# Step 6: Build IPA
|
|
397
|
+
build_success = run_flutter_command(
|
|
398
|
+
[
|
|
399
|
+
"flutter", "build", "ipa", "--release",
|
|
400
|
+
"--obfuscate", "--split-debug-info=./",
|
|
401
|
+
"--export-method", "app-store"
|
|
402
|
+
],
|
|
403
|
+
"Building IPA... "
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
if not build_success:
|
|
407
|
+
print(f"\n{RED}✗ IPA build failed!{NC}")
|
|
408
|
+
return False
|
|
409
|
+
|
|
410
|
+
# Step 7: Display IPA file info
|
|
411
|
+
ipa_output = PATHS['ipa_output']
|
|
412
|
+
display_build_size("ipa", ipa_output)
|
|
413
|
+
|
|
414
|
+
# Step 8: Success message and open directory
|
|
415
|
+
print(f"\n{GREEN}✓ IPA built successfully!{NC}")
|
|
416
|
+
print(f"{BLUE}IPA location: {ipa_output}{NC}")
|
|
417
|
+
print(f"\n{YELLOW}Next step: Upload to App Store Connect using Transporter or:{NC}")
|
|
418
|
+
print(f" {GREEN}xcrun altool --upload-app -f <path-to-ipa> -t ios -u <apple-id> -p <app-specific-password>{NC}")
|
|
419
|
+
open_directory(str(ipa_output))
|
|
420
|
+
return True
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
@timer_decorator
|
|
424
|
+
def release_run():
|
|
425
|
+
"""Build & Install Release APK"""
|
|
426
|
+
return common_build_process(
|
|
427
|
+
build_name="Release APK",
|
|
428
|
+
build_command=[
|
|
429
|
+
"flutter", "build", "apk", "--release", "--obfuscate",
|
|
430
|
+
"--target-platform", "android-arm64", "--split-debug-info=./"
|
|
431
|
+
],
|
|
432
|
+
build_description="Building APK... ",
|
|
433
|
+
output_dir=PATHS['apk_output'],
|
|
434
|
+
file_extension="apk",
|
|
435
|
+
install_after=True
|
|
436
|
+
)
|
managers/datetime.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
DateTime Manager - Open device Date & Time settings with auto-time disabled
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
from common_utils import RED, GREEN, YELLOW, BLUE, NC
|
|
9
|
+
from core.state import get_selected_device
|
|
10
|
+
from managers.device import ensure_device_connected, build_adb_cmd
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _run_adb(args, timeout=5):
|
|
14
|
+
return subprocess.run(
|
|
15
|
+
build_adb_cmd(args),
|
|
16
|
+
capture_output=True,
|
|
17
|
+
text=True,
|
|
18
|
+
encoding='utf-8',
|
|
19
|
+
errors='replace',
|
|
20
|
+
timeout=timeout,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def open_datetime_settings():
|
|
25
|
+
"""Disable auto-time and open Date & Time settings on the device."""
|
|
26
|
+
if not ensure_device_connected(
|
|
27
|
+
"No device connected!",
|
|
28
|
+
"Connect a device via USB or use 'fdev mirror --wireless' first",
|
|
29
|
+
):
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
print(f"{GREEN}✓ Device: {get_selected_device()}{NC}")
|
|
33
|
+
|
|
34
|
+
print(f"{BLUE}Disabling automatic date & time...{NC}")
|
|
35
|
+
result = _run_adb(["shell", "settings put global auto_time 0"])
|
|
36
|
+
if result.returncode != 0:
|
|
37
|
+
print(f"{RED}✗ Failed to disable auto-time{NC}")
|
|
38
|
+
print(f"{YELLOW}{result.stderr.strip()}{NC}")
|
|
39
|
+
return False
|
|
40
|
+
print(f"{GREEN}✓ Auto-time disabled{NC}")
|
|
41
|
+
|
|
42
|
+
print(f"{BLUE}Opening Date & Time settings on device...{NC}")
|
|
43
|
+
result = _run_adb(["shell", "am start -a android.settings.DATE_SETTINGS"])
|
|
44
|
+
if result.returncode != 0:
|
|
45
|
+
print(f"{RED}✗ Failed to open settings{NC}")
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
print(f"{GREEN}✓ Settings opened — set the date/time manually on device{NC}")
|
|
49
|
+
return True
|
managers/device.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Device Manager - Device selection and ADB commands
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
from common_utils import RED, GREEN, YELLOW, BLUE, NC
|
|
9
|
+
from core.state import get_selected_device, set_selected_device
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_device_model(serial):
|
|
13
|
+
"""
|
|
14
|
+
Get the device model name for a given device serial.
|
|
15
|
+
Returns: Model name string or empty string if failed
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
result = subprocess.run(
|
|
19
|
+
["adb", "-s", serial, "shell", "getprop", "ro.product.model"],
|
|
20
|
+
capture_output=True,
|
|
21
|
+
text=True,
|
|
22
|
+
encoding='utf-8',
|
|
23
|
+
errors='replace',
|
|
24
|
+
timeout=3
|
|
25
|
+
)
|
|
26
|
+
if result.returncode == 0:
|
|
27
|
+
return result.stdout.strip()
|
|
28
|
+
return ""
|
|
29
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
30
|
+
return ""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_all_connected_devices():
|
|
34
|
+
"""
|
|
35
|
+
Get all connected Android devices/emulators
|
|
36
|
+
Returns: List of device serials, or empty list if none found
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
result = subprocess.run(
|
|
40
|
+
["adb", "devices"],
|
|
41
|
+
capture_output=True,
|
|
42
|
+
text=True,
|
|
43
|
+
encoding='utf-8',
|
|
44
|
+
errors='replace',
|
|
45
|
+
timeout=5
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if result.returncode == 0:
|
|
49
|
+
devices = []
|
|
50
|
+
lines = result.stdout.strip().split('\n')
|
|
51
|
+
# Skip the first line ("List of devices attached")
|
|
52
|
+
for line in lines[1:]:
|
|
53
|
+
if line.strip() and '\tdevice' in line:
|
|
54
|
+
# Extract device serial (first part before tab)
|
|
55
|
+
serial = line.split('\t')[0].strip()
|
|
56
|
+
devices.append(serial)
|
|
57
|
+
return devices
|
|
58
|
+
return []
|
|
59
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def select_device_if_multiple():
|
|
64
|
+
"""
|
|
65
|
+
Check for connected devices and prompt user to select if multiple found.
|
|
66
|
+
Sets the global SELECTED_DEVICE variable.
|
|
67
|
+
Returns: True if device selected/available, False if no devices
|
|
68
|
+
"""
|
|
69
|
+
devices = get_all_connected_devices()
|
|
70
|
+
|
|
71
|
+
if not devices:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
if len(devices) == 1:
|
|
75
|
+
# Only one device, auto-select it
|
|
76
|
+
set_selected_device(devices[0])
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
# Multiple devices found, ask user to select
|
|
80
|
+
print(f"\n{YELLOW}Multiple devices detected:{NC}")
|
|
81
|
+
for i, device in enumerate(devices, 1):
|
|
82
|
+
model = get_device_model(device)
|
|
83
|
+
model_str = f" {model}" if model else ""
|
|
84
|
+
# Check if it's a network device
|
|
85
|
+
if ':' in device:
|
|
86
|
+
print(f" {i}.{model_str} {device} {BLUE}(wireless){NC}")
|
|
87
|
+
else:
|
|
88
|
+
print(f" {i}.{model_str} {device} {GREEN}(USB){NC}")
|
|
89
|
+
|
|
90
|
+
print()
|
|
91
|
+
while True:
|
|
92
|
+
try:
|
|
93
|
+
choice = input(f"Select device (1-{len(devices)}): ").strip()
|
|
94
|
+
index = int(choice) - 1
|
|
95
|
+
if 0 <= index < len(devices):
|
|
96
|
+
set_selected_device(devices[index])
|
|
97
|
+
print(f"{GREEN}✓ Selected: {devices[index]}{NC}\n")
|
|
98
|
+
return True
|
|
99
|
+
else:
|
|
100
|
+
print(f"{RED}Invalid choice. Please enter a number between 1 and {len(devices)}{NC}")
|
|
101
|
+
except ValueError:
|
|
102
|
+
print(f"{RED}Invalid input. Please enter a number{NC}")
|
|
103
|
+
except KeyboardInterrupt:
|
|
104
|
+
print(f"\n{YELLOW}Selection cancelled{NC}")
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def build_adb_cmd(cmd_list, require_device=True):
|
|
109
|
+
"""
|
|
110
|
+
Build ADB command with device selection if needed.
|
|
111
|
+
Parameters:
|
|
112
|
+
cmd_list: List of command parts (e.g., ["shell", "pm", "clear", "package"])
|
|
113
|
+
require_device: If True, adds -s flag when SELECTED_DEVICE is set
|
|
114
|
+
Returns: Complete command list ready for subprocess
|
|
115
|
+
"""
|
|
116
|
+
selected_device = get_selected_device()
|
|
117
|
+
|
|
118
|
+
# Start with "adb"
|
|
119
|
+
adb_cmd = ["adb"]
|
|
120
|
+
|
|
121
|
+
# Add device selection if needed
|
|
122
|
+
if require_device and selected_device:
|
|
123
|
+
adb_cmd.extend(["-s", selected_device])
|
|
124
|
+
|
|
125
|
+
# Add the rest of the command
|
|
126
|
+
adb_cmd.extend(cmd_list)
|
|
127
|
+
|
|
128
|
+
return adb_cmd
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def ensure_device_connected(error_message=None, additional_help=None):
|
|
132
|
+
"""
|
|
133
|
+
Wrapper function to check and select Android device
|
|
134
|
+
|
|
135
|
+
Parameters:
|
|
136
|
+
error_message: Custom error message (default: "No Android device connected!")
|
|
137
|
+
additional_help: Additional help text to display after error
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Boolean - True if device selected/available, False otherwise
|
|
141
|
+
"""
|
|
142
|
+
if not select_device_if_multiple():
|
|
143
|
+
# Use custom error message or default
|
|
144
|
+
if error_message:
|
|
145
|
+
print(f"{RED}Error: {error_message}{NC}")
|
|
146
|
+
else:
|
|
147
|
+
print(f"{RED}Error: No Android device connected!{NC}")
|
|
148
|
+
|
|
149
|
+
# Display additional help if provided
|
|
150
|
+
if additional_help:
|
|
151
|
+
print(f"{YELLOW}{additional_help}{NC}")
|
|
152
|
+
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def get_usb_devices():
|
|
159
|
+
"""
|
|
160
|
+
Get only USB connected devices (excludes wireless/network devices)
|
|
161
|
+
Returns: List of USB device serials
|
|
162
|
+
"""
|
|
163
|
+
all_devices = get_all_connected_devices()
|
|
164
|
+
# Filter out wireless devices (they contain ':' in serial like '192.168.0.131:5555')
|
|
165
|
+
usb_devices = [d for d in all_devices if ':' not in d]
|
|
166
|
+
return usb_devices
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def select_usb_device():
|
|
170
|
+
"""
|
|
171
|
+
Prompt user to select a USB device if multiple are connected.
|
|
172
|
+
Sets the global SELECTED_DEVICE variable.
|
|
173
|
+
Returns: True if device selected/available, False if no devices
|
|
174
|
+
"""
|
|
175
|
+
devices = get_usb_devices()
|
|
176
|
+
|
|
177
|
+
if not devices:
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
if len(devices) == 1:
|
|
181
|
+
# Only one USB device, auto-select it
|
|
182
|
+
set_selected_device(devices[0])
|
|
183
|
+
return True
|
|
184
|
+
|
|
185
|
+
# Multiple USB devices found, ask user to select
|
|
186
|
+
print(f"\n{YELLOW}Multiple USB devices detected:{NC}")
|
|
187
|
+
for i, device in enumerate(devices, 1):
|
|
188
|
+
model = get_device_model(device)
|
|
189
|
+
model_str = f" {model}" if model else ""
|
|
190
|
+
print(f" {i}.{model_str} {device} {GREEN}(USB){NC}")
|
|
191
|
+
|
|
192
|
+
print()
|
|
193
|
+
while True:
|
|
194
|
+
try:
|
|
195
|
+
choice = input(f"Select USB device (1-{len(devices)}): ").strip()
|
|
196
|
+
index = int(choice) - 1
|
|
197
|
+
if 0 <= index < len(devices):
|
|
198
|
+
set_selected_device(devices[index])
|
|
199
|
+
print(f"{GREEN}✓ Selected: {devices[index]}{NC}\n")
|
|
200
|
+
return True
|
|
201
|
+
else:
|
|
202
|
+
print(f"{RED}Invalid choice. Please enter a number between 1 and {len(devices)}{NC}")
|
|
203
|
+
except ValueError:
|
|
204
|
+
print(f"{RED}Invalid input. Please enter a number{NC}")
|
|
205
|
+
except KeyboardInterrupt:
|
|
206
|
+
print(f"\n{YELLOW}Selection cancelled{NC}")
|
|
207
|
+
return False
|