以下の内容はhttps://bluebirdofoz.hatenablog.com/entry/2025/06/14/220930より取得しました。


MetaQuestでOnApplicationPauseとOnApplicationFocusの動作を確認する

本日はMetaQuestの小ネタ枠です。
MetaQuestでOnApplicationPauseとOnApplicationFocusの動作を確認したので記事に残します。

OnApplicationPauseとOnApplicationFocusとは

OnApplicationPauseとOnApplicationFocusはUnityのMonoBehaviourクラスで提供されるメソッドです。
これらのメソッドはアプリケーションのライフサイクルイベントを監視し、適切な処理を実行するために使用されます。

OnApplicationPause(bool pauseStatus)

OnApplicationPauseメソッドはアプリが一時停止または再開された時に呼び出されます。
pauseStatusがtrueの場合は一時停止、falseの場合は再開を意味します
本メソッドはMonoBehaviourで実装された全てのスクリプトで機能します。
docs.unity3d.com

Questアプリではプレイヤーがアプリを閉じた場合に一時停止が通知されます。
このため、OnApplicationPauseメソッドを使ってウィンドウが開いたことを検知できます。

OnApplicationFocus(bool hasFocus)

OnApplicationFocusメソッドはアプリがフォーカスを取得またはロストしたときに呼び出されます。
hasFocusがtrueの場合はフォーカス獲得、falseの場合はフォーカス喪失を意味します
本メソッドはMonoBehaviourで実装された全てのスクリプトで機能します。
docs.unity3d.com

Questアプリではプレイヤーがメニューボタンでウィンドウを開くとフォーカスをロストします。
このため、OnApplicationFocusメソッドを使ってウィンドウが開いたことを検知できます。

MetaQuestでのOnApplicationPauseとOnApplicationFocusの動作

以下のアプリ終了時にドキュメントフォルダに終了時刻を記録したテキストを出力するスクリプトを作成しました。
スクリプトを組み込んだアプリをQuest 3実機上で起動してOnApplicationPauseとOnApplicationFocusの動作を確認しました。

using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;

public class DocumentAccessTest : MonoBehaviour
{
#if UNITY_ANDROID
    /// <summary>
    /// OnApplicationFocusが発生した際にドキュメントフォルダにログを出力する
    /// </summary>
    private void OnApplicationFocus(bool focus)
    {
        string documentFolderPath = GetDocumentFolderPathPerAndroid29();
        string logFilePath = System.IO.Path.Combine(documentFolderPath, "OnApplicationFocusLog.txt");
        string focusTime = System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
        string logText = $"OnApplicationFocus: {focus} at {focusTime}\n";
        try
        {
            System.IO.File.AppendAllText(logFilePath, logText);
        }
        catch (System.Exception e)
        {
            Debug.LogError("Failed to write focus log: " + e.Message);
        }
    }

    /// <summary>
    /// OnApplicationPauseが発生した際にドキュメントフォルダにログを出力する
    /// </summary>
    private void OnApplicationPause(bool pause)
    {
        string documentFolderPath = GetDocumentFolderPathPerAndroid29();
        string logFilePath = System.IO.Path.Combine(documentFolderPath, "ApplicationPauseLog.txt");
        string pauseTime = System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
        string logText = $"OnApplicationPause: {pause} at {pauseTime}\n";
        try
        {
            System.IO.File.AppendAllText(logFilePath, logText);
        }
        catch (System.Exception e)
        {
            Debug.LogError("Failed to write pause log: " + e.Message);
        }
    }

    /// <summary>
    /// OnApplicationQuitが発生した際にドキュメントフォルダにログを出力する
    /// </summary>
    private void OnApplicationQuit()
    {
        string documentFolderPath = GetDocumentFolderPathPerAndroid29();
        string logFilePath = System.IO.Path.Combine(documentFolderPath, "OnApplicationQuitLog.txt");
        string exitTime = System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
        string logText = $"OnApplicationQuit: {exitTime}\n";
        try
        {
            // ドキュメントフォルダにログを出力
            System.IO.File.AppendAllText(logFilePath, logText);
        }
        catch (System.Exception e)
        {
            Debug.LogError("Failed to write exit log: " + e.Message);
        }
    }

    async void Start()
    {
        // AndroidSDK29以降では予めストレージへのアクセス許可をユーザに問い合わせて許可を取得する必要がある
        await GetStoragePermissionAsync();

        // ドキュメントフォルダのパスを取得する
        string documentFolderPath = GetDocumentFolderPathPerAndroid29();

        // テストとしてドキュメントフォルダにフォルダを作成する
        string folderPath = System.IO.Path.Combine(documentFolderPath, "TestFolder");
        if (!System.IO.Directory.Exists(folderPath)) System.IO.Directory.CreateDirectory(folderPath);
    }

    // 参考 https://communityforums.atmeta.com/t5/Quest-Development/Scoped-Storage-and-VR/td-p/1043602
    // AndroidSDK29以上でドキュメントフォルダへのアクセス権限を取得するための処理

    /// <summary>
    /// AndroidSDK29以降のドキュメントパスを取得する
    /// </summary>
    /// <returns></returns>
    private string GetDocumentFolderPathPerAndroid29()
    {
        // AndroidSDK29以降は/sdcard/Documents/のパスでアクセスできる
        string documentPath = "/sdcard/Documents/";
        return documentPath;
    }

    /// <summary>
    /// AndroidSDK29以降のフォルダアクセスの許可を取得する
    /// ユーザが許可するまで待機する
    /// </summary>
    /// <returns></returns>
    private async Task<bool> GetStoragePermissionAsync()
    {
        if (!UserHasManageExternalStoragePermission())
        {
            // ユーザに許可を求める
            AskForManageStoragePermission();
            // ユーザが許可するまで待機する
            while (!UserHasManageExternalStoragePermission())
            {
                await Task.Delay(100);
            }
        }

        return true;
    }

    private bool m_FolderPermissionOverride = false;

    /// <summary>
    /// アプリがフォルダアクセスの許可を持っているかどうか確認する
    /// </summary>
    private bool UserHasManageExternalStoragePermission()
    {
        bool isExternalStorageManager = false;
        try
        {
            AndroidJavaClass environmentClass = new AndroidJavaClass("android.os.Environment");
            isExternalStorageManager = environmentClass.CallStatic<bool>("isExternalStorageManager");
        }
        catch (AndroidJavaException e)
        {
            Debug.LogError("Java Exception caught and ignored: " + e.Message);
            Debug.LogError("Assuming this means this device doesn't support isExternalStorageManager.");
        }

        return m_FolderPermissionOverride || isExternalStorageManager;
    }

    /// <summary>
    /// ユーザにフォルダアクセスの許可を求める
    /// </summary>
    private void AskForManageStoragePermission()
    {
        try
        {
            using var unityClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
            using AndroidJavaObject currentActivityObject = unityClass.GetStatic<AndroidJavaObject>("currentActivity");
            string packageName = currentActivityObject.Call<string>("getPackageName");
            using var uriClass = new AndroidJavaClass("android.net.Uri");
            using AndroidJavaObject uriObject =
                uriClass.CallStatic<AndroidJavaObject>("fromParts", "package", packageName, null);
            using var intentObject = new AndroidJavaObject("android.content.Intent",
                "android.settings.MANAGE_APP_ALL_FILES_ACCESS_PERMISSION", uriObject);
            intentObject.Call<AndroidJavaObject>("addCategory", "android.intent.category.DEFAULT");
            currentActivityObject.Call("startActivity", intentObject);
        }
        catch (AndroidJavaException e)
        {
            m_FolderPermissionOverride = true;
            Debug.LogError("Java Exception caught and ignored: " + e.Message);
            Debug.LogError("Assuming this means we don't need android.settings.MANAGE_APP_ALL_FILES_ACCESS_PERMISSION (e.g., Android SDK < 30)");
        }
    }
#endif
}

終了時の処理を確認するため、以下のドキュメントフォルダへのアクセス方法を利用しています。
bluebirdofoz.hatenablog.com

実機での動作確認

Quest実機上で以下のメニュー画面を開き、10秒待って[閉じる]ボタンを実行してアプリを閉じてみました。

結果、ドキュメントフォルダにOnApplicationPauseとOnApplicationFocusの実行ログが出力されました。
実行タイミングを確認すると、OnApplicationPauseは[閉じる]ボタンを押したタイミング、OnApplicationFocusはメニューを開いたタイミングで実行されていました。


[閉じる]ボタンによるアプリの終了はOnApplicationQuitでは取得できませんが、OnApplicationPauseを参照すれば検知できることが分かりました。
bluebirdofoz.hatenablog.com




以上の内容はhttps://bluebirdofoz.hatenablog.com/entry/2025/06/14/220930より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14