|
@@ -0,0 +1,360 @@
|
|
|
|
|
+#!/usr/bin/env python3
|
|
|
|
|
+"""
|
|
|
|
|
+ZenTap Android Emulator Launcher
|
|
|
|
|
+Starts the ZenTap app in an Android emulator with selectable resolution and options.
|
|
|
|
|
+"""
|
|
|
|
|
+
|
|
|
|
|
+import os
|
|
|
|
|
+import sys
|
|
|
|
|
+import subprocess
|
|
|
|
|
+import time
|
|
|
|
|
+import argparse
|
|
|
|
|
+from pathlib import Path
|
|
|
|
|
+
|
|
|
|
|
+# Common resolutions and their names
|
|
|
|
|
+RESOLUTIONS = {
|
|
|
|
|
+ '1': {'name': 'Phone Small (360x640)', 'width': 360, 'height': 640, 'density': 160},
|
|
|
|
|
+ '2': {'name': 'Phone Medium (411x731)', 'width': 411, 'height': 731, 'density': 160},
|
|
|
|
|
+ '3': {'name': 'Phone Large (414x896)', 'width': 414, 'height': 896, 'density': 160},
|
|
|
|
|
+ '4': {'name': 'Tablet 7" (600x960)', 'width': 600, 'height': 960, 'density': 160},
|
|
|
|
|
+ '5': {'name': 'Tablet 10" (800x1280)', 'width': 800, 'height': 1280, 'density': 160},
|
|
|
|
|
+ '6': {'name': 'Foldable (673x841)', 'width': 673, 'height': 841, 'density': 160},
|
|
|
|
|
+ '7': {'name': 'Desktop (1920x1080)', 'width': 1920, 'height': 1080, 'density': 160},
|
|
|
|
|
+ '8': {'name': 'Custom (specify)', 'width': None, 'height': None, 'density': 160}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+class ZenTapEmulatorLauncher:
|
|
|
|
|
+ def __init__(self):
|
|
|
|
|
+ self.project_root = Path(__file__).parent.parent
|
|
|
|
|
+ self.android_sdk_root = self.find_android_sdk()
|
|
|
|
|
+ self.emulator_path = self.find_emulator_path()
|
|
|
|
|
+
|
|
|
|
|
+ def find_android_sdk(self):
|
|
|
|
|
+ """Find Android SDK root directory"""
|
|
|
|
|
+ possible_paths = [
|
|
|
|
|
+ os.environ.get('ANDROID_SDK_ROOT'),
|
|
|
|
|
+ os.environ.get('ANDROID_HOME'),
|
|
|
|
|
+ Path.home() / 'Android' / 'Sdk',
|
|
|
|
|
+ Path.home() / 'Library' / 'Android' / 'sdk', # macOS
|
|
|
|
|
+ Path('/opt/android-sdk'),
|
|
|
|
|
+ Path('/usr/local/android-sdk')
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ for path in possible_paths:
|
|
|
|
|
+ if path and Path(path).exists():
|
|
|
|
|
+ return Path(path)
|
|
|
|
|
+
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ def find_emulator_path(self):
|
|
|
|
|
+ """Find emulator executable"""
|
|
|
|
|
+ if self.android_sdk_root:
|
|
|
|
|
+ emulator_path = self.android_sdk_root / 'emulator' / 'emulator'
|
|
|
|
|
+ if emulator_path.exists():
|
|
|
|
|
+ return emulator_path
|
|
|
|
|
+
|
|
|
|
|
+ # Try to find in PATH
|
|
|
|
|
+ try:
|
|
|
|
|
+ result = subprocess.run(['which', 'emulator'], capture_output=True, text=True)
|
|
|
|
|
+ if result.returncode == 0:
|
|
|
|
|
+ return Path(result.stdout.strip())
|
|
|
|
|
+ except:
|
|
|
|
|
+ pass
|
|
|
|
|
+
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ def get_available_avds(self):
|
|
|
|
|
+ """Get list of available Android Virtual Devices"""
|
|
|
|
|
+ if not self.emulator_path:
|
|
|
|
|
+ return []
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ result = subprocess.run([str(self.emulator_path), '-list-avds'],
|
|
|
|
|
+ capture_output=True, text=True)
|
|
|
|
|
+ if result.returncode == 0:
|
|
|
|
|
+ return [line.strip() for line in result.stdout.strip().split('\n') if line.strip()]
|
|
|
|
|
+ except:
|
|
|
|
|
+ pass
|
|
|
|
|
+
|
|
|
|
|
+ return []
|
|
|
|
|
+
|
|
|
|
|
+ def create_avd_if_needed(self, avd_name, resolution_info):
|
|
|
|
|
+ """Create a new AVD if it doesn't exist"""
|
|
|
|
|
+ avds = self.get_available_avds()
|
|
|
|
|
+ if avd_name in avds:
|
|
|
|
|
+ return True
|
|
|
|
|
+
|
|
|
|
|
+ print(f"Creating new AVD: {avd_name}")
|
|
|
|
|
+
|
|
|
|
|
+ # Use avdmanager to create AVD
|
|
|
|
|
+ if self.android_sdk_root:
|
|
|
|
|
+ avdmanager_path = self.android_sdk_root / 'cmdline-tools' / 'latest' / 'bin' / 'avdmanager'
|
|
|
|
|
+ if not avdmanager_path.exists():
|
|
|
|
|
+ avdmanager_path = self.android_sdk_root / 'tools' / 'bin' / 'avdmanager'
|
|
|
|
|
+
|
|
|
|
|
+ if avdmanager_path.exists():
|
|
|
|
|
+ try:
|
|
|
|
|
+ # Create AVD with specified resolution
|
|
|
|
|
+ cmd = [
|
|
|
|
|
+ str(avdmanager_path), 'create', 'avd',
|
|
|
|
|
+ '-n', avd_name,
|
|
|
|
|
+ '-k', 'system-images;android-34;google_apis;x86_64',
|
|
|
|
|
+ '-d', 'pixel',
|
|
|
|
|
+ '--force'
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ result = subprocess.run(cmd, input='no\n', text=True, capture_output=True)
|
|
|
|
|
+ return result.returncode == 0
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ print(f"Failed to create AVD: {e}")
|
|
|
|
|
+
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ def wait_for_emulator(self, max_wait=120):
|
|
|
|
|
+ """Wait for emulator to boot completely"""
|
|
|
|
|
+ print("Waiting for emulator to boot...")
|
|
|
|
|
+ start_time = time.time()
|
|
|
|
|
+
|
|
|
|
|
+ while time.time() - start_time < max_wait:
|
|
|
|
|
+ try:
|
|
|
|
|
+ result = subprocess.run(['adb', 'shell', 'getprop', 'sys.boot_completed'],
|
|
|
|
|
+ capture_output=True, text=True, timeout=5)
|
|
|
|
|
+ if result.returncode == 0 and result.stdout.strip() == '1':
|
|
|
|
|
+ print("Emulator is ready!")
|
|
|
|
|
+ return True
|
|
|
|
|
+ except:
|
|
|
|
|
+ pass
|
|
|
|
|
+
|
|
|
|
|
+ print(".", end="", flush=True)
|
|
|
|
|
+ time.sleep(2)
|
|
|
|
|
+
|
|
|
|
|
+ print("\nTimeout waiting for emulator to boot")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ def launch_emulator(self, avd_name, resolution_info, no_window=False, cold_boot=False):
|
|
|
|
|
+ """Launch the emulator with specified options"""
|
|
|
|
|
+ if not self.emulator_path:
|
|
|
|
|
+ print("Error: Emulator not found!")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ cmd = [str(self.emulator_path), '-avd', avd_name]
|
|
|
|
|
+
|
|
|
|
|
+ # Add resolution parameters if specified
|
|
|
|
|
+ if resolution_info['width'] and resolution_info['height']:
|
|
|
|
|
+ cmd.extend(['-skin', f"{resolution_info['width']}x{resolution_info['height']}"])
|
|
|
|
|
+
|
|
|
|
|
+ # Add optional parameters
|
|
|
|
|
+ if no_window:
|
|
|
|
|
+ cmd.append('-no-window')
|
|
|
|
|
+ if cold_boot:
|
|
|
|
|
+ cmd.append('-wipe-data')
|
|
|
|
|
+
|
|
|
|
|
+ # Performance optimizations
|
|
|
|
|
+ cmd.extend([
|
|
|
|
|
+ '-gpu', 'host',
|
|
|
|
|
+ '-no-boot-anim',
|
|
|
|
|
+ '-memory', '2048'
|
|
|
|
|
+ ])
|
|
|
|
|
+
|
|
|
|
|
+ print(f"Starting emulator with command: {' '.join(cmd)}")
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ # Start emulator in background
|
|
|
|
|
+ process = subprocess.Popen(cmd)
|
|
|
|
|
+ time.sleep(5) # Give emulator time to start
|
|
|
|
|
+
|
|
|
|
|
+ if process.poll() is None: # Process is still running
|
|
|
|
|
+ return True
|
|
|
|
|
+ else:
|
|
|
|
|
+ print("Emulator failed to start")
|
|
|
|
|
+ return False
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ print(f"Failed to start emulator: {e}")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ def build_and_install_app(self, build_mode='debug'):
|
|
|
|
|
+ """Build and install the ZenTap app"""
|
|
|
|
|
+ print(f"Building app in {build_mode} mode...")
|
|
|
|
|
+
|
|
|
|
|
+ # Change to project directory
|
|
|
|
|
+ os.chdir(self.project_root)
|
|
|
|
|
+
|
|
|
|
|
+ # Build the app
|
|
|
|
|
+ build_cmd = ['flutter', 'build', 'apk']
|
|
|
|
|
+ if build_mode == 'debug':
|
|
|
|
|
+ build_cmd.append('--debug')
|
|
|
|
|
+ elif build_mode == 'release':
|
|
|
|
|
+ build_cmd.append('--release')
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ result = subprocess.run(build_cmd, check=True)
|
|
|
|
|
+ print("Build successful!")
|
|
|
|
|
+ except subprocess.CalledProcessError:
|
|
|
|
|
+ print("Build failed!")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ # Install the app
|
|
|
|
|
+ print("Installing app on emulator...")
|
|
|
|
|
+ try:
|
|
|
|
|
+ if build_mode == 'debug':
|
|
|
|
|
+ apk_path = 'build/app/outputs/flutter-apk/app-debug.apk'
|
|
|
|
|
+ else:
|
|
|
|
|
+ apk_path = 'build/app/outputs/flutter-apk/app-release.apk'
|
|
|
|
|
+
|
|
|
|
|
+ subprocess.run(['adb', 'install', '-r', apk_path], check=True)
|
|
|
|
|
+ print("App installed successfully!")
|
|
|
|
|
+ return True
|
|
|
|
|
+ except subprocess.CalledProcessError:
|
|
|
|
|
+ print("Installation failed!")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ def launch_app(self):
|
|
|
|
|
+ """Launch the ZenTap app"""
|
|
|
|
|
+ print("Launching ZenTap...")
|
|
|
|
|
+ try:
|
|
|
|
|
+ subprocess.run([
|
|
|
|
|
+ 'adb', 'shell', 'am', 'start',
|
|
|
|
|
+ '-n', 'hu.fsociety.zentap/hu.fsociety.zentap.MainActivity'
|
|
|
|
|
+ ], check=True)
|
|
|
|
|
+ print("App launched successfully!")
|
|
|
|
|
+ return True
|
|
|
|
|
+ except subprocess.CalledProcessError:
|
|
|
|
|
+ print("Failed to launch app!")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ def interactive_mode(self):
|
|
|
|
|
+ """Run in interactive mode with menu selection"""
|
|
|
|
|
+ print("=== ZenTap Android Emulator Launcher ===\n")
|
|
|
|
|
+
|
|
|
|
|
+ # Check prerequisites
|
|
|
|
|
+ if not self.emulator_path:
|
|
|
|
|
+ print("Error: Android emulator not found!")
|
|
|
|
|
+ print("Please install Android SDK and set ANDROID_SDK_ROOT environment variable.")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ # Show available AVDs
|
|
|
|
|
+ avds = self.get_available_avds()
|
|
|
|
|
+ print("Available Android Virtual Devices:")
|
|
|
|
|
+ if avds:
|
|
|
|
|
+ for i, avd in enumerate(avds, 1):
|
|
|
|
|
+ print(f" {i}. {avd}")
|
|
|
|
|
+ else:
|
|
|
|
|
+ print(" No AVDs found. Will create a new one.")
|
|
|
|
|
+
|
|
|
|
|
+ print("\nSelect resolution:")
|
|
|
|
|
+ for key, info in RESOLUTIONS.items():
|
|
|
|
|
+ print(f" {key}. {info['name']}")
|
|
|
|
|
+
|
|
|
|
|
+ # Get user input
|
|
|
|
|
+ resolution_choice = input(f"\nChoose resolution (1-{len(RESOLUTIONS)}): ").strip()
|
|
|
|
|
+ if resolution_choice not in RESOLUTIONS:
|
|
|
|
|
+ print("Invalid choice!")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ resolution_info = RESOLUTIONS[resolution_choice]
|
|
|
|
|
+
|
|
|
|
|
+ # Handle custom resolution
|
|
|
|
|
+ if resolution_choice == '8':
|
|
|
|
|
+ try:
|
|
|
|
|
+ width = int(input("Enter width: "))
|
|
|
|
|
+ height = int(input("Enter height: "))
|
|
|
|
|
+ resolution_info['width'] = width
|
|
|
|
|
+ resolution_info['height'] = height
|
|
|
|
|
+ except ValueError:
|
|
|
|
|
+ print("Invalid resolution values!")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ # Select or create AVD
|
|
|
|
|
+ avd_name = f"ZenTap_{resolution_info['width']}x{resolution_info['height']}"
|
|
|
|
|
+
|
|
|
|
|
+ # Additional options
|
|
|
|
|
+ print("\nAdditional options:")
|
|
|
|
|
+ cold_boot = input("Cold boot (wipe data)? (y/N): ").lower().startswith('y')
|
|
|
|
|
+ build_mode = input("Build mode (debug/release) [debug]: ").strip() or 'debug'
|
|
|
|
|
+
|
|
|
|
|
+ return self.run_full_cycle(avd_name, resolution_info, cold_boot, build_mode)
|
|
|
|
|
+
|
|
|
|
|
+ def run_full_cycle(self, avd_name, resolution_info, cold_boot=False, build_mode='debug'):
|
|
|
|
|
+ """Run the complete cycle: create AVD, launch emulator, build, install, and run app"""
|
|
|
|
|
+
|
|
|
|
|
+ # Create AVD if needed
|
|
|
|
|
+ if avd_name not in self.get_available_avds():
|
|
|
|
|
+ if not self.create_avd_if_needed(avd_name, resolution_info):
|
|
|
|
|
+ print("Failed to create AVD. Using existing AVD if available.")
|
|
|
|
|
+ avds = self.get_available_avds()
|
|
|
|
|
+ if avds:
|
|
|
|
|
+ avd_name = avds[0]
|
|
|
|
|
+ print(f"Using existing AVD: {avd_name}")
|
|
|
|
|
+ else:
|
|
|
|
|
+ print("No AVDs available!")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ # Launch emulator
|
|
|
|
|
+ if not self.launch_emulator(avd_name, resolution_info, cold_boot=cold_boot):
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ # Wait for emulator to boot
|
|
|
|
|
+ if not self.wait_for_emulator():
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ # Build and install app
|
|
|
|
|
+ if not self.build_and_install_app(build_mode):
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ # Launch app
|
|
|
|
|
+ if not self.launch_app():
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ print("\n=== ZenTap is now running in the emulator! ===")
|
|
|
|
|
+ print("Press Ctrl+C to stop the emulator when done.")
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ # Keep script running
|
|
|
|
|
+ while True:
|
|
|
|
|
+ time.sleep(1)
|
|
|
|
|
+ except KeyboardInterrupt:
|
|
|
|
|
+ print("\nStopping emulator...")
|
|
|
|
|
+ subprocess.run(['adb', 'emu', 'kill'], capture_output=True)
|
|
|
|
|
+ print("Emulator stopped.")
|
|
|
|
|
+
|
|
|
|
|
+ return True
|
|
|
|
|
+
|
|
|
|
|
+def main():
|
|
|
|
|
+ parser = argparse.ArgumentParser(description='Launch ZenTap in Android emulator')
|
|
|
|
|
+ parser.add_argument('--resolution', '-r', choices=list(RESOLUTIONS.keys()),
|
|
|
|
|
+ help='Resolution preset')
|
|
|
|
|
+ parser.add_argument('--width', '-w', type=int, help='Custom width')
|
|
|
|
|
+ parser.add_argument('--height', type=int, help='Custom height')
|
|
|
|
|
+ parser.add_argument('--avd', '-a', help='Specific AVD name to use')
|
|
|
|
|
+ parser.add_argument('--cold-boot', '-c', action='store_true', help='Cold boot (wipe data)')
|
|
|
|
|
+ parser.add_argument('--build-mode', '-b', choices=['debug', 'release'], default='debug',
|
|
|
|
|
+ help='Build mode')
|
|
|
|
|
+ parser.add_argument('--list-avds', '-l', action='store_true', help='List available AVDs')
|
|
|
|
|
+
|
|
|
|
|
+ args = parser.parse_args()
|
|
|
|
|
+
|
|
|
|
|
+ launcher = ZenTapEmulatorLauncher()
|
|
|
|
|
+
|
|
|
|
|
+ if args.list_avds:
|
|
|
|
|
+ avds = launcher.get_available_avds()
|
|
|
|
|
+ print("Available AVDs:")
|
|
|
|
|
+ for avd in avds:
|
|
|
|
|
+ print(f" - {avd}")
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ if args.resolution or (args.width and args.height):
|
|
|
|
|
+ # Command line mode
|
|
|
|
|
+ if args.resolution:
|
|
|
|
|
+ resolution_info = RESOLUTIONS[args.resolution].copy()
|
|
|
|
|
+ else:
|
|
|
|
|
+ resolution_info = {'width': args.width, 'height': args.height, 'density': 160}
|
|
|
|
|
+
|
|
|
|
|
+ avd_name = args.avd or f"ZenTap_{resolution_info['width']}x{resolution_info['height']}"
|
|
|
|
|
+
|
|
|
|
|
+ launcher.run_full_cycle(avd_name, resolution_info, args.cold_boot, args.build_mode)
|
|
|
|
|
+ else:
|
|
|
|
|
+ # Interactive mode
|
|
|
|
|
+ launcher.interactive_mode()
|
|
|
|
|
+
|
|
|
|
|
+if __name__ == '__main__':
|
|
|
|
|
+ main()
|