しっぽを追いかけて

ぐるぐるしながら考えています

Unity と猫の話題が中心   掲載内容は個人の私見であり、所属組織の見解ではありません

Xamarin.Forms で Microsoft Band の通知機能を利用する

Microsoft Band の Xamarin.Forms サンプルはこれが最後、通知機能を利用してみます

ソースコードの一式は下記にあります!

細かい実装などはこちらを参照ください

github.com


ソリューションはこんな感じになりました

f:id:matatabi_ux:20150503191709p:plain

通知機能管理クラスを追加しました・・Android の方は iOS とほぼ同じため省略

通知機能はアプリタイルと関連があり、メッセージ送信やダイアログ表示はアプリタイルが登録されていないとできないので、ViewModel や画面はアプリタイル管理機能のものを拝借しています

まずは通知機能管理クラス

/// <summary>
/// iOS 用通知機能管理クラス
/// </summary>
public class NativeBandNotificationManager : IBandNotificationManager
{
    /// <summary>
    /// 通知機能管理クラス
    /// </summary>
    private Native.Notifications.IBandNotificationManager manager = null;

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="client">接続クライアント</param>
    public NativeBandNotificationManager(Native.BandClient client)
    {
        this.manager = client.NotificationManager;
    }

    /// <summary>
    /// メッセージを通知する
    /// </summary>
    /// <param name="tileId">アプリタイルのID</param>
    /// <param name="title">タイトル</param>
    /// <param name="body">本文</param>
    /// <param name="timestamp">日時</param>
    /// <param name="flags">フラグ</param>
    /// <param name="token">中断トークン</param>
    /// <returns>Task</returns>
    [Obsolete("CancellationToken is not supported for iOS.")]
    public Task SendMessageAsync(Guid tileId, string title, string body, DateTimeOffset timestamp, MessageFlags flags, CancellationToken token)
    {
        return this.SendMessageAsync(tileId, title, body, timestamp, flags);
    }

    /// <summary>
    /// メッセージを通知する
    /// </summary>
    /// <param name="tileId">アプリタイルのID</param>
    /// <param name="title">タイトル</param>
    /// <param name="body">本文</param>
    /// <param name="timestamp">日時</param>
    /// <param name="flags">フラグ</param>
    /// <returns>Task</returns>
    public Task SendMessageAsync(Guid tileId, string title, string body, DateTimeOffset timestamp, MessageFlags flags = MessageFlags.None)
    {
        var nativeFlag = flags == MessageFlags.None ? Native.Notifications.MessageFlags.None : Native.Notifications.MessageFlags.ShowDialog;
        var timespan = timestamp.Subtract(new DateTime(2001, 1, 1, 0, 0, 0)).TotalSeconds;
        return Native.Notifications.BandNotificationManagerExtensions.SendMessageTaskAsync(
            this.manager,
            new NSUuid(tileId.ToString("D")), title, body, NSDate.FromTimeIntervalSinceReferenceDate(timespan), nativeFlag);
    }

    /// <summary>
    /// ダイアログを表示する
    /// </summary>
    /// <param name="tileId">アプリタイルのID</param>
    /// <param name="title">タイトル</param>
    /// <param name="body">本文</param>
    /// <param name="token">中断トークン</param>
    /// <returns>Task</returns>
    [Obsolete("CancellationToken is not supported for iOS.")]
    public Task ShowDialogAsync(Guid tileId, string title, string body, CancellationToken token)
    {
        return this.ShowDialogAsync(tileId, title, body);
    }

    /// <summary>
    /// ダイアログを表示する
    /// </summary>
    /// <param name="tileId">アプリタイルのID</param>
    /// <param name="title">タイトル</param>
    /// <param name="body">本文</param>
    /// <returns>Task</returns>
    public Task ShowDialogAsync(Guid tileId, string title, string body)
    {
        return Native.Notifications.BandNotificationManagerExtensions.ShowDialogTaskAsync(
            this.manager, new NSUuid(tileId.ToString("D")), title, body);
    }

    /// <summary>
    /// 振動させる
    /// </summary>
    /// <param name="vibrationType">振動タイプ</param>
    /// <param name="token">中断トークン</param>
    /// <returns>Task</returns>
    [Obsolete("CancellationToken is not supported for iOS.")]
    public Task VibrateAsync(VibrationType vibrationType, CancellationToken token)
    {
        return this.VibrateAsync(vibrationType);
    }

    /// <summary>
    /// 振動させる
    /// </summary>
    /// <param name="vibrationType">振動タイプ</param>
    /// <returns>Task</returns>
    public Task VibrateAsync(VibrationType vibrationType)
    {
        var nativeType = Native.Notifications.VibrationType.RampDown;
        switch (vibrationType)
        {
            case VibrationType.RampDown:
                nativeType = Native.Notifications.VibrationType.RampDown;
                break;

            case VibrationType.RampUp:
                nativeType = Native.Notifications.VibrationType.RampUp;
                break;

            case VibrationType.NotificationOneTone:
                nativeType = Native.Notifications.VibrationType.NotificationOneTone;
                break;

            case VibrationType.NotificationTwoTone:
                nativeType = Native.Notifications.VibrationType.NotificationTwoTone;
                break;

            case VibrationType.NotificationAlarm:
                nativeType = Native.Notifications.VibrationType.NotificationAlarm;
                break;

            case VibrationType.NotificationTimer:
                nativeType = Native.Notifications.VibrationType.NotificationTimer;
                break;

            case VibrationType.OneToneHigh:
                nativeType = Native.Notifications.VibrationType.OneToneHigh;
                break;

            case VibrationType.TwoToneHigh:
                nativeType = Native.Notifications.VibrationType.TwoToneHigh;
                break;

            case VibrationType.ThreeToneHigh:
                nativeType = Native.Notifications.VibrationType.ThreeToneHigh;
                break;

        }
        return Native.Notifications.BandNotificationManagerExtensions.VibrateTaskAsync(this.manager, nativeType);
    }
}

振動の列挙型の入れ替え分岐が面倒ですが、実はタイムスタンプの変換もひとくせあります

// iOS の日時変換
var iosDate = NSDate.FromTimeIntervalSinceReferenceDate(
    timestamp.Subtract(new DateTime(2001, 1, 1, 0, 0, 0)).TotalSeconds);

// Android の日時変換
var droidDate = new Date((long)timestamp.Subtract(new DateTime(1970, 1, 1, 0, 0, 0)).TotalSeconds);

iOS は 2001/1/1 からで、Android は 1970/1/1 からの経過時間で変換しないとだめみたいです

iOS の方はちょっと精度的にどうなんでしょうか;

あとは ViewModel

/// <summary>
/// アプリタイル情報 ViewModel
/// </summary>
public class TilesViewModel : BindableBase
{
    /// <summary>
    /// 接続クライアント
    /// </summary>
    private IBandClient client = null;

    /// <summary>
    /// アプリタイル管理クラス
    /// </summary>
    private IBandTileImageManager manager = null;

    /// <summary>
    /// アプリタイルID
    /// </summary>
    private static readonly Guid TileId = Guid.Parse("e26c7ebb-5f51-4194-8140-1af8c001a8d7");

    /// <summary>
    /// アプリタイル取得コマンド
    /// </summary>
    public ICommand PullCommand { get; private set; }

    /// <summary>
    /// アプリタイル追加/削除コマンド
    /// </summary>
    public ICommand ToggleCommand { get; private set; }

    /// <summary>
    /// メッセージ送信コマンド
    /// </summary>
    public ICommand SendMessageCommand { get; private set; }

    /// <summary>
    /// ダイアログ表示コマンド
    /// </summary>
    public ICommand ShowDialogCommand { get; private set; }

    /// <summary>
    /// 振動コマンド
    /// </summary>
    public ICommand VibrationCommand { get; private set; }

    ~ 中略 ~

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="client">接続クライアント</param>
    [InjectionConstructor]
    public TilesViewModel(IBandClient client, IBandTileImageManager manager)
    {
        this.IsBusy = true;
        this.client = client;
        this.manager = manager;

        this.PullCommand = DelegateCommand.FromAsyncHandler(this.Pull);
        this.ToggleCommand = DelegateCommand.FromAsyncHandler(this.Toggle);
        this.SendMessageCommand = DelegateCommand.FromAsyncHandler(this.SendMessage);
        this.ShowDialogCommand = DelegateCommand.FromAsyncHandler(this.ShowDialog);
        this.VibrationCommand = DelegateCommand.FromAsyncHandler(this.Vibration);

        this.IsBusy = false;
    }

    ~ 中略 ~

    /// <summary>
    /// メッセージ送信
    /// </summary>
    /// <returns>Task</returns>
    private async Task SendMessage()
    {
        this.IsBusy = true;

        await this.client.NotificationManager.SendMessageAsync(
            TileId, 
            "matatabi", 
            "No cat no life",
            new DateTimeOffset(DateTime.Now),
            MessageFlags.None);

        this.IsBusy = false;
    }

    /// <summary>
    /// ダイアログ表示
    /// </summary>
    /// <returns>Task</returns>
    private async Task ShowDialog()
    {
        this.IsBusy = true;

        await this.client.NotificationManager.ShowDialogAsync(
            TileId, 
            "matatabi", 
            "No cat no life");

        this.IsBusy = false;
    }

    /// <summary>
    /// 振動
    /// </summary>
    /// <returns>Task</returns>
    private async Task Vibration()
    {
        this.IsBusy = true;

        await this.client.NotificationManager.VibrateAsync(VibrationType.NotificationAlarm);

        this.IsBusy = false;
    }
}

といっても表示項目はないのでコマンド類を追加して通知機能管理クラスをたたくだけ

最後に XAML 改修

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:cv="clr-namespace:XamarinBandSample.Converters;assembly=XamarinBandSample"
             xmlns:c="clr-namespace:XamarinBandSample.Controls;assembly=XamarinBandSample"
             xmlns:t="clr-namespace:XamarinBandSample.Triggers;assembly=XamarinBandSample"
             xmlns:prismmvvm="clr-namespace:Prism.Mvvm;assembly=XamarinBandSample"
             prismmvvm:ViewModelLocator.AutoWireViewModel="true"
             x:Class="XamarinBandSample.Views.TopPage">
  <ContentPage.Resources>
    <ResourceDictionary>
      <cv:NegativeConverter x:Key="NegativeConverter"/>
      <cv:ColorConverter x:Key="ColorConverter"/>
    </ResourceDictionary>
  </ContentPage.Resources>

    ~ 中略 ~

    <!-- Tiles Info Pain-->
    <ScrollView IsVisible="{Binding ShowTiles}"
                Orientation="Vertical">
      <Grid BindingContext="{Binding Tiles}"
            Padding="10"
            RowSpacing="10"
            VerticalOptions="StartAndExpand">
        <Grid.RowDefinitions>
          <RowDefinition Height="102"/>
          <RowDefinition Height="Auto"/>
          <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <Grid Grid.Row="0"
              ColumnSpacing="10"
              IsVisible="{Binding ExistsTile}">
          <Grid.ColumnDefinitions>
            <ColumnDefinition Width="102"/>
            <ColumnDefinition Width="*"/>
          </Grid.ColumnDefinitions>
          <BoxView Grid.Column="0"
                   WidthRequest="102"
                   HeightRequest="102"
                   BackgroundColor="{Binding BaseColor.Color, Converter={StaticResource ColorConverter}}"/>
          <Image Grid.Column="0"
                 Source="{Binding Icon}"
                 HorizontalOptions="Center"
                 VerticalOptions="Center"
                 WidthRequest="46"
                 HeightRequest="46"/>
          <Label Grid.Column="1"
                 Text="{Binding TileName}"
                 HorizontalOptions="StartAndExpand"
                 VerticalOptions="CenterAndExpand"
                 FontSize="Medium"/>
        </Grid>

        <Label Grid.Row="0"
               IsVisible="{Binding ExistsTile, Converter={StaticResource NegativeConverter}}"
               Text="No tiles registered"
               HeightRequest="102"
               HorizontalOptions="Center"
               VerticalOptions="Center"
               FontSize="Medium"/>

       <StackLayout Grid.Row="1"
                     Orientation="Vertical"
                     Spacing="5"
                     VerticalOptions="CenterAndExpand"
                     HorizontalOptions="CenterAndExpand">
          <Button Text="Send Message"
                  IsEnabled="{Binding IsEnableTileManage}"
                  Command="{Binding SendMessageCommand}"
                  VerticalOptions="Center"
                  FontSize="Medium"/>
          <Button Text="Show Dialog"
                  IsEnabled="{Binding IsEnableTileManage}"
                  Command="{Binding ShowDialogCommand}"
                  VerticalOptions="Center"
                  FontSize="Medium"/>
          <Button Text="Vibration"
                  IsEnabled="{Binding IsEnableTileManage}"
                  Command="{Binding VibrationCommand}"
                  VerticalOptions="Center"
                  FontSize="Medium"/>
        </StackLayout>

        <StackLayout Grid.Row="2"
                     Orientation="Horizontal"
                     Spacing="10"
                     HorizontalOptions="End">
          <Button Text="Pull"
                  IsEnabled="{Binding IsBusy, Converter={StaticResource NegativeConverter}}"
                  Command="{Binding PullCommand}"
                  VerticalOptions="Center"
                  FontSize="Medium"/>
          <Button Text="Add Tile"
                  IsVisible="{Binding ExistsTile, Converter={StaticResource NegativeConverter}}"
                  IsEnabled="{Binding IsBusy, Converter={StaticResource NegativeConverter}}"
                  Command="{Binding ToggleCommand}"
                  VerticalOptions="Center"
                  FontSize="Medium"/>
          <Button Text="Remove Tile"
                  IsVisible="{Binding ExistsTile}"
                  IsEnabled="{Binding IsEnableTileManage}"
                  Command="{Binding ToggleCommand}"
                  VerticalOptions="Center"
                  FontSize="Medium"/>
        </StackLayout>
      </Grid>

    </ScrollView>

  </Grid>

</ContentPage>

各 Command に対応するボタンを追加しました

さっそくおためし実行

f:id:matatabi_ux:20150503195617p:plain

「Show Dialog」をタップすると

f:id:matatabi_ux:20150503195905j:plain

すぐにダイアログ表示・・・小さいアイコンはどうやらちゃんと登録されていたようです

さらに「Send Message」をタップしてアプリタイルを開くと

f:id:matatabi_ux:20150503200052j:plain

Band に日本時間の設定がないので日時がおかしいですが、ちゃんとメッセージも残っていました

ダイアログの場合はすぐに表示されますが、アプリタイルには残りません

メッセージ送信はアプリタイルに記録が残り、MessageFlags.ShowDialog のオプションを指定すると送信後すぐにダイアログ表示もできるようです

Microsoft Band は Xamarin の中の人が SDK の Binding コードを用意してくれていたので Xamarin.Forms にかんたんに移植できると思っていましたが・・・そんなことはありませんでした;

各プラットフォームごとに作法が変わったり、継承不可 internal クラスとか盛りだくさんで結構きつかったです;