using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using UnityEditor; using UnityEditor.Build; using UnityEditor.Build.Reporting; using UnityEngine; using Debug = UnityEngine.Debug; public class PostBuildAdbInstall : IPostprocessBuildWithReport { private const string PrefKey = "PerfectWorld/AutoAdbInstallAfterAndroidBuild"; public int callbackOrder => 200; [MenuItem("Tools/Perfect World/Auto install APK after build (toggle)")] private static void ToggleAutoInstall() { bool v = !EditorPrefs.GetBool(PrefKey, true); EditorPrefs.SetBool(PrefKey, v); Debug.Log("[AdbInstall] Auto install after Android build: " + (v ? "ON" : "OFF")); } [MenuItem("Tools/Perfect World/Auto install APK after build (toggle)", true)] private static bool ToggleAutoInstallValidate() { UnityEditor.Menu.SetChecked("Tools/Perfect World/Auto install APK after build (toggle)", EditorPrefs.GetBool(PrefKey, true)); return true; } [MenuItem("Tools/Perfect World/Install APK to device (choose file)")] private static void InstallApkFromPicker() { string p = EditorUtility.OpenFilePanel("Pick APK", "", "apk"); if (string.IsNullOrEmpty(p)) return; RunAdbInstall(p); } public void OnPostprocessBuild(BuildReport report) { if (report.summary.platform != BuildTarget.Android) return; if (report.summary.result != BuildResult.Succeeded) return; if (!EditorPrefs.GetBool(PrefKey, true)) return; string outputPath = report.summary.outputPath; string apkPath = ResolveApkPath(outputPath); if (string.IsNullOrEmpty(apkPath)) { if (!string.IsNullOrEmpty(outputPath) && outputPath.EndsWith(".aab", StringComparison.OrdinalIgnoreCase) && File.Exists(outputPath)) { Debug.LogWarning( "[AdbInstall] Output is .aab; adb install needs an APK. Disable App Bundle for dev, or use menu to install a built APK."); } else { Debug.LogWarning("[AdbInstall] No .apk found. Build output: " + outputPath); } return; } RunAdbInstall(apkPath); } private static string ResolveApkPath(string outputPath) { if (string.IsNullOrEmpty(outputPath)) return null; if (File.Exists(outputPath) && outputPath.EndsWith(".apk", StringComparison.OrdinalIgnoreCase)) return outputPath; if (File.Exists(outputPath) && outputPath.EndsWith(".aab", StringComparison.OrdinalIgnoreCase)) return null; if (Directory.Exists(outputPath)) { var apks = new List(Directory.GetFiles(outputPath, "*.apk", SearchOption.AllDirectories)); if (apks.Count == 0) return null; apks.Sort((a, b) => File.GetLastWriteTimeUtc(b).CompareTo(File.GetLastWriteTimeUtc(a))); return apks[0]; } return null; } private static void RunAdbInstall(string apkPath) { string adb = FindAdbExecutable(); if (string.IsNullOrEmpty(adb)) { Debug.LogError( "[AdbInstall] adb.exe not found. Install Android Build Support (SDK) in Unity Hub, or set ANDROID_HOME, or add platform-tools to PATH. Editor: " + (EditorApplication.applicationPath ?? "(null)")); return; } string serial = GetSingleDeviceOrFirst(adb, out string listErr); if (serial == null) { if (!string.IsNullOrEmpty(listErr)) Debug.LogWarning("[AdbInstall] " + listErr); return; } string argPrefix = string.IsNullOrEmpty(serial) ? "" : ("-s " + serial + " "); string args = argPrefix + "install -r \"" + apkPath + "\""; var psi = new ProcessStartInfo(adb, args) { UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, }; try { using (var p = Process.Start(psi)) { if (p == null) { Debug.LogError("[AdbInstall] Failed to start adb."); return; } string std = p.StandardOutput.ReadToEnd(); string err = p.StandardError.ReadToEnd(); p.WaitForExit(120000); if (p.ExitCode == 0) Debug.Log("[AdbInstall] Installed: " + apkPath + "\n" + std + err); else Debug.LogError("[AdbInstall] Install failed (exit " + p.ExitCode + "): " + apkPath + "\n" + std + err); } } catch (Exception ex) { Debug.LogError("[AdbInstall] " + ex.Message); } } private static string GetSingleDeviceOrFirst(string adb, out string error) { error = null; var psi = new ProcessStartInfo(adb, "devices") { UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, }; string stdout; try { using (var p = Process.Start(psi)) { if (p == null) { error = "Could not run adb devices."; return null; } stdout = p.StandardOutput.ReadToEnd(); p.WaitForExit(10000); } } catch (Exception ex) { error = ex.Message; return null; } var onlines = new List(); var lines = stdout.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < lines.Length; i++) { if (i == 0 && lines[i].IndexOf("List of devices", StringComparison.OrdinalIgnoreCase) >= 0) continue; int tab = lines[i].IndexOf('\t'); if (tab < 0) continue; string id = lines[i].Substring(0, tab).Trim(); string st = lines[i].Substring(tab + 1).Trim(); if (st.Equals("device", StringComparison.OrdinalIgnoreCase)) onlines.Add(id); } if (onlines.Count == 0) { error = "No Android device in state \"device\" (enable USB debugging, cable, allow PC)."; return null; } if (onlines.Count == 1) return ""; Debug.LogWarning("[AdbInstall] Multiple devices - installing to: " + onlines[0]); return onlines[0]; } private static string FindAdbExecutable() { foreach (string root in EnumerateAndroidSdkRoots()) { string a = TryAdbInSdk(root); if (!string.IsNullOrEmpty(a)) return a; } var pathvar = Environment.GetEnvironmentVariable("PATH"); if (!string.IsNullOrEmpty(pathvar)) { foreach (var part in pathvar.Split(new char[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries)) { var t = part.Trim(); if (t.Length > 1 && t[0] == '"' && t[t.Length - 1] == '"') t = t.Substring(1, t.Length - 2); var candidate = Path.Combine(t, "adb.exe"); if (File.Exists(candidate)) return candidate; } } return null; } private static IEnumerable EnumerateAndroidSdkRoots() { var seen = new HashSet(StringComparer.OrdinalIgnoreCase); void Add(string p) { if (string.IsNullOrEmpty(p)) return; p = p.Trim(); if (p.Length == 0) return; seen.Add(p); } Add(Environment.GetEnvironmentVariable("ANDROID_HOME")); Add(Environment.GetEnvironmentVariable("ANDROID_SDK_ROOT")); string ext = GetUnityAndroidSdkRootPathFromSettings(); if (!string.IsNullOrEmpty(ext)) Add(ext); if (EditorPrefs.HasKey("AndroidSdkRoot")) Add(EditorPrefs.GetString("AndroidSdkRoot", "")); try { string editorExe = EditorApplication.applicationPath; if (!string.IsNullOrEmpty(editorExe)) { string editorDir = Path.GetDirectoryName(editorExe); if (!string.IsNullOrEmpty(editorDir)) Add(Path.Combine(editorDir, "Data", "PlaybackEngines", "AndroidPlayer", "SDK")); } } catch { } Add(Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "AppData", "Local", "Android", "Sdk")); return seen; } private static string GetUnityAndroidSdkRootPathFromSettings() { try { var t = Type.GetType("UnityEditor.Android.AndroidExternalToolsSettings, UnityEditor.Android.EModule"); if (t == null) return null; var p = t.GetProperty("sdkRootPath", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); if (p == null) return null; var s = p.GetValue(null) as string; if (string.IsNullOrEmpty(s)) return null; if (Directory.Exists(s)) return s; } catch { } return null; } private static string TryAdbInSdk(string sdkRoot) { if (string.IsNullOrEmpty(sdkRoot) || !Directory.Exists(sdkRoot)) return null; var adb = Path.Combine(sdkRoot, "platform-tools", "adb.exe"); if (File.Exists(adb)) return adb; adb = Path.Combine(sdkRoot, "adb.exe"); if (File.Exists(adb)) return adb; return null; } }