しばやん雑記

ASP.NET とメイドさんが大好きなフリーランスのプログラマーのブログ

Xamarin.Android で Runtime Permissions を実装した時にはまったのでメモ

最近は Xamarin.Android も少しだけ弄っていますが、Android 6.0 で追加された Runtime Permissions に対応している時に、よく分からない挙動ではまったのでメモしておきます。

その挙動とは RequestPermissions で複数のパーミッションの確認を行い、結果として全てのパーミッションが取れているのかを LINQ でサクッと確認しようとした時でした。ちなみに以下のようなコードを書きました。

[Activity(Label = "PermissionTest", MainLauncher = true, Icon = "@mipmap/icon")]
public class MainActivity : Activity, ActivityCompat.IOnRequestPermissionsResultCallback
{
    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);

        // Set our view from the "main" layout resource
        SetContentView(Resource.Layout.Main);

        // Get our button from the layout resource,
        // and attach an event to it
        Button button = FindViewById<Button>(Resource.Id.myButton);

        button.Click += (sender, e) =>
        {
            ActivityCompat.RequestPermissions(this, new[]
            {
                Manifest.Permission.WriteExternalStorage, Manifest.Permission.Camera
            }, 0);
        };
    }

    public void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Permission[] grantResults)
    {
        if (grantResults.All(x => x == Permission.Granted))
        {
            new AlertDialog.Builder(this)
                           .SetTitle("Runtime Permission")
                           .SetMessage("All Granted")
                           .SetPositiveButton("OK", (_, __) => {  })
                           .Show();
        }
    }
}

実行すると、iOS のようにパーミッションの確認ダイアログが表示されます。今回の場合は複数のパーミッションを要求しているので、複数回表示されます。

f:id:shiba-yan:20160303103413p:plain

そして OnRequestPermissionsResult の中で grantResults が全て Permission.Granted かどうかを確認したつもりですが、実行してみるとアプリケーションが例外で終了してしまいました。

しかも、その例外の内容がとても不思議でした。

[Mono] Assembly Ref addref PermissionTest[0xad36ad80] -> System.Core[0xad36baa0]: 3
[] System.Int32[] doesn't implement interface System.Collections.Generic.IEnumerable<Android.Content.PM.Permission>
[libc] Fatal signal 6 (SIGABRT), code -6 in tid 1807 (.permissiontest)

grantResults は Permission の配列なはずですが、実際には int の配列として渡されてきているようです。デバッガーで止めて確認すると、本当に何故か int が渡されていました。

f:id:shiba-yan:20160303104930p:plain

Xamarin.Android の不具合なのかも知れませんが、とりあえず Permission にキャストし直すことにしました。

public void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Permission[] grantResults)
{
    if (grantResults.Cast<Permission>().All(x => x == Permission.Granted))
    {
        new AlertDialog.Builder(this)
                       .SetTitle("Runtime Permission")
                       .SetMessage("All Granted")
                       .SetPositiveButton("OK", (_, __) => {  })
                       .Show();
    }
}

これで All がちゃんと動作するので、パーミッションが全て取れていればダイアログが出るようになりました。

f:id:shiba-yan:20160303105939p:plain

かなり気持ち悪い挙動ですが、何度も触る部分では無いので妥協しました。Android 6.0 だけではなく、以前のバージョンでも同じ挙動になるみたいなので、微妙に注意が必要な気がします。

Xamarin で Razor Template が使えることを知らなかった

ここ最近はずっと Xamarin.iOS を使ってアプリの開発を少しやっているのですが、その途中で新規作成ダイアログに Preprocessed Razor Template という項目を見つけたので試してみました。

f:id:shiba-yan:20160217233628p:plain

Razor 以外には T4 も使えるみたいですが、個人的には Razor で十分な気がしています。

ちゃんと本家にドキュメントもありました。名前の通り Razor で書いたテンプレートを予め C# にコンパイルしておく機能のようです。Razor Engine とは異なり事前にコンパイルされるので、とても高速なはずです。

Building HTML views using Razor Templates - Xamarin

アプリケーション側で WebView を使って動的にページを作成して表示する場合に、Razor でテンプレートとモデルを作成しておけば、MVC アプリケーションと同様にレンダリングした結果を得ることができます。

そして気が付いていなかったのですが、プロジェクトテンプレートにある WebView App を選ぶと、Razor Template を使ってプロジェクトが作成されるみたいです。

f:id:shiba-yan:20160217233403p:plain

説明をよく見ると、ちゃんと Razor Template Engine を使うと書いてあります。

実際にプロジェクトを作成すると、MVC アプリケーションのようなフォルダ構成になっていました。

f:id:shiba-yan:20160217233410p:plain

cshtml を編集し、保存したタイミングで C# へのコンパイルが行われるので、特にコンパイルを意識することなく書けてとても良い感じです。

プリコンパイル結果はファイルと同名のクラスとして出力されるので、そのクラスをインスタンス化して GenerateString を呼び出すだけという簡単な API です。

f:id:shiba-yan:20160217233902p:plain

別に HTML の生成に限らず、Razor Template は文字列の生成に使うことができるので、プリコンパイルされる汎用的なテンプレートエンジンだと考えると応用範囲がかなり広がりそうです。

もっと早く知っていれば、楽を出来た部分がかなり多かったなぁと思いました。

おまけ : Razor のプリコンパイル

ここまで Xamarin の話でしたが、Visual Studio も大昔から拡張機能をインストールすることで、同様に Razor のプリコンパイルを行えます。成り立ちの関係から、こちらは MVC 寄りになります。

Generator として Template を選択すると、Xamarin に近い形でのプリコンパイルが行われます。

Xamarin.iOS でも Application Insights を使ってみる

最近は @normalian が Application Insights SDK for Java を弄っていたので、何となく iOS で Application Insights SDK を使ってみようと思いました。

ちゃんと Microsoft から iOS 向けの SDK が公開されています。

Microsoft/ApplicationInsights-iOS · GitHub

f:id:shiba-yan:20150603211415p:plain

残念なことに、まだ Xamarin で使えるバインディングが無さそうだったので、自作して試してみました。

追記

Microsoft から Xamarin 向けの Application Insights SDK がリリースされました。

この記事の後半は独自にライブラリへのバインディングを作る際の参考にしてください。

バインディングを作成

Xamarin Studio で iOS 向けのバインディングプロジェクトを作成し、Objective Sharpie で定義をある程度自動生成するという流れです。つまり、伊勢さんの記事と同じことをやります。

いつからなのかは分かりませんが、Objective Sharpie 自体が GUI では無くなったみたいなので、頑張ってコマンドを叩いて定義を作成していきます。

Objective Sharpie - Xamarin

幸いなことに、Application Insights SDK for iOS は Framework として提供されているので、ダウンロードした Framework に対して 1 回コマンドを叩くだけです。

sharpie bind -framework ApplicationInsights.framework -sdk iphoneos8.3

sdk のパラメータはインストール済みの SDK に合わせて指定します。これで自動生成されました。

f:id:shiba-yan:20150603212138p:plain

ApiDefinition.cs と StructsAndEnums.cs が同じディレクトリに作成されているので、この中身をバインディングプロジェクトにコピーします。名前空間が付いていないので、ファイルコピーだと面倒です。

f:id:shiba-yan:20150603212742p:plain

他には Framework からスタティックライブラリをコピーする必要がありますが、とりあえず省略してビルドを通すように頑張っていきます。

とても面倒なことに、Application Insights SDK for iOS はショートカット用にインスタンスメソッドとスタティックメソッドが、同じ名前でたくさん定義されてます。

f:id:shiba-yan:20150603213218p:plain

このあたりの調整は面倒くさそうだったので、ひとまずはスタティックなメソッドがあるものはそれを優先して、インスタンスメソッドはコメントアウトしておくことにします。

作成したバインディングプロジェクトを GitHub で公開したので、適当にビルドしてください。

ビルドが無事に成功すると dll が生成されます。何と無く Release ビルドにしておきました。

f:id:shiba-yan:20150603215131p:plain

この dll を Xamarin.iOS アプリケーション側の参照に追加することで、やっと Application Insights が使えるようになります。次は実際にアプリケーションに組み込んでみます。

アプリケーションに組み込む

適当に Xamarin.iOS アプリケーションを作成しておき、参照の編集ダイアログからついさっきビルドした ApplicationInsights.dll を参照に追加しておきます。

f:id:shiba-yan:20150603215503p:plain

これで SDK が使えるようになったので、AppDelegate.cs に Application Insights の初期化コードを追加します。

public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
{
    MSAIApplicationInsights.SetupWithInstrumentationKey("...");
    MSAIApplicationInsights.Start();

    return true;
}

InstrumentationKey は Application Insights を作成すると取得できます。疲れてきたので Application Insights を作る部分は紹介しません。

起動したかどうかだけというのも面白くないので、もう少し AppDelegate.cs にコードを追加します。よく使いそうなトレースメッセージを吐き出すコードを仕込んでみます。

public override void DidEnterBackground(UIApplication application)
{
    MSAITelemetryManager.TrackTraceWithMessage("Enter Background");
}

public override void WillEnterForeground(UIApplication application)
{
    MSAITelemetryManager.TrackTraceWithMessage("WillEnterForeground");
}

これで準備ができたので、実際にアプリケーションをシミュレーターで動かして、本当にプレビューポータルの Application Insights から見れるのか確かめてみます。

プレビューポータルから確認

Azure のプレビューポータルから該当する Application Insights を選んで表示してみました。ちゃんとユーザーとセッション数、そしてクラッシュログも取れていることが確認できますね。

f:id:shiba-yan:20150603221346p:plain

ブレードの下の方にある Diagnostic search を開くと、アプリケーションから送信されてきた情報を一覧で表示したり、絞り込んで確認することができます。

f:id:shiba-yan:20150603220942p:plain

仕込んでおいたメッセージが送信されていることも確認できました。絞り込みが結構便利そうです。

クラッシュログも取得できていましたが、dSYM のアップロードに対応していないみたいなので、スタックトレースやメッセージに関しては不完全な状態ですね。

f:id:shiba-yan:20150603221916p:plain

まだ beta3 ですし今後に期待しておきたいと思います。それよりも PCL 化してくれたら、わざわざバインディングを作る必要もないのに。と少し思いました。*1

*1:エラーハンドリング周りは PCL では難しいとは思いつつも要望したい

Xamarin.iOS の Unified API で Azure 通知ハブを使ってみた

酢酸先生が通知ハブ使ってて楽しそうだったので、自分も Xamarin.iOS で作ってるアプリ向けに通知ハブを組み込むことにしました。Azure 公式サイトにドキュメントが揃っているので便利です。

Get started with Notification Hubs

ただし、日本語版だとスクリーンショットがおかしくなっているらしいので、英語版を見ながら組み込みました。しかし Xamarin.iOS 向けには Microsoft から公式なライブラリが出ておらず、ソースをダウンロードしてビルドしろと書いてあるのに、そのリンク先が 404 で絶望でした。

なんとかサンプルコードから dll を拾ってきて、アプリに組み込んでみると非情なエラーが出ました。

f:id:shiba-yan:20141204152754p:plain

_人人人人人人人人人人人_
> Unified API 非対応 <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y ̄

ソースコードは見つからないし、Unified API だと使えないしで最悪だったのですが、Xamarin Component Store で Azure Messaging というパッケージが公開されていました。

f:id:shiba-yan:20141204150437p:plain

Azure Messaging / Components / Xamarin

Xamarin が出しているパッケージみたいですが、中身は WindowsAzure.Messaging だったので、Azure 公式サイトのドキュメントと同じ書き方で使えます。

なので、デバイストークンを貰う部分のコードもとてもシンプルなままです。

public override void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken)
{
    var hub = new SBNotificationHub(ConnectionString, NotificationHubPath);

    hub.RegisterNativeAsync(deviceToken, null, error =>
    {
    });
}

ちなみに iOS 8 からプッシュ通知周りの登録 API が結構変わっていたみたいですが、UIDevice にyouiされている CheckSystemVersion メソッドを使って簡単に処理を切り分けることができました。

public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
{
    if (UIDevice.CurrentDevice.CheckSystemVersion(8, 0))
    {
        var settings = UIUserNotificationSettings.GetSettingsForTypes(UIUserNotificationType.Alert | UIUserNotificationType.Badge | UIUserNotificationType.Sound, null);

        UIApplication.SharedApplication.RegisterUserNotificationSettings(settings);
        UIApplication.SharedApplication.RegisterForRemoteNotifications();
    }
    else
    {
        UIApplication.SharedApplication.RegisterForRemoteNotificationTypes(UIUserNotificationType.Alert | UIUserNotificationType.Badge | UIUserNotificationType.Sound);
    }

    return true;
}

この辺りはとても便利ですね。

実際に管理ポータルのデバッグタブから iOS 向けに通知を送信して確認してみます。

f:id:shiba-yan:20141204160852p:plain

送信ボタンを押してから、送信が完了するまでに少し時間がかかることもありますが、しばらく待つと iOS 側にプッシュ通知が無事に届いたことが確認できました。

f:id:shiba-yan:20141204160947p:plain

来年から 64bit 対応必須の関係で Unified API に移行しておかないと大変らしいので、これからはライブラリ周りの Unified API 対応で悩むことも多くなりそうです。

Xamarin.iOS と TestFlight を組み合わせた時に復帰のタイミングで落ちて困った話

開発中の Xamarin.iOS を使ったアプリに TestFlight SDK を組み込んでテスト配布していたところ、端末がスリープ後に復帰させたタイミングでクラッシュするという現象に悩まされました。

ちなみに、以下が TestFlight 側で確認できたクラッシュログになります。

f:id:shiba-yan:20140812103402p:plain

明らかに TestFlight SDK が持っている TFLog で落ちているようにしか見えないので、とりあえず TestFlight SDK のバージョンを 3.0.0 から 3.2.0 へアップデートしました。バインディングライブラリのビルド方法に関しては Xamarin.iOS で作ったアプリに TestFlight SDK を組み込む方法 - しばやん雑記 を参照してください。

しかし、これだけだと自分の環境では改善が見られなかったため、普段使っている TestFlight.TakeOff メソッドではなく、スレッドセーフ版のメソッドを使うようにしました。

public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
    TestFlight.TakeOffThreadSafe("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx");
}

TakeOff を TakeOffThreadSafe に修正するだけで、スレッドセーフ版になります。ここまで行うことで TFLog での謎クラッシュは発生しなくなりました。

TestFlight のバインディングライブラリは、何をやっているのか興味を持ったので少し調べてみると、基本的には半自動生成が行われたようなコードになっていましたが、スレッドセーフ版は少し異なっていました。

public static void TakeOffThreadSafe(String applicationToken)
{
    IntPtr sigbus = Marshal.AllocHGlobal (512);
    IntPtr sigsegv = Marshal.AllocHGlobal (512);
    IntPtr sigpipe = Marshal.AllocHGlobal (512);

    // Store Mono SIGSEGV and SIGBUS handlers
    sigaction (Signal.SIGBUS, IntPtr.Zero, sigbus);
    sigaction (Signal.SIGSEGV, IntPtr.Zero, sigsegv);
    sigaction (Signal.SIGPIPE, IntPtr.Zero, sigpipe);

    // Enable crash reporting libraries
    //MonoTouch.TestFlight.TestFlight.SetDeviceIdentifier(UIDevice.CurrentDevice.UniqueIdentifier);
    //MonoTouch.TestFlight.TestFlight.SetDeviceIdentifier(MonoTouch.AdSupport.ASIdentifierManager.SharedManager.AdvertisingIdentifier.ToString());
    TestFlight.TakeOff(applicationToken);

    // Restore Mono SIGSEGV and SIGBUS handlers
    sigaction (Signal.SIGBUS, sigbus, IntPtr.Zero);
    sigaction (Signal.SIGSEGV, sigsegv, IntPtr.Zero);
    sigaction (Signal.SIGPIPE, sigpipe, IntPtr.Zero);

    Marshal.FreeHGlobal (sigbus);
    Marshal.FreeHGlobal (sigsegv);
    Marshal.FreeHGlobal (sigpipe);
}
monotouch-bindings/testflight-threadsafe.cs at master · mono/monotouch-bindings · GitHub

残念ながら Linux のシステムコール周りには詳しくないのですが、コメントから読み取るに sigaction を TestFlight の TakeOff メソッドを呼び出す前に Mono が既にセットしているハンドラを退避させて、TakeOff メソッド呼び出し後に元に戻すという処理をしているだけのようです。

これで何故スレッドセーフになるのか、いまいち理解が追い付いていないですが、とりあえず TFLog でのクラッシュが改善されたので良いことにします。

Using TestFlight.dll with your own iOS App

Simply add TestFlight.dll to your project's References in MonoDevelop and you are good to go!

To use the thread safe TakeOff methode. Change TestFlight.TakeOff(token) to TestFlight.TakeOffThreadSafe(token) This works much better with MonoTouch. Because it doesn't override the Build-in (.NET) Error handling.

monotouch-bindings/README.md at master · mono/monotouch-bindings · GitHub

後からよく見たら、公式にスレッドセーフ版の TakeOffThreadSafe を使った方が良いと書かれていました。さっきのシステムコールをいろいろ呼び出している部分は、Mono のエラーハンドリングをオーバーライドさせないためのコードだったようですね。

Xamarin.iOS で Facebook ログインを実装してみた話

iOS アプリで Facebook ログインを実装するためには、公式に提供されている Facebook iOS SDK を使うのが非常に手っ取り早いです。

Login for iOS

この SDK を使うと Facebook のネイティブアプリがインストールされている場合には、そのログイン情報を使ってくれるので非常に便利ですが、例によって Xamarin からそのまま使うことは出来ないです。

しかし、Xamarin Components Store として Facebook iOS SDK をインストール出来るようになっています。

f:id:shiba-yan:20140114011004p:plain

Facebook iOS SDK / Components / Xamarin

ちょっと前までは Binding に問題があり、SDK を追加すると Xcode を使ったインターフェースの修正などが行えなくなってしまっていましたが、最近公開された 3.11.0 ではそのバグが修正されていました。

[Facebook] Updated to version 3.11.0 and fixed a bug with Xibs that caus... · d0e1370 · mono/monotouch-bindings · GitHub

自分はこの問題に打ち当たったので、自分で DLL をビルドしましたが、公式で対応してくれたので実にありがたいことです。

プロジェクトに Facebook iOS SDK を追加し終わったら、後は Objective-C 版と変わらないコードを書くだけです。まずは AppDelegate ですが、AppId の設定と Facebook ネイティブアプリや Safari へ遷移してから戻ってきたときのハンドリングを追加します。

public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
	FBSettings.DefaultAppID = "************";
	FBSettings.DefaultDisplayName = "sampleapp";
}

public override bool OpenUrl(UIApplication application, NSUrl url, string sourceApplication, NSObject annotation)
{
	return FBSession.ActiveSession.HandleOpenURL(url);
}

public override void OnActivated(UIApplication application)
{
	FBSession.ActiveSession.HandleDidBecomeActive();
}

この辺りはテンプレ的なコードなので、特に気にすることなく追加しておけばいいかと思います。

後は実際にログインを開始するためのコードですが、今回は適当にボタンを追加して、そのアクション内でログインセッションを開始させてみます。

partial void loginClick(NSObject sender)
{
	var newSession = new FBSession();

	// セットしておかないと落ちる
	FBSession.ActiveSession = newSession;

	newSession.Open((session, status, error) =>
	{
		if (status == FBSessionState.Open)
		{
			// アクセストークンを取得した!
			var accessToken = session.AccessTokenData.AccessToken;
		}
	});
}

特に難しいことはしていないですね。実際にこのコードをシミュレータで実行してみます。

f:id:shiba-yan:20140114011019p:plain

URL Type を Info.plist に追加していないので、アプリ内で WebView を使って表示されていますが、適切に URL Type を設定しておけば、Facebook ネイティブアプリがインストールされている場合にはネイティブアプリで、それ以外の場合は Safari で表示されるようになります。

Xamarin Components Store には Json.NET や Azure Mobile Service なども用意されているので、インストールして試してみるのも面白いと思いました。

Xamarin.iOS で作ったアプリに TestFlight SDK を組み込む方法

f:id:shiba-yan:20140112185756p:plain

TestFlight » Beta Testing On The Fly

最近は Xamarin を使って iOS 向けアプリを書いていたりするんですが、テスター向けにアプリを配布するのが非常にめんどくさかったので、今回は TestFlight を使って配布することにしました。

普通に OTA でアプリを配布できるのが便利なのですが、用意されている SDK を組み込むとクラッシュレポートとか、セッション情報を見れたりするみたいなので組み込んでみます。

TestFlight » Beta Testing On The Fly

ちょっと調べてみた感じでは TestFlight SDK の iOS 向けバインディングは GitHub にて公開されているみたいですが、バイナリ自体は配布対象となっていないみたいでした。なので、ソースをダウンロードして自分でビルドすることにしました。

mono/monotouch-bindings · GitHub

MonoTouch (Xamarin.iOS) 向けのバインディングプロジェクトが沢山用意されています。とりあえずこのリポジトリを clone するか、Zip でダウンロードするなどして、ローカルに持ってきます。

ビルドするためには TestFlight SDK の 2.0.2 が必要になるみたいですが、現在の最新版は 2.1.3 なのでそれをダウンロードして、GitHub からダウンロードしてきたソースの中に TestFlight というディレクトリがあるので、さらにその中の binding ディレクトリに保存します。

f:id:shiba-yan:20140112190230p:plain

ちなみにファイル名は 2.0.2 と変更しないと make で失敗するので変更してあります。

後は binding ディレクトリまでターミナルで移動して、make コマンドを実行するだけです。

f:id:shiba-yan:20140112190442p:plain

これで TestFlight.dll がビルドされたので、後は Xamarin Studio でプロジェクトの参照に追加するだけです。ネイティブなバイナリ自体が DLL に埋め込まれているみたいなので、単体ファイルで問題ないみたいですね。

参照を追加したら、アプリの AppDelegate 内に TestFlight 開始用のコードを追加するだけで完了です。

public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
	TestFlight.TakeOff("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx");
}

そして最後に Xamarin Studio を使って TestFlight へ Ad-hoc ビルドされたバイナリをアップロードします。これは公式のドキュメントが詳しいので、そっちを参照してください。

TestFlight Support | Xamarin

ちなみに TestFlight へのアップロードが完了すると、ダッシュボードから組み込まれている SDK のバージョンの確認が出来ます。

f:id:shiba-yan:20140112191542p:plain

ここでインストール数やクラッシュ回数も分かるので非常に便利ですね。

上手く設定が完了していると、TestFlight SDK を組み込んだアプリをインストールして、起動が行われたかといった情報が見れると思います。

f:id:shiba-yan:20140112191139p:plain

今は UDID が取れなくなっているので Anonymous なユーザーとして扱われています。クラッシュした場合は別にクラッシュレポートのページがあるので、そちらから確認できるはずです。

テスターがどのバージョンをインストールして、どのぐらいの時間実行しているかなど簡単に見れるので、バグの発見に役に立ちそうです。