UnityでiOSのPushNotification(APNS)を実装する方法

UnityでAndroidのPushNotification(GCM)を実装する方法 | ひささん日記のとき同様、Apple Push Notifications (PNS for iOS) – Unity | Games2win Developersこちらのサイトのとおりやってみました。

iOS版の場合、NotificationServicesを使って自前で書く方法がちょいちょいググると出てくるんですが、この方法はremoteNotificationCountが1以上の場合かどうかで判断していて、ポーリングしないとダメみたいであんまりやりたくありませんでした。
たとえOnFixedUpdate内でやったとしても常にPushNotificationがあるかどうかのために回すのはあんまりイケていないです。
このあたりの議論は、Unity 3.5 and Push Notificationsここに載っています。

その点、prime[31] – Unity plugin documentationはイベントハンドラを登録しておくことができるので、ポーリングする必要がありません。
今回はこのAssetの使い方がメインになります。

事前にPushNotification用の.p12(証明書)を作成する

このあたりはググるとすぐに出てくるので割愛。
.pemファイルを作る方法が多いのですが、キーチェーンアクセスから.p12ファイルを書き出してそれを使っても動くので、今回は.p12ファイルを使っています。
(どうゆう環境でPushNotificationするかによるのかもしれません)

PushNotification用のunitypackageをimportする

Asset Storeから prime31 Etcetera Plugin などで検索し、購入後にimportしておきます。

demoのシーンを使ってPushNotificationを試してみる

demoにシーンがあるので、そちらを表示しておく。

EtceteraEventListener.cs

StringBuilderを使うので、以下を追加。

using System.Text;

アプリ起動時にPushNotification用のdeviceTokenを取得するAPIを実行します。(詳細は別クラスで)
ここのremoteRegistrationSucceededの中でdeviceTokenが取れるので、PushNotificationを配信するサーバーに登録しましょう。

private string regURL = null;

void remoteRegistrationSucceeded( string deviceToken )
{
  Debug.Log( "remoteRegistrationSucceeded with deviceToken: " + deviceToken );

  //string apns_env = "production";
  string apns_env = "develop";

  StringBuilder urlString = new StringBuilder ();

  urlString.AppendFormat ("&appName={0}", WWW.EscapeURL ("app_name")); // provided by G2W
  urlString.AppendFormat ("&app_id={0}", WWW.EscapeURL ("PNS_ID")); // provided by G2W
  urlString.AppendFormat ("&appversion={0}", WWW.EscapeURL ("APP_Version")); // provided by G2W
  urlString.AppendFormat ("&deviceuid={0}", WWW.EscapeURL ("00"));
  urlString.AppendFormat ("&devicetoken={0}", WWW.EscapeURL (deviceToken));
  urlString.AppendFormat ("&devicename={0}", WWW.EscapeURL (SystemInfo.deviceName));
  urlString.AppendFormat ("&devicemodel={0}", WWW.EscapeURL (SystemInfo.deviceModel));
  urlString.AppendFormat ("&deviceversion={0}", WWW.EscapeURL (SystemInfo.graphicsDeviceVersion));
  urlString.AppendFormat ("&apns_env={0}", WWW.EscapeURL (apns_env));

  Debug.Log ("http://" + ("hoge.com/" + urlString));
  regURL = urlString.ToString ();

  StartCoroutine ("RegDeviceForPush");
}

IEnumerator RegDeviceForPush ()
{
  WWW w = new WWW ("http://" + ("hoge.com/" + regURL), UTF8Encoding.UTF8.GetBytes (regURL));
  yield return w;

  Debug.Log ("EEEEE: " + w.error);
}

アプリがフォアグラウンドでPushNotificationを受けた場合。

void remoteNotificationReceived( IDictionary notification )
{
  Debug.Log( "remoteNotificationReceived" );
  Prime31.Utils.logObject( notification );
}

アプリがバックグラウンドでPushNotificationを受け、さらにその通知自体をタップした場合。

void remoteNotificationReceivedAtLaunch( IDictionary notification )
{
  Debug.Log( "remoteNotificationReceivedAtLaunch" );
  Prime31.Utils.logObject( notification );
}

EtceteraGUIManagerTwo.cs

アプリ起動時にEtceteraBinding.registerForRemoteNotifcationsを呼んで、deviceTokenを登録します。

if( GUILayout.Button( "Register for Push" ) )
{
  EtceteraBinding.registerForRemoteNotifcations( P31RemoteNotificationType.Alert | P31RemoteNotificationType.Badge | P31RemoteNotificationType.Sound );
}

タイミングの話し

PushNotificationするときに必ずこの話しが出てきちゃう(ぼくだけかな?)んですが、ネイティブの場合は、

アプリ側でPushが来るパターンは以下の3つ

  1. フォアグラウンドでプッシュ通知を受け取ったとき
  2. アプリのプロセスがバックグラウンドで生きているときにプッシュ通知を受け取り、ユーザーがアクションボタンをタップしたとき
  3. アプリのプロセスがバックグラウンドで生きていないときにプッシュ通知を受け取り、ユーザーがアクションボタンをタップしたとき

ただ、2の項目は、アプリ内にバッジのようなものを表示する場合は、 ここではやらずapplicationDidBecomeActiveでやるようにしている。
これの理由は、バックグラウンドにいてアクションボタンではなく、ホーム画面からアプリアイコンをタップされた場合の処理が必要なので、
あえて、applicationDidBecomeActiveでやっているのです。
ネイティブの実装例は、PushNotification-iOSandAndroid/iOS/AppDelegate.m at master · hisasann/PushNotification-iOSandAndroidこちらをご覧ください。

問題はこの2のapplicationDidBecomeActiveでやるような処理をUnityでどうやるかですね。

ここでNotificationServicesを使ってもできそうなんですが、
せっかくなので、Etceteraを使ってみましょう。

if( EtceteraBinding.getBadgeCount() > 0)
{
  // 何か処理をゴニョゴニョ

  // バッジの数値をクリア
  EtceteraBinding.setBadgeCount( -1 );
}

アプリケーションがOnResumeしたタイミングでこんな感じでPushNotificationが来ていたかどうかを確認できます。

参考リンク

prime[31] – Unity plugin documentation
Unity and IOS push notifications – Google Docs

詳細! Objective-C iPhoneアプリ開発 入門ノート Xcode5+iOS7対応
大重 美幸
ソーテック社
売り上げランキング: 2,016

UnityでAndroidのPushNotification(GCM)を実装する方法

ググってもあんまり有力な記事がなく、AssetStoreを探してみても良さそうなのがなくまだまだUnityでのPushNotificationはこれからなのかなーという印象を受けました。

過去にネイティブでやったときのコードはgithubにまとめてあるので、そちらをご覧ください。
PushNotification-iOSandAndroid/Android at master · hisasann/PushNotification-iOSandAndroid

そんな中、試してみようかと感じた記事がこちら。
Android PNS for Unity Games | Games2win Developers
iOS版の記事もあり、そちらはPrime31のAssetを使っていて、良さそう!と思いました。

Asset Store – Android Etcetera PluginこれのiOS版にはPushNotificationのコードが入っていますが、Android版にはない様子。
でも↑の記事の中でつかつているunitypackageの中に少しPrime31のコードもあったので、そちらを使うで十分そう。

事前にSender Id、PushNotification用のIdを作成する

よくやる作業なので、ここでの説明は割愛。

この作業はGoogle Developers Consoleで行います。
ProductIdというのができていたのですが、そちらではPushNotificationがうまく動かず、Project Numberを使うといけました。

PushNotification用のunitypackageをimportする

GoogleCloudMessaging-Modified.unitypackage – Google Drive
こちらからDLし、Unityにインポートしておきます。

AndroidManifest.xmlを作成する

↑のunitypackageを入れると、prime[31]のエディター拡張がインストールされるので、それを使うのが便利です。
メニューからprime[31] → Generate AndroidManifest.xml Fileこれを実行すると、

Assets/Plugins/Android

の下に、AndroidManifest.xmlファイルが作成されます。
すでにある場合はマージしてくれるようです。

一応貼っておくとこんな感じ。

<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.unity3d.player"
    android:installLocation="preferExternal"
    android:theme="@android:style/Theme.NoTitleBar"
    android:versionCode="1"
    android:versionName="1.0">
    <supports-screens
        android:smallScreens="true"
        android:normalScreens="true"
        android:largeScreens="true"
        android:xlargeScreens="true"
        android:anyDensity="true"/>

    <application
    android:icon="@drawable/app_icon"
        android:label="@string/app_name"
        android:debuggable="true">
        <activity android:name="com.prime31.UnityPlayerNativeActivity" android:screenOrientation="portrait"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <meta-data android:name="unityplayer.UnityActivity" android:value="true" />
            <meta-data android:name="unityplayer.ForwardNativeEventsToDalvik" android:value="true" />
        </activity>

        <receiver
            android:name="com.prime31.GCMBroadcastReceiver"
            android:permission="com.google.android.c2dm.permission.SEND">
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.RECEIVE"/>
                <action android:name="com.google.android.c2dm.intent.REGISTRATION"/>
                <category android:name="PACKAGE_NAME"/>
            </intent-filter>
        </receiver>

    <meta-data android:name="com.prime31.GoogleCloudMessagingPlugin" android:value="UnityPlayerActivityProxy"/>
  </application>

  <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
  <uses-permission android:name="android.permission.INTERNET"/>
  <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
  <uses-permission android:name="android.permission.USE_CREDENTIALS"/>
  <permission android:name="PACKAGE_NAME.permission.C2D_MESSAGE" android:protectionLevel="signature"/>
  <uses-permission android:name="PACKAGE_NAME.permission.C2D_MESSAGE"/>

</manifest>

重要な箇所は、

<receiver
    android:name="com.prime31.GCMBroadcastReceiver"
    android:permission="com.google.android.c2dm.permission.SEND">
    <intent-filter>
        <action android:name="com.google.android.c2dm.intent.RECEIVE"/>
        <action android:name="com.google.android.c2dm.intent.REGISTRATION"/>
        <category android:name="PACKAGE_NAME"/>
    </intent-filter>
</receiver>

  <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
  <uses-permission android:name="android.permission.INTERNET"/>
  <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
  <uses-permission android:name="android.permission.USE_CREDENTIALS"/>
  <permission android:name="PACKAGE_NAME.permission.C2D_MESSAGE" android:protectionLevel="signature"/>
  <uses-permission android:name="PACKAGE_NAME.permission.C2D_MESSAGE"/>

ここ。

PACKAGE_NAMEというのが3箇所あります。
ここを自分のUnityProjectのBundleIdと同じにします。

はじめ、1個目のPACKAGE_NAMEしか見えていなく、ここだけ直してビルドしていたらいっこうに通らなくてハマりました。
そんときのエラーはこちら。
Android SDK Manifest file error: INSTALL_PARSE_FAILED_MANIFEST_MALFORMED – Stack Overflow

demoのシーンを使ってPushNotificationを試してみる

demoにシーンがあるので、そちらを表示しておく。
EventListenerとuiというGameObjectがあるので、そのソースを表示する。

GoogleCloudMessagingEventListener.cs

大事なメンバはnotificationReceivedEventメソッドとregistrationSucceededEventメソッドです。
元記事のサンプルをもとに書くとこんな感じです。

using UnityEngine;
using System;
using System.Text;
using System.Collections;
using System.Collections.Generic;

public class GoogleCloudMessagingEventListener : MonoBehaviour
{
#if UNITY_ANDROID
  private string regURL;
  private const string host = "test.com/add/";

  //PNS ID provided by the publishing team
  private const string pnsID = "7";

  //PNS Environment = should be 'production' in the release build
  private const string pnsEnvironment = "sandbox";

  //APP version
  private const string pnsAppVersion = "1.0";

  void OnEnable()
  {
    // Listen to all events for illustration purposes
    GoogleCloudMessagingManager.notificationReceivedEvent += notificationReceivedEvent;
    GoogleCloudMessagingManager.registrationSucceededEvent += registrationSucceededEvent;
    GoogleCloudMessagingManager.unregistrationFailedEvent += unregistrationFailedEvent;
    GoogleCloudMessagingManager.registrationFailedEvent += registrationFailedEvent;
    GoogleCloudMessagingManager.unregistrationSucceededEvent += unregistrationSucceededEvent;
  }


  void OnDisable()
  {
    // Remove all event handlers
    GoogleCloudMessagingManager.notificationReceivedEvent -= notificationReceivedEvent;
    GoogleCloudMessagingManager.registrationSucceededEvent -= registrationSucceededEvent;
    GoogleCloudMessagingManager.unregistrationFailedEvent -= unregistrationFailedEvent;
    GoogleCloudMessagingManager.registrationFailedEvent -= registrationFailedEvent;
    GoogleCloudMessagingManager.unregistrationSucceededEvent -= unregistrationSucceededEvent;
  }



  void notificationReceivedEvent( Dictionary<string,object> jsonDict )
  {
    Debug.Log( "notificationReceivedEvent" );
    Prime31.Utils.logObject( jsonDict );

    int badgeNum = int.Parse(jsonDict["badge"].ToString());
    string openURL = jsonDict["open_url"].ToString();
    if(!string.IsNullOrEmpty(openURL))
    {
      Application.OpenURL(openURL);
    }
    GoogleCloudMessaging.setBadge(badgeNum);
    GoogleCloudMessaging.cancelAll();
    showBadge();
  }


  void registrationSucceededEvent( string registrationId )
  {
    Debug.Log( "registrationSucceededEvent: " + registrationId );

    StringBuilder urlString = new StringBuilder ();
    urlString.AppendFormat('?registration_id={0}',WWW.EscapeURL(registrationId));
    urlString.AppendFormat('?app_id={0}',WWW.EscapeURL(pnsID));
    urlString.AppendFormat('?app_version={0}',WWW.EscapeURL(pnsAppVersion));
    urlString.AppendFormat('?apns_env={0}',WWW.EscapeURL(pnsEnvironment));

    regURL = urlString.ToString ();
    Debug.Log("regURL: " + regURL);

    StartCoroutine ("RegDeviceForPush");
  }


  void unregistrationSucceededEvent()
  {
    Debug.Log( "UnregistrationSucceededEvent" );
  }

  IEnumerator RegDeviceForPush ()
  {
    WWW w = new WWW ("http://" + (host + regURL), UTF8Encoding.UTF8.GetBytes (regURL));
    yield return w;
  }

#endif
}

GoogleCloudMessagingUI.cs

重要な箇所は、

GoogleCloudMessaging.register( "1234567890" );  // sender id

string pushIOApiKey = "lkajdsflkajsdflkajsdflkjadsflkj";  // push key

です。

using UnityEngine;
using System;
using System.Net;
using System.Collections;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using System.Net.Security;
using Prime31;
using System.Text;
using System.Security.Cryptography;


public class GoogleCloudMessagingUI : Prime31.MonoBehaviourGUI
{
#if UNITY_ANDROID
  private string _registrationId;

  void Start()
  {
    // listen for successful registration so we can save off the registrationId in case we want to use it for Push.io registration.
    GoogleCloudMessagingManager.registrationSucceededEvent += regId =>
    {
      Debug.Log("regId: " + regId);
      _registrationId = regId;
    };
  }


  void OnGUI()
  {
    beginColumn();

    if( GUILayout.Button( "Check for Notifications" ) )
    {
      GoogleCloudMessaging.checkForNotifications();
    }


    if( GUILayout.Button( "Register" ) )
    {
      // replace this with your sender ID!!!
      GoogleCloudMessaging.register( "1234567890" );
    }


    if( GUILayout.Button( "Unregister" ) )
    {
      GoogleCloudMessaging.unRegister();
    }


    if( GUILayout.Button( "Cancel All Pending Notifications" ) )
    {
      GoogleCloudMessaging.cancelAll();
    }


    endColumn( false );
  }
#endif
}

アプリを起動して、Registerボタンを押すと、以下のようにLogCatに出力されます。

04-30 15:02:50.225: I/GCM(9377): GCM config loaded
04-30 15:02:50.346: I/Prime31-GCMReceiver(17557): recieved broadcast. message type from bundle is null
04-30 15:02:50.346: I/Prime31-GCMReceiver(17557): received a null message type so we arent interested. aborting
04-30 15:02:50.406: I/Unity(17557): registrationSucceededEvent: lkajsdflkjasdflkjadsf;lkjadslfkjasd;lfkjalkjasdfl;kjasdf;lkjasdf;lkjasdf
04-30 15:02:50.406: I/Unity(17557):
04-30 15:02:50.406: I/Unity(17557): (Filename: ./artifacts/AndroidManagedGenerated/UnityEngineDebug.cpp Line: 53)
04-30 15:02:50.406: I/Unity(17557): regId: lkajsdflkjasdflkjadsf;lkjadslfkjasd;lfkjalkjasdfl;kjasdf;lkjasdf;lkjasdf
04-30 15:02:50.406: I/Unity(17557):
04-30 15:02:50.406: I/Unity(17557): (Filename: ./artifacts/AndroidManagedGenerated/UnityEngineDebug.cpp Line: 53)

ここのregistrationSucceededEventの中でregisterIdが取れるので、PushNotificationを配信するサーバーに登録しましょう。

割りと書くコードも少なく、すっきり書けるのでこの方法が僕の中でいまのところ安定しています。
これからいいAssetとかが見つかれば、そちらの記事をまた書こうかと思います。

追記:

タイミングの話し

UnityでiOSのPushNotification(APNS)を実装する方法 | ひささん日記こちらにも書いたのですが、PushNotificationを受け取るタイミングでひとつ試してなかったのがありました。
それが、アプリがバックグラウンドにいるときにPushNotificationを受け取って、その後にホーム画面のアプリアイコンをタップした場合にPushNotificationが来ていたかどうかを確認する方法です。
長いですね。

結論から言うと、おそらくこの場合のPushNotificationの確認はできません。
調べたことを順に書いておきます。

prime[31] – Unity plugin documentationここを見てみると、checkForNotificationsというメソッドがあることに気が付きました。
書いてあるコメントにもなんだかそれっぽいことが書かれていたので、ここを解析していきました。

googlecloudmessagingplugin.jarの中にあるclassファイルを全部解凍してみて、見てみると、
GCMBroadcastReceiverというクラスがありました。
この中で、

private void sendNotification(Context context, Bundle bundle, String jsonPayload)
{
    String launchClassName = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()).getComponent().getClassName();
    ComponentName comp = new ComponentName(context.getPackageName(), launchClassName);
    Intent notificationIntent = (new Intent()).setComponent(comp);
    notificationIntent.putExtra("notificationData", jsonPayload);
    PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0x10000000);
    NotificationManager noteManager = (NotificationManager)context.getSystemService("notification");
    android.support.v4.app.NotificationCompat.Builder noteBuilder = new android.support.v4.app.NotificationCompat.Builder(context);
    noteBuilder.setContentIntent(pendingIntent);
    noteBuilder.setAutoCancel(true);
    noteBuilder.setSmallIcon(context.getApplicationInfo().icon);
    noteBuilder.setContentTitle(context.getApplicationInfo().loadLabel(context.getPackageManager()));
    if(bundle.containsKey("message"))
        noteBuilder.setContentText(bundle.getString("message"));
    else
        noteBuilder.setContentText("");
    String tickerText = "Push Notification Received (default tickerText)";
    noteBuilder.setDefaults(1);
    if(bundle.containsKey("message"))
        tickerText = bundle.getString("message");
    noteBuilder.setTicker(tickerText);
    noteBuilder.setWhen(System.currentTimeMillis());
    noteManager.notify(101, noteBuilder.build());
    Log.i("Prime31-GCMReceiver", "notification posted");
}

ここが、PushNotificationを受けたときに、メッセージを表示している箇所です。
notificationIntent.putExtraで必要な情報をIntent経由で渡すんだろうなーという感じです。

そして、GoogleCloudMessagingPluginクラスの中に、該当のクラスがあるので、ここで取得しているんだろうなーという印象。

public void checkForNotifications()
{
    Bundle intentExtras = _lastIntent == null ? getActivity().getIntent().getExtras() : _lastIntent.getExtras();
    if(intentExtras != null && intentExtras.containsKey("notificationData"))
        receivedNotification(intentExtras.getString("notificationData"));
}

とくに何かバグがありそうな気配もないですし、以前は有料?としてAssetStoreで売られていたAssetなので、そんな不備がある可能性は低い。

そこでprime31のgithubに書かれている、送る側のコードに問題があるんじゃないかと思い、自分のコードは使わず、Simple PHP script showing how to send an Android push notification. Be sure to replace the API_ACCESS_KEY with a proper one from the Google API’s Console page. To use the script, just call scriptName.php?id=THE_DEVICE_REGISTRATION_IDこのコードを使いました。

それでもやはりバックグラウンドで受けて、フォアグラウンドになったあとにcheckForNotificationsしてもうんともすんとも言わず。

そのときのログに出ていたこのCANCELLEDが問題なのかなーとググってみても解決策はなし。

05-02 20:56:00.291: W/GCM/DMM(9377): broadcast intent callback: result=CANCELLED forIntent { act=com.google.android.c2dm.intent.RECEIVE pkg=hoge.hoge (has extras) }

そこで再度、sendNotificationメソッドの中を見ていたら、PendingIntentというのを使っているのに気がついた。

あ!
あああ!

android初心者プログラミング: PendingIntentによると。

例えば、Notificationに使う場合は、
通知バーをタップしたタイミングでIntentが発行されるという感じのようです。

via: android初心者プログラミング: PendingIntent

PushNotificationを受けて、通知バーよりタップされた場合にPendingIntent経由でextraDataが登録され、そのあとにcheckForNotificationsを実行するとPushNotificationで来た情報が取得できます。
なんという限定的!

これを回避するために、Broadcastする方法がありますが、このプラグインでは採用されていません。

Intent intent = new Intent(ConstantsUtil.DISPLAY_MESSAGE_ACTION);
intent.putExtra(ConstantsUtil.GCM_EXTRA_BADGE, badge);
context.sendBroadcast(intent);

解決策としては、アプリ起動時に毎回PushNotificationがあったかどうかを確認するAPIを呼ぶとかですかねー
まだどうするのが最適解か考え中です。

難しい、、、

Androidの絵本 スマートフォンアプリ開発を始める9つの扉
株式会社アンク
翔泳社
売り上げランキング: 100,308

Push通知をまとめてみた(PushNotification) – iOS and Android

hisasann/PushNotification-iOSandAndroid · GitHubにあげておきました。
iOSとAndroidはディレクトリを分けて各ファイルを置いておきました。

プロジェクト自体は重いのでpushしていないです。
README.mdに基本的なことを記載。

iOS

PushNotification-iOSandAndroid/iOS at master · hisasann/PushNotification-iOSandAndroid · GitHub

release用のPush通知証明書ファイルでためす場合は、AdHocとして入れればOK!

Android

PushNotification-iOSandAndroid/Android at master · hisasann/PushNotification-iOSandAndroid · GitHub

はじめてのAndroidアプリ開発―Android4対応版 (TECHNICAL MASTER)
山田 祥寛
秀和システム
売り上げランキング: 10,811