shuhelohelo’s blog

Xamarin.Forms多めです.

Xamarin.FormsのShellでアプリ起動時のデフォルト表示ページを指定する

デフォルトページを指定する

特に指定しない場合,アプリケーション開始時のデフォルトのページは一番先頭に定義されたShellContentのページになる.

アプリケーションのページの定義順とは関係なくデフォルトページを指定したい場合は以下記事のようにする.

medium.com

ShellにはCurrentItemというプロパティがあり,ここに現在表示されているコンテンツが格納されている.

このShellの階層毎にこのCurrentItemにコンテンツを指定していけば良い.

これはXamlではなくコードビハインドで行うので,各要素にx:Nameで名前をつけておく必要がある.

例えば,Shell -> Animals(FlyoutItem) -> Domestic(Tab) -> Dogs(ShellContent)をデフォルトページにしたい場合は,Shellのコードビハインドで以下のようにする.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Xamarin.Forms;
using Xamarin.Forms.Xaml;
using XFShellRouting.Views;

namespace XFShellRouting
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class MainShell : Shell
    {
        public MainShell()
        {
            InitializeComponent();

            //ShellのCurrentItemを指定(shellAnimalsという名前のFlyoutItem)
            CurrentItem = shellAnimals;

            //shellAnimalsのCurrentItemを指定(shellDomesticという名前のTab)
            shellAnimals.CurrentItem = shellDomestic;

            //shellDomesticのCurrentItemを指定(shellDogsという名前のShellContent)
            shellDomestic.CurrentItem = shellDogs;

        }
    }
}

f:id:shuhelohelo:20200531165052p:plain

FlyoutItem直下のShellContentをデフォルトとして指定する場合

Shell -> Animals(FlyoutItem) -> Elephants(ShellContent)をデフォルトページに設定する場合,前述の指定方法だけではうまくいかない.

Elephantsページは表示されるが,下タブのElephantsが選択状態にならない.

この下タブまで選択状態にするためには,対象のShellContentShellSectionで囲み,ShellSectionにx:Nameで名前を設定してTitleでタイトルを設定する必要がある.

<?xml version="1.0" encoding="utf-8" ?>
<Shell
    x:Class="XFShellRouting.MainShell"
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:d="http://xamarin.com/schemas/2014/forms/design"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:views="clr-namespace:XFShellRouting.Views"
    mc:Ignorable="d">

    <FlyoutItem
        x:Name="shellAnimals"
        FlyoutDisplayOptions="AsMultipleItems"
        Route="animals">
        <Tab
            x:Name="shellDomestic"
            Title="Domestic"
            Route="domestic">
            <ShellContent
                Title="Cats"
                ContentTemplate="{DataTemplate views:CatsPage}"
                Route="cats" />
            <ShellContent
                x:Name="shellDogs"
                Title="Dogs"
                ContentTemplate="{DataTemplate views:DogsPage}"
                Route="dogs" />
        </Tab>
        <ShellContent
            Title="Monkeys"
            ContentTemplate="{DataTemplate views:MonkeysPage}"
            Route="monkeys" />
        <ShellSection x:Name="shellElephants" Title="Elephants">
            <ShellContent
                Title="Elephants"
                ContentTemplate="{DataTemplate views:ElephantsPage}"
                Route="elephants" />
        </ShellSection>
        <ShellContent
            Title="Bears"
            ContentTemplate="{DataTemplate views:BearsPage}"
            Route="bears" />
    </FlyoutItem>
    <ShellContent
        Title="About"
        ContentTemplate="{DataTemplate views:AboutPage}"
        Route="about" />
</Shell>

f:id:shuhelohelo:20200531164902p:plain

ソースコード

github.com

Xamrin.FormsでDebugとReleaseで設定ファイルを切り替える方法 メモ

こちらの記事のとおりにやってみる. redth.codes

これは読み込む設定ファイルがDebugビルドとReleaseビルドで切り替わるようにするもので,asp.netのuser secretのように「接続情報などの機密情報をgit管理されるプロジェクトファイル群の外側におくことで機密情報が公開されないようにする」ものではないので注意.

設定ファイルを作成する

Releaseビルド用のsettings.jsonファイルとDebugビルド用のlocal.settings.jsonファイルを作成する.

ファイルの中はそれぞれ以下のように記述する.

settings.json

{
  "apiUrlBase": "https://myserver.com/api/",
  "apiKey": "123456"
}

local.settings.json

{
  "apiUrlBase": "localhost",
  "apiKey": "abcdefg"
}

設定ファイルをEmbedded Resourceに設定する

settings.jsonとlocal.settings.jsonファイルのプロパティを開きます.

f:id:shuhelohelo:20200528115540p:plain

Build Actionの項目をEmbedded resourceに変更します.

f:id:shuhelohelo:20200528115637p:plain

この時点で共有プロジェクトのcsprojファイルには以下のように2つのファイルがEmbedded resourceとして登録されていることがわかります.

  <ItemGroup>
    <EmbeddedResource Include="Settings\local.settings.json" />
    <EmbeddedResource Include="Settings\settings.json" />
  </ItemGroup>

ビルド条件を追加する

ConditionにそれぞれのファイルをEmbedded resourceとしてアプリケーションに取り込む条件を設定します.

  <ItemGroup>
    <EmbeddedResource Include="Settings\settings.json"
                      Condition="'$(Configuration)' != 'Debug' or !Exists('Settings\local.settings.json')" />
    <EmbeddedResource Include="Settings\local.settings.json"
                      Link="settings.json"
                      Condition="'$(Configuration)' == 'Debug' and Exists('Settings\local.settings.json')" />
  </ItemGroup>

条件は以下のとおりです.

  • Debugビルドではない,または,local.settings.jsonファイルがない場合はsettings.jsonを使用する.

  • Debugビルド,かつ,local.settings.jsonファイルが存在する場合はlocal.settings.jsonを使用する.

local.settings.jsonにはConditionの他にLink="settings.json"という指定があります.

この指定で,local.settings.jsonファイルがsettings.jsonという名前でアプリに組み込まれます.これで,どちらのファイルが取り込まれたとしても,アプリケーション内で読み込む際にはsettings.jsonと指定することができます.

設定ファイルを読み込む

設定ファイルはJSONで記述されているので,これをオブジェクトに変換するためにSystem.Text.JsonをNugetからインストールしておきます.

f:id:shuhelohelo:20200528133216p:plain

Embedded resourceなファイルを読み込む手順は以下のとおりです.

            var assembly = Assembly.GetExecutingAssembly();

            var resName = assembly.GetManifestResourceNames()
                ?.FirstOrDefault(r => r.EndsWith("settings.json", StringComparison.OrdinalIgnoreCase));

            using var file = assembly.GetManifestResourceStream(resName);

            using var sr = new StreamReader(file);

            var jsonText = sr.ReadToEnd();

ここまでで文字列としてのJSONを読み込むことができました.

JSONのプロパティの値を取得する

プロパティの値を取得するためには文字列としてのJSONをオブジェクトに変換(デシリアライズ,逆シリアル化)する必要があります.

このとき,JSONのデータを表現するクラスを作成(定義が既知)してそれを利用してデシリアライズ→プロパティ取得する方法と,定義が未知のデータをデシリアライズ→プロパティ取得する方法があります.

今回はプロパティの数も少ないシンプルな内容なので,クラスを定義しない方法を取ります.

参考: C# で定義が未知の Json を扱う (.NET Core / System.Text.Json) - rksoftware

            var jsonDocument = JsonDocument.Parse(jsonText);
            jsonDocument.RootElement.TryGetProperty("apiUrlBase", out var apiUrlBaseElement);
            var apiUrlBase = apiUrlBaseElement.GetString();

TryGetPropertyメソッド,またはGetPropertyメソッドでJSONのプロパティ名を文字列で指定することで,そのプロパティのオブジェクトを取得できます.

そこから文字列としてプロパティのデータを取得するためにGetStringメソッドを使います.

これをバインディングで画面に表示させます.

実行結果

Debugビルドの場合:

f:id:shuhelohelo:20200528142900p:plain

Releaseビルドの場合:

f:id:shuhelohelo:20200528143132p:plain

それぞれ,異なる設定ファイルの内容が表示されます.

エミュレータでReleaseしたときにエラーが出る場合

エミュレータでReleaseで実行した際に以下のようなエラーが出ることがあります.

ADB0020: Android ABI mismatch. You are deploying an app supporting 'armeabi-v7a;arm64-v8a' ABIs to an incompatible device of ABI 'x86'. You should either create an emulator matching one of your app's ABIs or add 'x86' to the list of ABIs your app builds for.           

このような場合は,以下のようにします.

まず,Androidプロジェクトのプロパティを開きます. f:id:shuhelohelo:20200528143800p:plain

プロパティが開いたら,左側のAndroid Optionsタブを選択し,ページを一番下までスクロールさせるとAdvancedというボタンがあるので,押します.

f:id:shuhelohelo:20200528144110p:plain

詳細設定のウィンドウが表示されるので,Supported architecturesの項目でx86x86_64にもチェックを入れます.

f:id:shuhelohelo:20200528144327p:plain

これでエミュレータでも実行できます.

日本語対応について

JSONデータに日本語が含まれていた場合をみてみます.

local.settings.jsonファイルを以下のように変更します.

{
  "apiUrlBase": "これはlocalhostです",
  "apiKey": "abcdefg"
}

実行すると,文字化けしています. f:id:shuhelohelo:20200528145052p:plain

f:id:shuhelohelo:20200528145120p:plain

これはVisual StudioJSONファイルをテンプレートから作成すると,そのテンプレート自体がShif-JISで保存されていて,アプリケーション側ではそれをUTF-8で開くので結果文字化けします.

VS CodeなどのエディタでUTF-8で保存し直せばOKです.

f:id:shuhelohelo:20200528150724p:plain

f:id:shuhelohelo:20200528150747p:plain

文字コード問題の対処

  1. UTF-8で保存し直す.
  2. Visual Studio内で作成せず,外部でUTF-8で作成した後にVisual Studioのプロジェクトに取り込む.
  3. そもそもVisual StudioのファイルのテンプレートをUTF-8で保存する

3について,以下が詳しいです.

  • テンプレートファイルをUTF-8で保存し直す. qiita.com

  • UTF-8テンプレートファイルを作成して追加する qiita.com

ところでこのテンプレートファイルの文字コードはなに由来で決まるんだろうか.

OSのロケールをもとにVisual Studioのインストール時に決まるのかな?

ソースコード

今回のソースコードはこちら.

github.com

Xamarin.Forms(Android側)で顔検出 メモ

Android端末上での顔検出手段

  • Camera2はプレビュー取得時に顔検出も行えるのでそれを有効にする
  • Xamarin.Firebase.ML.Visionパッケージを使う

Camera2のプレビュー取得時顔検出について

プレビューと同時に顔検出を行う機能は,アプリがカメラからのプレビュー表示も行う場合は一石二鳥なので便利.画像形式の変換なども不要で検出結果が得られるのもメリット.

しかし,取得結果がカメラの取付け向きを基準としているので枠線の表示を行う場合は座標などを変換する必要がでてくる.

プレビュー時にセットでやってくれるものなので,任意の画像を処理をさせることはできない.

サンプルコード:

GitHub - shuheydev/XFGetCameraData at FaceDetectionByCaptureResult

FaceDetectionByCaptureResultリポジトリ

参考

qiita.com

顔検出までの流れや,カメラの設定についてわかりやすかった.

Xamarin.Androidだと,検出された顔情報は以下のように取得する.

            //Face[]の取得方法
            //https://forums.xamarin.com/discussion/95912/xamarin-studio-android-face-detection-with-
            var f = this._owner.CaptureResult?.Get(CaptureResult.StatisticsFaces);//Java.Lang.Objectが返ってくるので↓で変換する
            Android.Hardware.Camera2.Params.Face[] faces = f.ToArray<Android.Hardware.Camera2.Params.Face>();

Xamarin.Firebase.ML.Visionについて

GoogleのFirebase ML VisionAPIを使う.Firebaseのアカウントやプロジェクトを作成しなくても使える.

プレビューやカメラは全く関係なく,任意の画像に対して顔検出処理を行うことができるので,使い勝手が良い.

サンプルコード:

GitHub - shuheydev/XFGetCameraData at FaceDetectionUsingMLKit

FaceDetectionUsingMLKitリポジトリ. リアルタイムではなく,画像を一定フレーム毎にキャプチャしてそれを処理.している.

参考

var options = new FirebaseOptions.Builder()
.SetApplicationId("Firebase-App-Id")//必須
.SetApiKey("Firebase-Api-Key")//オプション
.SetDatabaseUrl("Firebase-Database-Url")//オプション
.SetGcmSenderId("Firebase-Sender-Id")//オプション
.Build();

var firebaseApp = FirebaseApp.InitializeApp(this, options);

Xamarin.Formsでバックグラウンド(非アクティブ時でも)で位置情報を取得し続ける(Android編)

Android8以上でも非アクティブな状態で処理を継続する手段としてForeground Serviceがある

これを使ってカウントアップを行う簡単なサンプルアプリを作った.

shuhelohelo.hatenablog.com

さて,バックグラウンドで数を1ずつ増やすタスクを実行しているわけですが,このタスクをGPSを取得するものに置き換えればよいということ.

引き続き,前回と同じリポジトリにブランチを足して変更を加える.

GetLocationというブランチにした.

github.com

位置情報の取得

Xamarin.Essentialsには位置情報を取得するAPIが用意されている.

docs.microsoft.com

それを使ってTaskCounterクラス内のRunCounterメソッドを書き換える.

        public async Task RunCounter(CancellationToken token)
        {
            //GPSの精度をHighに ←これと
            var request = new GeolocationRequest(GeolocationAccuracy.High);

            await Task.Run(async () =>
            {
                for (long i = 0; i < long.MaxValue; i++)
                {
                    token.ThrowIfCancellationRequested();

                    await Task.Delay(1000);

                    //ここから
                    //位置情報取得
                    var location = await Geolocation.GetLocationAsync(request);

                    var message = new TickedMessage
                    {
                        Message = $"Count : {i.ToString()}, Lat = {location.Latitude}, Lon = {location.Longitude}"
                    };
                    //ここまで

                    MainThread.BeginInvokeOnMainThread(() =>
                    {
                        MessagingCenter.Send<TickedMessage>(message, nameof(TickedMessage));
                    });
                }
            }, token);
        }

こんな感じで1秒毎に位置情報を取得するタスクが完成.

位置情報を拾う部分はXamarin.EssentialsのAPIを使って以下のように書く.

var request = new GeolocationRequest(GeolocationAccuracy.High);

var location = await Geolocation.GetLocationAsync(request);

位置情報取得の許可

位置情報の取得にはPermission(許可)が必要なので,これをユーザーに確認する処理も書く.

これもXamarin.Essentials(1.5以上)のPermissionsというAPIを使う.

docs.microsoft.com

バックグラウンドタスクが実行される前に,許可を得たいので,タスク開始ボタンが押されたときに許可を得る.

開始ボタンを押したときのイベントハンドラは以下のようになる.

        private async void Button_LongRunningTaskStart_Clicked(object sender, EventArgs e)
        {
            //位置情報取得の許可状態を確認
            var status = await Permissions.CheckStatusAsync<Permissions.LocationAlways>();
            if (status != PermissionStatus.Granted)
            {
                //許可されていなかった場合はユーザーに確認する
                status = await Permissions.RequestAsync<Permissions.LocationAlways>();
                //ユーザーが拒否した場合は(´・ω・`)
                if (status != PermissionStatus.Granted)
                    return;
            }

            var message = new StartLongRunningTaskMessage();
            MessagingCenter.Send(message, nameof(StartLongRunningTaskMessage));
        }

AndroidManifestにも以下の2つのPermissionにチェックをつけておく.

f:id:shuhelohelo:20200524213319p:plain

もしくはAndroidManifest.xmlに追加する.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.companyname.xfforegroundservicepractice" android:installLocation="auto">
    <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28" />
    <application android:label="XFForegroundServicePractice.Android"></application>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
</manifest>

実行結果

開始ボタンを押すと,位置情報の取得に関する許可を求めるダイアログが表示されるので,「Allow all the time」を選択.

f:id:shuhelohelo:20200524213803p:plain

このように一定間隔で現在地の経緯度が更新されることが確認できる.

f:id:shuhelohelo:20200524214251p:plain

ちなみに,Xamarin.Essentialsで位置情報を取得するたびに,Androidの位置情報マークが点滅するので少し鬱陶しいかもしない.

他のアプリを利用中でも動き続けるので,ランニングや散歩などを記録するアプリなどに使える.

ソースコード

github.com

GetLocationブランチが今回のコード

注意

頻繁な位置情報の取得はバッテリーの消費が激しいので,取得間隔を適宜変更する必要がある.

Xamarin.Formsでバックグラウンド(非アクティブ時でも)で処理を継続する(Android編)

はじめに

この記事は,アプリケーションが非アクティブの状態でも動作を継続させる方法.

各プラットフォームごとに実装が異なり,この記事はAndroidについて.

アプリケーションが非アクティブ(アプリをユーザーが操作中ではない)ときにも,継続して処理を行うことが目的.

例えば,ランニングの経路を記録するアプリを作る場合,非アクティブな状態でも定期的に位置情報を取得する必要がある.これについては次の記事に書いてある.

shuhelohelo.hatenablog.com

Androidのバックグラウンドタスクの実行制限について

Android開発者サイトにもあるようにバックグラウンドタスクの実行には以下の制限があるため,非アクティブ時には一定時間後に停止する.

developer.android.com

多くの Android アプリやサービスは同時に実行することができます。 たとえば、あるウィンドウでゲームをプレイしながら別のウィンドウでウェブをブラウジングしているときに、別のアプリで音楽を再生できます。 同時に実行するアプリが多いほど、システムに大きな負荷がかかります。 さらに多くのアプリやサービスがバックグラウンドで実行されると、システムにかかる負荷が増えて、音楽アプリが突然シャットダウンするなど、ユーザー エクスペリエンスが低下します。

Android 8.0 では、このような問題の発生頻度を抑えるため、ユーザーがアプリを直接操作していないときにアプリで実行できる動作を制限しています。 アプリの動作は次の 2 つの方法で制限されます。

- バックグラウンド サービスの制限事項: アプリがアイドル状態にある場合、バックグラウンド サービスの使用を制限します。 これは、ユーザーが認識しやすいフォアグラウンド サービスには適用されません。

- ブロードキャストの制限事項: 限定的な例外を除き、アプリはマニフェストを使用して暗黙的なブロードキャストを登録できません。 ただし、アプリは実行時にこれらのブロードキャストを登録でき、アプリを明確に対象とする明示的なブロードキャストについては、マニフェストを使って登録できます。

# 解決方法

この制限は以下の状態には適用されない.

  • 可視アクティビティがある(アクティビティが開始されているか一時停止されているかに関係なく)。
  • フォアグラウンド サービスを使用している。
  • 別のフォアグラウンド アプリが、該当アプリのいずれかのサービスにバインドされるか、該当アプリのいずれかのコンテンツ プロバイダを使用することで、該当アプリに接続している。 たとえば、別のアプリが次のいずれかのサービスにバインドされると、アプリがフォアグラウンドになります。
    • IME
    • 壁紙サービス
    • 通知リスナー
    • 音声またはテキスト サービス

このうちのForeground Serviceを使用することで,上記の制限を受けないようにすることができる.

実装

Xamarin.FormsからNativeのバックグラウンドタスクを実行する

iOSAndroidではバックグラウンドタスクを実行する方法が大きく異なるため,インターフェースで共通化することができない.

そこで,MessagingCenterを利用することで,各プラットフォームのバックグラウンドタスクを開始する.

robgibbens.com

f:id:shuhelohelo:20200524135639p:plain

また,各プラットフォームで実行しているバックグラウンドタスクからデータをXamarin.Forms側に送る場合にも同様にMessagingCenterを使う.

この部分については上記のチュートリアルの通りに行う.バックグラウンドでカウントアップして,それをUIに表示するシンプルなアプリ.

Xamarin.Forms側の設定

Forms側では,Native側から送られてくるデータを受け取るようにMessagingCenterのメッセージを購読する.

MainPageのコンストラクタにでも以下のように記述する.

            MessagingCenter.Subscribe<TickedMessage>(this, nameof(TickedMessage), message =>
            {
                Message = message.Message;
            });

TickedMessageクラスはNative側から送られてくるデータを格納するためのクラス.

    public class TickedMessage
    {
        public string Message { get; set; }
    }

これで,NativeからTickedMessageオブジェクトのメッセージが送られてきたときに,その内容をMainPage内のMessageプロパティに入れる処理が行われる.

Android側の設定

Native側ではForms側からバックグラウンドの開始,停止のメッセージを受け取るようにする.

MainActivity.cs内のOnCreateメソッド内にMessagingCenterのメッセージ購読処理を記述する.

メッセージを受け取った際に,サービスの開始,停止を行っている.

            //Forms側からのバックグラウンドタスク開始,停止のメッセージ購読
            MessagingCenter.Subscribe<StartLongRunningTaskMessage>(this, nameof(StartLongRunningTaskMessage), _ =>
            {
                var intent = new Intent(this, typeof(LongRunningTaskService));
                StopService(intent);//タスクが重複しないように終了させてから.
                //Android8.0以上はStartForegroundServiceを使う.
                if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
                {
                    StartForegroundService(intent);
                }
                else
                {
                    StartService(intent);
                }
            });
            MessagingCenter.Subscribe<StopLongRunningTaskMessage>(this, nameof(StopLongRunningTaskMessage), _ =>
            {
                var intent = new Intent(this, typeof(LongRunningTaskService));
                StopService(intent);
            });

LongRunningTaskServiceAndroidで実行されるサービスを表すクラスで,Serviceクラスを継承し,[Service]Attributeをつける.

サービスについて参考: docs.microsoft.com

   [Service]
    public class LongRunningTaskService : Service
    {
        CancellationTokenSource _cts;

        public override IBinder OnBind(Intent intent)
        {
            return null;
        }

        public override StartCommandResult OnStartCommand(Intent intent, StartCommandFlags flags, int startId)
        {
            _cts = new CancellationTokenSource();

            Task.Run(() => {
                try
                {
                    //Xamarin.Fomrs側のコードを実行する.
                                        //処理自体は共通のコードを実行したい
                    var counter = new TaskCounter();
                    counter.RunCounter(_cts.Token).Wait();
                }
                catch (OperationCanceledException)
                {
                }
                finally
                {
                    if (_cts.IsCancellationRequested)
                    {
                        var message = new CancelledMessage();
                        Device.BeginInvokeOnMainThread(
                            () => MessagingCenter.Send(message, "CancelledMessage")
                        );
                    }
                }

            }, _cts.Token);

            return StartCommandResult.Sticky;
        }

        public override void OnDestroy()
        {
            if (_cts != null)
            {
                _cts.Token.ThrowIfCancellationRequested();

                _cts.Cancel();
            }
            base.OnDestroy();
        }
    }

メモ:

OnstartCommandメソッドの戻り値について

www.gigas-jp.com

  ●START_NOT_STICKY
  Serviceがkillされた場合、Serviceは再起動しません。

 ●START_STICKY
  デフォルトではこの設定になります。
  Serviceがkillされた場合、直ちにServiceの再起動を行います。
  再起動時、前回起動時のIntentは再配信されず、
  複数のServiceが起動していても再起動するServiceは1つです。

 ●START_REDELIVER_INTENT
  Serviceがkillされた場合、直ちにServiceの再起動を行います。
  再起動時、最後に使用したIntentを使用します。
  また、複数のServiceが起動している場合、すべて再起動します。

 ●START_STICKY_COMPATIBLILITY
  START_STICKYの互換バージョンです。Android2.0未満ではこれがデフォルト設定です。

実際に実行するタスクはこちら.

    public class TaskCounter
    {
        //このメソッドがバックグラウンドで実行される
        public async Task RunCounter(CancellationToken token)
        {
            await Task.Run(async () =>
            {
                for (long i = 0; i < long.MaxValue; i++)
                {
                    token.ThrowIfCancellationRequested();

                    await Task.Delay(1000);
                    var message = new TickedMessage
                    {
                        Message = i.ToString()
                    };

                    MainThread.BeginInvokeOnMainThread(() =>
                    {
                        MessagingCenter.Send<TickedMessage>(message, nameof(TickedMessage));
                    });
                }
            }, token);
        }
    }

Xamarin.Formsから実行

MainPageに開始,停止を行うボタンと,バックグラウンドタスクでのカウントを表示するラベルを配置する.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
    x:Class="XFForegroundServicePractice.MainPage"
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:d="http://xamarin.com/schemas/2014/forms/design"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <StackLayout>
        <!--  Place new controls here  -->
        <Label
            FontSize="100"
            HorizontalOptions="Center"
            Text="{Binding Message}"
            VerticalOptions="CenterAndExpand" />
        <Button
            x:Name="Button_LongRunningTaskStart"
            Clicked="Button_LongRunningTaskStart_Clicked"
            Text="Start Task" />
        <Button
            x:Name="Button_LongRunningTaskStop"
            Clicked="Button_LongRunningTaskStop_Clicked"
            Text="Stop Task" />
    </StackLayout>

</ContentPage>

それぞれのボタンが押されたときにMessagingCenterでメッセージを送る.

        private void Button_LongRunningTaskStart_Clicked(object sender, EventArgs e)
        {
            var message = new StartLongRunningTaskMessage();
            MessagingCenter.Send(message, nameof(StartLongRunningTaskMessage));
        }

        private void Button_LongRunningTaskStop_Clicked(object sender, EventArgs e)
        {
            var message = new StopLongRunningTaskMessage();
            MessagingCenter.Send(message, nameof(StopLongRunningTaskMessage));
        }

ここまででXamarin.FormsからAndroidのバックグラウンドタスクを実行できる.

ただし,この時点では前述の制限がかかり,非アクティブな状態では1分ほどで停止する.

実行結果

この時点での実行結果は以下のとおり.

f:id:shuhelohelo:20200524153624g:plain

スタートボタンを押すとカウントアップされていき,アプリを切り替えたり,ホーム画面を表示させている間もカウントアップが継続していることがわかる.

非アクティブにして1分以上経過すると,カウントアップが止まっていることも確認できる.

Foreground Serviceとしてタスクを実行する

Foreground Serviceとしてタスクを実行すれば,制限がかからない.

Foreground Serviceとして実行するためにはNotificationクラスのインスタンスを渡す必要がある.

これはForeground Serviceとしてタスクが開始されたことをユーザーに通知するため.

以下のようにNotification Channelとそれを使用するNotificationオブジェクトを作成するメソッドを用意する.

        //フォアグラウンドサービス用のNotification ChannelとNotificationを作成する
        private Notification CreateNotification()
        {
            #region Create Channel
            string channelId = "foregroundpractice";
            string channelName = "foregroundpractice";
            string channelDescription = "The foreground channel for notifications";
            int _pendingIntentId = 1;

            NotificationManager _notificationManager;
            _notificationManager = (NotificationManager)Android.App.Application.Context.GetSystemService(Android.App.Application.NotificationService);
            if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
            {
                var channelNameJava = new Java.Lang.String(channelName);
                var channel = new NotificationChannel(channelId, channelNameJava, NotificationImportance.Low)
                {
                    Description = channelDescription,
                };
                channel.EnableVibration(false);
                _notificationManager.CreateNotificationChannel(channel);
            }
            #endregion

            #region Create Notification
            Intent foregroundNotificationIntent = new Intent(Android.App.Application.Context, typeof(MainActivity));

            PendingIntent pendingIntent = PendingIntent.GetActivity(Android.App.Application.Context, _pendingIntentId, foregroundNotificationIntent, PendingIntentFlags.OneShot);

            NotificationCompat.Builder builder = new NotificationCompat.Builder(Android.App.Application.Context, channelId)
                .SetContentIntent(pendingIntent)
                .SetContentTitle("ForegroundServiceApp")
                .SetContentText("Foreground service started")
                .SetOngoing(true)
                .SetColor(ActivityCompat.GetColor(Android.App.Application.Context, Resource.Color.colorAccent))
                .SetLargeIcon(BitmapFactory.DecodeResource(Android.App.Application.Context.Resources, Resource.Drawable.launcher_foreground))
                .SetSmallIcon(Resource.Drawable.launcher_foreground);

            var notification = builder.Build();
            #endregion

            return notification;
        }

そして,OnStartCommandメソッドで以下のようにForeground Serviceを開始する.

            //Notificationの作成(Android 8.0以降では必須)
            var notification = CreateNotification();

            //Foreground Serviceとして実行
            //id:サービスを識別するためにアプリケーション内で一意である整数値。
            //notification:サービスが実行されている間、Android がステータスバーに表示する Notification オブジェクト。
            StartForeground(id: 1, notification: notification);

しかし,Foreground Serviceを実行するためにはPermissionが必要で,このままではエラーになる.

f:id:shuhelohelo:20200524160948p:plain

Foreground Serviceを許可する

AndroidプロジェクトのプロパティからPermissionを与える.

f:id:shuhelohelo:20200524164613p:plain

またはAndroidManifest.xmlに以下のようにPermissionを追加する.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.companyname.xfforegroundservicepractice" android:installLocation="auto">
    <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28" />
    <application android:label="XFForegroundServicePractice.Android"></application>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<!--これ-->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

</manifest>

これで実行すると,先程と同様にカウントアップが始まるが,今度はアプリケーションを非アクティブにしても動作は継続される.

実行中はこのように通知が表示され,ストップすると消える.

f:id:shuhelohelo:20200524171352p:plain

追記:

アプリを終了させても明示的にForeground Serviceを終了させないとForeground Serviceだけ動き続けるようだ.よく調べなければ.

ソースコード

github.com

  • masterブランチは制限がかかるバックグラウンドサービス
  • ForegroundServiceブランチは制限がかからないもの

Azure Notification HubsでXamarin.FormsアプリにPush通知 メモ (Android編)

このチュートリアルの通りにやってみる.

docs.microsoft.com

Azure Notification Hubsを使う場合の流れ

Firebase Cloud MessagingやApple Push Notification ServiceなどをAzure Notification Hubsを介して利用する形.

f:id:shuhelohelo:20200523224754p:plain

Azure Notification HubsはあくまでHubとしての役割を果たすものであって,それ単体でPush通知を行うサービスではない.

また,デバイスがPush通知を受け取ったときの処理は実装す

Xamarin.Android

ここでXamarin.Androidチュートリアルはこちら.

Android側のFirebase Cloud MessagingとAzure Notification Hubsを使うPush通知のチュートリアルです.

docs.microsoft.com

Firebase Cloud Messagingプロジェクトを作成

console.firebase.google.com

サインインする.

プロジェクトを作成する.

f:id:shuhelohelo:20200514020151p:plain

プロジェクトに名前をつける

f:id:shuhelohelo:20200514020350p:plain

Googleアナリティクスを有効にするか?

f:id:shuhelohelo:20200514020623p:plain

もう一つGoogleアナリティクスについて

f:id:shuhelohelo:20200514020811p:plain

プロジェクト作成完了

f:id:shuhelohelo:20200514021041p:plain

プロジェクトにAndroidアプリを追加する

プロジェクトの作成が完了すると,プロジェクトのページが表示されるのでアプリケーションを登録する.

f:id:shuhelohelo:20200514021410p:plain

アプリの登録にはAndroidのパッケージ名が必要なので,それを先に取得しておきます.

Xamarin.FormsのAndroidプロジェクトのプロパティを開き,「Android Manifest」タブを開くとそこにPackage Nameが記述されています.

f:id:shuhelohelo:20200514021924p:plain

これをFirebaseのアプリ登録画面に入力します.

f:id:shuhelohelo:20200514022126p:plain

google-services.jsonファイルをダウンロードする.

f:id:shuhelohelo:20200514022533p:plain

次のステップはスキップする.

f:id:shuhelohelo:20200514022838p:plain

次のステップもスキップする.

f:id:shuhelohelo:20200514023009p:plain

Androidアプリの追加が完了しました.

f:id:shuhelohelo:20200514023132p:plain

登録したアプリの設定画面を開くために「⚙」アイコンをクリックします.

f:id:shuhelohelo:20200514023337p:plain

以下のような設定画面が開きます.

f:id:shuhelohelo:20200514023729p:plain

クラウドメッセージング」タブを開いて「サーバーキー」をコピーします.

f:id:shuhelohelo:20200514024117p:plain

Azure Notification Hubsの作成

Azure Portalに移動する. portal.azure.com

「Notification Hub」リソースを作成する.

f:id:shuhelohelo:20200514025714p:plain

「Add」ボタンを押してNotification Hubを作成する.

f:id:shuhelohelo:20200514025831p:plain

以下のように項目を埋めて「Create」ボタンを押す.無料プランがあるので選んでおくこと.

f:id:shuhelohelo:20200514030729p:plain

リソースの作成が完了したらリソースに移動する.

左側のブレードから「Access Policies」を選択する.

f:id:shuhelohelo:20200514032430p:plain

ここでConnection Stringが2つ表示されるが,DefaultFullSharedAccessSignatureは使わないこと.こちらはバックエンドのサーバーアプリでのみ使用すること,とのこと.

Do not use the DefaultFullSharedAccessSignature policy in your application. This is meant to be used in your back end only.

Connection Stringは後で使う.

今度は左側のブレードから「Google(GCM/FCM)」を選択する.

f:id:shuhelohelo:20200514032808p:plain

ここの「API Key」の欄に,Firebaseの方の「サーバーキー」をコピペして「Save」ボタンを押す.

Visual Studio

今度はVisual Studio側の設定です.Xamarin.Formsプロジェクトを作成しておきます.

Nugetパッケージをインストールする

インストールするのは以下の3つのパッケージ.

  • Xamarin.GooglePlayServices.Base
  • Xamarin.Firebase.Messaging
  • Xamarin.Azure.NotificationHubs.Android

Androidプロジェクトで右クリックしてManage Nuget Packagesを選択してインストールを行う.

google-services.jsonファイルを追加する

ダウンロードしておいたgoogle-services.jsonファイルをAndroidプロジェクトに追加する.

そして,Build ActionをGoogleServicesJsonに設定する.

f:id:shuhelohelo:20200514040255p:plain

AndroidManifest.xmlの編集

以下のように追加します.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.companyname.xfazurenotificationhubs">
  <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="28" />

  <!-- 追加ここから 1-->
  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
  <uses-permission android:name="android.permission.WAKE_LOCK" />
  <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
  <!-- ここまで -->

  <application android:label="XFAzureNotificationHubs.Android">

    <!-- 追加ここから 2-->
    <receiver android:name="com.google.firebase.iid.FirebaseInstanceIdInternalReceiver" android:exported="false" />
    <receiver android:name="com.google.firebase.iid.FirebaseInstanceIdReceiver" android:exported="true" android:permission="com.google.android.c2dm.permission.SEND">
      <intent-filter>
        <action android:name="com.google.android.c2dm.intent.RECEIVE" />
        <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
        <category android:name="${applicationId}" />
      </intent-filter>
    </receiver>
    <!-- ここまで -->

  </application>
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>

www.c-sharpcorner.com

接続情報を保持するクラスを作成する

Constants.csファイルを共有プロジェクトに追加し,以下の情報をAzureから取得して保持するようにする. この情報はAndroid,iOSの両方から使う.

  • Listen connection string
  • hub name
    public static class Constants
    {
        public const string ListenConnectionString = "<DefaultListenSharedAccessSignature from Azure>";
        public const string NotificationHubName = "<Hub Name from Azure>";
    }

MainActivity.csの編集

MainActivity.csに以下usingを追加する.

using Android.Util;
using Android.Gms.Common;

次はMainActivityクラス内に以下を追加する.

public const string TAG = "MainActivity";
internal static readonly string CHANNEL_ID = "my_notification_channel";

MainActivityクラス内に次のメソッドを追加する.これはGoogle Play Serviceが有効かどうかを確認するためのもの.

public bool IsPlayServicesAvailable()
{
    int resultCode = GoogleApiAvailability.Instance.IsGooglePlayServicesAvailable(this);
    if (resultCode != ConnectionResult.Success)
    {
        if (GoogleApiAvailability.Instance.IsUserResolvableError(resultCode))
            Log.Debug(TAG, GoogleApiAvailability.Instance.GetErrorString(resultCode));
        else
        {
            Log.Debug(TAG, "This device is not supported");
            Finish();
        }
        return false;
    }

    Log.Debug(TAG, "Google Play Services is available.");
    return true;
}

更に,MainActivityクラス内にNotification Channelを作成するメソッドを追加する.

private void CreateNotificationChannel()
{
    if (Build.VERSION.SdkInt < BuildVersionCodes.O)
    {
        // Notification channels are new in API 26 (and not a part of the
        // support library). There is no need to create a notification
        // channel on older versions of Android.
        return;
    }

    var channelName = CHANNEL_ID;
    var channelDescription = string.Empty;
    var channel = new NotificationChannel(CHANNEL_ID, channelName, NotificationImportance.Default)
    {
        Description = channelDescription
    };

    var notificationManager = (NotificationManager)GetSystemService(NotificationService);
    notificationManager.CreateNotificationChannel(channel);
}

MainActivity.cs内のOnCreateメソッド内で,base.OnCreate(savedInstanceState);の後ろに以下を追加する.

if (Intent.Extras != null)
{
    foreach (var key in Intent.Extras.KeySet())
    {
        if(key!=null)
        {
            var value = Intent.Extras.GetString(key);
            Log.Debug(TAG, "Key: {0} Value: {1}", key, value);
        }
    }
}

IsPlayServicesAvailable();
CreateNotificationChannel();

MyFirebaseMessagingServiceクラスをAndroidプロジェクトに追加する.

以下のusingを追加する.

using Android.Util;
using Firebase.Messaging;
using Android.Support.V4.App;    
using WindowsAzure.Messaging;

MyFirebaseMessagingServiceクラスは以下のように宣言する. Push通知をクラウド側から受け取った場合にこのクラス内のOnMessageReceivedメソッドが実行される.

[Service]
[IntentFilter(new[] { "com.google.firebase.MESSAGING_EVENT" })]
[IntentFilter(new[] { "com.google.firebase.INSTANCE_ID_EVENT" })]
public class MyFirebaseMessagingService : FirebaseMessagingService
{
...
}

MyFirebaseMessagingServiceクラス内に以下を追加. Push通知を受け取った際の処理をOnMessageReceivedメソッド内に記述する.今回の例ではローカル通知に表示させている.

const string TAG = "MyFirebaseMsgService";
    NotificationHub hub;

    public override void OnMessageReceived(RemoteMessage message)
    {
        Log.Debug(TAG, "From: " + message.From);
        if (message.GetNotification() != null)
        {
            //These is how most messages will be received
            Log.Debug(TAG, "Notification Message Body: " + message.GetNotification().Body);
            SendNotification(message.GetNotification().Body);
        }
        else
        {
            //Only used for debugging payloads sent from the Azure portal
            SendNotification(message.Data.Values.First());

        }
    }

    void SendNotification(string messageBody)
    {
        var intent = new Intent(this, typeof(MainActivity));
        intent.AddFlags(ActivityFlags.ClearTop);
        var pendingIntent = PendingIntent.GetActivity(this, 0, intent, PendingIntentFlags.OneShot);

        var notificationBuilder = new NotificationCompat.Builder(this, MainActivity.CHANNEL_ID);

        notificationBuilder.SetContentTitle("FCM Message")
                    .SetSmallIcon(Resource.Drawable.ic_launcher)
                    .SetContentText(messageBody)
                    .SetAutoCancel(true)
                    .SetShowWhen(false)
                    .SetContentIntent(pendingIntent);

        var notificationManager = NotificationManager.FromContext(this);

        notificationManager.Notify(0, notificationBuilder.Build());
    }

さらにMyFirebaseMessagingServiceクラスに以下を追加する. ここのnew NotificationHub(Constants.NotificationHubName, Constants.ListenConnectionString, this);でAzure Notification Hubs`に端末を登録している. ということは,このコードが実行されるときにオンラインである必要があるな.

    public override void OnNewToken(string token)
    {
        Log.Debug(TAG, "FCM token: " + token);
        SendRegistrationToServer(token);
    }

    void SendRegistrationToServer(string token)
    {
        // Register with Notification Hubs
        hub = new NotificationHub(Constants.NotificationHubName,
                                    Constants.ListenConnectionString, this);

        var tags = new List<string>() { };
        var regID = hub.Register(token, tags.ToArray()).RegistrationId;

        Log.Debug(TAG, $"Successful registration of ID {regID}");
    }

プロジェクトをビルドする.

アプリを実行する.

Azure Notification Hubsからテスト送信する.

作成してあるNotification Hubリソースのページに移動し,左側のブレードからTest Sendを選択する.

f:id:shuhelohelo:20200514050644p:plain

PlatformにAndroidを指定して,「Send」ボタンを押す.

f:id:shuhelohelo:20200514051226p:plain

以下のメッセージが出れば送信成功.

f:id:shuhelohelo:20200514051331p:plain

少しすると実行中のアプリに以下のように通知が表示される.

f:id:shuhelohelo:20200514051420p:plain

今回のソースコード

github.com

メモ

Firebase Cloud Messagingを介したメッセージの受信は,アプリが起動していなくても行われる.

一度でも起動すれば,アプリケーションがFCMを利用することが登録?されるので,例えば端末再起動後にアプリを起動しなくても通知を受信できる.

端末起動後,メッセージ受信までに1分程かかる感じ.

developer.android.com

ASP.NETアプリから利用する

ASP.NETアプリからNotification Hubsを使ってPush通知を送ってみたくなりました.

こちらを参考にやってみます.

ASP .NET Core Web API with Azure Notification&nbsp;Hubdevislandblog.wordpress.com

上の記事ではWebAPIからNotification Hubsを利用していますが,渡しの場合は,Webアプリから使いたいと思います.

Webアプリ上のボタンを押すとランダムな応援メッセージが届くというものを作ってみたい.

まずはNotification Hubsに接続する情報を保持するクラスを作成.

    public static class NotificationHubConfiguration
    {
        public const string ConnectionString = "<DefaultFullSharedAccessSignature from Azure>";
        public const string NotificationHubName = "<Notification Hub Name from Azure>";
    }

次にappsettings.jsonに以下を追加します.

  "NotificationHub": {
    "ConnectionString": "<Endpoint...>",
    "HubName": "<DevIslandNotificationHub>"
  },

ここで,Connection Stringに使用するのはモバイルアプリのクライアント側とは異なりDefaultFullSharedAccessSignatureの方なので注意しましょう.

f:id:shuhelohelo:20200514201141p:plain

うーん,こっちの方がわかりやすいかな.

blog.verslu.is

           NotificationHubClient _hub = NotificationHubClient.CreateClientFromConnectionString("<DefaultFullSharedAccessSignature from Azure>", "<Notification Hub Name from Azure>");

                string message = "Hello World.";
                var androidMessage = "{\"data\":{\"message\": \"" + message + "\"}}";
                await _hub.SendFcmNativeNotificationAsync(androidMessage);

iOS

持ってないのでわかりません.

docs.microsoft.com

docs.microsoft.com

Xamarin.FormsでのPush通知について

追記

App Center Pushは廃止となり,Azure Notification Hubsを使うように,とのことです.

docs.microsoft.com

f:id:shuhelohelo:20200514005843p:plain


James Montemagnoさんによる説明.

montemagno.com

以下,その内容のメモ


Push通知を実現する方法は色々あるが,2つの選択肢を知っておけば良い.

Push通知の仕組み

Push通知を実現する方法は各プラットフォーム毎に異なる.

それぞれSDKがある.Push通知を完全にコントロールしたいのであれば,バックエンドを用意して以下のような仕組みをとる必要がある.

f:id:shuhelohelo:20200513221240p:plain

バイスの登録,リフレッシュ,各サービスとの通信について考慮し,しかも各プラットフォーム毎にSDKを使って実装を行わなければならない.

これはツラい.

我々クロスプラットフォーム開発者はそんなことはしないようにする.

選択肢は2つあって,クロスプラットフォーム開発のツラいところを軽くしてくれる.

Visual Studio App Center

最も簡単な方法. App Centerはたった1つのクロスプラットフォームSDKで数行のコードでPush通知を実現できる.

メリット

  • クロスプラットフォームSDK
  • すべてのサーバーの動作を扱うことができる
  • Push通知を送るためのUIがある
  • 複雑なPush通知を扱うAPIがある
  • 色,アイコン,サウンド,バッジなどのOSの機能を使える
  • データ分析に基づいてユーザーの分類ができる

以下のようにブラウザからPush通知の送信を行うことができる.

f:id:shuhelohelo:20200513222317p:plain

Push通知を行うためのWebAPIが用意されていて,それを自分のサーバーから利用することができる.

f:id:shuhelohelo:20200513222459p:plain

とても少ないコードで実現できる.

App Center Pushの使い方はこちら

docs.microsoft.com

デメリット

デメリットは多くはないが以下のようなものがある.

  • 各プラットフォームごとに通知を送る必要がある.
  • GroupingやIntentなどの面でできることに制限がある.

App Center Pushまとめ

8~9割ほどのアプリケーションはこれで十分.

Azure Notification Hubs

もし,Push通知について完全にコントロールしたいのであれば,いくらかアプリケーションでの設定が必要だけれども,Azure Notification Hubsを使うと良い.

Azure Notification Hubsは以下のように,,あなたのサーバーとデバイスの間に位置し,RegistrationとOfferを一元的に扱うことができる.

f:id:shuhelohelo:20200513223414p:plain

メリット

App Center Pushとの大きな違いは各プラットフォーム毎にデバイスの登録や,通知を受け取ったときの表示に関するコードを書かなければならないということ.

これはつまり,AndroidでBackground Serviceを,iOSで状態をハンドリングする処理を書かなければならないということ.

Groupingやインライン返信など何でもできる.

デメリット

  • サーバー(バックエンド)を用意しなければならない.
  • 実装面などでやることが若干多くなる

まとめ

通知は複雑そうに見えるが,App CenterやNotification Hubsを使うことで実装がとても簡単になる.

アプリ全体の9割ぐらいはApp Centerでいける.

もし,SMSアプリやEmailアプリなどのすごくカスタマイズが必要な場合はNotification Hubsを使うと良い.

自分の感想

なるほど.まずはApp Centerだな!