酢ろぐ!

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

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)

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

Azure Webサイトの利用料金について調べてみました(2014/9時点)

ASP.NET MVCでサイトを作るのが楽しい毎日です。

前々からWebサイトを6つ以上作るんだったら占有インスタンスでまとめた方が安上がりだよ!とは聞いていたのですが、共有に比べると段違いに値段が上がってしまうのでなかなか試せずにいました。先月末に無駄なサービスやサイトを切りまくってリストラしたおかげで 月3,000円くらいで運用できるようになってきました。

さて、本当にサイトを1つにまとめた方が安いのかどうかを考えてみたいと思います。

Azure Webサイトは、無料(Free)、共有(Shared)、基本(Basic)、標準(Standard)の4つのレベルで提供されています*1。このレベルの違いはCPUやメモリの差ではなく、自動スケールやバックアップ、ステージングに対応しているかどうか等の違いです。カスタムドメインを使うのであれば、共有以上のレベルが必要になります。

レベルの概念以外に、S(1)、M(2)、L(3)の3つのサイズという概念が存在しており、レベルとサイズを組み合わせることで掛かってくる費用が上下します。現ポータルだと違いが少し分かりにくいのですが、新ポータルであれば以下のように分かりやすく選択できるようになっています(これはウェブホスティングプランの選択画面)。

f:id:ch3cooh393:20140916145647p:plain

以下の表の基本と標準の利用価格は、全部S(1)サイズの時のものです。

レベル 無料 共有 基本(B1) 標準(S1)
カスタムドメイン ×
値段 0円/月 758円/月 3339円/月 4553円/月

基本と標準レベルは、インスタンスを占有できるので、サイトを同一インスタンス上で運用することができます。…ということは以下のようになります。

レベル 共有 基本(B1) 標準(S1)
1サイト 758円/月 3,339円/月 4,553円/月
2サイト 1,516円/月 3,339円/月 4,553円/月
3サイト 2,274円/月 3,339円/月 4,553円/月
4サイト 3,032円/月 3,339円/月 4,553円/月
5サイト 3,790円/月 3,339円/月 4,553円/月
6サイト 4,548円/月 3,339円/月 4,553円/月
7サイト 5,306円/月 3,339円/月 4,553円/月

あまり経験がないので分からないのですが、1インスタンスで7サイトを運用するのはどうなのかと思うわけなのですが、あくまでも値段だけで見た場合1インスタンスでサイトを載せようとした時の料金です。

  • 共有レベルで5サイト以上運用する場合には、基本(B1)レベルを使う方が安い
  • 共有レベルで7サイト以上運用する場合には、標準(S1)レベルを使う方が安い

*1:どうでも良いことなのですが、日本語訳が「基本」と「標準」の違いが分かりにくくて、いつもどっちが強いんだっけ?と考えてしまいます。

AED設置施設の登録数が10,000件を突破したようです

先週、Web版のAED検索を公開しましたね。

AED設置施設の登録データはすでに10,000件を超えたようです。たった今確認すると10,999件になっていました。

f:id:ch3cooh393:20140825182658p:plain

初音さんが施設情報の登録が完了した自治体のデータをオープンデータとして順次公開されています。

これらのAED設置施設データを使った他のアプリが出てくると嬉しいなと思います。

「AED検索 Web版」の開発に関しては一旦完了として、不都合が見つかり次第修正するようにしたいと思います。これからの課題としては、安定してサイトを稼働させられれば良いかなと考えています。

今後ともAED検索 Web版をよろしくお願いします。

ASP.NET MVC 5でAzure Webサイトにインストールされているフォントを調べる

Azure Webサイトにインストールされているフォントを調べてみましょう。

コントローラーとビューを実装する

Controllers\FontController.cs

using System.Linq;
using System.Web.Mvc;

namespace Test1.Controllers
{
    public class FontController : Controller
    {
        // GET: Font
        public ActionResult Index()
        {
            // インストールされているフォントコレクションを取得する
            var fonts = new System.Drawing.Text.InstalledFontCollection();

            ViewBag.Fonts = fonts.Families.Select(item => item.Name);

            return View();
        }
    }
}

Views\Font\Index.cshtml

@{
    ViewBag.Title = "Index";
}

<h2>Azure Webサイトにインストールされているフォント一覧</h2>

<ul>
    @foreach (string fontName in ViewBag.Fonts)
    {
        <li>@fontName</li>
    }
</ul>

Azure Webサイトにインストールされているフォント一覧

上記のページをAzure Webサイトへデプロイしてフォント一覧を表示させてみました。

f:id:ch3cooh393:20140823003916p:plain

表示されたのは下記のフォントのリストでした。

  • Aharoni
  • Aldhabi
  • Andalus
  • Angsana New
  • AngsanaUPC
  • Aparajita
  • Arabic Typesetting
  • Arial
  • Arial Black
  • Batang
  • BatangChe
  • Browallia New
  • BrowalliaUPC
  • Calibri
  • Calibri Light
  • Cambria
  • Cambria Math
  • Candara
  • Comic Sans MS
  • Consolas
  • Constantia
  • Corbel
  • Cordia New
  • CordiaUPC
  • Courier New
  • DaunPenh
  • David
  • DFKai-SB
  • DilleniaUPC
  • DokChampa
  • Dotum
  • DotumChe
  • Ebrima
  • Estrangelo Edessa
  • EucrosiaUPC
  • Euphemia
  • FangSong
  • Franklin Gothic Medium
  • FrankRuehl
  • FreesiaUPC
  • Gabriola
  • Gadugi
  • Gautami
  • Georgia
  • Gisha
  • Gulim
  • GulimChe
  • Gungsuh
  • GungsuhChe
  • Impact
  • IrisUPC
  • Iskoola Pota
  • JasmineUPC
  • KaiTi
  • Kalinga
  • Kartika
  • Khmer UI
  • KodchiangUPC
  • Kokila
  • Lao UI
  • Latha
  • Leelawadee
  • Levenim MT
  • LilyUPC
  • Lucida Console
  • Lucida Sans Unicode
  • Malgun Gothic
  • Mangal
  • Marlett
  • Meiryo
  • Meiryo UI
  • Microsoft Himalaya
  • Microsoft JhengHei
  • Microsoft JhengHei UI
  • Microsoft New Tai Lue
  • Microsoft PhagsPa
  • Microsoft Sans Serif
  • Microsoft Tai Le
  • Microsoft Uighur
  • Microsoft YaHei
  • Microsoft YaHei UI
  • Microsoft Yi Baiti
  • MingLiU
  • MingLiU-ExtB
  • MingLiU_HKSCS
  • MingLiU_HKSCS-ExtB
  • Miriam
  • Miriam Fixed
  • Mongolian Baiti
  • MoolBoran
  • MS Gothic
  • MS Mincho
  • MS PGothic
  • MS PMincho
  • MS UI Gothic
  • MV Boli
  • Myanmar Text
  • Narkisim
  • Nirmala UI
  • NSimSun
  • Nyala
  • Palatino Linotype
  • Plantagenet Cherokee
  • PMingLiU
  • PMingLiU-ExtB
  • Raavi
  • Rod
  • Sakkal Majalla
  • Segoe Print
  • Segoe Script
  • Segoe UI
  • Segoe UI Light
  • Segoe UI Semibold
  • Segoe UI Semilight
  • Segoe UI Symbol
  • Shonar Bangla
  • Shruti
  • SimHei
  • Simplified Arabic
  • Simplified Arabic Fixed
  • SimSun
  • SimSun-ExtB
  • Sylfaen
  • Symbol
  • Tahoma
  • Times New Roman
  • Traditional Arabic
  • Trebuchet MS
  • Tunga
  • Urdu Typesetting
  • Utsaah
  • Vani
  • Verdana
  • Vijaya
  • Vrinda
  • Webdings
  • Wingdings

ASP.NET MVC 5で動的に生成したグラデーションを描画した画像を返す

ASP.NET MVC 5でサーバーサイドで動的に生成した画像を返す - 酢ろぐ!」の応用例です。……といってもGraphicsクラスを使った描画と違いはありませんね。

using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Web.Mvc;

namespace Test1.Controllers
{
    public class Graphics2Controller : Controller
    {
        private Bitmap GetBackgroundImage(int width, int height,
            Color startColor, Color endColor)
        {
            var image = new Bitmap(width, height);

            using (var g = Graphics.FromImage(image))
            {
                g.SmoothingMode = SmoothingMode.HighQuality;

                // グラデーション描画用のブラシを作成
                var brush = new LinearGradientBrush(new Point(0, height),
                    new Point(width, 0), startColor, endColor);

                // 画像の背景を描画する
                g.FillRectangle(brush, new Rectangle(0, 0, width, height));
            }

            return image;
        }

        public ActionResult Index()
        {
            var width = 640;
            var height = 300;
            var startColor = Color.FromArgb(233, 19, 29);
            var endColor = Color.FromArgb(235, 115, 52);

            // 画像を生成する
            var ms = new MemoryStream();
            using (var image = GetBackgroundImage(width, height, startColor, endColor))
            {
                // 描画したビットマップをPNGとしてストリームへ保存する
                image.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
                ms.Position = 0;
            }

            // サーバーサイドで実装したPNG画像を返す
            return new FileStreamResult(ms, "image/png");
        }
    }
}

Indexメソッドが実行されると下図のような画像を表示します。

f:id:ch3cooh393:20140821023504p:plain

ASP.NET MVC 5でサーバーサイドで動的に生成した画像を返す

ASP.NET MVC 5でサーバーサイドで生成した画像を表示したくなることありませんか?サーバー側で持っているデータを元に、人気ランキングバナーやロウソク図を生成するのに使えそうですね。

例えば、htmlで以下のように書きます。

<img src="http://example.jp/Graphics/Index/" />

GraphicsコントローラーIndexアクションが実行されます。そのため、GraphicsControllerで画像を生成してファイルのストリームを返します。これだけ。

using System.Drawing;
using System.IO;
using System.Web.Mvc;

namespace Test1.Controllers
{
    public class GraphicsController : Controller
    {
        // GET: Graphics
        public ActionResult Index(string id)
        {
            if (string.IsNullOrWhiteSpace(id))
            {
                id = "何も入力されなかったですよ";
            }

            // 画像を生成する
            var ms = new MemoryStream();
            using (var image = new Bitmap(200, 200))
            {
                using (var g = Graphics.FromImage(image))
                {
                    // 画像の背景を描画する
                    var fillBrush = new SolidBrush(Color.Magenta);
                    g.FillRectangle(fillBrush, new Rectangle(0, 0, 200, 200));
                    fillBrush.Dispose();

                    // 文字列を描画する
                    var font = new Font("MS UI Gothic", 10);
                    var fontBrush = new SolidBrush(Color.White);
                    g.DrawString(id, font, fontBrush, new PointF(0,0));
                    fontBrush.Dispose();
                    font.Dispose();
                }

                // 描画したビットマップをPNGとしてストリームへ保存する
                image.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
                ms.Position = 0;
            }
            
            // サーバーサイドで実装した画像を返す
            return new FileStreamResult(ms, "image/png");
        }
    }
}

実行されると下図のような画像を表示します。

f:id:ch3cooh393:20140717121249p:plain

ASP.NET MVC 4でganrefをスクレイピングして、おねーさまを眺めれるようにしてみた。

元ネタ:Nancyでganrefをスクレイピングして、おねーさまを眺めれるようにしてみた。 - atelier:mitsuba

「ganref」っていうのが何かよくわかってないけど、おねーさまを眺めることができるサイトらしい。みつばたんのコードを参考にして、ASP.NET MVC 4用に書き換えてみました。

書いたコードはこんなかんじ。ソースコードは基本的にみつばたんの記事からのコピペなので、みつばたんが作ったサイトと同じ挙動になるはず。ちゃんと動いてなかったら教えてくださいな。

Controllers/GanrefController.cs

/ganref/でアクセスしたいので、Ganrefコントローラーを作成する。コントローラーの作成の仕方は「ASP.NET MVC 4で新規のプロジェクトを作成する - 酢ろぐ!」を参考にしてください。

using Sgml;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using System.Xml.Linq;

namespace Test1.Controllers
{
    public class GanrefController : Controller
    {
        // GET: /Ganref/Lady/{id}
        public ActionResult Lady(int? id = null)
        {
            // ページ数の指定がなかったり0だったら1に遷移させる
            if (!id.HasValue || id.Value == 0)
            {
                return new RedirectResult("~/Ganref/Lady/1", false);
            }

            // アクセスするURLの組み立て
            var urlString = string.Format("http://ganref.jp/photo_searches/result/page:{0}/sort:created/direction:desc?", id);
            var param = new Dictionary<string, string>();
            param["keyword"] = "";
            param["parent_category"] = "%E4%BA%BA%E7%89%A9";
            param["category"] = "%E5%A5%B3%E6%80%A7";
            urlString += string.Join("&", param.Select(p => string.Format("{0}={1}", p.Key, p.Value)));

            // HTMLのパース処理(みつばたんの処理をそのまま)
            var xml = default(XDocument);
            using (var sgml = new SgmlReader() { Href = urlString, IgnoreDtd = true })
            {
                xml = XDocument.Load(sgml);
            }

            var ns = xml.Root.Name.Namespace;
            var imgroot = xml.Descendants(ns + "div")
                .Where(div => div.FirstAttribute.Value == "thumb140");
            var imgUrls = imgroot.Select(xElement =>
            {
                return xElement.Descendants(ns + "a")
                    .Descendants(ns + "img")
                    .Skip(2).First()
                    .FirstAttribute.Value;
            });

            // ビューに直接文字列のリストを投げてる
            return View(imgUrls);
        }
    }

}

典型的なスクレイピング。のいえさんのブログを参考にしたらしいです。SgmlReaderは本当に素晴らしいHTMLパーサーだと思います。

パースした結果(画像URLのリスト、imgUrls)をViewへ渡しています。

Views/Ganref/Lady.cshtml

次にView側で渡された画像URLのリストをどのように展開するか?ですが、foreach文で渡されたList<string>を取り出しaタグのsrcに詰めています。htmlを書いているはずなのに、C#な構文で書けるのは本当楽ですね。

@{
    ViewBag.Title = "Ganref Lady";
}

<h2>Ganref Lady</h2>

<p>
    <a href="https://blog.ch3cooh.jp/">解説記事</a>    
</p>
<p>
    <a href="http://c-mitsuba.hatenablog.com/entry/2014/07/15/024702">元ネタ</a>
</p>

@foreach (string uriString in Model)
{
    <img src="@uriString" />
}

……ってことで、出来上がったものがこちらです。