/* * Copyright (c) Meta Platforms, Inc. and affiliates. * All rights reserved. * * Licensed under the Oculus SDK License Agreement (the "License"); * you may not use the Oculus SDK except in compliance with the License, * which is provided at the time of installation or download, or which * otherwise accompanies this software in either electronic or hard copy form. * * You may obtain a copy of the License at * * https://developer.oculus.com/licenses/oculussdk/ * * Unless required by applicable law or agreed to in writing, the Oculus SDK * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #if USING_XR_MANAGEMENT && (USING_XR_SDK_OCULUS || USING_XR_SDK_OPENXR) #define USING_XR_SDK #endif #if UNITY_2020_1_OR_NEWER #define REQUIRES_XR_SDK #endif // Unit made a change that broke `BuildOptions.AcceptExternalModificationsToPlayer` // Thread detailing issue: https://forum.unity.com/threads/oculus-build-invalidoperationexception-the-build-target-does-not-support-build-appending.994930/ #if UNITY_2020_1_OR_NEWER || UNITY_2019_4_OR_NEWER #define DONT_USE_BUILD_OPTIONS_EXTERNAL_MODIFICATIONS_FLAG #endif using UnityEngine; using UnityEditor; using System; using System.IO; using System.Diagnostics; using System.Collections.Generic; using System.Threading; /// /// Allows Oculus to build apps from the command line. /// [InitializeOnLoadAttribute] partial class OculusBuildApp : EditorWindow { static void SetPCTarget() { if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.StandaloneWindows) { EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTargetGroup.Standalone, BuildTarget.StandaloneWindows); } #if !USING_XR_SDK && !REQUIRES_XR_SDK UnityEditorInternal.VR.VREditor.SetVREnabledOnTargetGroup(BuildTargetGroup.Standalone, true); #pragma warning disable 618 PlayerSettings.virtualRealitySupported = true; #pragma warning restore 618 #endif AssetDatabase.SaveAssets(); } static void SetAndroidTarget() { EditorUserBuildSettings.androidBuildSubtarget = MobileTextureSubtarget.ASTC; EditorUserBuildSettings.androidBuildSystem = AndroidBuildSystem.Gradle; if (EditorUserBuildSettings.activeBuildTarget != BuildTarget.Android) { EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTargetGroup.Android, BuildTarget.Android); } #if !USING_XR_SDK && !REQUIRES_XR_SDK UnityEditorInternal.VR.VREditor.SetVREnabledOnTargetGroup(BuildTargetGroup.Standalone, true); #pragma warning disable 618 PlayerSettings.virtualRealitySupported = true; #pragma warning restore 618 #endif AssetDatabase.SaveAssets(); } #if UNITY_EDITOR_WIN && UNITY_ANDROID // Build setting constants const string REMOTE_APK_PATH = "/data/local/tmp"; const float USB_TRANSFER_SPEED_THRES = 25.0f; const float USB_3_TRANSFER_SPEED = 32.0f; const float TRANSFER_SPEED_CHECK_THRESHOLD = 4.0f; const int NUM_BUILD_AND_RUN_STEPS = 9; const int BYTES_TO_MEGABYTES = 1048576; // Build window variables (not saved) GUIStyle windowStyle; GUIStyle calloutStyle; string currConnectedDevice; Vector2 scrollViewPos; // Build window variables (saved) static bool saveKeystorePasswords; static bool isRunOnDevice; static string outputApkPath; // Progress bar variables static int totalBuildSteps; static int currentStep; static string progressMessage; // Build setting varaibles static string gradlePath; static string jdkPath; static string androidSdkPath; static string applicationIdentifier; static bool isDevelopmentBuild; static string productName; static string dataPath; static string gradleTempExport; static string gradleExport; static bool showCancel; static bool buildInProgress; static DirectorySyncer.CancellationTokenSource syncCancelToken; static Process gradleBuildProcess; static Thread buildThread; static bool? apkOutputSuccessful; [MenuItem("Oculus/OVR Build/OVR Build APK... %#k", false, 20)] static void Init() { EditorWindow.GetWindow(false, "OVR Build APK", true); OnBuildComplete(); } [MenuItem("Oculus/OVR Build/OVR Build APK And Run %k", false, 21)] static void InitAndRun() { var window = EditorWindow.GetWindow(false, "OVR Build APK", true); isRunOnDevice = true; // make sure the "And Run" part of the menu name is true window.StartBuild(); } private void OnEnable() { isRunOnDevice = EditorPrefs.GetBool("OVRBuild_RunOnDevice", false); saveKeystorePasswords = EditorPrefs.GetBool("OVRBuild_SaveKeystorePasswords", false); if (saveKeystorePasswords) { if (EditorPrefs.HasKey("OVRBuild_KeystorePassword")) PlayerSettings.Android.keystorePass = EditorPrefs.GetString("OVRBuild_KeystorePassword"); if (EditorPrefs.HasKey("OVRBuild_KeyAliasPassword")) PlayerSettings.Android.keyaliasPass = EditorPrefs.GetString("OVRBuild_KeyAliasPassword"); } outputApkPath = EditorPrefs.GetString("OVRBuild_BuiltAPKPath", ""); CheckADBDevices(out currConnectedDevice); } private void OnDisable() { EditorPrefs.SetBool("OVRBuild_RunOnDevice", isRunOnDevice); EditorPrefs.SetBool("OVRBuild_SaveKeystorePasswords", saveKeystorePasswords); if (saveKeystorePasswords) { EditorPrefs.SetString("OVRBuild_KeystorePassword", PlayerSettings.Android.keystorePass); EditorPrefs.SetString("OVRBuild_KeyAliasPassword", PlayerSettings.Android.keyaliasPass); } else { EditorPrefs.DeleteKey("OVRBuild_KeystorePassword"); EditorPrefs.DeleteKey("OVRBuild_KeyAliasPassword"); } EditorPrefs.SetString("OVRBuild_BuiltAPKPath", outputApkPath); } private void OnGUI() { if (windowStyle == null) { windowStyle = new GUIStyle(); windowStyle.margin = new RectOffset(10, 10, 10, 10); } if (calloutStyle == null) { calloutStyle = new GUIStyle(EditorStyles.label); calloutStyle.richText = true; calloutStyle.wordWrap = true; } // Fix progress bar window size minSize = new Vector2(500, 305); float oldLabelWidth = EditorGUIUtility.labelWidth; EditorGUIUtility.labelWidth = 160; EditorGUILayout.BeginVertical(windowStyle); GUILayout.BeginHorizontal(EditorStyles.helpBox); GUILayout.BeginVertical(); EditorGUILayout.LabelField("Builds created in the OVR Build APK window are identical to Unity-built APKs, but use the Gradle cache to only touch changed files, resulting in shorter build times.", calloutStyle); #if UNITY_2021_1_OR_NEWER if (EditorGUILayout.LinkButton("Documentation")) #else if (GUILayout.Button("Documentation", GUILayout.ExpandWidth(false))) #endif { Application.OpenURL("https://developer.oculus.com/documentation/unity/unity-build-android-tools/"); } GUILayout.EndVertical(); GUILayout.EndHorizontal(); EditorGUILayout.Space(15f); scrollViewPos = EditorGUILayout.BeginScrollView(scrollViewPos, GUIStyle.none, GUI.skin.verticalScrollbar); using (new EditorGUI.DisabledScope(buildInProgress)) { EditorGUILayout.BeginHorizontal(); outputApkPath = EditorGUILayout.TextField("Built APK Path", outputApkPath); if (GUILayout.Button("Browse...", GUILayout.Width(80))) DisplayAPKPathDialog(); EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); PlayerSettings.Android.bundleVersionCode = EditorGUILayout.IntField(new GUIContent("Version Number", "Builds uploaded to the Oculus storefront are required to have incrementing version numbers.\nThis value is exposed to players."), PlayerSettings.Android.bundleVersionCode, GUILayout.Width(220)); EditorGUI.BeginChangeCheck(); bool isAutoIncrement = PlayerPrefs.GetInt(OVRGradleGeneration.prefName, 0) != 0; isAutoIncrement = EditorGUILayout.ToggleLeft(new GUIContent("Auto-Increment?", "If true, version number will be automatically incremented after every successful build."), isAutoIncrement, EditorStyles.miniLabel, GUILayout.Width(120)); if (EditorGUI.EndChangeCheck()) OVRGradleGeneration.ToggleUtilities(); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(15f); EditorGUILayout.BeginHorizontal(); using (new EditorGUI.DisabledScope(string.IsNullOrEmpty(currConnectedDevice))) { isRunOnDevice = EditorGUILayout.Toggle("Install & Run on Device?", isRunOnDevice); GUILayout.FlexibleSpace(); EditorGUILayout.LabelField(string.IsNullOrEmpty(currConnectedDevice) ? "No device connected" : $"Device: {currConnectedDevice}"); } if (GUILayout.Button("Refresh", GUILayout.Width(80))) CheckADBDevices(out currConnectedDevice); EditorGUILayout.EndHorizontal(); EditorUserBuildSettings.development = EditorGUILayout.Toggle(new GUIContent("Development Build?", "Development builds allow you to debug scripts. However, they're slightly slower, and they're not allowed on the Oculus storefront."), EditorUserBuildSettings.development); EditorGUILayout.Space(15f); EditorGUILayout.BeginHorizontal(); saveKeystorePasswords = EditorGUILayout.Toggle(new GUIContent("Save Keystore Passwords?", "These values are also found in Project Settings > Player > [Android] > Publishing Settings > Project Keystore.\nStoring passwords is convenient, but reduces security."), saveKeystorePasswords); if (GUILayout.Button("Select Keystore...", GUILayout.Width(150))) SettingsService.OpenProjectSettings("Project/Player"); EditorGUILayout.EndHorizontal(); if (saveKeystorePasswords) { EditorGUI.indentLevel++; EditorGUILayout.LabelField("Keystore Path", PlayerSettings.Android.keystoreName); PlayerSettings.Android.keystorePass = EditorGUILayout.PasswordField("Keystore Password", PlayerSettings.Android.keystorePass); EditorGUILayout.LabelField("Key Alias Name", PlayerSettings.Android.keyaliasName); PlayerSettings.Android.keyaliasPass = EditorGUILayout.PasswordField("Alias Password", PlayerSettings.Android.keyaliasPass); EditorGUI.indentLevel--; } } EditorGUILayout.EndScrollView(); EditorGUILayout.Space(10); EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); //better to perform these at end of GUI bool shouldCancel = false; bool shouldBuild = false; if (showCancel) shouldCancel = GUILayout.Button("Cancel", GUILayout.Height(30), GUILayout.Width(100)); else { using (new EditorGUI.DisabledScope(buildInProgress)) { shouldBuild = GUILayout.Button("Build", GUILayout.Height(30), GUILayout.Width(100)); } } GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(10); // Show progress bar Rect progressRect = EditorGUILayout.GetControlRect(GUILayout.Height(25)); float progress = currentStep / (float)totalBuildSteps; EditorGUI.ProgressBar(progressRect, progress, progressMessage); EditorGUIUtility.labelWidth = oldLabelWidth; EditorGUILayout.EndVertical(); if (shouldBuild) StartBuild(); else if (shouldCancel) CancelBuild(); } void Update() { // Force window update if not in focus to ensure progress bar still updates var window = EditorWindow.focusedWindow; if (window != null && window.ToString().Contains("OculusBuildApp")) { Repaint(); } } void DisplayAPKPathDialog() { string fileName = "build.apk"; string path = ""; if (!string.IsNullOrEmpty(outputApkPath)) { try { path = Path.GetDirectoryName(outputApkPath); fileName = Path.GetFileName(outputApkPath); } catch (Exception) { //do nothing, we just have a malformed apkPath and should accept defaults } } outputApkPath = EditorUtility.SaveFilePanel("APK Path", path, fileName, "apk"); Repaint(); } void CancelBuild() { SetProgressBarMessage("Canceling . . ."); if (syncCancelToken != null) { syncCancelToken.Cancel(); } if (apkOutputSuccessful.HasValue && apkOutputSuccessful.Value) { buildThread.Abort(); OnBuildComplete(); } if (gradleBuildProcess != null && !gradleBuildProcess.HasExited) { var cancelThread = new Thread(delegate () { CancelGradleBuild(); }); cancelThread.Start(); } } void CancelGradleBuild() { Process cancelGradleProcess = new Process(); string arguments = "-Xmx1024m -classpath \"" + gradlePath + "\" org.gradle.launcher.GradleMain --stop"; var processInfo = new System.Diagnostics.ProcessStartInfo { WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal, FileName = jdkPath, Arguments = arguments, RedirectStandardInput = true, UseShellExecute = false, CreateNoWindow = true, RedirectStandardError = true, RedirectStandardOutput = true, }; cancelGradleProcess.StartInfo = processInfo; cancelGradleProcess.EnableRaisingEvents = true; cancelGradleProcess.OutputDataReceived += new DataReceivedEventHandler( (s, e) => { if (e != null && e.Data != null && e.Data.Length != 0) { UnityEngine.Debug.LogFormat("Gradle: {0}", e.Data); } } ); apkOutputSuccessful = false; cancelGradleProcess.Start(); cancelGradleProcess.BeginOutputReadLine(); cancelGradleProcess.WaitForExit(); OnBuildComplete(); } public void StartBuild() { showCancel = false; buildInProgress = true; InitializeProgressBar(NUM_BUILD_AND_RUN_STEPS); IncrementProgressBar("Exporting Unity Project . . ."); apkOutputSuccessful = null; syncCancelToken = null; gradleBuildProcess = null; UnityEngine.Debug.Log("OVRBuild: Starting Unity build ..."); SetupDirectories(); // 1. Get scenes to build in Unity, and export gradle project var buildResult = UnityBuildPlayer(); if (buildResult.summary.result == UnityEditor.Build.Reporting.BuildResult.Succeeded) { // Set static variables so build thread has updated data showCancel = true; gradlePath = OVRConfig.Instance.GetGradlePath(); jdkPath = OVRConfig.Instance.GetJDKPath(); androidSdkPath = OVRConfig.Instance.GetAndroidSDKPath(); applicationIdentifier = PlayerSettings.GetApplicationIdentifier(BuildTargetGroup.Android); isDevelopmentBuild = EditorUserBuildSettings.development; #if UNITY_2019_3_OR_NEWER productName = "launcher"; #else productName = Application.productName; #endif dataPath = Application.dataPath; buildThread = new Thread(delegate () { OVRBuildRun(); }); buildThread.Start(); return; } else if (buildResult.summary.result == UnityEditor.Build.Reporting.BuildResult.Cancelled) { UnityEngine.Debug.Log("Build cancelled."); } else { UnityEngine.Debug.Log("Build failed."); } OnBuildComplete(); } private UnityEditor.Build.Reporting.BuildReport UnityBuildPlayer() { // Unity introduced a possible bug with Unity 2020.1.10f1 that causes an exception // when building with the option 'BuildOptions.AcceptExternalModificationsToPlayer' // In order to maintain the same logic, we are using `EditorUserBuildSettings.exportAsGoogleAndroidProject` #if DONT_USE_BUILD_OPTIONS_EXTERNAL_MODIFICATIONS_FLAG bool previousExportAsGoogleAndroidProject = EditorUserBuildSettings.exportAsGoogleAndroidProject; EditorUserBuildSettings.exportAsGoogleAndroidProject = true; #endif try { var sceneList = GetScenesToBuild(); var buildOptions = BuildOptions.None; if (isDevelopmentBuild) buildOptions |= (BuildOptions.Development | BuildOptions.AllowDebugging); if (isRunOnDevice) buildOptions |= BuildOptions.AutoRunPlayer; #if !DONT_USE_BUILD_OPTIONS_EXTERNAL_MODIFICATIONS_FLAG buildOptions |= BuildOptions.AcceptExternalModificationsToPlayer; #endif var buildPlayerOptions = new BuildPlayerOptions { scenes = sceneList.ToArray(), locationPathName = gradleTempExport, target = BuildTarget.Android, options = buildOptions }; var buildResult = BuildPipeline.BuildPlayer(buildPlayerOptions); UnityEngine.Debug.Log(UnityBuildPlayerSummary(buildResult)); return buildResult; } finally { #if DONT_USE_BUILD_OPTIONS_EXTERNAL_MODIFICATIONS_FLAG EditorUserBuildSettings.exportAsGoogleAndroidProject = previousExportAsGoogleAndroidProject; #endif } } private static string UnityBuildPlayerSummary(UnityEditor.Build.Reporting.BuildReport report) { var sb = new System.Text.StringBuilder(); sb.Append($"Unity Build Player: Build {report.summary.result} ({report.summary.totalSize} bytes) in {report.summary.totalTime.TotalSeconds:0.00}s"); foreach (var step in report.steps) { sb.AppendLine(); if (step.depth > 0) { sb.Append(new String('-', step.depth)); sb.Append(' '); } sb.Append($"{step.name}: {step.duration:g}"); } return sb.ToString(); } private static void OVRBuildRun() { // 2. Process gradle project IncrementProgressBar("Processing gradle project . . ."); if (ProcessGradleProject()) { // 3. Build gradle project IncrementProgressBar("Starting gradle build . . ."); if (BuildGradleProject()) { CopyAPK(); // 4. Deploy and run if (isRunOnDevice) DeployAPK(); } } OnBuildComplete(); } private static bool BuildGradleProject() { gradleBuildProcess = new Process(); string arguments = "-Xmx4096m -classpath \"" + gradlePath + "\" org.gradle.launcher.GradleMain --profile "; if (isDevelopmentBuild) arguments += "assembleDebug -x validateSigningDebug"; else arguments += "assembleRelease -x verifyReleaseResources"; #if UNITY_2019_3_OR_NEWER var gradleProjectPath = gradleExport; #else var gradleProjectPath = Path.Combine(gradleExport, productName); #endif var processInfo = new System.Diagnostics.ProcessStartInfo { WorkingDirectory = gradleProjectPath, WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal, FileName = jdkPath, Arguments = arguments, RedirectStandardInput = true, UseShellExecute = false, CreateNoWindow = true, RedirectStandardError = true, RedirectStandardOutput = true, }; gradleBuildProcess.StartInfo = processInfo; gradleBuildProcess.EnableRaisingEvents = true; DateTime gradleStartTime = System.DateTime.Now; DateTime gradleEndTime = System.DateTime.MinValue; gradleBuildProcess.Exited += new System.EventHandler( (s, e) => { UnityEngine.Debug.Log("Gradle: Exited"); } ); gradleBuildProcess.OutputDataReceived += new DataReceivedEventHandler( (s, e) => { if (e != null && e.Data != null && e.Data.Length != 0 && (e.Data.Contains("BUILD") || e.Data.StartsWith("See the profiling report at:"))) { UnityEngine.Debug.LogFormat("Gradle: {0}", e.Data); if (e.Data.Contains("SUCCESSFUL")) { string buildFlavor = isDevelopmentBuild ? "debug" : "release"; UnityEngine.Debug.LogFormat("APK Build Completed: {0}", Path.Combine(gradleExport, $"build\\outputs\\apk\\{buildFlavor}", productName + $"-{buildFlavor}.apk").Replace("/", "\\")); if (!apkOutputSuccessful.HasValue) { apkOutputSuccessful = true; } gradleEndTime = System.DateTime.Now; } else if (e.Data.Contains("FAILED")) { apkOutputSuccessful = false; } } } ); gradleBuildProcess.ErrorDataReceived += new DataReceivedEventHandler( (s, e) => { if (e != null && e.Data != null && e.Data.Length != 0) { UnityEngine.Debug.LogErrorFormat("Gradle: {0}", e.Data); } apkOutputSuccessful = false; } ); gradleBuildProcess.Start(); gradleBuildProcess.BeginOutputReadLine(); IncrementProgressBar("Building gradle project . . ."); gradleBuildProcess.WaitForExit(); // Add a timeout for if gradle unexpectedlly exits or errors out Stopwatch timeout = new Stopwatch(); timeout.Start(); while (apkOutputSuccessful == null) { if (timeout.ElapsedMilliseconds > 5000) { UnityEngine.Debug.LogError("Gradle has exited unexpectedly."); apkOutputSuccessful = false; } System.Threading.Thread.Sleep(100); } return apkOutputSuccessful.HasValue && apkOutputSuccessful.Value; } private static bool ProcessGradleProject() { DateTime syncStartTime = System.DateTime.Now; DateTime syncEndTime = System.DateTime.MinValue; try { var ps = System.Text.RegularExpressions.Regex.Escape("" + Path.DirectorySeparatorChar); // ignore files .gradle/** build/** foo/.gradle/** and bar/build/** var ignorePattern = string.Format("^([^{0}]+{0})?(\\.gradle|build){0}", ps); var syncer = new DirectorySyncer(gradleTempExport, gradleExport, ignorePattern); syncCancelToken = new DirectorySyncer.CancellationTokenSource(); var syncResult = syncer.Synchronize(syncCancelToken.Token); syncEndTime = System.DateTime.Now; } catch (Exception e) { UnityEngine.Debug.Log("OVRBuild: Processing gradle project failed with exception: " + e.Message); return false; } if (syncCancelToken.IsCancellationRequested) { return false; } return true; } private static List GetScenesToBuild() { var sceneList = new List(); foreach (var scene in EditorBuildSettings.scenes) { // Enumerate scenes in project and check if scene is enabled to build if (scene.enabled) { sceneList.Add(scene.path); } } return sceneList; } public static bool CopyAPK() { string buildFlavor = isDevelopmentBuild ? "debug" : "release"; string apkPathLocal = Path.Combine(gradleExport, productName, $"build\\outputs\\apk\\{buildFlavor}", productName + $"-{buildFlavor}.apk"); if (File.Exists(apkPathLocal)) { try { File.Copy(apkPathLocal, outputApkPath, true); UnityEngine.Debug.Log($"OVRBuild: Output APK generated at {outputApkPath}"); Process.Start("explorer.exe", Path.GetDirectoryName(outputApkPath)); return true; } catch (Exception) { return false; } } return false; } public static bool DeployAPK() { // Create new instance of ADB Tool var adbTool = new OVRADBTool(androidSdkPath); if (adbTool.isReady) { string apkPathLocal; string buildFlavor = isDevelopmentBuild ? "debug" : "release"; string gradleExportFolder = Path.Combine(gradleExport, productName, $"build\\outputs\\apk\\{buildFlavor}"); // Check to see if gradle output directory exists gradleExportFolder = gradleExportFolder.Replace("/", "\\"); if (!Directory.Exists(gradleExportFolder)) { UnityEngine.Debug.LogError("Could not find the gradle project at the expected path: " + gradleExportFolder); return false; } // Search for output APK in gradle output directory apkPathLocal = Path.Combine(gradleExportFolder, productName + $"-{buildFlavor}.apk"); if (!System.IO.File.Exists(apkPathLocal)) { UnityEngine.Debug.LogError(string.Format("Could not find {0} in the gradle project.", productName + $"-{buildFlavor}.apk")); return false; } string output, error; DateTime timerStart; // Ensure that the Oculus temp directory is on the device by making it IncrementProgressBar("Making Temp directory on device"); string[] mkdirCommand = { "-d shell", "mkdir -p", REMOTE_APK_PATH }; if (adbTool.RunCommand(mkdirCommand, null, out output, out error) != 0) return false; // Push APK to device, also time how long it takes timerStart = System.DateTime.Now; IncrementProgressBar("Pushing APK to device . . ."); string[] pushCommand = { "-d push", "\"" + apkPathLocal + "\"", REMOTE_APK_PATH }; if (adbTool.RunCommand(pushCommand, null, out output, out error) != 0) return false; // Calculate the transfer speed and determine if user is using USB 2.0 or 3.0 // Only bother informing the user on non-trivial transfers, as for very short // periods of time, things like process creation overhead can dwarf the actual // transfer time. TimeSpan pushTime = System.DateTime.Now - timerStart; bool trivialPush = pushTime.TotalSeconds < TRANSFER_SPEED_CHECK_THRESHOLD; long? apkSize = (trivialPush ? (long?)null : new System.IO.FileInfo(apkPathLocal).Length); double? transferSpeed = (apkSize / pushTime.TotalSeconds) / BYTES_TO_MEGABYTES; bool informLog = transferSpeed.HasValue && transferSpeed.Value < USB_TRANSFER_SPEED_THRES; UnityEngine.Debug.Log("OVRADBTool: Push Success"); // Install the APK package on the device IncrementProgressBar("Installing APK . . ."); string apkPath = REMOTE_APK_PATH + "/" + productName + "-debug.apk"; apkPath = apkPath.Replace(" ", "\\ "); string[] installCommand = { "-d shell", "pm install -r", apkPath }; timerStart = System.DateTime.Now; if (adbTool.RunCommand(installCommand, null, out output, out error) != 0) return false; TimeSpan installTime = System.DateTime.Now - timerStart; UnityEngine.Debug.Log("OVRADBTool: Install Success"); // Start the application on the device IncrementProgressBar("Launching application on device . . ."); #if UNITY_2019_3_OR_NEWER string playerActivityName = "\"" + applicationIdentifier + "/com.unity3d.player.UnityPlayerActivity\""; #else string playerActivityName = "\"" + applicationIdentifier + "/" + applicationIdentifier + ".UnityPlayerActivity\""; #endif string[] appStartCommand = { "-d shell", "am start -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -S -f 0x10200000 -n", playerActivityName }; if (adbTool.RunCommand(appStartCommand, null, out output, out error) != 0) return false; UnityEngine.Debug.Log("OVRADBTool: Application Start Success"); IncrementProgressBar("Success!"); // If the user is using a USB 2.0 cable, inform them about improved transfer speeds and estimate time saved if (informLog) { float usb3Time = apkSize.Value / (USB_3_TRANSFER_SPEED * BYTES_TO_MEGABYTES); // `informLog` can't be true if `apkSize` is null. UnityEngine.Debug.Log(string.Format("OVRBuild has detected slow transfer speeds. A USB 3.0 cable is recommended to reduce the time it takes to deploy your project by approximatly {0:0.0} seconds", pushTime.TotalSeconds - usb3Time)); return true; } } else { UnityEngine.Debug.LogError("Could not find the ADB executable in the specified Android SDK directory."); } return false; } private static void OnBuildComplete() { showCancel = false; buildInProgress = false; currentStep = 0; SetProgressBarMessage("Waiting to build . . .", false); } private static bool CheckADBDevices(out string connectedDeviceName) { // Check if there are any ADB devices connected before starting the build process var adbTool = new OVRADBTool(OVRConfig.Instance.GetAndroidSDKPath()); connectedDeviceName = null; if (adbTool.isReady) { List devices = adbTool.GetDevices(); if (devices.Count == 0) { UnityEngine.Debug.LogError("No ADB devices connected. Connect a device to this computer to run APK."); return false; } else if (devices.Count > 1) { UnityEngine.Debug.LogError("Multiple ADB devices connected. Disconnect extra devices from this computer to run APK."); return false; } else { connectedDeviceName = devices[0]; return true; } } else { UnityEngine.Debug.LogError("OVR ADB Tool failed to initialize. Check the Android SDK path in [Edit -> Preferences -> External Tools]"); return false; } } private static void SetupDirectories() { gradleTempExport = Path.Combine(Path.Combine(Application.dataPath, "../Temp"), "OVRGradleTempExport"); gradleExport = Path.Combine(Path.Combine(Application.dataPath, "../Temp"), "OVRGradleExport"); if (!Directory.Exists(gradleExport)) { Directory.CreateDirectory(gradleExport); } } private static void InitializeProgressBar(int stepCount) { currentStep = 0; totalBuildSteps = stepCount; } private static void IncrementProgressBar(string message) { currentStep++; progressMessage = message; UnityEngine.Debug.Log("OVRBuild: " + message); } private static void SetProgressBarMessage(string message, bool log = true) { progressMessage = message; if (log) UnityEngine.Debug.Log("OVRBuild: " + message); } #endif //UNITY_EDITOR_WIN && UNITY_ANDROID }