"Install And Run"を作る

3年ぶりです。お久しぶりです。(その2)

さて、UnityでAndroidを使って実機デバッグをしていると、「なんで"Build And Run"はあるのに"Install And Run"がないの？？」と思うことがあります…よね？。変更点がなくてもビルドするとそれなりに時間かかるし。ということで、作ろう。

```C#:InstallAndRun.cs
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

// APKファイルのインストールと実行を行うメニュースクリプト
// (もちろんAndoridターゲット専用だけどWindowsとmacOSに対応)

public class InstallAndRun
{
	[MenuItem("File/Run without Installation", true)]
	public static bool IsRunValidated()
	{
		return EditorUserBuildSettings.activeBuildTarget == BuildTarget.Android && System.IO.File.Exists(EditorUserBuildSettings.GetBuildLocation(BuildTarget.Android));
	}

	[MenuItem("File/Run without Installation", false, 221)]
	public static void RunCommand()
    {
		// Run もしくは Restart
		string windowTitle = "Run";
		string adbPath = UnityEditor.Android.AndroidExternalToolsSettings.sdkRootPath + "/platform-tools/adb";
        if (Application.platform == RuntimePlatform.WindowsEditor) adbPath = adbPath.Replace("/", "\\") + ".exe";

		if (string.IsNullOrEmpty(adbPath) || !System.IO.File.Exists(adbPath))
		{
			EditorUtility.DisplayDialog(windowTitle + " - Error", $"AndroidSDK is not found. Please install AndroidSDK via Unity Hub.", "OK");
			return;
		}

		var runProcess = new System.Diagnostics.Process();
		runProcess.StartInfo.CreateNoWindow = true;
		runProcess.StartInfo.UseShellExecute = false;
		runProcess.StartInfo.FileName = adbPath;
		runProcess.StartInfo.Arguments = $"shell am start -S -n {Application.identifier}/com.unity3d.player.UnityPlayerActivity";	// "-S" で強制終了付きスタート
		runProcess.Start();
	}

	[MenuItem("File/Install And Run", true)]
	public static bool IsInstallAndRunCommandValidated()
    {
		// Androidプラットフォーム以外は動作未確認なので閉じておく
		return EditorUserBuildSettings.activeBuildTarget == BuildTarget.Android && System.IO.File.Exists(EditorUserBuildSettings.GetBuildLocation(BuildTarget.Android));
	}

	[MenuItem("File/Install And Run", false, 220)]
	public static void InstallAndRunCommand()
	{
		string windowTitle = "Install And Run";
		string adbPath = UnityEditor.Android.AndroidExternalToolsSettings.sdkRootPath + "/platform-tools/adb";
        if (Application.platform == RuntimePlatform.WindowsEditor) adbPath = adbPath.Replace("/", "\\") + ".exe";
		string apkPath = EditorUserBuildSettings.GetBuildLocation(BuildTarget.Android);

		if (string.IsNullOrEmpty(adbPath) || !System.IO.File.Exists(adbPath))
        {
			// AndroidSDK(adb)がインストールされているかどうか
			EditorUtility.DisplayDialog(windowTitle + " - Error", $"AndroidSDK is not found. Please install AndroidSDK via Unity Hub.", "OK");
			return;
		}

        // APKファイルが生成済みかどうか
        if (string.IsNullOrEmpty(apkPath) || !System.IO.File.Exists(apkPath))
        {
            EditorUtility.DisplayDialog(windowTitle + " - Error", $"Apk file is not found. Please build in advance.\n\n\"File\" -> \"Build Settings...\" -> \"Build\" or \"Build And Run\"", "OK");
            return;
        }

        // adb.exeを実行
        var process = new System.Diagnostics.Process();
        process.StartInfo.CreateNoWindow = true;
        process.StartInfo.UseShellExecute = false;
        process.StartInfo.RedirectStandardOutput = true;
        process.StartInfo.RedirectStandardError = true;
        process.StartInfo.FileName = adbPath;
        process.StartInfo.Arguments = "install -r -d \"" + apkPath + "\"";

        // 本当は転送するデバイスをBuildSettingsでの設定に合わせたいところだけど取得するAPIが見当たらないからデフォルトでいいや…。
        // 本当はプログレスバーくらい表示したいところだけどUnity側で何か出してくれているのでいいや…。

        process.Start();
        process.WaitForExit();

        // 結果発表
        if (process.ExitCode == 0)
        {
            // 先に実行する
            var runProcess = new System.Diagnostics.Process();
            runProcess.StartInfo.CreateNoWindow = true;
            runProcess.StartInfo.UseShellExecute = false;
            runProcess.StartInfo.FileName = adbPath;
            runProcess.StartInfo.Arguments = $"shell am start -n {Application.identifier}/com.unity3d.player.UnityPlayerActivity";  // 普通にスタート(インストールでアプリ強制終了済み)
            runProcess.Start();

            // 成功
            if (!EditorUtility.DisplayDialog(windowTitle + " - Success", "Successflly installed.\n\n" + apkPath, "OK", "Show in Explorer"))
            {
                // 必要ならapkファイルのフォルダをExplorerで開く
                EditorUtility.RevealInFinder(apkPath);
            }
        }
        else
        {
            // 失敗
            string errorMessage = process.StandardError.ReadToEnd();
            switch (process.ExitCode)
            {
                case -1:
                    // Android端末がPCに接続されていないっぽい
                    errorMessage = "Android device is not connected.";
                    break;
                default:
                    // 上記以外のエラーは標準エラー出力を見たほうが早い
                    break;
            }
            EditorUtility.DisplayDialog(windowTitle + " - Error", $"Install failed. Error {process.ExitCode}\n\n" + errorMessage, "OK");
        }
    }
}
```

使い方は

- スクリプトを任意のEditorフォルダに入れる。
- 事前に"Build"か"Build And Run"でビルドする。
  - 自前でビルドしたApkファイルがないとメニューを実行できない。
  - ビルドしたApkファイルを削除していなければ次回Editorを起動した時も実行可能。
- "Install And Run"を実行する → インストールと実行が行われる。
- "Run without Installation"を実行する → インストール済みのApkをリスタートする。

という感じ。

実装のキモはここらへんでございます。

- "Build"や"Build And Run"でビルド済みのAPKファイルのみを対象とする。
  - 設定をBuildSettingsと全く同じにしてスクリプトからビルドするのがわりと大変なので。 
- ビルド済みAPKファイルのパス
  - EditorUserBuildSettings.GetBuildLocation(BuildTarget.Android)
- インストール済みAndroidSDKのパス
  - UnityEditor.Android.AndroidExternalToolsSettings.sdkRootPath
- Unityで作ったアプリのアクティビティ名
  - $"{Application.identifier}/com.unity3d.player.UnityPlayerActivity"
- Androidでのインストール済みアプリの再実行
  - "adb shell am start -S -n アクティビティ名" ("-S"がキモ)
- いわゆる"Show in Explorer"もしくは"Show in Finder"
  - EditorUtility.RevealInFinder(ファイルパス)
- 面倒なのでAndroidビルド以外は非対応。
- もしかしたらLinux版でも動く？　

作ってから同僚が同じようなものを作っているのを知ったので、ここに放流する次第です…。