UnityでのPush通知実装メモ

# はじめに
新規開発のアプリでPush通知を実装しようとしたら思いの外苦労して、調べてもなかなかいい記事が見つからなかったので、自分のメモがてら記事にしようと思った内容です。
おいおいそんなことで詰まるなよというようなところで詰まっているかもしれないですが、初めて実装するのでご容赦を…

# 環境
Unity2021.3.18f
VisualStudio2022
Firebase Cloud Message
を使用しています。
AndroidとiOS両方に対応します。


# 必要なパッケージ
- [Firebase Cloud Messaging](https://firebase.google.com/download/unity?hl=ja)
こちらからダウンロードしたZipファイル解答し、中のFirebaseMessaging.unitypackageをインポートする

- [Mobile Notification](https://docs.unity3d.com/ja/Packages/com.unity.mobile.notifications@2.1/manual/index.html)
Unityのパッケージマネージャーからプロジェクトに追加する

# 実装について
基本的には[Firebase Cloud Messagingの導入ページ](https://firebase.google.com/docs/cloud-messaging/unity/client?hl=ja)に書かれている手順でOKです。

## Firebaseに端末を登録
```C#
public void RegisterCallbacks()
{
    // 簡略化の為にラムダ式を使用していますが、コールバックの登録解除などを行う場合は関数を定義しておくほうがいいでしょう
    FirebaseMessaging.TokenReceived += (_, args) => Debug.Log($"On Receive token: {args.Token}");
    FIrebaseMessaging.MessageReceived += (_, args) => Debug.Log($"On Received message: {args.Message}");
}
```
## Firebaseの依存解決
Androidの場合はFirebaseの依存関係を解決する必要がある為、以下のコードを先に実行してください。
```C#
var result = await Firebase.FirebaseApp.CheckAndFixDependenciesAsync();
if (result != Firebase.DependencyStatus.Available)
{
    UnityEngine.Debug.LogError($"Could not resolve all Firebase dependencies {result}");
    return;
}
```
## プッシュ通知を許可するか確認のダイアログを表示する
更にAndroid13から通知を表示するにはユーザーに通知を許可するかの確認を行い、許可を貰う必要がありますが、自動ではダイアログを表示しないので、自力で表示します。
そこで、Unity Mobile Notificationsの機能を使用して、ユーザーに確認を行うようにします。
コードは以下の通りで、念のためiOSでも表示できるように機能を用意しておきます。
```C#
// Androidはこちら
private async UniTask CheckPermission()
{
	var isCheckedPermission = false;
	var callbacks = new PermissionCallbacks();
	callbacks.PermissionGranted += _ => isCheckedPermisssion = true;
	callbacks.PermissionDenied += _ => isCheckedPermission = true;
	callbacks.PermissionDeniedAndDontAskAgain += isCheckedPermission = true;
	Permission.RequestUserPermission("android.permissin.POST_NOTIFICATIONS", callbacks);
	await UniTask.WaitUnitl(() => isCheckedPermission);
}

// iOSはこちら
private async UniTask CheckPermission()
{
	var option = AuthorizationOption.Alert | AutorizationOption.Badge;
	using (var request = new AuthorizationRequest(option, true))
	{
		await UniTask.WaitUntil(() => request.isFinished || !string.IsNullOrEmpty(request.Error));
		var response = $@"
RequestAuthorization:
finished: {request.IsFinished}
granted: {request.Granted}
error: {request.Error}
deviceToken: {request.DeviceToken}";
		Debug.Log(response);
	}
}
```

これらのコードで、バックグラウンド・フォアグラウンドの両方で通知を受け取り、ポップアップを表示することができます。

## アプリ内でPush通知の受け取りの可否を設定できるようにする
次にGoogleのポリシーに対応するため、アプリ内のオプションでプッシュ通知を受け取るかどうかの設定を変更できるようにします。
内容としては、OffにするならFirebaseに登録しているトークンを削除し、アプリ内で登録しているコールバックを削除します。
大体以下のコードで実現できます。
```C#
private async UniTask StartReceiveNotification()
{
	FirebaseMessaging.TokenReceived += OnReceivedToken;
	FirebaseMessaging.MessageReceived += OnMessageReceive;
	var token = await FirebaseMessaging.GetTokenAsync();
	Debug.Log($"Received token {token}");
}

private async UniTask StopReceiveNotification()
{
	await FirebaseMessaging.DeleteTokenAsync();
	FirebaseMessaging.TokenReceived -= OnReceivedToken;
	FirebaseMessaging.MessageReceived -= OnMessageReceive;
}
```
後は設定自体をPlayerPrefsなどの媒体に保存して、起動時にトークンの登録処理を行うかどうかの判定をすることで、対応が可能です。

# 詰まりポイント
## Androidでバックグラウンドの通知を受け取れない
Android13以降のOSでは、ユーザーが許可を出していないとアプリがバックグラウンドにある場合に通知を受け取れないようになっています。
こちらは、Firebaseの資料になくエラーログなども出ないため調査に時間がかかりました。
(もしかしたら見落としているだけかもしれません)
[プッシュ通知を許可するか確認のダイアログを表示する](#プッシュ通知を許可するか確認のダイアログを表示する)

## プッシュ通知の受け取り拒否について
やりたいことはプッシュ通知を受け取らないようにすることです。
以下のコードを呼び出すことで解決します。
```C#
FirebaseMessaging.DeleteTokenAsync()
```
Firebaseの資料にはトークンをプロジェクトから削除するとしか書かれておらず、ナンノコッチャと思っていましたが、**通知送信対象端末リスト**から削除してくれるそうです。
ちなみに登録しているのは以下のコードです。
```C#
var token = await FirebaseMessaging.GetTokenAsync();
```

## 端末上の通知ステータスを取得する方法
基本的にUnity Mobile Notificationの機能で取得できます。
こちらはOS依存でコードが変わります。
```C#
public bool HasAuthorized()
{
#if UNITY_ANDROID
    return AndroidNotificationCenter.UserPermissionToPost == PermissionStatus.Allowed;
#elif UNITY_IOS
    return iOSNotificationCenter.GetNotificationSettings().AuthorizationStatus ==
                   AuthorizationStatus.Authorized;
#else
    return false;
#endif
}
```
ですが、このままだとAndroid12以前のOSでは必ず許可されているという扱いになってしまい、本体の設定を変えてもプロジェクト側で検知できません。
こちらはネイティブプラグインを作成するか、以下のJavaのプログラムを呼び出すことで取得することができるようになります。
```C#
private bool HasAuthorizedOldAPI()
{
    try
    {
        using var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
        using var activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
        using var notificationClass = new AndroidJavaClass("androidx.core.app.NotificationManagerCompat");
        using var context = activity.Call<AndroidJavaObject>("getApplicationContext");
        using var notificationObject = notificationClass.CallStatic<AndroidJavaObject>("from", context);
        return notificationObject.Call<bool>("areNotificationsEnabled");
    }
    catch (Exception e)
    {
        Debug.LogException(e);
        return false;
    }
}
```
古いOSバージョンかどうかを確認するには以下のコードで確認できます。
```C#
using var version = new AndroidJavaClass("android.os.Build$VERSION");
    return version.GetStatic<int>("SDK_INT") < Android13Version;
```
最終的に以下のようなコードになりました。
```C#
public bool HasAuthorized()
{
#if UNITY_ANDROID
    if (IsOldAPI())
        return HasAuthorizedOldAPI();
    return AndroidNotificationCenter.UserPermissionToPost == PermissionStatus.Allowed;
#elif UNITY_IOS
    return iOSNotificationCenter.GetNotificationSettings().AuthorizationStatus ==
                   AuthorizationStatus.Authorized;
#else
    return false;
#endif
}

private bool HasAuthorizedOldAPI()
{
    try
    {
        using var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
        using var activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
        using var notificationClass = new AndroidJavaClass("androidx.core.app.NotificationManagerCompat");
        using var context = activity.Call<AndroidJavaObject>("getApplicationContext");
        using var notificationObject = notificationClass.CallStatic<AndroidJavaObject>("from", context);
        return notificationObject.Call<bool>("areNotificationsEnabled");
    }
    catch (Exception e)
    {
        Debug.LogException(e);
        return false;
    }
}

private bool IsOldAPI()
{
    using var version = new AndroidJavaClass("android.os.Build$VERSION");
        return version.GetStatic<int>("SDK_INT") < Android13Version;
}
```

## Androidでアプリがフォアグラウンドにあるとポップアップが出ない
アプリを起動しているときに通知を受け取れているのに、ポップアップが表示されませんでした。
(バックグラウンドのときはポップアップが表示される)
そこで、通知を受け取った際にUnity Mobile Notificationの機能でローカルプッシュ通知を表示することで解決しました。
```C#
public void Initialize()
{
    FirebaseMessaging.MessageReceived += OnMessageReceive;
}

protected override void OnMessageReceive(object sender, MessageReceivedEventArgs args)
{
    base.OnMessageReceive(sender, args);
    // Androidは何故かアプリがフォアグラウンドにあるとプッシュ通知のポップアップが出ないので、自力で出す。
    var notification = args.Message.Notification;
    SendNotification(notification.Title, notification.Body);
}

public override void SendNotification(string title, string message)
{
    var notification = new AndroidNotification(title, message, DateTime.Now)
    {
        SmallIcon = IconId,
        ShouldAutoCancel = true,
        ShowTimestamp = true
    };
    AndroidNotificationCenter.SendNotification(notification, RemoteChannelId);
}
```
今思えばプッシュ通知の許可をユーザーに取っていないことが原因だったような気がします。

## SignInWithAppleと併用する
OnPostprocessBuildでXCodeプロジェクトに変更を加える際に、不手際で正常に機能しなくなったので、そちらの解決メモ
```C#
// まずは駄目なコード
public void OnPostprocessBuild(BuildReport report)
{
    if (report.summary.platform != BuildTarget.iOS) return;
    var projectPath = PBXProject.GetPBXProjectPath(report.summary.outputPath);
    var project = new PBXProject();
    project.ReadFromString(File.ReadAllText(projectPath));
    var mainTargetGUID = project.GetUnityMainTargetGuid();
    var frameworkTargetGUID = project.GetUnityFrameworkTargetGuid();
    
    project.SetBuildProperty(mainTargetGUID, "ENABLE_BITCODE", "NO");
    project.SetBuildProperty(frameworkTargetGUID, "ENABLE_BITCODE", "NO");

    var manager = new ProjectCapabilityManager(projectPath, "Entitlements.entitlements", null, mainTargetGUID);
    manager.AddBackgroundModes(BackgroundModesOptions.RemoteNotifications);
    manager.AddPushNotifications(true);
    Debug.Log("Enable push notice");

    if (IsDevelopment(report))
    {
        // Enterpriseアカウントでビルドエラーを防ぐ対策
        // NOTE: この処理自体は capabilityManager.AddSignInWithAppleWithCompatibility() にも含まれている
        project.AddFrameworkToProject(frameworkTargetGUID, "AuthenticationServices.framework", true);
        project.WriteToFile(projectPath);
        Debug.Log("Enable sign in with apple in development mode");
    }
    else
    {
        project.WriteToFile(projectPath);
        var manager = new ProjectCapabilityManager(projectPath, "Entitlements.entitlements", null, mainTargetGUID);
        manager.AddSignInWithAppleWithCompatibility(frameworkTargetGUID);
        manager.WriteToFile();
        Debug.Log("Enable sign in with apple in release mode");
    }
    project.WriteToFile(projectPath);
    // 内部で保持しているProjectのインスタンスが異なるので、上記のコードで保存した内容が反映されていない
    // そのため、以下のコードで保存した際に上記までのprojectに施した修正がなかったことになってしまう。
    manager.WriteToFile();
}
```

大丈夫なコード
```C#
public void OnPostprocessBuild(BuildReport report)
{
    if (report.summary.platform != BuildTarget.iOS) return;

    var projectPath = PBXProject.GetPBXProjectPath(report.summary.outputPath);
    var isDevelopment = IsDevelopment(report);
    ModifyProject(projectPath, isDevelopment);
    ModifyCapability(projectPath, isDevelopment);
}

private void ModifyCapability(string projectPath, bool isDevelopment)
{
    var project = LoadProject(projectPath);
    var manager = new ProjectCapabilityManager(projectPath, "Entitlements.entitlements", null, project.GetUnityMainTargetGuid());
    manager.AddBackgroundModes(BackgroundModesOptions.RemoteNotifications);
    manager.AddPushNotifications(true);
    Debug.Log("Enable push notice");
    if (!isDevelopment)
    {
        manager.AddSignInWithAppleWithCompatibility(project.GetUnityFrameworkTargetGuid());
        Debug.Log("Enable sign in with apple in release mode");
    }
    manager.WriteToFile();
}

private PBXProject LoadProject(string projectPath)
{
    var project = new PBXProject();
    project.ReadFromString(File.ReadAllText(projectPath));
    return project;
}

private void ModifyProject(string projectPath, bool isDevelopment)
{
    var project = LoadProject(projectPath);
    var mainTargetGUID = project.GetUnityMainTargetGuid();
    var frameworkTargetGUID = project.GetUnityFrameworkTargetGuid();
    if (isDevelopment)
    {
        project.AddFrameworkToProject(frameworkTargetGUID, "AuthenticationServices.framework", true);
        Debug.Log("Enable sign in with apple in development mode");
    }
    project.AddFrameworkToProject(frameworkTargetGUID, "AppTrackingTransparency.framework", true);
    project.SetBuildProperty(mainTargetGUID, "ENABLE_BITCODE", "NO");
    project.SetBuildProperty(frameworkTargetGUID, "ENABLE_BITCODE", "NO");
    project.WriteToFile(projectPath);
}
```
`ProjectCapabilityManager`と`PBXProject`の変更を並行して行うと後に反映した内容で上書きをしてしまうので、順番に反映していくことで解決できます。
できるだけ効率よくしたくて、インスタンスを使いまわそうとしたのが良くない方向に行ってしまったようです。
ちゃんと中身を見て実装したほうがいいですね。

# おわりに
Push通知を実装する際に資料を探したものの古い記事しかなく、また断片的な資料ばかりで苦労したので、この資料をまとめました。
もし、この資料が皆さんの役に立てたのなら幸いです。