Ethna で作る Windows Phone 7 への Push Notification

ふと思い立ってWindows Phone 7のPush Notificationを試してみることにしました。

対向となるサービス込みでの構築が必要になるのでちょっと手間がかかりますが、仕組みはそんなに難しくありません。

  • WP7上でHttpNotificationChannelを作成します。
    • すると、Channel Uri が通知されます
  • Channel Uri を、送信元となるサービスへ登録します。
  • サービスは、登録された Channel Uri へデータを送付します
  • Microsoft のサーバーを経由して Push が WP7 へ着信します。

まあ、Windows AzureでできたらWindowsでそろってかっこよかったんですが、従量課金怖すぎるので、定額かつ低額なさくらVPSでEthnaを使って作ることにしました。

なお、今回のソースはすべてgithubにあげてありますので参考にどうぞ。

Webサービス側

1.Ethnaを入れる

なんかchannel-discoverしたらgoneとかでたのでURL直指定インストール。

# yum install php-pear
# pear install http://pear.ethna.jp/pear/Ethna-2.5.0.tgz
# pear install http://pear.ethna.jp/pear/Smarty-2.6.19.tgz
# pear install http://pear.ethna.jp/pear/simpletest-1.0.1.tgz
2.プロジェクト作る

プロジェクト作ってDBドライバをADOdbにします。

$ ethna add-project mpnstest
$ cd mpnstest
$ cd lib
$ wget http://sourceforge.net/projects/adodb/files/adodb-php5-only/adodb-511-for-php5/adodb511.tgz/download
$ tar xzf adodb511.tgz
$ mv adodb5 adodb
$ cd ..
$ vim app/Mpnstest_Controller.php
-        'db'            => 'Ethna_DB_PEAR',
+        'db'            => 'Ethna_DB_ADOdb',

この後、www ディレクトリにバーチャルホストでアクセスできるよう、Apacheを設定します。

3.DBを作ります
$ mysql -u root -p 
mysql> create database mpnstest;
mysql> grant all privileges on mpnstest.* to mpnstest@localhost identified by '********';
mysql> connect mpnstest;
mysql> create table devices (
     > id int unsigned auto_increment primary key,
     > endpoint_url text not null,
     > created datetime not null,
     > updated timestamp,
     > deleted smallint unsigned not null default 0)
     > type=InnoDB;
4. etc/mpnstest-ini.php 編集します

DBの接続設定として、以下1行追加します

'dsn' => 'mysql://mpnstest:********@127.0.0.1/mpnstest',
5. 登録APIを作る

$ ethna add-action api_regist
$ ethna add-template api_regist
$ vim app/action/Api/Regist.php
$ vim template/ja_JP/api/regist.tpl

6. WindowsPhonePushClient を用意する

Send Push Notifications to Windows Phone 7 from PHP : Dave Amenta .com という blog 記事にある WindowsPhonePushClient というクラスを使うと簡単に実装できるのでこちらを使わせて頂きます。

$ cd lib
$ wget -O wp7_push.php http://www.daveamenta.com/download/snippets/wp7_push.txt
$ cd ..
7. アクションを作る

$ ethna add-action send_index
$ ethna add-action send_toast
$ ethna add-template send_index
$ ethna add-template send_toast
$ vim app/action/Send/Index.php
$ vim app/action/Send/Toast.php
$ vim template/ja_JP/send/index.tpl
$ vim template/ja_JP/send/toast.tpl

Windows Phone 7側

1. VSでWindows Phone Applicationを作成

普通に作成すればよいです。

2. MainPage.xaml.cs に Push Notification との通信を記述
public partial class MainPage : PhoneApplicationPage
{
    // Constructor
    public MainPage()
    {
        CreatingANotificationChannel();
        InitializeComponent();
    }

    public HttpNotificationChannel myChannel;
    protected readonly string ChannelName = "MPNSTest";
    protected readonly string ServiceURL = "http://<your service url>/";
    protected readonly string NotificationUserIdSettingKey = "notification_user_id";

    public void CreatingANotificationChannel()
    {
        // 既存のチャンネルを探す
        myChannel = HttpNotificationChannel.Find(ChannelName);

        if (myChannel == null)
        {
            // チャンネルがなければ作成する
            myChannel = new HttpNotificationChannel(ChannelName);
            SetUpDelegates();

            // Openすると、ChannelUriUpdated が発行される
            myChannel.Open();

            myChannel.BindToShellToast();
        }
        else
        {
            SetUpDelegates();
        }

        // サービスを登録する
        if (myChannel.ChannelUri != null)
        {
            RegistToService(myChannel.ChannelUri.ToString());
        }
    }

    public void SetUpDelegates()
    {
        // イベントを定義する
        myChannel.ChannelUriUpdated += new EventHandler<NotificationChannelUriEventArgs>(myChannel_ChannelUriUpdated);
        myChannel.HttpNotificationReceived += new EventHandler<HttpNotificationEventArgs>(myChannel_HttpNotificationReceived);
        myChannel.ShellToastNotificationReceived += new EventHandler<NotificationEventArgs>(myChannel_ShellToastNotificationReceived);
        myChannel.ErrorOccurred += new EventHandler<NotificationChannelErrorEventArgs>(myChannel_ErrorOccurred);
    }

    void myChannel_ChannelUriUpdated(object sender, NotificationChannelUriEventArgs e)
    {
        // サービスにチャンネルを登録する
        Debug.WriteLine("Notification channel URI:" + e.ChannelUri.ToString());

        string channel = e.ChannelUri.ToString();
        RegistToService(channel);
    }

    private void RegistToService(string channel)
    {
        // すでにユーザーIDを持ってる場合はそれに対して更新をかける
        string serviceUri;
        if (IsolatedStorageSettings.ApplicationSettings.Contains(NotificationUserIdSettingKey))
        {
            serviceUri = string.Format("{0}?action_api_regist=1&channel={1}&user_id={2}", ServiceURL, Uri.EscapeDataString(channel), 
                IsolatedStorageSettings.ApplicationSettings[NotificationUserIdSettingKey]);
        }
        else
        {
            serviceUri = string.Format("{0}?action_api_regist=1&channel={1}", ServiceURL, Uri.EscapeDataString(channel));
        }

        // サービスに登録する
        WebClient wc = new WebClient();
        wc.DownloadStringCompleted += delegate(object dl_sender, DownloadStringCompletedEventArgs dl_e)
        {
            Debug.WriteLine(dl_e.Result);
            string[] results = dl_e.Result.Split('\n');

            // 成功していればユーザーIDを設定に保持する
            if (results[0] == "success")
            {
                if (IsolatedStorageSettings.ApplicationSettings.Contains(NotificationUserIdSettingKey)) {
                    IsolatedStorageSettings.ApplicationSettings[NotificationUserIdSettingKey] = results[1];
                }
                else {
                    IsolatedStorageSettings.ApplicationSettings.Add(NotificationUserIdSettingKey, results[1]);
                }
            }
        };
        Debug.WriteLine("Sending:" + serviceUri);
        wc.DownloadStringAsync(new Uri(serviceUri));
    }

    // トーストからアプリが起動したときの処理
    void myChannel_ShellToastNotificationReceived(object sender, NotificationEventArgs e)
    {
        if (e.Collection != null)
        {
            Dictionary<string, string> collection = (Dictionary<string, string>)e.Collection;
            System.Text.StringBuilder messageBuilder = new System.Text.StringBuilder();

            foreach (string elementName in collection.Keys)
            {
                //...
            }
        }
    }

    void myChannel_ErrorOccurred(object sender, NotificationChannelErrorEventArgs e)
    {
        switch (e.ErrorType)
        {
            case ChannelErrorType.ChannelOpenFailed:
                // ...
                break;
            case ChannelErrorType.MessageBadContent:
                // ...
                break;
            case ChannelErrorType.NotificationRateTooHigh:
                // ...
                break;
            case ChannelErrorType.PayloadFormatError:
                // ...
                break;
            case ChannelErrorType.PowerLevelChanged:
                // ...
                break;
        }
    }
}

実行結果

WP7上でアプリを起動すると、登録APIを通じてサービス側にURLが登録されます。

その後、http://設置先のURL/?action_send_index=1 へアクセスすると以下のような入力画面が現れます。

送信すると、Received と画面に表示され、端末にトーストが通知されます。

ただ、しばらくすると結果がDroppedとなって通らなくなるので、そこの原因はもうちょっと追ってみようと思います。

iPhoneApple Push Notification Service との違い

ところで、Push Notification はiPhoneの方が先に実装していて、基本的な仕組みはどちらも一緒です。

ただ、通信に使うプロトコルなどが微妙に異なるので、差異を紹介しておきましょう。

MPNS/WP7 APNS/iOS
登録 不要 必要*1
サービス→サーバー間プロトコル HTTP Apple独自
通知:バッジ(未読件数)
通知:タイル画像 ×
通知:トースト(メッセージ)
通知:サウンド ×

APNSは登録したり、独自のプロトコルをしゃべるサーバーを書いたりとやや面倒なのですが、WP7はHTTP POSTするだけでPushできてお手軽に実装できるのがいいですね!

APNSが有利なのは独自プロトコル故に大量配信に向くことと、アプリに埋め込んである任意のサウンドを鳴らせるところでしょうか。

*1:事前にiOS Provisioning Portalにて登録、証明書の発行が必要