酢ろぐ!

カレーが嫌いなスマートフォンアプリプログラマのブログ。

Azure Mobile ServicesからiOSアプリにプッシュ通知が送れない問題(追記あり、解決済み)

昨日1日Azure Mobile Services(以下、Mobileサービス)からiOSアプリにプッシュが送れない問題で悩んでいて解決しないので、ちょっと問題の切り分けのために現状を書いていきます。

iOSアプリに対してAzure Mobile ServicesのNotification Hubを使ってプッシュ通知を送る方法は下記の記事を参考にしてください。

MobileサービスからWindowsストアアプリにプッシュを送る方法

起動していたのがWindowsということもあって、まずは、Windowsストアアプリとの連携でテストしてみました。

  • サーバー側の準備
    • MobileサービスにパッケージIDとクライアントシークレットを登録
  • アプリ側
    • デバイスからチャンネルをモバイルサービスに送信する
  • サーバー側
    • 通知ハブのデバッグからトースト通知を送る
  • アプリ側
    • デバイスでトーストが表示される

ここまで30分くらい。TodoItemを送信したりしてテーブルにレコードが追加されるのを確認しながらだったのでプッシュ通知以外の部分で時間がかかってしまいましたが、プッシュ通知を使うのはとても簡単です。

MobileサービスからiOSアプリにプッシュを送る方法

次は本丸のiOSアプリとの連携です。

  • サーバー側の準備
    • KeyChainでcerファイルを生成する
    • iOS Dev Center*1でcerファイルをアップロードして証明書を生成する
    • 証明書をダウンロードして、p12ファイルに書き出し(これは後で使う)
    • アプリのプロビジョニングを再生成
    • Mobileサービスでp12ファイルをProductionとして登録
    • アプリをTestFlightで配信(AppStore用のプロビジョニングでビルド)
  • アプリ側
    • TestFlightからアプリをインストール
    • デバイスからデバイストークンをモバイルサービスに送信する
  • サーバー側
    • 通知ハブのデバッグからトースト通知を送る
  • アプリ側
    • プッシュ通知が飛んでこない

上記の2.のデバイストークンを送る処理は下記の通りです。iOS 8に対応させています。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
   // ....

    self.client = [MSClient clientWithApplicationURLString:@"https://example.azure-mobile.net/"
                                            applicationKey:@"<Application Key>"];
    
    if ([application respondsToSelector:@selector(registerUserNotificationSettings:)]) {
        
        // iOS 8 以降
        UIUserNotificationType types = UIUserNotificationTypeBadge | UIUserNotificationTypeAlert;
        UIUserNotificationSettings *settings
            = [UIUserNotificationSettings settingsForTypes:types categories:nil];
        [application registerUserNotificationSettings:settings];
        
    } else {
        
        // iOS 7.1 以前
        UIRemoteNotificationType types = UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeBadge;
        [application registerForRemoteNotificationTypes:types];
    }
    
    return YES;
}

- (void)application:(UIApplication*)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken
{
    NSLog(@"didRegisterForRemoteNotificationsWithDeviceToken %@", deviceToken);
    
    // <, >, 空白を削除
    NSString *token = [deviceToken description];
    token = [token stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]];
    token = [token stringByReplacingOccurrencesOfString:@" " withString:@""];

    NSLog(@"deviceToken %@", token);
    
    MSPush* push = [self.client push];
    [push registerNativeWithDeviceToken:[token dataUsingEncoding:NSUTF8StringEncoding]
        tags:nil completion:^(NSError *error) {
            
            if (error != nil) {
                
                UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"error"
                                                                message:[error description]
                                                               delegate:nil
                                                      cancelButtonTitle:@"OK"
                                                      otherButtonTitles:nil];
                [alert show];
                
            } else {
                
                UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"completed"
                                                                message:token
                                                               delegate:nil
                                                      cancelButtonTitle:@"OK"
                                                      otherButtonTitles:nil];
                [alert show];
            }
    }];
}

- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
    NSLog(@"didFailToRegisterForRemoteNotificationsWithError");
    
    UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"error"
                                                    message:[error description]
                                                   delegate:nil
                                          cancelButtonTitle:@"OK"
                                          otherButtonTitles:nil];
    [alert show];
    
}
 
- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings
{
    NSLog(@"didRegisterUserNotificationSettings");
    
    [application registerForRemoteNotifications];
}

// アプリが起動中とかに通知が飛んできたら呼ばれる
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
{
    NSLog(@"didReceiveRemoteNotification");
    
    UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"notification"
                                                    message:@"通知が来たよ"
                                                   delegate:nil
                                          cancelButtonTitle:@"OK"
                                          otherButtonTitles:nil];
    [alert show];
}

ちなみに送信側のコードはこんな感じ。ServiceBusのSDKを使っています。

var client = NotificationHubClient.CreateClientFromConnectionString(
    "Endpoint=sb://example-ns.servicebus.windows.net/;SharedAccessKeyName=DefaultFullSharedAccessSignature;SharedAccessKey=<access key>",
    "<hub name>", false);

// 現在、登録されているデバイスを確認する
var allRegistrations = await client.GetAllRegistrationsAsync(0);
var registrations = allRegistrations
    .Where(d => d is AppleRegistrationDescription)
    .Cast<AppleRegistrationDescription>();

// ここで1台登録されているのを確認
Console.WriteLine("registrations {0}", registrations.Count());

// 通知を送信!!!!
var json = "{\"aps\":{\"alert\":\"hoge\"}}";
await client.SendAppleNativeNotificationAsync(json);

// 5秒待つ
Thread.Sleep(5000);

allRegistrations = await client.GetAllRegistrationsAsync(0);
registrations = allRegistrations
    .Where(d => d is AppleRegistrationDescription)
    .Cast<AppleRegistrationDescription>();

Console.WriteLine("registrations {0}", registrations.Count());

// (おそらく送信に失敗したのか)登録デバイスが0台になっている

現状(12/4 11:15)

最初に証明書関係でトラブっているのではないかと疑いました。同じ証明書(p12ファイル)を使ってAmazon SNSを試してみました。

  • Amazon SNSで試してみる
    • p12ファイルを登録
    • アラートに表示させたデバイストークンを登録
    • プッシュを送信
    • iOSデバイスでトーストが表示される

このことから証明書自体に問題ないことと言えると思います。

現状(12/4 14:15)

しばやんに教えてもらっています。

通知ハブのデバックはProductionに対しては利用できないのでは?という回答。特に明記されているわけではないけれど、デバッグ用途だし、そういうものかもしれない。

こっちに関してはNotificationHubClientを生成するときに第3引数(enableTestSend)をfalseにしているので間違いではないはず……

var client = NotificationHubClient.CreateClientFromConnectionString(
    "Endpoint=sb://example-ns.servicebus.windows.net/;SharedAccessKeyName=DefaultFullSharedAccessSignature;SharedAccessKey=<access key>",
    "<hub name>", false);

現状(12/4 15:00)

接続文字列等の指定ミスでそもそも通知ハブから送信できていないのでは……とも思ったが、プッシュ送信する前の登録デバイス数が1で、プッシュ送信(とおそらくAPNsからのエラー通知)後に0になることから正しく取れていると思われる。

現状(12/4 15:20)

問題解決した。デバイストークンがおかしくて不正な値を通知ハブに登録していたようだ。