Help us understand the problem. What is going on with this article?

Google Fitでなにができるのか

More than 1 year has passed since last update.

Android Wearの影に隠れてあまり話題を聞かないGoogle Fit。
何ができるのかドキュメントを読んでまとめました
初期設定の方法も後ろの方に書いてあります。

Google Fit

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

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

  • なぜそのデータを扱うのかの説明をすること。
  • データの削除の要求には誠実に答えること。
  • Google Fitからデータを読み込んだ場合は、同じようにGoogle Fitにデータを書き込むこと。
  • Google Fit APIをフィットネス以外の目的では使ってならないこと。例えば医療保存、生体認証、販売や広告など。
  • Google Fitを利用する際は利用規約をよく読んでから使うこと。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に対応しているBLEデバイス のサーチ、およびBLEデバイスのデータにアクセスできます。

Fitness Data Types

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

Permission

GoogleApiClientを作成する際のScopeです。
頭にFITNESS_のプレフィックスがつきます。
データの内容、およびアクセスタイプによって異なります
複数要求する際は、複数記述します。

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種類あるデータ型のうちの一つです。
標準的なデータ型で、どのフィットネスアプリからでも読み書きできます。
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申請して許可を得ないといけません。
現在許可されているリストを見る限り、個人では難しそうです。

Shareable data types

3種類あるデータ型のうちの一つです。
Fitness#ConfigApi経由でどのアプリからでも取得できるデータです。
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も含め、すべてのデータ型をサポートすることを推奨しています。
一度に受け取れるデータは一つだけですが、アプリ間で相互に円滑な連携をさせるなのかなと思いました。
受け取った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を用いて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があります

  • 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を実装したクラスを持つアプリを作ることで、連携できるようになります。
しかも、そのアプリが端末にあれば、端末内の他のフィットネスアプリでもその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を確認
$ 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 開発者コンテスト というものが開催されているようです。
応募の締め切りが2015年2月17日なので、まだまだ時間は有ります。
入賞すればかなり大々的にGoogleからPushされるほか、スマートデバイスがいくつかもらえるようです。
新規以外にも、アップデートも対象になるようなので、わりと敷居が低い様に感じました。
興味をもった方は参加してみてはいかがでしょうか?

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


参考

Google Fit Platform Overview / Google Developers
Android Fit Samples / GitHub
Bluetooth のプロファイルについて調べたことのまとめ / Over&Out その後
How to get Google Play Services 6.5 and use granular dependency management / stackoverflow
Google Fit開発者コンテスト / Google Developers

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした