defmodule Mix.Tasks.Dala.SetupBluetoothWifi do @shortdoc "One-command setup for Bluetooth and WiFi on iOS and Android" @moduledoc """ One-command setup for Bluetooth and WiFi functionality in Dala apps. This task automates ALL production setup steps for Bluetooth and WiFi: ## iOS Setup 1. Adds Bluetooth source files to Xcode project 2. Links CoreBluetooth.framework 3. Adds required Info.plist entries: - NSBluetoothAlwaysUsageDescription - NSBluetoothPeripheralUsageDescription - NSLocalNetworkUsageDescription - NSBonjourServices 4. Patches AppDelegate to initialize Bluetooth bridge 5. Configures background Bluetooth modes ## Android Setup 1. Adds Bluetooth/WiFi permissions to AndroidManifest.xml 2. Adds required features for BLE 3. Copies DalaBridge.java if not present 4. Patches MainActivity to call DalaBridge.init() ## Usage # Set up everything (recommended) mix dala.setup_bluetooth_wifi # Set up specific platform mix dala.setup_bluetooth_wifi --platform ios mix dala.setup_bluetooth_wifi --platform android # Check current setup without making changes mix dala.setup_bluetooth_wifi --check # Verbose output mix dala.setup_bluetooth_wifi --verbose ## Options --platform PLATFORM Target platform: ios, android, or all (default: all) --check Verify setup without making changes --verbose Show detailed output --no-color Disable colored output --dry-run Alias for --check ## Examples # First time setup mix dala.setup_bluetooth_wifi # Verify everything is configured correctly mix dala.setup_bluetooth_wifi --check # Re-run after adding new permissions mix dala.setup_bluetooth_wifi --verbose ## Idempotent Safe to run multiple times - will not duplicate entries or cause conflicts. ## Prerequisites - iOS: Xcode project must exist in ios/ directory - Android: Android project must exist in android/ directory - Ruby (optional, for iOS Xcode project modification) """ use Mix.Task @switches [ platform: :string, check: :boolean, dry_run: :boolean, verbose: :boolean, no_color: :boolean ] @platforms [:ios, :android] @impl Mix.Task def run(argv) do {opts, _rest, _invalid} = OptionParser.parse(argv, strict: @switches) platforms = parse_platforms(opts) check_only? = Keyword.get(opts, :check, false) or Keyword.get(opts, :dry_run, false) verbose? = Keyword.get(opts, :verbose, false) color? = not Keyword.get(opts, :no_color, false) banner(color?) if check_only? do check_setup(platforms, color?, verbose?) else run_setup(platforms, color?, verbose?) end end # ── Setup ──────────────────────────────────────────────────────────────────── defp run_setup(platforms, color?, verbose?) do results = Enum.map(platforms, fn platform -> {platform, setup_platform(platform, color?, verbose?)} end) summary(results, color?) end defp setup_platform(:ios, color?, verbose?) do info(color?, "Setting up iOS Bluetooth/WiFi...") steps = [ {"Checking prerequisites", fn -> check_ios_prerequisites(color?) end}, {"Running iOS setup script", fn -> run_ios_setup(color?, verbose?) end}, {"Verifying setup", fn -> verify_ios_setup(color?) end} ] run_steps(steps, color?) end defp setup_platform(:android, color?, verbose?) do info(color?, "Setting up Android Bluetooth/WiFi...") steps = [ {"Checking prerequisites", fn -> check_android_prerequisites(color?) end}, {"Running Android setup script", fn -> run_android_setup(color?, verbose?) end}, {"Verifying setup", fn -> verify_android_setup(color?) end} ] run_steps(steps, color?) end defp run_steps(steps, color?) do Enum.reduce_while(steps, :ok, fn {name, step_fn}, _acc -> info(color?, " → #{name}...") case step_fn.() do :ok -> success(color?, " ✓ #{name} complete") {:cont, :ok} {:error, reason} -> error(color?, " ✗ #{name} failed: #{reason}") {:halt, {:error, reason}} end end) end # ── iOS Setup Steps ────────────────────────────────────────────────────────── defp check_ios_prerequisites(color?) do ios_dir = "ios" unless File.dir?(ios_dir) do warning(color?, " ⚠ No ios/ directory found") print_ios_manual_instructions(color?) {:error, "iOS directory not found"} else # Check for Xcode project xcode_projects = Path.wildcard("#{ios_dir}/**/*.xcodeproj") ++ Path.wildcard("#{ios_dir}/**/*.xcworkspace") if Enum.empty?(xcode_projects) do warning(color?, " ⚠ No Xcode project found in ios/") print_ios_manual_instructions(color?) {:error, "Xcode project not found"} else :ok end end end defp run_ios_setup(color?, verbose?) do script_path = "scripts/ios_setup.sh" if File.exists?(script_path) do args = if verbose?, do: ["--verbose"], else: [] case System.cmd("bash", [script_path | args], stderr_to_stdout: true) do {output, 0} -> if verbose?, do: Mix.shell().info(output) :ok {output, _exit_code} -> error(color?, output) {:error, "iOS setup script failed"} end else warning(color?, " ⚠ iOS setup script not found, using Elixir module") Dala.Setup.IOS.setup_bluetooth() end end defp verify_ios_setup(_color?) do checks = [ {"Bluetooth files", Dala.Setup.IOS.bluetooth_files_present?()}, {"Xcode project", Dala.Setup.IOS.xcode_project_exists?()} ] failed = Enum.filter(checks, fn {_name, result} -> not result end) |> Enum.map(fn {name, _} -> name end) if Enum.empty?(failed) do :ok else {:error, "Missing: #{Enum.join(failed, ", ")}"} end end # ── Android Setup Steps ────────────────────────────────────────────────────── defp check_android_prerequisites(color?) do android_dir = "android" unless File.dir?(android_dir) do warning(color?, " ⚠ No android/ directory found") print_android_manual_instructions(color?) {:error, "Android directory not found"} else # Check for AndroidManifest.xml manifests = Path.wildcard("#{android_dir}/**/AndroidManifest.xml") if Enum.empty?(manifests) do warning(color?, " ⚠ No AndroidManifest.xml found in android/") print_android_manual_instructions(color?) {:error, "AndroidManifest.xml not found"} else :ok end end end defp run_android_setup(color?, verbose?) do script_path = "scripts/android_setup.sh" if File.exists?(script_path) do args = if verbose?, do: ["--verbose"], else: [] case System.cmd("bash", [script_path | args], stderr_to_stdout: true) do {output, 0} -> if verbose?, do: Mix.shell().info(output) :ok {output, _exit_code} -> error(color?, output) {:error, "Android setup script failed"} end else warning(color?, " ⚠ Android setup script not found, using Elixir module") Dala.Setup.Android.setup_bluetooth(nil) Dala.Setup.Android.setup_wifi(nil) Dala.Setup.Android.ensure_bridge_init() end end defp verify_android_setup(_color?) do checks = [ {"AndroidManifest.xml", Dala.Setup.Android.manifest_present?()}, {"DalaBridge.java", Dala.Setup.Android.bluetooth_files_present?()} ] failed = Enum.filter(checks, fn {_name, result} -> not result end) |> Enum.map(fn {name, _} -> name end) if Enum.empty?(failed) do :ok else {:error, "Missing: #{Enum.join(failed, ", ")}"} end end # ── Check Mode ─────────────────────────────────────────────────────────────── defp check_setup(platforms, color?, verbose?) do info(color?, "Checking Bluetooth/WiFi setup...\n") results = Enum.map(platforms, fn platform -> {platform, check_platform(platform, color?, verbose?)} end) check_summary(results, color?) end defp check_platform(:ios, color?, verbose?) do info(color?, "iOS:") checks = [ {"Bluetooth files present", Dala.Setup.IOS.bluetooth_files_present?()}, {"Xcode project exists", Dala.Setup.IOS.xcode_project_exists?()}, {"Info.plist configured", check_ios_plist(color?)} ] Enum.each(checks, fn {name, result} -> if result do success(color?, " ✓ #{name}") else error(color?, " ✗ #{name}") end end) if verbose? do info(color?, "\n Run `mix dala.setup_bluetooth_wifi --platform ios` to fix issues") end :ok end defp check_platform(:android, color?, verbose?) do info(color?, "Android:") checks = [ {"AndroidManifest.xml present", Dala.Setup.Android.manifest_present?()}, {"DalaBridge.java present", Dala.Setup.Android.bluetooth_files_present?()}, {"Permissions configured", check_android_permissions(color?)} ] Enum.each(checks, fn {name, result} -> if result do success(color?, " ✓ #{name}") else error(color?, " ✗ #{name}") end end) if verbose? do info(color?, "\n Run `mix dala.setup_bluetooth_wifi --platform android` to fix issues") end :ok end defp check_ios_plist(_color?) do case Path.wildcard("ios/**/Info.plist") do [] -> false paths -> Enum.any?(paths, &plist_has_bluetooth_keys?/1) end end defp plist_has_bluetooth_keys?(path) do content = File.read!(path) String.contains?(content, "NSBluetoothAlwaysUsageDescription") end defp check_android_permissions(_color?) do case Dala.Setup.Android.find_manifest() do nil -> false manifest_path -> manifest_has_bluetooth_permissions?(manifest_path) end end defp manifest_has_bluetooth_permissions?(path) do content = File.read!(path) String.contains?(content, "BLUETOOTH_SCAN") and String.contains?(content, "BLUETOOTH_CONNECT") end # ── Manual Instructions ────────────────────────────────────────────────────── defp print_ios_manual_instructions(color?) do info(color?, "") info(color?, """ Manual iOS Setup Required ========================= 1. Create an iOS project in the ios/ directory 2. Add these keys to your Info.plist: NSBluetoothAlwaysUsageDescription Need Bluetooth for device communication NSBluetoothPeripheralUsageDescription Need Bluetooth for device communication NSLocalNetworkUsageDescription Need local network access for WiFi discovery 3. Link CoreBluetooth.framework in Xcode 4. Add Bluetooth source files to your Xcode project 5. Initialize in AppDelegate: // Swift DalaBluetoothBridge.ensureLinked() // Objective-C [DalaBluetoothBridge ensureLinked]; """) end defp print_android_manual_instructions(color?) do info(color?, "") info(color?, """ Manual Android Setup Required ============================= 1. Create an Android project in the android/ directory 2. Add these permissions to AndroidManifest.xml: 3. Add uses-feature: 4. Initialize DalaBridge in MainActivity.onCreate(): // Java DalaBridge.init(getApplicationContext()); // Kotlin DalaBridge.init(applicationContext) """) end # ── Helpers ────────────────────────────────────────────────────────────────── defp parse_platforms(opts) do case Keyword.get(opts, :platform, "all") do "all" -> @platforms "ios" -> [:ios] "android" -> [:android] other -> Mix.raise("Unknown platform: #{other}. Use ios, android, or all.") end end # ── Output ─────────────────────────────────────────────────────────────────── defp banner(color?) do Mix.shell().info(""" #{color(color?, :cyan, "╔══════════════════════════════════════════════════════╗", color?)} #{color(color?, :cyan, "║ Dala Bluetooth/WiFi Setup ║", color?)} #{color(color?, :cyan, "╚══════════════════════════════════════════════════════╝", color?)} """) end defp summary(results, color?) do Mix.shell().info("") Enum.each(results, fn {:ios, :ok} -> success(color?, "✓ iOS setup complete") {:ios, {:error, reason}} -> error(color?, "✗ iOS setup failed: #{reason}") {:android, :ok} -> success(color?, "✓ Android setup complete") {:android, {:error, reason}} -> error(color?, "✗ Android setup failed: #{reason}") end) Mix.shell().info("") if Enum.all?(results, fn {_, result} -> result == :ok end) do success(color?, "All platforms configured successfully!") Mix.shell().info("") Mix.shell().info(""" Next steps: 1. Open your Xcode project and verify the Bluetooth files are added 2. Open your Android project and verify DalaBridge.init() is called 3. Test with: Dala.Bluetooth.state() 4. Run diagnostics: Dala.Setup.print_diagnostic() """) else warning(color?, "Some platforms had errors. Check the output above for details.") end end defp check_summary(results, color?) do Mix.shell().info("") all_ok = Enum.all?(results, fn {_, :ok} -> true _ -> false end) if all_ok do success(color?, "✓ All checks passed!") else error(color?, "✗ Some checks failed. Run without --check to fix issues.") end end defp info(color?, msg), do: Mix.shell().info(color(color?, :default, msg, color?)) defp success(color?, msg), do: Mix.shell().info(color(color?, :green, msg, color?)) defp warning(color?, msg), do: Mix.shell().info(color(color?, :yellow, msg, color?)) defp error(color?, msg), do: Mix.shell().error(color(color?, :red, msg, color?)) defp color(true, :cyan, text, _), do: IO.ANSI.cyan() <> text <> IO.ANSI.reset() defp color(true, :green, text, _), do: IO.ANSI.green() <> text <> IO.ANSI.reset() defp color(true, :yellow, text, _), do: IO.ANSI.yellow() <> text <> IO.ANSI.reset() defp color(true, :red, text, _), do: IO.ANSI.red() <> text <> IO.ANSI.reset() defp color(true, :default, text, _), do: text defp color(false, _, text, _), do: text end # Alias for easier discovery defmodule Mix.Tasks.Dala.BtSetup do @shortdoc "Alias for dala.setup_bluetooth_wifi" @moduledoc "Alias for `mix dala.setup_bluetooth_wifi`" use Mix.Task @impl Mix.Task def run(argv) do Mix.Tasks.Dala.SetupBluetoothWifi.run(argv) end end