Google Fitでなにができるのか

Android Wearの影に隠れてあまり話題を聞かないGoogle Fit。
何ができるのか[ドキュメント](https://developers.google.com/fit/overview)を読んでまとめました
初期設定の方法も後ろの方に書いてあります。

## Google Fit
Google Fitとは、 __フィットネスデータ(Fitness Data)をデバイスやアプリをまたいで一元管理できる機能__ です。

Google Fitを使うにあたって、開発者が留意しておかなければならないことがあります。下記の5つです。

 * なぜそのデータを扱うのかの説明をすること。
 * データの削除の要求には誠実に答えること。
 * Google Fitからデータを読み込んだ場合は、同じようにGoogle Fitにデータを書き込むこと。
 * Google Fit APIをフィットネス以外の目的では使ってならないこと。例えば医療保存、生体認証、販売や広告など。
 * Google Fitを利用する際は[利用規約](https://developers.google.com/fit/terms?hl=ja)をよく読んでから使うこと。Google fit APIを利用した場合はその規約に同意したものとする。

### できること
 * ウェアラブルデバイスやその他のセンサーからの __データを保存__ できる
 * 他のアプリによって生成された __データにアクセス__ できる
 * ウェアラブルデバイスやその他のセンサーが更新された際も __データは保持__ される

### アーキテクチャ
次の4つのコンポーネントで構成されています。

 * The fitness store
   * フィットネスデータを保存
 * The sensor framework
   * 各種センサーをGoogle Fit APIsを用いてfitness storeへ簡単にアクセスできるフレームワーク
 * Permissions and user controls
   * データのアクセス権限とオプトイン
 * Google Fit APIs
   * Android用APIとREST API

上記の4つの構成で、 __Platformを選ばず__ Google Fitを利用できます。

## Android Apis
Android用に用意されているのは次の5本のAPIです。

### Sensors API 
 * デバイスに備わっているセンサーにアクセスしてデータを受け取るためのAPI。センサーの生データを __リアルタイムに受け取ることが可能__ です。

### Recording API 
 * データをfitness storeに保存するためのAPI。送りたいデータを購読(Subscribe)することでバッテリー効率の良い送信が可能になります。 __購読している間は自動__ でfitness storeへ送られます。

### History API 
 * fitness storeのデータにアクセスするAPI。InsertやDelete、Readingなどのような、 __大量のデータを扱う__ 際に用います。

### Session API 
 * 行った運動の期間の情報をつけるAPI。その運動の終了時や、Google Fitの外からimportするデータにも __Sessionを付け加える__ ことができます。また、他のアプリのSessionをIntentで受け取ったり、Broadcastを飛ばすこともできます。

### Bluetooth Low Energy API 
 * BLEデバイスに接続するためのAPI。 __[GATT profile](https://developer.bluetooth.org/TechnologyOverview/Pages/BLE.aspx)に対応しているBLEデバイス__ のサーチ、およびBLEデバイスのデータにアクセスできます。


## Fitness Data Types
`com.google.~`という名前空間でデータ型が用意されています。独自のものも定義できます。
このデータ型が、 __fitness storeに保存されたり、各種センサーから取得するデータ__ です。
データは、瞬間値と統計値の2種類あり、データ型は3種類に分かれています。
また、データの内容によりPermissionが異なります。
瞬間値は、頭に`TYPE_`、統計値は、頭に`AGGREGATE_`のプレフィックスがつきます。

### Permission
`GoogleApiClient`を作成する際の`Scope`です。
頭に`FITNESS_`のプレフィックスがつきます。
[データの内容、およびアクセスタイプによって異なります](https://developers.google.com/fit/android/authorization?hl=ja#fitness_scopes)。
複数要求する際は、複数記述します。

```MainActivity.java
...
    private void buildFitnessClient() {
        // Create the Google API Client
        mClient = new GoogleApiClient.Builder(this)
                .addApi(Fitness.API)
                .addScope(new Scope(Scopes.FITNESS_ACTIVITY_READ_WRITE))
                .addScope(new Scope(Scopes.FITNESS_LOCATION_READ_WRITE))
                .addScope(new Scope(Scopes.FITNESS_BODY_READ_WRITE))
...
```

### Public data types
3種類あるデータ型のうちの一つです。
[標準的なデータ型](https://developers.google.com/fit/android/data-types#public_data_types)で、どのフィットネスアプリからでも読み書きできます。
`com.google.step_count.delta`だと、`最後の読み込み時(History API)からどれだけ歩いたか`を表します。Permissionは`Activity`で、単位は`steps (int—count)`です。

### Private custom data types
3種類あるデータ型のうちの一つです。
アプリ固有のデータ型を定義できますが条件があります。

   * 既存のものに似たものがないか
   * 明瞭な名前であるか
   * 名前は正しくデータを表しているか
   * プレフィックスにパッケージネームをつける

__他のアプリとは共有できないデータ__ になるので、使いどころが限られる気がします。
例えば、`com.sakebook.android.sample.fit_count_smoke`などが考えられます。
共有したい場合は、次の`Shareable data types`を使うか、Googleへ`custom data`を[申請](https://developers.google.com/fit/android/data-types?hl=ja#request_a_new_shareable_data_type)して許可を得ないといけません。
現在許可されている[リスト](https://developers.google.com/fit/android/data-types?hl=ja#available_shareable_data_types)を見る限り、個人では難しそうです。

### Shareable data types
3種類あるデータ型のうちの一つです。
[`Fitness#ConfigApi`](http://developer.android.com/reference/com/google/android/gms/fitness/ConfigApi.html#readDataType(com.google.android.gms.common.api.GoogleApiClient, java.lang.String))経由でどのアプリからでも取得できるデータです。
NIKEのFuelBandの`NIKEFUEL`などがあります。

## Session
`Session API`で扱うことになる情報です。Sessionはメタデータであり、 __実際のフィットネスデータを含めません__ 。

 * start-timeとend-timeで成り立っており、後からfitness storeへアクセスする際にわかりやすいように良い名前をつけましょう。

 * __Sessionは必須ではない__ ですが、自身のアプリで取得したデータなどをわかりやすくするためにも、つけたほうが良いです。自身でメタデータを実装しなくても良いなどの利点もあります。
 * Sessionは __自身が生成されたアプリのパッケージネーム__ を持っています。

運動中にインターバルを設けたい場合は、`com.google.activity.segment`というSessionを用います。
例えばランニングをしていて、数分休憩したのちに再開する場合などは、数分休憩という状態を`com.google.activity.segment`で表現できます。


## アプリ・デバイス連携
他のフィットネスアプリや対応しているデバイスとの連携についてです。
データも、自身が生成されたアプリのパッケージネームを持つことができるので、 __複数のアプリのデータが存在しても正しく自身のアプリのデータをfitness storeへ送れます__。

### 他のフィットネスアプリのIntent受け取り
`AndroidManifest.xml`に記述が必要です。

```AndroidManifest.xml
...
<intent-filter>
    <action android:name="vnd.google.fitness.VIEW" />
    <data android:mimeType="vnd.google.fitness.data_type/com.google.step_count.cumulative" />
    <data android:mimeType="vnd.google.fitness.data_type/com.google.step_count.delta" />
</intent-filter>
...
```
例では2つしかデータ型を書いていないですが、実際は[custom data typeも含め、すべてのデータ型をサポートすることを推奨](https://developers.google.com/fit/android/data-attribution?hl=ja#receive_intents_from_fitness_apps)しています。
一度に受け取れるデータは一つだけですが、アプリ間で相互に円滑な連携をさせるなのかなと思いました。
受け取ったIntentには次の値がBundleされています。

 * `com.google.gms.fitness.start_time`
 * `com.google.gms.fitness.end_time`
 * `com.google.gms.fitness.data_source`
 * `com.google.gms.fitness.session`

以下のように受け取ります。

```RelationActivity.java
...
void onCreate (Bundle savedInstanceState) {
    ...
    Intent intent = getIntent();
    String action = intent.getAction();
    String type = intent.getType();
    String supportedType = DataType.getMimeType(DataType.TYPE_STEP_COUNT_DELTA);

    if (Intent.ACTION_VIEW.equals(action) &&
        supportedType.compareTo(type) == 0) {

        // Get the intent extras
        long startTime = Fitness.getStartTime(intent, TimeUnit.MILLISECONDS);
        long endTime = Fitness.getEndTime(intent, TimeUnit.MILLISECONDS);
        DataSource dataSource = DataSource.extract(intent);
        Session session = Session.extract(intent);

        // Show the session in your app
        ...
    }
}
...
```

### 他のフィットネスアプリへデータを表示させる
[`HistoryApi#ViewIntentBuilder`](http://developer.android.com/reference/com/google/android/gms/fitness/HistoryApi.ViewIntentBuilder.html)を用いてIntentを作成します。

```MyActivity.java
...
// Inside your activity
long startTime = ...;
long endTime = ...;
DataSource dataSource = ...;

Intent fitIntent = new HistoryApi.ViewIntentBuilder(this)
    .setTimeInterval(startTime, endTime, TimeUnit.MILLISECONDS)
    .setDataSource(dataSource)
    .setPreferredApplication("com.example.app") // optional
    .build();
...
```

### Sessionのブロードキャスト受け取り
startとendそれぞれ異なるReceiverで受け取ります。

 * start

```AndroidManifest.xml
...
<receiver android:name="com.example.SessionStartBroadcastReceiver" >
    <intent-filter>
        <action android:name="com.google.android.gms.fitness.session_start" />
        <data android:mimeType="vnd.google.fitness.activity_type/running" />
    </intent-filter>
</receiver>
...
```
上の例ではrunning開始の`activity_type`を受け取っていますが、[様々なtypeがあります](http://developer.android.com/reference/com/google/android/gms/fitness/FitnessActivities.html)。

 * end

```AndroidManifest.xml
...
<receiver android:name="com.example.SessionEndBroadcastReceiver" >
    <intent-filter>
        <action android:name="com.google.android.gms.fitness.session_end" />
        <data android:mimeType="vnd.google.fitness.activity_type/running" />
    </intent-filter>
</receiver>
...
```
receiverとactionが微妙に異なっています。

### GATTに対応していないBLEデバイスの連携
GATTに対応しているBLEデバイスは`Bluetooth Low Energy API`で接続ができます。それ以外の[GATTに対応していないBLEデバイスも、Google Fitの`SensorService`を実装したクラスを持つアプリを作ることで、連携できる](https://developers.google.com/fit/android/new-sensors?hl=ja)ようになります。
しかも、そのアプリが端末にあれば、端末内の他のフィットネスアプリでもそのBLEデバイスを使えるように、Google Fitがよしなにしてくれるようです。

## 初期設定
一通り説明したので、ここから初期設定に移ります。

### Google Developer Console
1. Google Developer Consoleで登録
 * プロジェクトを作成
 * `APIと認証` -> `API`から`Fitness API`をONにしてください。
     * リクエストの制限は`86,400 リクエスト数/日`です。
2. Client IDの作成
 * `APIと認証` -> `認証情報`から`OAuth`の方を選択
 * アプリケーションの種類は`インストールされているアプリケーション`を選択
 * インストールされているアプリケーションの種類は`Android`を選択
 * 適切なパッケージ名を入力
 * APKの署名に使うkeystoreのSHA1を入力
     * 開発用として`debug.keystore`を用いる
     * `keytool`コマンドでSHA1を確認

```sh
$ keytool -exportcert -alias androiddebugkey -keystore ~/.android/debug.keystore -list -v

キーストアのパスワードを入力してください:  
#android と入力
別名: androiddebugkey
作成日: 2013/04/01
エントリ・タイプ: PrivateKeyEntry
証明書チェーンの長さ: 1
証明書[1]:
所有者: CN=Android Debug, O=Android, C=US
発行者: CN=Android Debug, O=Android, C=US
シリアル番号: xxxxxxxx
有効期間の開始日: Mon Apr 01 23:44:47 JST 2013終了日: Wed Mar 25 23:44:47 JST 2043
証明書のフィンガプリント:
         MD5:  xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx
         SHA1: yy:yy:yy:yy:yy:yy:yy:yy:yy:yy:yy:yy:yy:yy:yy:yy
         SHA256: zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz:zz
         署名アルゴリズム名: SHA1withRSA
         バージョン: 3
```
   * ディープリンクは利用しないので無効で構いません。

3.同意画面の作成

 * `APIと認証` -> `同意画面`を選択
   * 適当なサービス名をつけて、保存してください。 

### プロジェクト設定
1.Moduleの`builg.gradle`に追加

 * `GooglePlay Services6.1以降`が必須ですが、6.5から分割されたので、折角なのでそちらを用います。

```builg.gradle
...
dependencies {
    compile fileTree(dir: 'libs', include:
    compile 'com.google.android.gms:play-services-fitness:6.5.+'
}
```
2.`AndroidManifest.xml`にmeta-dataを追加

```AndroidManifest.xml
...
<application
...
    <meta-data android:name="com.google.android.gms.version"
            android:value="@integer/google_play_services_version" />
</application>
...
```
3.チュートリアルにあるように、起動時のActivityに`Fitness API`を利用するための同意画面表示用、および接続のコードを追加します。

```MainActivity.java
public class MainActivity extends ActionBarActivity {

    private static final int REQUEST_OAUTH = 1;

    /**
     *  Track whether an authorization activity is stacking over the current activity, i.e. when
     *  a known auth error is being resolved, such as showing the account chooser or presenting a
     *  consent dialog. This avoids common duplications as might happen on screen rotations, etc.
     */
    private static final String AUTH_PENDING = "auth_state_pending";
    private boolean authInProgress = false;
    private GoogleApiClient mClient = null;

    private static final String TAG = BuildConfig.APPLICATION_ID;

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

        if (savedInstanceState != null) {
            authInProgress = savedInstanceState.getBoolean(AUTH_PENDING);
        }

        buildFitnessClient();
    }


    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    @Override
    protected void onStart() {
        super.onStart();
        // Connect to the Fitness API
        Log.i(TAG, "Connecting...");
        mClient.connect();
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (mClient.isConnected()) {
            mClient.disconnect();
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_OAUTH) {
            authInProgress = false;
            if (resultCode == RESULT_OK) {
                // Make sure the app is not already connected or attempting to connect
                if (!mClient.isConnecting() && !mClient.isConnected()) {
                    mClient.connect();
                }
            }
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putBoolean(AUTH_PENDING, authInProgress);
    }

    /**
     *  Build a {@link GoogleApiClient} that will authenticate the user and allow the application
     *  to connect to Fitness APIs. The scopes included should match the scopes your app needs
     *  (see documentation for details). Authentication will occasionally fail intentionally,
     *  and in those cases, there will be a known resolution, which the OnConnectionFailedListener()
     *  can address. Examples of this include the user never having signed in before, or having
     *  multiple accounts on the device and needing to specify which account to use, etc.
     */
    private void buildFitnessClient() {
        // Create the Google API Client
        mClient = new GoogleApiClient.Builder(this)
                .addApi(Fitness.API)
                .addScope(new Scope(Scopes.FITNESS_LOCATION_READ))
                .addConnectionCallbacks(
                        new GoogleApiClient.ConnectionCallbacks() {

                            @Override
                            public void onConnected(Bundle bundle) {
                                Log.i(TAG, "Connected!!!");
                                // Now you can make calls to the Fitness APIs.
                                // Put application specific code here.
                            }

                            @Override
                            public void onConnectionSuspended(int i) {
                                // If your connection to the sensor gets lost at some point,
                                // you'll be able to determine the reason and react to it here.
                                if (i == GoogleApiClient.ConnectionCallbacks.CAUSE_NETWORK_LOST) {
                                    Log.i(TAG, "Connection lost.  Cause: Network Lost.");
                                } else if (i == GoogleApiClient.ConnectionCallbacks.CAUSE_SERVICE_DISCONNECTED) {
                                    Log.i(TAG, "Connection lost.  Reason: Service Disconnected");
                                }
                            }
                        }
                )
                .addOnConnectionFailedListener(
                        new GoogleApiClient.OnConnectionFailedListener() {
                            // Called whenever the API client fails to connect.
                            @Override
                            public void onConnectionFailed(ConnectionResult result) {
                                Log.i(TAG, "Connection failed. Cause: " + result.toString());
                                if (!result.hasResolution()) {
                                    // Show the localized error dialog
                                    GooglePlayServicesUtil.getErrorDialog(result.getErrorCode(),
                                            MainActivity.this, 0).show();
                                    return;
                                }
                                // The failure has a resolution. Resolve it.
                                // Called typically when the app is not yet authorized, and an
                                // authorization dialog is displayed to the user.
                                if (!authInProgress) {
                                    try {
                                        Log.i(TAG, "Attempting to resolve failed connection");
                                        authInProgress = true;
                                        result.startResolutionForResult(MainActivity.this,
                                                REQUEST_OAUTH);
                                    } catch (IntentSender.SendIntentException e) {
                                        Log.e(TAG,
                                                "Exception while starting resolution activity", e);
                                    }
                                }
                            }
                        }
                )
                .build();
    }
}
```

次のようなエラーがでる場合があります。
```
Connection failed. Cause: ConnectionResult{statusCode=unknown status code 5005, resolution=null}
```
これは、Google Developer Consoleで、同意画面が作成されていない可能性があります。一度確認して、保存してみてください。

これで一通り動かすことができます。実装部分が少ないですが、以上です。

----

## Developer Challenge
現在、 __[Google Fit 開発者コンテスト](https://developers.google.com/fit/challenge/)__ というものが開催されているようです。
応募の締め切りが2015年2月17日なので、まだまだ時間は有ります。
入賞すればかなり大々的にGoogleからPushされるほか、スマートデバイスがいくつかもらえるようです。
新規以外にも、アップデートも対象になるようなので、わりと敷居が低い様に感じました。
興味をもった方は参加してみてはいかがでしょうか？

では、ほんとに以上です。

---
### 参考
[Google Fit Platform Overview / Google Developers](https://developers.google.com/fit/overview)
[Android Fit Samples / GitHub](https://github.com/googlesamples/android-fit)
[Bluetooth のプロファイルについて調べたことのまとめ / Over&Out その後](http://d.hatena.ne.jp/shu223/20140109/1389216052)
[How to get Google Play Services 6.5 and use granular dependency management / stackoverflow](http://stackoverflow.com/questions/27353619/how-to-get-google-play-services-6-5-and-use-granular-dependency-management)
[Google Fit開発者コンテスト / Google Developers](https://developers.google.com/fit/challenge/)