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.
managers/app.py ADDED
@@ -0,0 +1,541 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ App Manager - Install, uninstall, clear data functions
4
+ """
5
+
6
+ import subprocess
7
+
8
+ from common_utils import RED, GREEN, YELLOW, BLUE, NC
9
+ from core.constants import PATTERNS, PATHS
10
+ from managers.device import (
11
+ get_all_connected_devices,
12
+ ensure_device_connected,
13
+ build_adb_cmd,
14
+ )
15
+ from managers.build import run_flutter_command, get_package_name
16
+
17
+
18
+ def is_flutter_project_root():
19
+ """
20
+ Check if current directory is a Flutter project root.
21
+ Returns True if pubspec.yaml exists in current directory.
22
+ """
23
+ return PATHS['pubspec'].exists()
24
+
25
+
26
+ def is_app_installed(package_name):
27
+ """
28
+ Check if an app with the given package name is installed on the Android device.
29
+ Uses 'adb shell pm path' first, then falls back to 'pm list packages' to detect
30
+ ghost packages (e.g. packages with missing APK but existing database entry that
31
+ cause INSTALL_FAILED_UPDATE_INCOMPATIBLE errors).
32
+
33
+ Returns: True if installed, False otherwise
34
+ """
35
+ try:
36
+ # Primary check: adb shell pm path (detects packages with APK on disk)
37
+ result = subprocess.run(
38
+ build_adb_cmd(["shell", "pm", "path", package_name]),
39
+ capture_output=True,
40
+ text=True,
41
+ encoding='utf-8',
42
+ errors='replace',
43
+ timeout=5
44
+ )
45
+ # If app is installed, output will be like: "package:/data/app/..."
46
+ # If not installed, output will be empty or error
47
+ if result.returncode == 0 and result.stdout.strip().startswith("package:"):
48
+ return True
49
+
50
+ # Fallback: pm list packages (detects ghost packages where APK is missing
51
+ # but package entry still exists in the system database)
52
+ result = subprocess.run(
53
+ build_adb_cmd(["shell", "pm", "list", "packages", package_name]),
54
+ capture_output=True,
55
+ text=True,
56
+ encoding='utf-8',
57
+ errors='replace',
58
+ timeout=5
59
+ )
60
+ if result.returncode == 0 and f"package:{package_name}" in result.stdout:
61
+ return True
62
+
63
+ return False
64
+ except (subprocess.TimeoutExpired, Exception):
65
+ return False
66
+
67
+
68
+ def get_current_foreground_app():
69
+ """
70
+ Get the package name of currently running foreground app
71
+ Returns: (platform, package_name) tuple or (None, None) if failed
72
+ """
73
+ # Check for iOS simulator first
74
+ try:
75
+ ios_check = subprocess.run(
76
+ ["xcrun", "simctl", "list", "devices", "booted"],
77
+ capture_output=True,
78
+ text=True,
79
+ encoding='utf-8',
80
+ errors='replace',
81
+ timeout=5
82
+ )
83
+ ios_connected = ios_check.returncode == 0 and "Booted" in ios_check.stdout
84
+
85
+ if ios_connected:
86
+ # Get foreground app on iOS simulator
87
+ result = subprocess.run(
88
+ ["xcrun", "simctl", "spawn", "booted", "launchctl", "list"],
89
+ capture_output=True,
90
+ text=True,
91
+ encoding='utf-8',
92
+ errors='replace',
93
+ timeout=5
94
+ )
95
+
96
+ if result.returncode == 0:
97
+ # Get frontmost app using AppleScript (macOS only)
98
+ applescript = '''tell application "System Events"
99
+ set frontApp to name of first application process whose frontmost is true
100
+ return frontApp
101
+ end tell'''
102
+
103
+ try:
104
+ front_result = subprocess.run(
105
+ ["osascript", "-e", applescript],
106
+ capture_output=True,
107
+ text=True,
108
+ encoding='utf-8',
109
+ errors='replace',
110
+ timeout=3
111
+ )
112
+
113
+ if front_result.returncode == 0:
114
+ app_name = front_result.stdout.strip()
115
+ if app_name and app_name.lower() == "simulator":
116
+ # Try to get bundle ID from iOS simulator
117
+ # For now, return a placeholder - iOS doesn't easily expose current app
118
+ print(f"{YELLOW}iOS Simulator is running but cannot automatically detect current app{NC}")
119
+ print(f"{YELLOW}Please use 'fdev uninstall' to manually uninstall{NC}")
120
+ return ("ios", None)
121
+ except Exception:
122
+ pass
123
+
124
+ except (FileNotFoundError, subprocess.TimeoutExpired):
125
+ pass
126
+
127
+ # Check for Android device
128
+ try:
129
+ devices = get_all_connected_devices()
130
+ android_connected = len(devices) > 0
131
+
132
+ if android_connected:
133
+ # Select device if multiple connected
134
+ if not ensure_device_connected():
135
+ return (None, None)
136
+
137
+ # Get current foreground app on Android
138
+ result = subprocess.run(
139
+ build_adb_cmd(["shell", "dumpsys", "window", "windows"]),
140
+ capture_output=True,
141
+ text=True,
142
+ encoding='utf-8',
143
+ errors='replace',
144
+ timeout=5
145
+ )
146
+
147
+ if result.returncode == 0:
148
+ # Parse output to find current focused app
149
+ for line in result.stdout.split('\n'):
150
+ if 'mCurrentFocus' in line or 'mFocusedApp' in line or 'mFocusedWindow' in line:
151
+ # Extract package name using regex
152
+ match = PATTERNS['foreground_app'].search(line)
153
+ if match:
154
+ package_name = match.group(1)
155
+ return ("android", package_name)
156
+
157
+ # Fallback: Try using dumpsys activity
158
+ result2 = subprocess.run(
159
+ build_adb_cmd(["shell", "dumpsys", "activity", "activities"]),
160
+ capture_output=True,
161
+ text=True,
162
+ encoding='utf-8',
163
+ errors='replace',
164
+ timeout=5
165
+ )
166
+
167
+ if result2.returncode == 0:
168
+ # Try multiple patterns for different Android versions
169
+ # Pattern 1: mResumedActivity (older Android)
170
+ match = PATTERNS['resumed_activity'].search(result2.stdout)
171
+ if match:
172
+ package_name = match.group(1)
173
+ return ("android", package_name)
174
+
175
+ # Pattern 2: topResumedActivity or ResumedActivity (newer Android)
176
+ match = PATTERNS['top_resumed_activity'].search(result2.stdout)
177
+ if match:
178
+ package_name = match.group(1)
179
+ return ("android", package_name)
180
+
181
+ except (FileNotFoundError, subprocess.TimeoutExpired):
182
+ pass
183
+
184
+ return (None, None)
185
+
186
+
187
+ def clear_app_data():
188
+ """
189
+ Automatically clear data of currently running foreground app
190
+ Works for both Android and iOS on all operating systems (macOS, Linux, Windows)
191
+ """
192
+ print(f"{YELLOW}Clearing app data for foreground app...{NC}\n")
193
+
194
+ # Get current foreground app
195
+ platform_type, package_name = get_current_foreground_app()
196
+
197
+ if not platform_type:
198
+ print(f"{RED}Error: No device/simulator detected!{NC}")
199
+ print(f"{YELLOW}Please connect an Android device or start an iOS simulator{NC}")
200
+ return False
201
+
202
+ if not package_name:
203
+ print(f"{RED}Error: Could not detect foreground app{NC}")
204
+ print(f"{YELLOW}Make sure an app is running in the foreground{NC}")
205
+ return False
206
+
207
+ print(f"{BLUE}Platform: {platform_type.upper()}{NC}")
208
+ print(f"{BLUE}App: {package_name}{NC}\n")
209
+
210
+ # Ask for confirmation
211
+ user_input = input(f"Clear data for {GREEN}{package_name}{NC}? (Y/n): ")
212
+ if user_input.lower() == 'n':
213
+ print(f"{YELLOW}Operation cancelled{NC}")
214
+ return False
215
+
216
+ # Clear data based on platform
217
+ if platform_type == "android":
218
+ # Clear app data on Android
219
+ success = run_flutter_command(
220
+ build_adb_cmd(["shell", "pm", "clear", package_name]),
221
+ f"Clearing app data... "
222
+ )
223
+
224
+ if success:
225
+ print(f"\n{GREEN}✓ App data cleared successfully!{NC}")
226
+ print(f"{BLUE}You can now relaunch the app from the device{NC}")
227
+ else:
228
+ print(f"\n{RED}✗ Failed to clear app data!{NC}")
229
+ print(f"{YELLOW}Make sure the app package name is correct{NC}")
230
+
231
+ return success
232
+
233
+ elif platform_type == "ios":
234
+ # iOS doesn't support direct app data clearing via command line
235
+ # We need to uninstall and reinstall the app
236
+ print(f"{YELLOW}iOS Note: iOS doesn't support clearing app data directly{NC}")
237
+ print(f"{YELLOW}Options:{NC}")
238
+ print(f"{BLUE} 1. Reset app data from device: Settings → General → iPhone Storage → [App] → Delete App{NC}")
239
+ print(f"{BLUE} 2. Uninstall and reinstall the app using 'fdev uninstall' then 'fdev install'{NC}")
240
+ print(f"{BLUE} 3. On simulator: Device → Erase All Content and Settings{NC}")
241
+
242
+ # Offer to uninstall
243
+ user_choice = input(f"\nWould you like to uninstall the app instead? (y/N): ")
244
+ if user_choice.lower() == 'y':
245
+ return uninstall_app()
246
+
247
+ return False
248
+
249
+ return False
250
+
251
+
252
+ def _uninstall_from_project_root():
253
+ """
254
+ Uninstall app using project files (build.gradle / Info.plist).
255
+ Called when running from Flutter project root directory.
256
+ """
257
+ # Check which device is connected
258
+ try:
259
+ ios_check = subprocess.run(
260
+ ["xcrun", "simctl", "list", "devices", "booted"],
261
+ capture_output=True,
262
+ text=True,
263
+ encoding='utf-8',
264
+ errors='replace',
265
+ timeout=5
266
+ )
267
+ ios_connected = ios_check.returncode == 0 and "Booted" in ios_check.stdout
268
+ except (FileNotFoundError, subprocess.TimeoutExpired):
269
+ ios_connected = False
270
+
271
+ devices = get_all_connected_devices()
272
+ android_connected = len(devices) > 0
273
+
274
+ # Handle iOS Simulator
275
+ if ios_connected and not android_connected:
276
+ print(f"{BLUE}iOS Simulator detected{NC}")
277
+
278
+ info_plist_path = PATHS['info_plist']
279
+ if not info_plist_path.exists():
280
+ print(f"{RED}Error: Info.plist not found{NC}")
281
+ return False
282
+
283
+ try:
284
+ result = subprocess.run(
285
+ ["/usr/libexec/PlistBuddy", "-c", "Print CFBundleIdentifier", str(info_plist_path)],
286
+ capture_output=True,
287
+ text=True,
288
+ encoding='utf-8',
289
+ errors='replace',
290
+ check=True
291
+ )
292
+ bundle_id = result.stdout.strip()
293
+
294
+ if not bundle_id or bundle_id.startswith("$("):
295
+ bundle_id = get_package_name()
296
+ if not bundle_id:
297
+ print(f"{RED}Cannot get bundle identifier{NC}")
298
+ return False
299
+
300
+ print(f"{BLUE}Bundle ID: {bundle_id}{NC}")
301
+
302
+ success = run_flutter_command(
303
+ ["xcrun", "simctl", "uninstall", "booted", bundle_id],
304
+ "Uninstalling from iOS simulator... "
305
+ )
306
+
307
+ if success:
308
+ print(f"\n{GREEN}✓ App uninstalled successfully from iOS!{NC}")
309
+ else:
310
+ print(f"\n{RED}✗ Failed to uninstall app from iOS!{NC}")
311
+ return success
312
+
313
+ except subprocess.CalledProcessError as e:
314
+ print(f"{RED}Error getting bundle identifier: {e}{NC}")
315
+ return False
316
+
317
+ # Handle Android Device
318
+ elif android_connected:
319
+ print(f"{BLUE}Android device detected{NC}")
320
+
321
+ if not ensure_device_connected("Device selection failed!"):
322
+ return False
323
+
324
+ package_name = get_package_name()
325
+ if not package_name:
326
+ print(f"{RED}Cannot proceed without package name{NC}")
327
+ return False
328
+
329
+ print(f"{BLUE}Package name: {package_name}{NC}")
330
+
331
+ # Check if the app is installed on the device
332
+ if is_app_installed(package_name):
333
+ print(f"{GREEN}✓ App is installed on device{NC}\n")
334
+ success = run_flutter_command(
335
+ build_adb_cmd(["uninstall", package_name]),
336
+ "Uninstalling from Android... "
337
+ )
338
+
339
+ if success:
340
+ print(f"\n{GREEN}✓ App uninstalled successfully from Android!{NC}")
341
+ else:
342
+ print(f"\n{RED}✗ Failed to uninstall app from Android!{NC}")
343
+ return success
344
+ else:
345
+ # App not detected by pm - but may still exist as a ghost package
346
+ # (e.g. missing APK but database entry remains, causing
347
+ # INSTALL_FAILED_UPDATE_INCOMPATIBLE). Try adb uninstall first
348
+ # since it can remove ghost entries that pm path/list can't see.
349
+ print(f"{YELLOW}⚠️ App not detected on device, attempting uninstall anyway...{NC}\n")
350
+
351
+ success = run_flutter_command(
352
+ build_adb_cmd(["uninstall", package_name]),
353
+ "Uninstalling from Android... "
354
+ )
355
+
356
+ if success:
357
+ print(f"\n{GREEN}✓ App uninstalled successfully from Android!{NC}")
358
+ return True
359
+
360
+ # adb uninstall also failed - fall back to foreground app detection
361
+ print(f"\n{YELLOW}⚠️ Could not uninstall {package_name}{NC}")
362
+ print(f"{YELLOW} Detecting foreground app instead...{NC}\n")
363
+
364
+ _, foreground_package = get_current_foreground_app()
365
+
366
+ if not foreground_package:
367
+ print(f"{RED}Error: Could not detect foreground app{NC}")
368
+ print(f"{YELLOW}Make sure an app is running in the foreground{NC}")
369
+ return False
370
+
371
+ print(f"{BLUE}Foreground App: {foreground_package}{NC}\n")
372
+
373
+ # Ask for confirmation before uninstalling foreground app
374
+ print(f"{RED}⚠️ WARNING: This will UNINSTALL the foreground app!{NC}")
375
+ user_input = input(f"Uninstall {GREEN}{foreground_package}{NC}? (Y/n): ")
376
+ if user_input.lower() == 'n':
377
+ print(f"{YELLOW}Operation cancelled{NC}")
378
+ return False
379
+
380
+ success = run_flutter_command(
381
+ build_adb_cmd(["uninstall", foreground_package]),
382
+ "Uninstalling foreground app... "
383
+ )
384
+
385
+ if success:
386
+ print(f"\n{GREEN}✓ Foreground app uninstalled successfully!{NC}")
387
+ else:
388
+ print(f"\n{RED}✗ Failed to uninstall foreground app!{NC}")
389
+ return success
390
+
391
+ else:
392
+ print(f"{RED}No device/simulator detected!{NC}")
393
+ print(f"{YELLOW}Please connect an Android device or start an iOS simulator{NC}")
394
+ return False
395
+
396
+
397
+ def _uninstall_foreground_app():
398
+ """
399
+ Uninstall the currently running foreground app.
400
+ Called when NOT in Flutter project root directory.
401
+ Shows extra warnings and requires confirmation.
402
+ """
403
+ print(f"{YELLOW}⚠️ Not in Flutter project directory{NC}")
404
+ print(f"{YELLOW} Detecting foreground app instead...{NC}\n")
405
+
406
+ # Get current foreground app
407
+ platform_type, package_name = get_current_foreground_app()
408
+
409
+ if not platform_type:
410
+ print(f"{RED}Error: No device/simulator detected!{NC}")
411
+ print(f"{YELLOW}Please connect an Android device or start an iOS simulator{NC}")
412
+ return False
413
+
414
+ if not package_name:
415
+ print(f"{RED}Error: Could not detect foreground app{NC}")
416
+ print(f"{YELLOW}Make sure an app is running in the foreground{NC}")
417
+ return False
418
+
419
+ # Show app info with warning
420
+ print(f"{BLUE}Platform: {platform_type.upper()}{NC}")
421
+ print(f"{BLUE}Detected App: {package_name}{NC}\n")
422
+
423
+ # Extra warning for foreground app uninstall
424
+ print(f"{RED}⚠️ WARNING: This will UNINSTALL the app completely!{NC}")
425
+ print(f"{RED} All app data will be permanently deleted.{NC}\n")
426
+
427
+ # Simple Y/n confirmation
428
+ user_input = input(f"Uninstall {GREEN}{package_name}{NC}? (Y/n): ")
429
+ if user_input.lower() == 'n':
430
+ print(f"{YELLOW}Operation cancelled{NC}")
431
+ return False
432
+
433
+ # Proceed with uninstall
434
+ if platform_type == "android":
435
+ success = run_flutter_command(
436
+ build_adb_cmd(["uninstall", package_name]),
437
+ "Uninstalling from Android... "
438
+ )
439
+
440
+ if success:
441
+ print(f"\n{GREEN}✓ App uninstalled successfully from Android!{NC}")
442
+ else:
443
+ print(f"\n{RED}✗ Failed to uninstall app from Android!{NC}")
444
+ return success
445
+
446
+ elif platform_type == "ios":
447
+ success = run_flutter_command(
448
+ ["xcrun", "simctl", "uninstall", "booted", package_name],
449
+ "Uninstalling from iOS simulator... "
450
+ )
451
+
452
+ if success:
453
+ print(f"\n{GREEN}✓ App uninstalled successfully from iOS!{NC}")
454
+ else:
455
+ print(f"\n{RED}✗ Failed to uninstall app from iOS!{NC}")
456
+ return success
457
+
458
+ return False
459
+
460
+
461
+ def uninstall_app():
462
+ """
463
+ Uninstall the app from connected device (Android/iOS).
464
+
465
+ If running from Flutter project root: uses build.gradle/Info.plist for package name.
466
+ If NOT in project root: detects foreground app with extra safety confirmation.
467
+ """
468
+ print(f"{YELLOW}Uninstalling app from device...{NC}\n")
469
+
470
+ # Check if we're in a Flutter project root
471
+ if is_flutter_project_root():
472
+ print(f"{GREEN}✓ Flutter project detected{NC}\n")
473
+ return _uninstall_from_project_root()
474
+ else:
475
+ return _uninstall_foreground_app()
476
+
477
+
478
+ def install_apk():
479
+ """
480
+ Installs the built APK on a connected Android device using adb.
481
+ Tries to install arm64-v8a APK first if available.
482
+ Handles signature mismatch by uninstalling existing app first.
483
+ Automatically launches the app after successful installation.
484
+ """
485
+ # Select device if multiple connected
486
+ if not ensure_device_connected():
487
+ return False
488
+
489
+ apk_dir = PATHS['apk_output']
490
+ apk_files = [str(f) for f in apk_dir.glob("*.apk")] if apk_dir.exists() else []
491
+ if not apk_files:
492
+ print(f"{RED}No APK found to install!{NC}")
493
+ return False
494
+
495
+ # Try to install arm64-v8a APK first, otherwise use the first apk
496
+ target_apk = None
497
+ for apk_path in apk_files:
498
+ if "arm64-v8a" in apk_path:
499
+ target_apk = apk_path
500
+ break
501
+ if not target_apk:
502
+ target_apk = apk_files[0]
503
+
504
+ print(f"{YELLOW}Installing {target_apk}...{NC}")
505
+
506
+ # First try normal install
507
+ success = run_flutter_command(build_adb_cmd(["install", "-r", target_apk]), "Installing on device... ")
508
+
509
+ if not success:
510
+ # Get package name dynamically
511
+ package_name = get_package_name()
512
+ if not package_name:
513
+ print(f"{RED}Cannot proceed without package name{NC}")
514
+ return False
515
+
516
+ print(f"{YELLOW}Installation failed, trying to uninstall existing app first...{NC}")
517
+ print(f"{BLUE}Package name: {package_name}{NC}")
518
+
519
+ # Try to uninstall existing app first using dynamic package name
520
+ run_flutter_command(build_adb_cmd(["uninstall", package_name]), "Uninstalling existing app... ")
521
+ # Then try to install again
522
+ success = run_flutter_command(build_adb_cmd(["install", target_apk]), "Reinstalling on device... ")
523
+
524
+ # Launch app after successful installation
525
+ if success:
526
+ package_name = get_package_name()
527
+ if package_name:
528
+ print(f"{YELLOW}Launching app...{NC}")
529
+ # Launch the app using monkey command (works on all devices)
530
+ launch_success = run_flutter_command(
531
+ build_adb_cmd(["shell", "monkey", "-p", package_name, "-c", "android.intent.category.LAUNCHER", "1"]),
532
+ "Starting app on device... "
533
+ )
534
+ if launch_success:
535
+ print(f"{GREEN}✓ App launched successfully!{NC}")
536
+ else:
537
+ print(f"{YELLOW}App installed but failed to launch. Please open manually.{NC}")
538
+ else:
539
+ print(f"{YELLOW}App installed but package name not found for launching.{NC}")
540
+
541
+ return success