2015/04/28

Android : AndroidStudio + PowerMock でstaticメソッドをmockする

確認環境

  • Android Studio 1.1
  • Android Gradle Plugin 1.1
  • JUnit 4.12
  • PowerMock 1.6.2

はじめに

Android Studio 1.1(android gradle plugin 1.1)からUnitTestがサポートされた.
今回はPowerMockを使ってstatic methodをモックするUnitTestを書いた.

Android StudioでUnitTestを開始するには下記を参考.

UnitTestの準備が整ったならモジュールのbuild.gradleに下記の依存ライブラリを記述する.

    // UnitTestにはtestCompileキーワードを使用
    testCompile 'junit:junit:4.12'

    // PowerMockのライブラリを指定
    testCompile 'org.powermock:powermock-module-junit4:1.6.2'
    testCompile 'org.powermock:powermock-api-mockito:1.6.2'

次はoptionだが, Android StudioのUnitTestは実行時にandroid.jarのモックとなるmockable-android*.jarを作成する.
ここで定義されるメソッドスタブはすべてRuntimeExceptionをスローするように実装される.
これをExceptionではなくDefault value(false etc.)を返却するように変更するには下記を指定する.

    testOptions {
        unitTests.returnDefaultValues = true
    }

あとはいつもの方法でPowerMockを使えばOK.

  • TestRunnerにPowerMockRunnerを指定
    @RunWith(PowerMockRunner.class)
    public class MyTestCase extends TestCase {
  • TestCaseを継承したクラスにアノテーションを追加
    @RunWith(PowerMockRunner.class)
    @PrepareForTest(Static.class) // Staticメソッドをモックするクラスを指定(,で複数指定可)
    public class MyTestCase extends TestCase {

テストコード

    @Test
    public void testMockStaticMethod() throws Exception {
        assertFalse("オリジナルはfalseを返す", Target.staticBoolean());

        PowerMockito.mockStatic(Target.class);
        when(Target.staticBoolean()).thenReturn(true);
        assertTrue("モックはtrueを返す", Target.staticBoolean());
        verifyStatic(times(1));
    }

    @Test
    public void testMockConstructor() throws Exception {
        assertEquals("Valueは引数なしのコンストラクタで初期化される",
                new Value(), new Target().getValue());

        Value mockValue = new Value(100);
        assertNotEquals("引数ありで初期化されたValueとでは等価ではなくなる",
                mockValue, new Target().getValue());

        PowerMockito.whenNew(Value.class).withNoArguments().thenReturn(mockValue);
        assertEquals("Valueの引数なしコンストラクタでモックが返される",
                mockValue, new Target().getValue());
        verifyNew(Value.class).withNoArguments();

        mockValue = new Value(500);
        assertEquals("Valueの引数ありコンストラクタはモックされていない",
                mockValue, new Value(500));

        mockValue = new Value(1000);
        PowerMockito.whenNew(Value.class).withArguments(anyInt()).thenReturn(mockValue);
        assertEquals("Valueの引数ありコンストラクタもモックされる",
                mockValue, new Target(500).getValue());
        verifyNew(Value.class, atLeastOnce()).withArguments(anyInt());
    }

    @Test
    public void testLogger() throws Exception {
        PowerMockito.mockStatic(Log.class);
        Mockito.when(Log.isLoggable(anyString(), anyInt())).thenReturn(true);
        assertTrue("モックしてtrueを返す", Log.isLoggable("mocktag", Log.VERBOSE));

        assertTrue(BuildConfig.DEBUG);

        // static finalな定数をreflectionで変更.
        Field field = BuildConfig.class.getField("DEBUG");
        field.setAccessible(true);
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
        field.set(null, false);

        assertFalse(BuildConfig.DEBUG);
    }

以上.

Android : EventBusでonEventが認識されない

onEventを実装しているがEventBusExceptionが発生し, Subscriberの登録に失敗する.

EventBusはアプリのパッケージ名(applicationId)がjava, javaxまたはandroidから始まるものはSystemクラスのものとしてSubscriber登録をスキップする.

// de.greenrobot.event.SubscriberMethodFinder#findSubscriberMethods
if (name.startsWith("java.") || name.startsWith("javax.") || name.startsWith("android.")) {
    // Skip system classes, this just degrades performance
    break;
}

下記のEventBusExceptionが発生している場合はパッケージ名にこれらが含まれていないか一度確認してみるとよい.

de.greenrobot.event.EventBusException: Subscriber class HogeActivity has no public methods called onEvent

確認バージョン: EventBus 2.4.0

2015/04/24

Android : Google Sign-In

Prerequisites

  • Android 2.3 以上, あるいは最新のGoogle Play Storeを搭載していること.
  • 最新のAndroid SDK Toolsをインストールしていること.
  • Google Play Service SDKをインストールしていること.
  • コンパイルバージョンがAndroid2.3以上であること.

Step 1: Google Sign-In APIの有効化

  1. Google Developers Consoleへ移動
  2. プロジェクトを作成する
  3. AndroidアプリケーションとしてクライアントIDを作成する

NOTE
デバッグ用.keystoreのSHA1ハッシュを確認するには次のコマンドを参照.

keytool -exportcert -alias androiddebugkey -keystore path-to-debug-or-production-keystore -list -v

デバッグ用.keystoreはMacだと~/.android/debug.keystore. Windowsでは%USERPROFILE%\.android\debug.keystoreに標準で格納されている.

デバッグ用keystoreのパスワードはandroid.

Step 2: Android Studio Project の設定

build.gradleのdependenciesセクションにGoogle Play Serviceを追加.


dependencies {
    compile 'com.google.android.gms:play-services:7.0.0'
}

Step 3: Permission宣言

アプリケーションが使用するGoogle Play ServiceのバージョンをAndroidManifestのapplicationタグ配下に宣言する.

<meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />

Google APIにアクセスするためINTERNETパーミッションを宣言する.

<uses-permission android:name="android.permission.INTERNET" />

アカウント名を取得するためGET_ACCOUNTパーミッションを宣言する.

<uses-permission android:name="android.permission.GET_ACCOUNTS" />

OAuth 2.0 tokenを取得するためUSE_CREDENTIALSパーミッションを宣言する.

<uses-permission android:name="android.permission.USE_CREDENTIALS" />

GoogleApiClient の初期化

GoogleApiClientはGoogle Play Serviceに接続されるServiceConnectionのラッパーとして作用する.
GoogleApiClientはGoogle Sign-In APIと通信し, 非同期通信によりサービスを有効化した後機能する. そのためには,

  • Google Play Serviceがデバイス上で実行されており, あなたのActivityがService connectionに接続できていること.
  • ユーザがあなたのアプリでアカウントを選択し, ユーザのアカウントがあなたのアプリに権限を与えていること.

GoogleApiClientのライフサイクルをActivityのライフサイクルの中で管理するための一般的な方法は下記.

  1. Activity.onCreateでGoogleApiClientを初期化
  2. Activity.onStartでGoogleApiClient.connectを起動
  3. Activity.onStopでGoogleApiClient.disconnectを起動

これを実行するとActivityにはコネクションが有効化あるいは接続失敗したことがConnectionCallbacksとOnConnectionFailedListenerに通知される.

addScopeメソッドでアプリが許容を求めるスコープを定義できる.

  • 基本的なプロフィール情報が必要であればprofile
  • ユーザのGoogle account emailアドレスが必要であればemail
import android.app.Activity;
import android.content.IntentSender.SendIntentException;
import android.os.Bundle;
import android.view.View;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.Scope;
import com.google.android.gms.plus.Plus;

public class MainActivity extends Activity implements
        GoogleApiClient.ConnectionCallbacks,
        GoogleApiClient.OnConnectionFailedListener,
        View.OnClickListener {

    /* Request code used to invoke sign in user interactions. */
    private static final int RC_SIGN_IN = 0;

    /* Client used to interact with Google APIs. */
    private GoogleApiClient mGoogleApiClient;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mGoogleApiClient = new GoogleApiClient.Builder(this)
                .addConnectionCallbacks(this)
                .addOnConnectionFailedListener(this)
                .addApi(Plus.API)
                .addScope(new Scope("profile"))
                .build();

       findViewById(R.id.sign_in_button).setOnClickListener(this);
    }

    @Override
    protected void onStart() {
        super.onStart();
        mGoogleApiClient.connect();
    }

    @Override
    protected void onStop() {
        super.onStop();
        mGoogleApiClient.disconnect();
    }

    @Override
    public void onClick(View v) {
      // ...
    }

    // ...

  }

GoogleApiClientがコネクションを有効化できなかった時, onConnectionFailedのコールバックが呼ばれる. これにはConnectionResultが渡されるためエラーの解決に使用できる. ConnectionResult.getResolution()を呼ぶとユーザにアカウントを選択する操作を促すためのPendingIntentを得ることができる.
(例えば年とワーク接続を有効かするように求めたり, アカウントの選択をユーザに求めたりする)

@Override
public void onConnectionFailed(ConnectionResult result) {
  if (!mIntentInProgress && result.hasResolution()) {
    try {
      mIntentInProgress = true;
      result.startResolutionForResult(this, RC_SIGN_IN);
    } catch (SendIntentException e) {
      // The intent was canceled before it was sent.  Return to the default
      // state and attempt to connect to get an updated ConnectionResult.
      mIntentInProgress = false;
      mGoogleApiClient.connect();
    }
  }
}

@Override
public void onConnected(Bundle connectionHint) {
  // We've resolved any connection errors.  mGoogleApiClient can be used to
  // access Google APIs on behalf of the user.
}

@Override
protected void onActivityResult(int requestCode, int responseCode, Intent intent) {
  if (requestCode == RC_SIGN_IN) {
    mIntentInProgress = false;

    if (!mGoogleApiClient.isConnected()) {
      mGoogleApiClient.reconnect();
    }
  }
}

クライアントのサービス接続が確立されたらGoogleApiClient.disconnectをonStopメソッドの中で呼び出しこれを切断する必要がある.

Google Play ServiceはActivityがサービスコネクションをロストした際に呼ばれるonConnectionSuspendedコールバックを呼び出す. 一般的にこの場合はユーザが解決を試みることができるようにサービスへの再接続を試みる.

@Override
public void onConnectionSuspended(int cause) {
  mGoogleApiClient.connect();
}

Google Sign-In ボタンの追加

  1. アプリケーションのレイアウトにSignInButtonを追加
<com.google.android.gms.common.SignInButton
    android:id="@+id/sign_in_button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />
  1. OnClickListenerを登録する
findViewById(R.id.sign_in_button).setOnClickListener(this);
  1. Sign-Inボタンを使用するにはActivityのSign-Inフローを変更する必要がある. ActivityがOnCnnectionFaildコールバックを受け取ったらすぐに問題解決のためのユーザインタラクションを開始する. Activityはその間Sing-inボタンのクリックを抑止するべき.
/**
 * True if the sign-in button was clicked.  When true, we know to resolve all
 * issues preventing sign-in without waiting.
 */
private boolean mSignInClicked;

/**
 * True if we are in the process of resolving a ConnectionResult
 */
private boolean mIntentInProgress;

@Override
public void onConnectionFailed(ConnectionResult result) {
  if (!mIntentInProgress) {
    if (mSignInClicked && result.hasResolution()) {
      // The user has already clicked 'sign-in' so we attempt to resolve all
      // errors until the user is signed in, or they cancel.
      try {
        result.startResolutionForResult(this, RC_SIGN_IN);
        mIntentInProgress = true;
      } catch (SendIntentException e) {
        // The intent was canceled before it was sent.  Return to the default
        // state and attempt to connect to get an updated ConnectionResult.
        mIntentInProgress = false;
        mGoogleApiClient.connect();
      }
    }
  }
}
  1. ユーザがSign-Inボタンをクリックした後, mSignInClickedフラグをセットし, onConnectionFailedで接続エラーを解決する必要がある. 解決可能なエラーはユーザにサービスへの接続とアプリへの権限承認を促す.
public void onClick(View view) {
  if (view.getId() == R.id.sign_in_button && !mGoogleApiClient.isConnecting()) {
    mSignInClicked = true;
    mGoogleApiClient.connect();
  }
}
  1. 接続が確立された場合はmSignInClickedフラグをリセットする.
protected void onActivityResult(int requestCode, int responseCode, Intent intent) {
  if (requestCode == RC_SIGN_IN) {
    if (responseCode != RESULT_OK) {
      mSignInClicked = false;
    }

    mIntentInProgress = false;

    if (!mGoogleApiClient.isConnected()) {
      mGoogleApiClient.reconnect();
    }
  }
}
  1. ユーザがSign-Inを無事終えるとonConnectedが呼ばれる. この時点でユーザの名前を取得するか認証リクエストを作成することができる.
@Override
public void onConnected(Bundle connectionHint) {
  mSignInClicked = false;
  Toast.makeText(this, "User is connected!", Toast.LENGTH_LONG).show();
}

以上.

2015/04/20

Android : GoogleAnalyticsの導入

GoogleAnalyticsの簡単なサンプルです.

Google Analytics Setup

分析対象のアプリケーションからデータを受け取るアナリティクスプロパティとアプリビューを作成します.
詳細はGoogle Analytics V4の”詳細”セクションを参照.

Add Google Play Services to Your Project

Google Play ServiceとGoogle Analyticsのビルドルールをgradleのdependenciesに追加.
下記は執筆時点の最新版を指定している. 実際には下記URLを参考にする.

[Setting Up Google Play Services(https://developer.android.com/google/play-services/setup.html?hl=ja#Setup)

dependencies {
    compile 'com.google.android.gms:play-services:7.0.0'  // Google Play Service
    compile 'com.google.android.gms:play-services-analytics:7.0.0' // Google Analytics
}

Add Permission to You Application

Google Analyticsを利用するにはネットワークアクセスが必要となるため, 次のpermissionを追加する.

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

Use Tracker

Trackerは自身で管理する. Analyticsのオーバーカウントを防ぐためにApplicationクラスで作成・管理する.

public class MyApplication extends Application {
    private final String PROPERTY_ID = "UA-XXXXXXXX-X";

    private Tracker tracker;

    synchronized Tracker getTracker() {
        if (tracker == null) {
            GoogleAnalytics analytics = GoogleAnalytics.getInstance(this);
            // Trackerの初期化はR.xmlからも可能.
            // https://developers.google.com/analytics/devguides/collection/android/v4/?hl=ja#analytics-xml
            tracker = analytics.newTracker(PROPERTY_ID);
        }
        return tracker;
    }
}

トラッキング対象のイベントでTrackerを取得しイベント送信すればOK.

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        MyApplication app = (MyApplication) getApplication();
        Tracker t = app.getTracker();
        // Classインスタンスから名前取得する場合は難読化に注意.
        t.setScreenName("android.m.yuki.googleanalyticssample.MainActivity");
        t.send(new HitBuilders.AppViewBuilder().build());
    }

サンプルの実装は下記.

Reference

Copyright 2015 yuki312 All Right Reserved.

Licensed under the Apache License, Version 2.0 (the “License”);
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

2015/04/15

Android : gradleからdeploygateへアップロードする

Get Started.

  1. Projectルート直下にあるbuild.gradleにclasspathを追加.
  2. Moduleのbuild.gradleにdeploygate taskを追加.
  3. deploygate taskのパラメタをカスタマイズ
  4. gradle uploadDeployGateでアップロード

Edit build.gradle

Projectルート直下にあるbuild.gradleにあるclasspathに下記を追加する.

dependencies {
    // ...
    classpath 'com.deploygate:gradle:0.6.2'
}

NOTE: Module直下のbuild.gradleではない

Module直下にあるbuild.gradleにdeploygate taskを追加する.
deploygate taskのテンプレートは下記.

apply plugin: 'deploygate'

deploygate {
  userName = "[owner name]"
  token = "[token]"

  apks {
    release {
      sourceFile = file("[apk1 file path]")
    }

    debug {
      sourceFile = file("[apk2 file path]")

      //Below is optional
      message = "test upload2 sample"
      visibility = "public" // default private
      distributionKey = "[distribution_key]"
      releaseNote = "release note sample"
    }
  }
}

各パラメータに指定する情報は下記サイトを参照.
https://deploygate.com/docs/api

Param Description
owner アプリの所有者名. Endpoint APIパスの一部で使用される.
token API Key.
sourceFile アップロードするアプリバイナリ.
message Push時に付与するメッセージ(optional)
visibility 新たにアップするアプリのプライバシー設定(optional)

deploygateのEndpoint api uriは次のフォーマットに従う.
https://deploygate.com/api/users/[owner name]/apps

owner nameは自身のdeploygateユーザページから確認できる.

tokenには発行されているAPI Keyを指定する. API keyはプロフィール設定から確認可能.

sourceFileにはアップロードするapkバイナリのパスを指定する.

messageには任意のメッセージを設定できる. このメッセージはテスタにも公開される.

visibilityアプリを新たにアップロードする際に指定できる公開設定. publicかprivate(default)を指定可能. この値は新しくアプリをアップロードする場合に参照され, アプリ更新時には無視される.

tokenを直接build.gradleに記載しないため, 環境変数やlocal.propertiesを読み込む方法をとるのが一般的である.
下記はlocal.propertiesに追加したプロパティdev.key.deploygateをtokenとするサンプル.

deploygate {
    // load local.propertis
    Properties properties = new Properties()
    properties.load(project.rootProject.file('local.properties').newDataInputStream())

    userName = "yuki312"
    token = properties.getProperty('dev.key.deploygate')

    apks {
        release {
            sourceFile = file("./build/outputs/apk/app-release.apk")
        }

        debug {
            sourceFile = file("./build/outputs/apk/app-debug.apk")

            //Below is optional
            message = "test message"
            visibility = "public" // default private
        }
    }
}

Reference

2015/04/14

Travis CIをつかってGitHubのREADME.mdにbuildバッジをつけるまで.

Travis CI Site: https://travis-ci.org/
Travis Lint: http://lint.travis-ci.org/

Travis CIをつかってGitHubのREADME.mdにbuildバッジをつけるまで.

Steps.

まずはTravis CIに追跡させたいGitHub上のリポジトリを登録し紐づける.
Travis CIをつかったみたに詳しい説明が書かれている.

登録が終わったらGitHubのリポジトリに.travis.ymlファイルを追加する.

language: android
jdk: oraclejdk7

android:
  components:
    # The BuildTools version used by your project
    - build-tools-22.0.0

    # The SDK version used to compile your project
    - android-22

    # Use extra android repo locations to find the support library etc...
    - extra

# Emulator Management: Create, Start and Wait
before_script:
  - echo no | android create avd --force -n test -t android-19 --abi armeabi-v7a
  - emulator -avd test -no-skin -no-audio -no-window &
  - android-wait-for-emulator
  - adb shell input keyevent 82 &

この状態でgit pushするとTravis CIがこれをhookし, .ymlの記載に従ってビルドを始める.

ビルドの状態をREADME.mdへ表示するため, Travis CIページの右上にあるビルドバッジをクリックするとバッジへのリンク文字列が表示される.
いくつかフォーマットがあるが, master / markdown で選択しておくとよい.

Reference

2015/04/12

Bluetooth Low Energy

Android 4.3 (API Level 18) introduces built-in platform support for Bluetooth Low Energy in the central role and provides APIs that apps can use to discover devices, query for services, and read/write characteristics. In contrast to Classic Bluetooth, Bluetooth Low Energy (BLE) is designed to provide significantly lower power consumption. This allows Android apps to communicate with BLE devices that have low power requirements, such as proximity sensors, heart rate monitors, fitness devices, and so on.

Android4.3(API Level 18)よりBLE向けとなるCentral role APIの提供が開始された.
このAPIを使用することにより, アプリはBLEデバイスを探索・発見することができ, serviceを問い合わせ, characteristicsのread/writeが可能となる.
従来のBluetooth,と比べてBLEは大幅に低い消費電力となるよう設計されている. これはAndroidアプリケーションがBLEデバイスとcommunicateするのに近接センサーや心拍数モニタ, フィットネス機器のように少ない電力で済むことを意味する.

Key Terms and Concepts

Here is a summary of key BLE terms and concepts:

  • Generic Attribute Profile (GATT) —The GATT profile is a general specification for sending and receiving short pieces of data known as “attributes” over a BLE link. All current Low Energy application profiles are based on GATT.

    • The Bluetooth SIG defines many profiles for Low Energy devices. A profile is a specification for how a device works in a particular application. Note that a device can implement more than one profile. For example, a device could contain a heart rate monitor and a battery level detector.
  • Attribute Protocol (ATT) —GATT is built on top of the Attribute Protocol (ATT). This is also referred to as GATT/ATT. ATT is optimized to run on BLE devices. To this end, it uses as few bytes as possible. Each attribute is uniquely identified by a Universally Unique Identifier (UUID), which is a standardized 128-bit format for a string ID used to uniquely identify information. The attributes transported by ATT are formatted as characteristics and services.

  • Characteristic —A characteristic contains a single value and 0-n descriptors that describe the characteristic’s value. A characteristic can be thought of as a type, analogous to a class.

  • Descriptor —Descriptors are defined attributes that describe a characteristic value. For example, a descriptor might specify a human-readable description, an acceptable range for a characteristic’s value, or a unit of measure that is specific to a characteristic’s value.

  • Service —A service is a collection of characteristics. For example, you could have a service called “Heart Rate Monitor” that includes characteristics such as “heart rate measurement.” You can find a list of existing GATT-based profiles and services on bluetooth.org.

これらはBLEのキーワードとコンセプトである.

  • Generic Attribute Profile (GATT) GATTプロファイルはBLE通信によるデータの送信や既知の”attributes”の断片を受信するための基本仕様である. 全てのBLEアプリケーションのプロファイルはGATTプロファイルがベースになる.

    • Bluetooth SIGではいつくかのBLEプロファイルが定義されている. プロファイルはデバイスが特定のアプリケーションでどのように動作するかの仕様である. デバイスは複数のプロファイルを実装できる点には注意すること. 例えばあるデバイスは心拍数モニターと電力消費モニターを含むことができる.
  • Attribute Protocal (ATT) - GATTはAttribute Prorotocol(ATT)の上位に組み込まれる. これはGATT/ATTと呼ばれる. ATTはBLEデバイスでの実行に最適化されており, 少ないバイトの使用で実行できるよう作られている. それぞれのattributeは標準化された128bit形式の文字列IDであるUniversally Unique Identifier(UUID)で識別される. attributesはATTによりcharacteristicsとserviceの形式にされて転送される.

  • Characteristic - characteristicは1つの値と0-nのcharacteristicに言及するdescriptorを含んでいる. characteristicはタイプやクラスに似たものと考えることができる.

  • Descriptor - descriptorはcharacteristicの値を説明するattributeとして定義される. 例えば, characteristicの値が許容可能な範囲を人間が読めるdescriptionとして定義されるものであったり, characteristicの値の単位を表すものであったりする.

  • Service - serviceはcharacteristicのコレクションである. 例えば, “Heart rate Monitor”と呼ばれるサービスには”heart rate measurement”という名のcharacteristicが含まれる. GATT-ベースのプロファルとサービスはbluetooth.orgで見つけることができる.

Roles and Responsibilities

Here are the roles and responsibilities that apply when an Android device interacts with a BLE device:

  • Central vs. peripheral. This applies to the BLE connection itself. The device in the central role scans, looking for advertisement, and the device in the peripheral role makes the advertisement.
  • GATT server vs. GATT client. This determines how two devices talk to each other once they’ve established the connection.

To understand the distinction, imagine that you have an Android phone and an activity tracker that is a BLE device. The phone supports the central role; the activity tracker supports the peripheral role (to establish a BLE connection you need one of each—two things that only support peripheral couldn’t talk to each other, nor could two things that only support central).

Once the phone and the activity tracker have established a connection, they start transferring GATT metadata to one another. Depending on the kind of data they transfer, one or the other might act as the server. For example, if the activity tracker wants to report sensor data to the phone, it might make sense for the activity tracker to act as the server. If the activity tracker wants to receive updates from the phone, then it might make sense for the phone to act as the server.

In the example used in this document, the Android app (running on an Android device) is the GATT client. The app gets data from the GATT server, which is a BLE heart rate monitor that supports the Heart Rate Profile. But you could alternatively design your Android app to play the GATT server role. See BluetoothGattServer for more information.

ここではAndroidデバイスがBLEデバイスと作用し合う際の役割と応答性について述べる.

  • Central vs. peripheral. BLEコネクションに適用される形態である. centralなデバイスはスキャンし, advertisementを探す. peripheralなデバイスはadvertisementを作成する.
  • GATT server vs. GATT client. 2つのデバイス接続確立後の通信方法を決定する.

各役割の違いを理解するためにAndroid PhoneとBLEデバイスであるActivity trackerを例に挙げる.
PhoneはCentralの役割を果たす. Activity trackerはperipheralの役割を果たす(BLE接続するにはPeripheralだけでは成り立たないし, Centralだけでも成り立たない. 両方の役割をもつデバイスが揃う必要がある.)

PhoneとActivity trackerが接続を確立した後, デバイスは互いにGATT metadataの転送を開始する. 転送データの種類に応じてどちらかがserverとして動作する. 例えば, もしActivity trackerがセンサーデータをPhoneにレポートしたい場合, Activity trackerがserverとなるかもしれない. 一方でもしActivity trackerがPhoneからデータを受信してデータをアップデートしたい場合はPhoneがserverになるかもしれない.

このドキュメントではAndroidデバイスで実行されるAndroid アプリケーションはGATT clientとして使用される. アプリはHeart Rate ProfileをサポートするBLE heart rate monitorのデータをGATT serverから受信する. もしGATT serverとして振る舞うアプリを作成したい場合はBluetoothGattServer により詳しい情報が書いてある.

BLE Permissions

In order to use Bluetooth features in your application, you must declare the Bluetooth permission BLUETOOTH. You need this permission to perform any Bluetooth communication, such as requesting a connection, accepting a connection, and transferring data.

If you want your app to initiate device discovery or manipulate Bluetooth settings, you must also declare the BLUETOOTH_ADMIN permission. Note: If you use the BLUETOOTH_ADMIN permission, then you must also have the BLUETOOTH permission.

BLEアプリケーションはBluetooth機能を使用する. そのためにはBLUETOOTHパーミッションを宣言する必要がある. アプリがBluetoothコミュニケーション(接続要求/接続受理/データ転送)をする場合にこのパーミッションを宣言する必要がある.

もしBluetoothに関する設定を操作するのであればBLUETOOTH_ADMINのパーミッションも宣言する必要がある. NOTE: もしBLUETOOTH_ADMINのパーミッションを使用するのであれば同時にBLUETOOTHのパーミッションも必要である.

Declare the Bluetooth permission(s) in your application manifest file. For example:

アプリケーションのAndroidManifestでこれらのパーミッションを宣言するには下記を参照.

<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>

If you want to declare that your app is available to BLE-capable devices only, include the following in your app’s manifest:

もしBLE機能を備えたデバイスでのみアプリを有効にしたいのであれば次の1行をAndroidManifestに加えると良い. これによりBLE機能を備えていないデバイスではアプリは無効となりインストールされない.

<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>

However, if you want to make your app available to devices that don’t support BLE, you should still include this element in your app’s manifest, but set required="false". Then at run-time you can determine BLE availability by using PackageManager.hasSystemFeature():

しかし, BLE機能を備えていないデバイスでもアプリは有効にしインストールまでは進めたい場合はrequired="false"で宣言しておく. アプリ実行時にBLE機能の有無を調べる場合はPackageManager.hasSystemFeature()を使用する.

// Use this check to determine whether BLE is supported on the device. Then
// you can selectively disable BLE-related features.
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
    Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show();
    finish();
}

Setting Up BLE

Before your application can communicate over BLE, you need to verify that BLE is supported on the device, and if so, ensure that it is enabled. Note that this check is only necessary if <uses-feature.../> is set to false.

If BLE is not supported, then you should gracefully disable any BLE features. If BLE is supported, but disabled, then you can request that the user enable Bluetooth without leaving your application. This setup is accomplished in two steps, using the BluetoothAdapter.

アプリケーションでBLE通信を始める前に, デバイスがBLE機能を有しているか検証する必要ががる. これはAndroidManifestの<uses-feature.../>required="false"を設定している場合に必要である.

デバイスがBLEをサポートしていないのであればそれに関わる機能を全て無効かする必要がある. BLEをサポートしているものの機能が無効かされている場合, ユーザにこれを有効とするようリクエストすることができる. これにはBluetoothAdapterを使って2つの手順を実行する.

  1. Get the BluetoothAdapter
    The BluetoothAdapter is required for any and all Bluetooth activity. The BluetoothAdapter represents the device’s own Bluetooth adapter (the Bluetooth radio). There’s one Bluetooth adapter for the entire system, and your application can interact with it using this object. The snippet below shows how to get the adapter. Note that this approach uses getSystemService() to return an instance of BluetoothManager, which is then used to get the adapter. Android 4.3 (API Level 18) introduces BluetoothManager:

  2. Enable Bluetooth
    Next, you need to ensure that Bluetooth is enabled. Call isEnabled() to check whether Bluetooth is currently enabled. If this method returns false, then Bluetooth is disabled. The following snippet checks whether Bluetooth is enabled. If it isn’t, the snippet displays an error prompting the user to go to Settings to enable Bluetooth:

  1. BluetoothAdapterを取得する
    BluetoothAdapterはBluetoothを使用する場合に必要となる. BluetoothAdapterはデバイス自身のBluetoothアダプタ(Bluetooth radio)に相当する. システムが持つBluetoothアダプタを使用してアプリケーションは通信することができる. 次のスニペットはadapterを取得するものである. これはgetSystemService()を使用してBluetoothManagerを得るアプローチである. BluetoothManagerはAndroid 4.3(API Level 18)から導入されている.

    // Initializes Bluetooth adapter.
    final BluetoothManager bluetoothManager =
            (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
    mBluetoothAdapter = bluetoothManager.getAdapter();
  2. Bluetoothの有効化
    次にBluetoothを有効化する. isEnabled()でBluetoothが現在有効かどうかを検証できる. もしこのメソッドがfalseを返せばBluetoothは無効化されている. 次のスニペットはBluetoothが有効かどうかを検証し, 無効であればユーザにBluetoothを有効にするよう促すプロンプトを表示する.

    private BluetoothAdapter mBluetoothAdapter;
    ...
    // Ensures Bluetooth is available on the device and it is enabled. If not,
    // displays a dialog requesting user permission to enable Bluetooth.
    if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
        Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    }

Finding BLE Device

To find BLE devices, you use the startLeScan() method. This method takes a BluetoothAdapter.LeScanCallback as a parameter. You must implement this callback, because that is how scan results are returned. Because scanning is battery-intensive, you should observe the following guidelines:

  • As soon as you find the desired device, stop scanning.
  • Never scan on a loop, and set a time limit on your scan. A device that was previously available may have moved out of range, and continuing to scan drains the battery.

The following snippet shows how to start and stop a scan:

BLEデバイスを見つけるためにstartLeScan()メソッドが使用できる. このメソッドはBluetoothAdapter.LeScanCallbackをパラメータに取る. あなたはこのcallbackを実装し, デバイススキャン結果を確認する. デバイススキャンはバッテリーを多く消費するため次のガイドラインに従うこと.

  • 期待するデバイスが見つかったらスキャンを停止する.
  • スキャンし続けないこと. timeoutを設けること. 接続されたデバイスが圏外移動し, スキャンを続けているとバッテリーを消費し続けることになる.

次はスキャンの開始と停止のスニペットである.

/**
 * Activity for scanning and displaying available BLE devices.
 */
public class DeviceScanActivity extends ListActivity {

    private BluetoothAdapter mBluetoothAdapter;
    private boolean mScanning;
    private Handler mHandler;

    // Stops scanning after 10 seconds.
    private static final long SCAN_PERIOD = 10000;
    ...
    private void scanLeDevice(final boolean enable) {
        if (enable) {
            // Stops scanning after a pre-defined scan period.
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    mScanning = false;
                    mBluetoothAdapter.stopLeScan(mLeScanCallback);
                }
            }, SCAN_PERIOD);

            mScanning = true;
            mBluetoothAdapter.startLeScan(mLeScanCallback);
        } else {
            mScanning = false;
            mBluetoothAdapter.stopLeScan(mLeScanCallback);
        }
        ...
    }
...
}

If you want to scan for only specific types of peripherals, you can instead call startLeScan(UUID[], BluetoothAdapter.LeScanCallback), providing an array of UUID objects that specify the GATT services your app supports.

Here is an implementation of the BluetoothAdapter.LeScanCallback, which is the interface used to deliver BLE scan results:

もし特定のperipheralのみを探す場合, 代わりにstartLeScan(UUID[], BluetoothAdapter.LeScanCallback)を使うことができる. アプリケーションがサポートしているGATT serviceのUUIDのarrayを提供すればよい.

下記はBluetoothAdapter.LeScanCallbackを実装し, BLEデバイスのスキャン結果を使用するサンプルである.

private LeDeviceListAdapter mLeDeviceListAdapter;
...
// Device scan callback.
private BluetoothAdapter.LeScanCallback mLeScanCallback =
        new BluetoothAdapter.LeScanCallback() {
    @Override
    public void onLeScan(final BluetoothDevice device, int rssi,
            byte[] scanRecord) {
        runOnUiThread(new Runnable() {
           @Override
           public void run() {
               mLeDeviceListAdapter.addDevice(device);
               mLeDeviceListAdapter.notifyDataSetChanged();
           }
       });
   }
};

Note: You can only scan for Bluetooth LE devices or scan for Classic Bluetooth devices, as described in Bluetooth. You cannot scan for both Bluetooth LE and classic devices at the same time.

NOTE: アプリはBLEデバイスがClassic BTデバイスどちらか片方しか一度にスキャンできない. これについてはBluetoothで述べている.

Connecting to a GATT Server

The first step in interacting with a BLE device is connecting to it— more specifically, connecting to the GATT server on the device. To connect to a GATT server on a BLE device, you use the connectGatt() method. This method takes three parameters: a Context object, autoConnect (boolean indicating whether to automatically connect to the BLE device as soon as it becomes available), and a reference to a BluetoothGattCallback:

BLEデバイスと通信するためのfirst stepとしてそれと接続すること. すなわちデバイスからGATT serverに接続する. BLEデバイスがGATT serverに接続するにはconnectGatt()メソッドを使用する. このメソッドはContext, autoConnect(利用可能になったBLEデバイスに自動接続するかどうかのフラグ), そしてBluetoothGattCallbackをパラメータにとる.

mBluetoothGatt = device.connectGatt(this, false, mGattCallback);

This connects to the GATT server hosted by the BLE device, and returns a BluetoothGatt instance, which you can then use to conduct GATT client operations. The caller (the Android app) is the GATT client. The BluetoothGattCallback is used to deliver results to the client, such as connection status, as well as any further GATT client operations.

In this example, the BLE app provides an activity (DeviceControlActivity) to connect, display data, and display GATT services and characteristics supported by the device. Based on user input, this activity communicates with a Service called BluetoothLeService, which interacts with the BLE device via the Android BLE API:

これはBLEデバイスによってホストされたGATT serverへの接続と, BluetoothGattのインスタンスを返却する. BluetoothGattインスタンスでGATT clientとしての操作が可能になる. 呼び出し元であるAndroid アプリがGATT clientとして振る舞う時, BluetoothGattCallbackは接続状態や他のGATT client操作を伝送するために使用される.

今回の例では, BLEアプリはDeviceControlActivityでBLEデバイスに接続し, データを表示し, GATT serviceとデバイスがサポートしているcharacteristicsを表示する. ユーザ入力を契機に
Android BLE APIを使用してBLEデバイスと通信するBluetoothLeServiceとコミュニケーションする.

// A service that interacts with the BLE device via the Android BLE API.
public class BluetoothLeService extends Service {
    private final static String TAG = BluetoothLeService.class.getSimpleName();

    private BluetoothManager mBluetoothManager;
    private BluetoothAdapter mBluetoothAdapter;
    private String mBluetoothDeviceAddress;
    private BluetoothGatt mBluetoothGatt;
    private int mConnectionState = STATE_DISCONNECTED;

    private static final int STATE_DISCONNECTED = 0;
    private static final int STATE_CONNECTING = 1;
    private static final int STATE_CONNECTED = 2;

    public final static String ACTION_GATT_CONNECTED =
            "com.example.bluetooth.le.ACTION_GATT_CONNECTED";
    public final static String ACTION_GATT_DISCONNECTED =
            "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED";
    public final static String ACTION_GATT_SERVICES_DISCOVERED =
            "com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED";
    public final static String ACTION_DATA_AVAILABLE =
            "com.example.bluetooth.le.ACTION_DATA_AVAILABLE";
    public final static String EXTRA_DATA =
            "com.example.bluetooth.le.EXTRA_DATA";

    public final static UUID UUID_HEART_RATE_MEASUREMENT =
            UUID.fromString(SampleGattAttributes.HEART_RATE_MEASUREMENT);

    // Various callback methods defined by the BLE API.
    private final BluetoothGattCallback mGattCallback =
            new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status,
                int newState) {
            String intentAction;
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                intentAction = ACTION_GATT_CONNECTED;
                mConnectionState = STATE_CONNECTED;
                broadcastUpdate(intentAction);
                Log.i(TAG, "Connected to GATT server.");
                Log.i(TAG, "Attempting to start service discovery:" +
                        mBluetoothGatt.discoverServices());

            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                intentAction = ACTION_GATT_DISCONNECTED;
                mConnectionState = STATE_DISCONNECTED;
                Log.i(TAG, "Disconnected from GATT server.");
                broadcastUpdate(intentAction);
            }
        }

        @Override
        // New services discovered
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
            } else {
                Log.w(TAG, "onServicesDiscovered received: " + status);
            }
        }

        @Override
        // Result of a characteristic read operation
        public void onCharacteristicRead(BluetoothGatt gatt,
                BluetoothGattCharacteristic characteristic,
                int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
            }
        }
     ...
    };
...
}

When a particular callback is triggered, it calls the appropriate broadcastUpdate() helper method and passes it an action. Note that the data parsing in this section is performed in accordance with the Bluetooth Heart Rate Measurement profile specifications:

特定のcallbackをトリガした際に, 適切なbroadcastUpdate()ヘルパメソッドを呼び出しactionを渡す. このセクションでBluetooth Heart Rate Measurementプロファイルの使用に従ってデータをパースする.

private void broadcastUpdate(final String action) {
    final Intent intent = new Intent(action);
    sendBroadcast(intent);
}

private void broadcastUpdate(final String action,
                             final BluetoothGattCharacteristic characteristic) {
    final Intent intent = new Intent(action);

    // This is special handling for the Heart Rate Measurement profile. Data
    // parsing is carried out as per profile specifications.
    if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) {
        int flag = characteristic.getProperties();
        int format = -1;
        if ((flag & 0x01) != 0) {
            format = BluetoothGattCharacteristic.FORMAT_UINT16;
            Log.d(TAG, "Heart rate format UINT16.");
        } else {
            format = BluetoothGattCharacteristic.FORMAT_UINT8;
            Log.d(TAG, "Heart rate format UINT8.");
        }
        final int heartRate = characteristic.getIntValue(format, 1);
        Log.d(TAG, String.format("Received heart rate: %d", heartRate));
        intent.putExtra(EXTRA_DATA, String.valueOf(heartRate));
    } else {
        // For all other profiles, writes the data formatted in HEX.
        final byte[] data = characteristic.getValue();
        if (data != null && data.length > 0) {
            final StringBuilder stringBuilder = new StringBuilder(data.length);
            for(byte byteChar : data)
                stringBuilder.append(String.format("%02X ", byteChar));
            intent.putExtra(EXTRA_DATA, new String(data) + "\n" +
                    stringBuilder.toString());
        }
    }
    sendBroadcast(intent);
}

Back in DeviceControlActivity, these events are handled by a BroadcastReceiver:

DeviceControlActivityに戻って, これらのイベントはBroadcastReceiverでハンドルされる.

// Handles various events fired by the Service.
// ACTION_GATT_CONNECTED: connected to a GATT server.
// ACTION_GATT_DISCONNECTED: disconnected from a GATT server.
// ACTION_GATT_SERVICES_DISCOVERED: discovered GATT services.
// ACTION_DATA_AVAILABLE: received data from the device. This can be a
// result of read or notification operations.
private final BroadcastReceiver mGattUpdateReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        final String action = intent.getAction();
        if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) {
            mConnected = true;
            updateConnectionState(R.string.connected);
            invalidateOptionsMenu();
        } else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) {
            mConnected = false;
            updateConnectionState(R.string.disconnected);
            invalidateOptionsMenu();
            clearUI();
        } else if (BluetoothLeService.
                ACTION_GATT_SERVICES_DISCOVERED.equals(action)) {
            // Show all the supported services and characteristics on the
            // user interface.
            displayGattServices(mBluetoothLeService.getSupportedGattServices());
        } else if (BluetoothLeService.ACTION_DATA_AVAILABLE.equals(action)) {
            displayData(intent.getStringExtra(BluetoothLeService.EXTRA_DATA));
        }
    }
};

Reading BLE Attributes

Once your Android app has connected to a GATT server and discovered services, it can read and write attributes, where supported. For example, this snippet iterates through the server’s services and characteristics and displays them in the UI:

AndroidアプリがGATT serverへの接続を保持し, serviceを発見し, それのattributeを読み書きでき, それらがサポートされている場合. 例えば次のスニペットはGATT serverがもつserviceとcharacteristicsを反復しながらUI表示するものである.

public class DeviceControlActivity extends Activity {
    ...
    // Demonstrates how to iterate through the supported GATT
    // Services/Characteristics.
    // In this sample, we populate the data structure that is bound to the
    // ExpandableListView on the UI.
    private void displayGattServices(List<BluetoothGattService> gattServices) {
        if (gattServices == null) return;
        String uuid = null;
        String unknownServiceString = getResources().
                getString(R.string.unknown_service);
        String unknownCharaString = getResources().
                getString(R.string.unknown_characteristic);
        ArrayList<HashMap<String, String>> gattServiceData =
                new ArrayList<HashMap<String, String>>();
        ArrayList<ArrayList<HashMap<String, String>>> gattCharacteristicData
                = new ArrayList<ArrayList<HashMap<String, String>>>();
        mGattCharacteristics =
                new ArrayList<ArrayList<BluetoothGattCharacteristic>>();

        // Loops through available GATT Services.
        for (BluetoothGattService gattService : gattServices) {
            HashMap<String, String> currentServiceData =
                    new HashMap<String, String>();
            uuid = gattService.getUuid().toString();
            currentServiceData.put(
                    LIST_NAME, SampleGattAttributes.
                            lookup(uuid, unknownServiceString));
            currentServiceData.put(LIST_UUID, uuid);
            gattServiceData.add(currentServiceData);

            ArrayList<HashMap<String, String>> gattCharacteristicGroupData =
                    new ArrayList<HashMap<String, String>>();
            List<BluetoothGattCharacteristic> gattCharacteristics =
                    gattService.getCharacteristics();
            ArrayList<BluetoothGattCharacteristic> charas =
                    new ArrayList<BluetoothGattCharacteristic>();
           // Loops through available Characteristics.
            for (BluetoothGattCharacteristic gattCharacteristic :
                    gattCharacteristics) {
                charas.add(gattCharacteristic);
                HashMap<String, String> currentCharaData =
                        new HashMap<String, String>();
                uuid = gattCharacteristic.getUuid().toString();
                currentCharaData.put(
                        LIST_NAME, SampleGattAttributes.lookup(uuid,
                                unknownCharaString));
                currentCharaData.put(LIST_UUID, uuid);
                gattCharacteristicGroupData.add(currentCharaData);
            }
            mGattCharacteristics.add(charas);
            gattCharacteristicData.add(gattCharacteristicGroupData);
         }
    ...
    }
...
}

Receiving GATT Notifications

It’s common for BLE apps to ask to be notified when a particular characteristic changes on the device. This snippet shows how to set a notification for a characteristic, using the setCharacteristicNotification() method:

BLEアプリに共通することとして, デバイス上で特定のcharacteristicの変更通知を受け取ることがある. 次のスニペットはsetCharacteristicNotification()メソッドを用いてcharacteristicの変更通知を受け取る方法である.

private BluetoothGatt mBluetoothGatt;
BluetoothGattCharacteristic characteristic;
boolean enabled;
...
mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
...
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
        UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);

Once notifications are enabled for a characteristic, an onCharacteristicChanged() callback is triggered if the characteristic changes on the remote device:

characteristicの変更通知が有効になると, リモートデバイスのcharacteristicが変更されるとonCharacteristicChanged()が呼ばれる.

@Override
// Characteristic notification
public void onCharacteristicChanged(BluetoothGatt gatt,
        BluetoothGattCharacteristic characteristic) {
    broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
}

Closing the Client App

Once your app has finished using a BLE device, it should call close() so the system can release resources appropriately:

アプリがBLEデバイスの使用を終える時, close()メソッドを呼び出し, システムが適切にリソースを解放できるようにすること.

public void close() {
    if (mBluetoothGatt == null) {
        return;
    }
    mBluetoothGatt.close();
    mBluetoothGatt = null;
}

参考

License:
Portions of this page are modifications based on work created and shared by the Android Open Source Project and used according to terms described in the Creative Commons 2.5 Attribution License.