ROMANCE DAWN for the new world

Microsoft Azure を中心とした技術情報を書いています。

Windows Azure Notification Hub を使ってプッシュ通知する(前編)

Windows Azure – 技術者でつなぐ日めくりカレンダー の 3/24 の記事です。Windows Azure Notification Hub (通知ハブ)を使って、Windows Store アプリへのプッシュ通知を試してみました。記事が長くなったので、2回に分けて投稿します。

まずは、プッシュ通知の仕組みを理解するために、通知ハブを使わずに実装してみました。こちらのMSDNが分かりやすかったです。今回は、通知ハブがテーマなので、アプリをWindows ストアと関連付けるなどの事前準備は完了しているものとします。

チャネルURIの登録

Windows Store アプリのチャネルURIを受け取る Web API を作ります。POST api/channel  へリクエストすると、Azure Table Storage にチャネルURIを保持します。

#ChannelController.cs
public class ChannelController : ApiController
{
    private CloudTable table;
 
    public ChannelController()
    {
        var storageAccount = CloudStorageAccount.Parse(CloudConfigurationManager.GetSetting("StorageConnectionString"));
        var tableClient = storageAccount.CreateCloudTableClient();
        this.table = tableClient.GetTableReference("Channel");
        this.table.CreateIfNotExists();
    }
 
    public async Task<IHttpActionResult> Post([FromBody]string uri)
    {
        var channel = new Channel {
            PartitionKey = "storeapp",
            RowKey = String.Format("{0:D19}", DateTime.MaxValue.Ticks - DateTime.UtcNow.Ticks),
            ChannelUri = uri };
        await this.table.ExecuteAsync(TableOperation.Insert(channel));
        return Ok();
    }
}

チャネルURIの送信

Windows Store アプリからチャネルURIを送信します。Appクラスの OnLaunched メソッドなどで、先ほど作った Web API にリクエストします。

#App.xaml.cs
var channel = await PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync();
var json = await JsonConvert.SerializeObjectAsync(channel.Uri);
var res = await new HttpClient().PostAsync("http://localhost:12101/api/channel", new StringContent(json, Encoding.UTF8, "application/json"));
res.EnsureSuccessStatusCode();

ライブタイルのプッシュ通知

Windows プッシュ通知サービス(WNS)に通知を送る Web APIを作ります。WNS に認証を受けて、Azure Table Storage から取得したチャネルURIに通知を送信する NotificationClient クラスを作りました。POST api/tile へリクエストすると、WNS にプッシュ通知します。

#TileController.cs
public class TileController : ApiController
{
    public async Task<IHttpActionResult> Post([FromBody]string xml)
    {
        await new NotificationClient().PushAsync("wns/tile", xml, new PushSettings());
        return Ok();
    }
}
#NotificationClient.cs
internal class NotificationClient
{
    private const string SID = "ms-app://xxx";
    private const string ClientSecret = "xxx";
    private CloudTable table;
 
    public NotificationClient()
    {
        var storageAccount = CloudStorageAccount.Parse(CloudConfigurationManager.GetSetting("StorageConnectionString"));
        var tableClient = storageAccount.CreateCloudTableClient();
        this.table = tableClient.GetTableReference("Channel");
        this.table.CreateIfNotExists();
    }
 
    public async Task PushAsync(string type, string xml, PushSettings settings)
    {
        // WNSに認証を受ける
        var oAuthToken = await this.GetOAuthTokenAsync();
 
        // チャネルを取得
        var query = new TableQuery<Channel>().Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, "storeapp"));
        foreach (var channel in this.table.ExecuteQuery(query))
        {
            try
            {
                // WNSに通知を送信
                await this.PushToWNSAsync(oAuthToken, channel.ChannelUri, xml, type, settings);
            }
            catch (Exception ex)
            {
                Trace.TraceError(ex.Message);
                this.table.Execute(TableOperation.Delete(channel));
            }
        }
    }
 
    private async Task<string> GetOAuthTokenAsync()
    {
        var content = new FormUrlEncodedContent(new Dictionary<string, string>
                    {
                        { "grant_type", "client_credentials" },
                        { "client_id", SID },
                        { "scope", "notify.windows.com" },
                        { "client_secret",ClientSecret },
                    });
 
        var res = await new HttpClient().PostAsync("https://login.live.com/accesstoken.srf", content);
        res.EnsureSuccessStatusCode();
 
        var json = await res.Content.ReadAsStringAsync();
        return JsonConvert.DeserializeObject<OAuthToken>(json).AccessToken;
    }
 
    private async Task PushToWNSAsync(string oAuthToken, string channelUri, string xml, string type, PushSettings settings)
    {
        var client = new HttpClient();
        client.DefaultRequestHeaders.Add("X-WNS-Type", type);
        client.DefaultRequestHeaders.Add("Authorization", String.Format("Bearer {0}", oAuthToken));
        if (String.IsNullOrEmpty(settings.CachePolicy) == false)
        {
            client.DefaultRequestHeaders.Add("X-WNS-Cache-Policy", HttpUtility.UrlEncode(settings.CachePolicy));
        }
        if (String.IsNullOrEmpty(settings.RequestForStatus) == false)
        {
            client.DefaultRequestHeaders.Add("X-WNS-RequestForStatus", HttpUtility.UrlEncode(settings.RequestForStatus));
        }
        if (String.IsNullOrEmpty(settings.Tag) == false)
        {
            client.DefaultRequestHeaders.Add("X-WNS-Tag", HttpUtility.UrlEncode(settings.Tag));
        }
        if (String.IsNullOrEmpty(settings.TTL) == false)
        {
            client.DefaultRequestHeaders.Add("X-WNS-TTL", HttpUtility.UrlEncode(settings.TTL));
        }
 
        HttpContent httpContent = new StringContent(xml, Encoding.UTF8, "text/xml");
        var res = await client.PostAsync(channelUri, httpContent);
        res.EnsureSuccessStatusCode();
    }
}
 
[DataContract]
internal class OAuthToken
{
    [DataMember(Name = "access_token")]
    public string AccessToken { get; set; }
 
    [DataMember(Name = "token_type")]
    public string TokenType { get; set; }
}
 
public class Channel : TableEntity
{
    public string ChannelUri { get; set; }
}
 
internal class PushSettings
{
    public string CachePolicy { get; set; }
    public string RequestForStatus { get; set; }
    public string Tag { get; set; }
    public string TTL { get; set; }
}

結果確認

タイルテンプレートの XML を api/tile へ POST すると、プッシュ通知が行われます。"wns/badge"や"wns/toast"を渡す Web API を作れば、バッチやトーストにも対応できます。PushSettingsクラスでは、要求ヘッダーで通知キューや有効期限などの設定を行うことも可能です。

まとめ

チャネルURIの管理やWNSへの送信など、バックエンドロジックの実装が地味に大変です。後編では、通知ハブを使って実装してみます。

gooner.hateblo.jp