2015年10月07日

Nearby Messages APIを試してみる

久しぶりにガンプラを買いました、森脇です。
今回はGoogleが新たに提供を開始したNeaby Messages APIについて書いていきたいと思います。

Neaby Messages APIとは

Neaby Messages APIは、Googleが提供しているAPIの一つで、近くにあるスマートフォンやタブレット端末同士でのメッセージのやり取りを可能にするAPIです。

近距離での通信といえばNFCやBluetoothでのデータ送受信が頭に浮かびますが、Neaby Messages APIの場合はそうではなく、あくまでデータの送受信はサーバーを介して行われます。ただ、その送受信の際にBluetooth、BLE、WiFi、超音波などを用いることで相手を特定しています。
上記のような仕組みになりますのでネットワーク接続は必須となりますが、送信側の端末と受信側の端末が同じネットワークに接続している必要はありません。
ちなみにNearbyのもう一つのAPI、Nearby Connections APIは同じネットワークに接続している端末間でのデータの送受信を可能にするAPIですので注意が必要です。

事前準備

それではNeaby Messages APIを使うための準備を進めていきましょう。
まずGoogle Developers ConsoleにてAPIキーを取得します。GoogleMapsなどのAPIキーを取得する場合と手順は同じです。詳しい手順はこちらに記載してあります。

次にプロジェクトにGoogle Play Servicesを追加します。
build.gradleに以下の記述を追加します。Nearby Messages APIはGoogle Play Services 7.8.0以降でのみ扱えます。今回は8.1.0を使用します。
apply plugin: 'android'
...

dependencies {
    compile 'com.google.android.gms:play-services:8.1.0'
// Nearbyだけ追加したい場合は以下の記述でも問題ない
//    compile 'com.google.android.gms:play-services-nearby:8.1.0'
}
AndroidManifestに以下の記述を追加します。
<application ...>
    <meta-data
        android:name="com.google.android.nearby.messages.API_KEY"
        android:value="先ほど作成したAPIキー" />
    <activity>
    ...
    </activity>
</application>
これで準備は完了です。それでは以下に簡単な実装の流れを書いていきます。

Google API Clientに接続

Nearby Messages APIを使うには、まずGoogle API Clientを作成する必要があります。
この際、接続に成功した場合と失敗した場合のコールバックをそれぞれ追加することができます。
mGoogleApiClient = new GoogleApiClient.Builder(this)
     .addApi(Nearby.MESSAGES_API)
     .addConnectionCallbacks(this)
     .addOnConnectionFailedListener(this)
     .build();
Google API Clientへの接続に成功した場合の処理を以下のように記述します。ここではNearby Messages APIを使うにあたってユーザーからパーミッションの許可を得ているかどうかを確認しています。そして許可を得ていることが確認できた場合、メッセージを受信できる状態に移行します。
@Override
public void onConnected(Bundle bundle) {
    Nearby.Messages.getPermissionStatus(mGoogleApiClient)
            .setResultCallback(new ErrorCheckingCallback("getPermissionStatus", new Runnable() {
                @Override
                public void run() {
                    // メッセージの受信
                    subscribe();
                }
            }));
}
こちらを参考にNearby Messages APIを使用した際の共通のコールバックとして以下のように定義しています。パーミッションエラーが出た場合のみユーザーにパーミッションの許可を促すダイアログを表示します。
private class ErrorCheckingCallback implements ResultCallback<Status> {
    private final String method;
    private final Runnable runOnSuccess;

    private ErrorCheckingCallback(String method) {
        this(method, null);
    }

    private ErrorCheckingCallback(String method, @Nullable Runnable runOnSuccess) {
        this.method = method;
        this.runOnSuccess = runOnSuccess;
    }

    @Override
    public void onResult(@NonNull Status status) {
        if (status.isSuccess()) {
            Log.i(TAG, method + " succeeded.");
            if (runOnSuccess != null) {
                runOnSuccess.run();
            }
        } else {
            // Currently, the only resolvable error is that the device is not opted
            // in to Nearby. Starting the resolution displays an opt-in dialog.
            if (status.hasResolution()) {
                if (!mResolvingError) {
                    try {
                        // パーミッション確認ダイアログの表示
                        status.startResolutionForResult(MainActivity.this,
                                REQUEST_RESOLVE_ERROR);
                        mResolvingError = true;
                    } catch (IntentSender.SendIntentException e) {
                        Log.d(TAG, method + " failed with exception: " + e);
                    }
                } else {
                    // This will be encountered on initial startup because we do
                    // both publish and subscribe together.  So having a toast while
                    // resolving dialog is in progress is confusing, so just log it.
                    Log.i(TAG, method + " failed with status: " + status
                            + " while resolving error.");
                }
            } else {
                Log.d(TAG, method + " failed with : " + status
                        + " resolving error: " + mResolvingError);
            }
        }
    }
}
パーミッション確認ダイアログの結果はonActivityResultで受け取ります。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == REQUEST_RESOLVE_ERROR) {
        mResolvingError = false;
        // User was presented with the Nearby opt-in dialog and pressed "Allow".
        if (resultCode == Activity.RESULT_OK) {
            // メッセージの受信
            subscribe();
        } else {
            Toast.makeText(this, "Failed to resolve error with code " + resultCode,
                    Toast.LENGTH_LONG).show();
        }
    }
}
またNearby Messages APIはバッテリーの消耗が激しいため、必ずライフサイクルに応じて接続を解除するようにしてください。
@Override
protected void onStart() {
    super.onStart();
    // 復帰時に再接続
    if (!mGoogleApiClient.isConnected()) {
        mGoogleApiClient.connect();
    }
}

@Override
protected void onStop() {
    // バッテリー消費を抑えるために必ず接続を解除する
    if (mGoogleApiClient.isConnected() && !isChangingConfigurations()) {
        unsubscribe();
        unpublish();
        mGoogleApiClient.disconnect();
    }
    super.onStop();
}

メッセージの送受信

まず送受信するメッセージはバイト配列でやり取りされます。データサイズは3KB以内が推奨とされていて、画像や動画などの大きなファイルのやり取りは想定されていません。 送信の場合、PublishOptionsでメッセージの送受信が有効な距離や送信状態を維持する時間などをStrategyにて設定することができます。またGoogle Play Services8.1.0以降から送信状態が終了した際のコールバックを設定することができます。
/**
 * メッセージの送信
 */
private void publish() {
    // GoogleApiClientに接続しているか確認
    if (!isApiClientConnected()) {
        return;
    }
    PublishOptions options = new PublishOptions.Builder()
            .setStrategy(PUB_SUB_STRATEGY)
            .setCallback(new PublishCallback() {
                @Override
                public void onExpired() {
                    super.onExpired();
                    // 送信状態が終了した時のコールバック
                    Log.d(TAG, "publish onExpired");
                }
            })
            .build();

    Nearby.Messages.publish(mGoogleApiClient, mMessage, options)
            .setResultCallback(new ErrorCheckingCallback("publish"));
}

/**
 * メッセージの送信解除
 */
private void unpublish() {
    // GoogleApiClientに接続しているか確認
    if (!isApiClientConnected()) {
        return;
    }
    Nearby.Messages.unpublish(mGoogleApiClient, mMessage)
            .setResultCallback(new ErrorCheckingCallback("unpublish"));
}
受信の場合は、あらかじめメッセージを受信した時の処理としてMessageListenerを定義しておきます。
mMessageListener = new MessageListener() {
    @Override
    public void onFound(final Message message) {
        // メッセージを受信した時の処理
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                SimpleMessage simpleMessage = SimpleMessage.fromMessage(message);
                Toast.makeText(getApplicationContext(), simpleMessage.getId(), Toast.LENGTH_LONG).show();
            }
        });
    }
};
SubscribeOptionsではPublishOptionsと同様の設定に加えて、Strategyにて受信するメッセージのフィルターを設定することができます。
/**
 * メッセージの受信
 */
private void subscribe() {
    // GoogleApiClientに接続しているか確認
    if (!isApiClientConnected()) {
        return;
    }
    SubscribeOptions options = new SubscribeOptions.Builder()
            .setStrategy(PUB_SUB_STRATEGY)
            .setCallback(new SubscribeCallback() {
                @Override
                public void onExpired() {
                    super.onExpired();
                    // 受信状態が終了した時のコールバック
                    Log.d(TAG, "subscribe onExpired");
                }
            })
            .build();

    Nearby.Messages.subscribe(mGoogleApiClient, mMessageListener, options)
            .setResultCallback(new ErrorCheckingCallback("subscribe"));
}

/**
 * メッセージの受信解除
 */
private void unsubscribe() {
    // GoogleApiClientに接続しているか確認
    if (!isApiClientConnected()) {
        return;
    }
    Nearby.Messages.unsubscribe(mGoogleApiClient, mMessageListener)
            .setResultCallback(new ErrorCheckingCallback("unsubscribe"));
}
以上です。ソースコードはこちらに置いておきます。
こちらのサンプルでは受信が成功すると相手の端末上のUUIDがToastで表示されます。
device-2015-10-07-170127.png

所感

アプリへ組み込む場面がかなり限定される機能ではありますが、今までQRコードを撮影したり、振った端末同士の位置情報を介していたID交換などがよりスムーズに行えるようになるかもしれないですね。実装自体はかなりお手軽というわけではなく、少しクセのあるAPIだとは思いますが、AndroidだけではなくiOSでも利用可能ということなので、そのあたりも含めて今後の開発に期待していきたいです。

参考

Nearby  |  Google Developers
Nearby Messages APIでチャットみたいなのを作ってみる
posted by Seesaa京都スタッフ at 17:48| Comment(0) | Android | このブログの読者になる | 更新情報をチェックする

2015年10月01日

Xcodeで行うUIテスト

iOS担当のエモトです.

今回は約3ヶ月前に公開したテストに関する記事の続きになります.前回において,User Interface Testing / Recording のことに触れましたが,当時はXcode7がbetaだったので控えましたが,ついに問題なく書ける日がやってきました.

UIテストを使ってみる

使い方は非常に簡単です.シミュレーターを起動して,赤い丸ボタンの録画ボタンを押して,実際のUI操作を録画するだけです.
uiteest.png

そして,そのテストコードを動かせば,録画された動作を自動で行います.XCTAssertを組み合わせれば,画面操作して得られた結果を判定することもできます.簡単な操作でUIテストが行えるのでとても魅力です,しかし,問題として,まだ非同期テストには対応していませんので,万能ではありません.使用する場所は限られますが,標準でUIテストをサポートしてくれたのはうれしいことですね.
posted by Seesaa京都スタッフ at 12:00| Comment(0) | iOS | このブログの読者になる | 更新情報をチェックする

2015年06月17日

テストフレームワークXCTest

iOS担当のエモトです.私事ではありますが,ラブライブ関係者様,劇場版の興行収益ランキング1位おめでとうございます.私も微力ながら貢献させていただきました.

さて先日のWWDCにて, Xcodeのアップデートが発表されました.ネイティヴアプリに変わったwatchOS2やオープンソースになるSwift2.0などと比べると話題性はあまり大きくないですが,他の新機能にはなかなか興味深いものがあり,Xcode7の正式リリースが楽しみです.

私が気になるものの1つは,User Interface Testing / Recording です.XcodeはテストフレームワークXCTestを用いたテストが可能ですが,UIテストは非対応でした.UIテスト用のサードライブラリはこれまでもありましたが,近年iOS端末の解像感が多様化してきたので使用を控えていました.標準でサポートされるのであれば,是非使っていきたいですね.

そのUI Testing / Recordingの紹介といきたいですが,Xcode7はいまbeta(2015/06/17 現在)ということもあり控えるとして,今回はテストフレームワークXCTestの簡単な使い方です(UI Testing / Recordingは一般公開されてからですね).

XCTestを使ってみる

例として,二つの実数を加算するメソッドをもつクラス(Calculator)を作成して,このメソッドをテストしてみます.実際は単純な足し算ですが,あとで非同期でのテストを試すため,わざと遅延処理をいれた完了ブロックを追加しています.

Calculator.h
#import <Foundation/Foundation.h>

extern NSString *const kNotificationCalculatorFinished;
extern NSString *const kNotificationCalculatorFailed;
typedef void (^CalculatorBlock) (float answer);

@interface Calculator : NSObject

@property (nonatomic) float answer;
@property (nonatomic, retain) NSString *answerString;

-(float)add:(float)v1
         to:(float)v2
 completion:(CalculatorBlock)completion;

@end
Calculator.m
#import "Calculator.h"

NSString *const kNotificationCalculatorFinished = @"kNotificationCalculatorFinished";
NSString *const kNotificationCalculatorFailed = @"kNotificationCalculatorFailed";
NSTimeInterval const kDelayInterval = 0.5;

@implementation Calculator

-(float)add:(float)v1 to:(float)v2 completion:(CalculatorBlock)completion
{
    self.answer = v1+v2;
    self.answerString = [NSString stringWithFormat:@"%@", @(self.answer)];
    
    if (completion) {
        dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW,
                                             kDelayInterval * (NSTimeInterval)NSEC_PER_SEC);
        dispatch_after(when, dispatch_get_main_queue(), ^{
            completion(self.answer);
        });
    }
    
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    [nc postNotificationName:kNotificationCalculatorFinished
                      object:nil];
    
    return self.answer;
}
@end

テストをしてみた

プロジェクト作成時に特に設定を変更していなければ「“プロジェクト名”Tests」というフォルダーがプロジェクトに作成されていると思います(ない場合はターゲット追加からテストを追加してください).その中でmファイルにテストコードを書いていきます.例として,加算メソッドのテストを書いてみます.
#import <UIKit/UIKit.h>
#import <XCTest/XCTest.h>
#import "Calculator.h"

@interface SampleTestAppTests : XCTestCase
{
    Calculator *calculator_;
}
@end

@implementation SampleTestAppTests

- (void)setUp {
    [super setUp];
    calculator_ = [[Calculator alloc] init];
}

- (void)tearDown {
    [super tearDown];
}

- (void)testAdd
{
    float v1 = 1;
    float v2 = 2;
    float v3 = [calculator_ add:v1 to:v2 completion:nil];
    XCTAssertEqual(v3, 3);
}
@end
ここで決まりとして,テストコードは頭文字に必ずtestをつけます.この頭文字がないとテストコードと認識してくれません.テストコードを書き終えたら,左側のツリー状からコードを選択するか,ソース部の行番号の左あたりをクリックしてテストを実行します.なにも問題がなければテストは成功です.

xctest_run2.png

非同期でもテストがしたい

テストは非同期にも対応しています.幾つかの方法があり,以下のようにしてテストコードを書くことができます.
- (void)testAddAsync
{
    XCTestExpectation *expectation = [self expectationWithDescription:@"add async"];
    
    float v1 = 1;
    float v2 = 2;
    [calculator_ add:v1 to:v2 completion:^(float answer) {
        XCTAssertEqual(answer, 3);
        [expectation fulfill];
    }];
    
    [self waitForExpectationsWithTimeout:1 handler:^(NSError *error) {
        NSLog(@"%s, %@", __func__, error.localizedDescription);
    }];
}

- (void)testAddNotification
{
    XCTestExpectation *exp = [self expectationForNotification:kNotificationCalculatorFinished
                                                       object:nil
                                                      handler:^BOOL(NSNotification * __nonnull notification)
                              {
                                  NSLog(@"%s, notification %@", __func__, notification);
                                  [exp fulfill];
                                  return true;
                              }];
    
    float v1 = 1;
    float v2 = 2;
    [calculator_ add:v1 to:v2 completion:nil];
    
    [self waitForExpectationsWithTimeout:1 handler:^(NSError *error) {
        NSLog(@"%s, %@", __func__, error.localizedDescription);
    }];
}

- (void)testAddKVO1
{
    [self keyValueObservingExpectationForObject:calculator_
                                        keyPath:@"answerString"
                                  expectedValue:@"3"];
    
    float v1 = 1;
    float v2 = 2;
    [calculator_ add:v1 to:v2 completion:nil];
    
    [self waitForExpectationsWithTimeout:1 handler:^(NSError *error) {
        NSLog(@"%s, %@", __func__, error.localizedDescription);
    }];
}

- (void)testAddKVO2
{
    [self keyValueObservingExpectationForObject:calculator_
                                        keyPath:@"answerString"
                                        handler:^BOOL(id  __nonnull observedObject, NSDictionary * __nonnull change)
     {
         NSLog(@"%s, observedObject %@, change %@", __func__, observedObject, change);
         
         NSString *correct = @"3";
         
         //        Calculator *tempCalc = (Calculator*)observedObject;
         NSString *newStr = change[@"new"];
         return [newStr isEqualToString:correct];
     }];
    
    float v1 = 1;
    float v2 = 2;
    [calculator_ add:v1 to:v2 completion:nil];
    
    [self waitForExpectationsWithTimeout:1 handler:^(NSError *error) {
        NSLog(@"%s, %@", __func__, error.localizedDescription);
    }];
}


XCTestに関して簡単に紹介しました.より詳細な情報はアップルが公式ドキュメントを公開しているので,それを参照してもらえると幸いです.
posted by Seesaa京都スタッフ at 17:16| Comment(0) | iOS | このブログの読者になる | 更新情報をチェックする