しっぽを追いかけて

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

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

Xamarin.Forms で Microsoft Band に接続してみたい

Visual StudioMicrosoft Band に接続ができたので、今度は iOS だけでなく、AndroidWindows Phone でも試せるように(実機ないけど)Xamarin.Forms でサンプルアプリを作ってみたいと思います

Xamarin の中の人が作った Portable Class Library もあるんですが・・・

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

#if __ANDROID__
using Android.App;
#endif

#if __ANDROID__ || __IOS__ || WINDOWS_PHONE_APP
using NativeBandClientManager = Microsoft.Band.BandClientManager;
#endif

namespace Microsoft.Band.Portable
{
    public class BandClientManager
    {
        private static Lazy<BandClientManager> instance;

        static BandClientManager()
        {
            instance = new Lazy<BandClientManager>(() => new BandClientManager());
        }

        public static BandClientManager Instance
        {
            get { return instance.Value; }
        }

        public async Task<IEnumerable<BandDeviceInfo>> GetPairedBandsAsync()
        {
#if __ANDROID__
            return NativeBandClientManager.Instance.GetPairedBands().Select(b => new BandDeviceInfo(b));
#elif __IOS__
            return NativeBandClientManager.Instance.AttachedClients.Select(b => new BandDeviceInfo(b));
#elif WINDOWS_PHONE_APP
            return (await NativeBandClientManager.Instance.GetBandsAsync()).Select(b => new BandDeviceInfo(b));
#else
            return null;
#endif

        }

        public async Task<BandClient> ConnectAsync(BandDeviceInfo info)
        {
#if __ANDROID__
            var nativeClient = NativeBandClientManager.Instance.Create(Application.Context, info.Native);
            var result = await nativeClient.ConnectTaskAsync();
            return new BandClient(nativeClient);
#elif __IOS__
            await NativeBandClientManager.Instance.ConnectTaskAsync(info.Native);
            return new BandClient(info.Native);
#elif WINDOWS_PHONE_APP
            var nativeClient = await NativeBandClientManager.Instance.ConnectAsync(info.Native);
            return new BandClient(nativeClient);
#else
            return null;
#endif
        }
    }
}

こんな #if プラットフォーム分岐処理ばかりで保守性がちょっと・・・という感じだったので作ってみたのが次のコード

github.com

※ 順次改修していく予定なので、この記事の内容が現時点のソースより古い可能性があります

ソリューション構成はこんな感じ

f:id:matatabi_ux:20150419222705p:plain

Xamarin.Forms の共通コードからは Band SDK の各種インタフェースごしにアクセスし、各プラットフォーム側にはその実装クラスを作って、共通コードから固有ロジックを呼び出せるようにしています

AndroidiOS 用の Microsoft Band SDK は Xamarin Studio で Binding したクラスライブラリをいったんビルドしたのち、できた dll を InfrastructiureAssemblies フォルダにコピーして参照して利用しています(詳しくは 前回の記事 を参照)

Xamarin.Forms のアセンブリプロファイルは、Silverlight はずしています

f:id:matatabi_ux:20150418221124p:plain

これは Prism.Mvvm を使っているためで、それ以上の意味はないです!

f:id:matatabi_ux:20150418221358p:plain

今回は Xamarin.Forms で Band に接続するために iOS ではこんなコードを書いてます

extern alias ios;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Foundation;
using global::Microsoft.Band;
using Native = ios::Microsoft.Band;
using UIKit;

namespace XamarinBandSample.iOS.Band
{
    /// <summary>
    /// iOS 用 BandClientManager
    /// </summary>
    public class NativeBandClientManager : IBandClientManager
    {
        /// <summary>
        /// 登録 Band デバイス情報を取得する
        /// </summary>
        /// <returns>登録 Band デバイス情報</returns>
        public Task<IBandInfo[]> GetBandsAsync()
        {
            return Task.FromResult<IBandInfo[]>((
                from c in new List<Native.BandClient>(Native.BandClientManager.Instance.AttachedClients)
                select new NativeBandInfo(c) as IBandInfo).ToArray());
        }

        /// <summary>
        /// Band デバイスに接続する
        /// </summary>
        /// <param name="device">Band デバイス情報</param>
        /// <returns>Band 接続サービス</returns>
        public async Task<IBandClient> ConnectAsync(IBandInfo bandInfo)
        {
            var client = Native.BandClientManager.Instance.AttachedClients.FirstOrDefault(c => c.Name.Equals(bandInfo.Name));

            if (client != null && !client.IsDeviceConnected)
            {
                await Native.BandClientManagerExtensions.ConnectTaskAsync(Native.BandClientManager.Instance, client);
            }

            return new NativeBandClient(client);
        }
    }
}

Objective-C 用に提供された SDK を Xamarin 用に Binding しているので、微妙に利用手順を変える必要があります

例えば、Microsoft.Band.iOS.dll と Microsoft.Band.dll には同じ名前空間、クラス名のクラスが含まれており衝突するので、Microsoft.Band.iOS.dll には 'ios' というエイリアスをつけています

f:id:matatabi_ux:20150419223631p:plain

通常は 'global' になっているところを変更したうえで、上記のコードのように extern alias ios; のおまじないを付ければ、Microsoft.Band.iOS.dll 由来のものと Microsoft.Band.dll 由来のものとを区別して指定できるというわけです

Android の場合だと・・・

extern alias android;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using global::Microsoft.Band;
using Native = android::Microsoft.Band;

namespace XamarinBandSample.Droid.Band
{
    /// <summary>
    /// Android 用 Microsoft Band デバイス管理クラス
    /// </summary>
    public class NativeBandClientManager : IBandClientManager
    {
        /// <summary>
        /// 登録 Band デバイス情報を取得する
        /// </summary>
        /// <returns>登録 Band デバイス情報</returns>
        public Task<IBandInfo[]> GetBandsAsync()
        {
            return Task.FromResult<IBandInfo[]>((
                from i in Native.BandClientManager.Instance.GetPairedBands()
                select new NativeBandInfo(i)).ToArray());
        }

        /// <summary>
        /// Band デバイスに接続する
        /// </summary>
        /// <param name="device">Band デバイス情報</param>
        /// <returns>Band 接続サービス</returns>
        public async Task<IBandClient> ConnectAsync(IBandInfo bandInfo)
        {
            var info = bandInfo as NativeBandInfo;
            if (info == null)
            {
                throw new InvalidOperationException("Parameter 'device' is not BandDevice type.");
            }
            var client = Native.BandClientManager.Instance.Create(Application.Context, info.DeviceInfo);

            if (client != null && !client.IsConnected)
            {
                var result = await Native.BandClientExtensions.ConnectTaskAsync(client);
            }

            return new NativeBandClient(client);
        }
    }
}

こんな感じ

当然ですが、Windows Phone はそのまま利用するだけなので、App.xaml.cs を下記のようにするだけで大丈夫でした

/// <summary>
/// 既定の Application クラスに対してアプリケーション独自の動作を実装します。
/// </summary>
public sealed partial class App : Application
{
    private TransitionCollection transitions;

    /// <summary>
    /// 単一アプリケーション オブジェクトを初期化します。これは、実行される作成したコードの
    /// 最初の行であり、main() または WinMain() と論理的に等価です。
    /// </summary>
    public App()
    {
        this.InitializeComponent();
        this.Suspending += this.OnSuspending;

        // Microsoft Band デバイス管理クラスを DI コンテナに登録
        XamarinBandSample.App.Container.RegisterType<IBandClientManager, BandClientManager>(new ContainerControlledLifetimeManager());
        XamarinBandSample.App.Container.RegisterInstance<IBandClientManager>(BandClientManager.Instance);
    }
    
    ~ 中略 ~
}

実際に接続する部分は結構バラバラですがインタフェースで抽象化しているので、呼び出し側の Xamarin.Forms のコード(ViewModel)はわりとスッキリ

/// <summary>
/// トップ画面の ViewModel
/// </summary>
public class TopPageViewModel : BindableBase
{
    /// <summary>
    /// Microsoft Band デバイス管理クラス
    /// </summary>
    private IBandManager manager = null;

    ~ 中略 ~

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="manager">Microsoft Band デバイス管理クラス</param>
    [InjectionConstructor]
    public TopPageViewModel(IBandManager manager)
    {
        this.manager = manager;
        this.ConnectCommand = DelegateCommand.FromAsyncHandler(this.Connect);
    }

    /// <summary>
    /// 接続処理
    /// </summary>
    /// <returns>Task</returns>
    private async Task Connect()
    {
        var devices = await this.manager.GetBandsAsync();
            
        if (!devices.Any())
        {
            await App.Current.MainPage.DisplayAlert("Warning", "No Microsoft Band found.", "OK");
            return;
        }

        var device = devices.First();
        this.ConnectMessage = "Connecting to Band...";
        var client = await this.manager.ConnectAsync(device);

        if (client == null)
        {
            await App.Navigation.CurrentPage.DisplayAlert(
                "Error",
                string.Format("Failed to connect to Microsoft Band '{0}'.", device.Name),
                "OK");
            return;
        }

        // 別の場所から利用できるように DI コンテナに登録
        App.Container.RegisterInstance<IBandClient>(client, new ContainerControlledLifetimeManager());
        App.Container.RegisterInstance<IBandInfo>(device, new ContainerControlledLifetimeManager());

        this.ConnectMessage = string.Empty;
        this.BandName = device.Name;

        this.IsConnected = true;
        await App.Current.MainPage.DisplayAlert(
            "Connected",
            string.Format("Microsoft Band '{0}' connected.", device.Name),
            "OK");
    }
}

ごちゃごちゃしている部分をインタフェースが隠蔽してくれるので、画面の 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:prismmvvm="clr-namespace:Prism.Mvvm;assembly=XamarinBandSample"
             prismmvvm:ViewModelLocator.AutoWireViewModel="true"
             x:Class="XamarinBandSample.Views.TopPage">
  <ContentPage.Resources>
    <ResourceDictionary>
      <cv:NegativeConverter x:Key="NegativeConverter"/>
    </ResourceDictionary>
  </ContentPage.Resources>
  <ContentPage.Padding>
    <OnPlatform x:TypeArguments="Thickness" iOS="0,20,0,0"/>
  </ContentPage.Padding>

  <StackLayout Orientation="Vertical"
               Padding="10"
               Spacing="10">
    
    <Label Text="Band Test"
           FontSize="Large"
           HorizontalOptions="Center"
           VerticalOptions="Center"/>

    <StackLayout Orientation="Vertical"
                 Spacing="5">
      <Button Text="Connect Band"
              FontSize="Medium"
              HorizontalOptions="Start"
              VerticalOptions="Center"
              IsEnabled="{Binding IsConnected, Converter={StaticResource NegativeConverter}}"
              Command="{Binding ConnectCommand}"/>
      <Label Text="{Binding ConnectMessage}"
             Opacity="0.6"
             FontSize="Small"/>
    </StackLayout>

    <StackLayout Orientation="Vertical"
                 Spacing="5">
      <Label Text="Device Name:"
             FontSize="Medium"/>
      <Label Text="{Binding BandName}"
             FontSize="Small"/>
    </StackLayout>

  </StackLayout>
  
</ContentPage>

実機が iPhone しかないですが実行してみました!

f:id:matatabi_ux:20150418223731p:plain f:id:matatabi_ux:20150418223940p:plain

接続成功!!