Expo Pedometer（歩数計）を使うための設定方法や不具合について

##はじめに

Expoでユーザーの歩数を取得する[Pedometer API](https://docs.expo.io/versions/latest/sdk/pedometer/)を検証しているのですが、いくつか留意が必要なようなのでまとめてみます。

##使用方法

機能自体は非常にシンプルで、以下の３つのみです。

1. 使用可能かどうかのチェック
2. 期間を指定して歩数を取得
3. 歩数のアップデート（差分）を監視

まずは`expo-sensors`をinstall/importします。

```js
import { Pedometer } from "expo-sensors";
```

### 1. 使用可能かどうかのチェック

```js
const isAvailable = await Pedometer.isAvailableAsync(); // Boolean
```

デバイスで歩数計が使用可能かどうかを非同期でチェックします。
iOSの場合はこの時にダイアログが表示されます。

### 2. 期間を指定して歩数を取得

```js
const end = new Date(); // end: 現在時間
const start = new Date();
start.setDate(end.getDate() - 1); // start: 24時間前

try {
  const { steps } = await Pedometer.getStepCountAsync(start, end);
  console.log(steps); // Number
} catch (e) {
  console.log(e);
}
```

Date型のstartとendを指定してその間の歩数を非同期で取得します。

### 3. 歩数のアップデート（差分）を監視

```js
Pedometer.watchStepCount(({ steps }) => {
  console.log(steps); // Number
});
```

アプリ起動中に端末を持って歩くと、その都度差分の歩数が通知されます。
傾向としては一歩ごとではなくある程度まとまった時間でカウントされるようです。
また、アプリがBackgroundで起動中（他のアプリを起動していたり、ホーム画面に戻ったり）の場合にもカウントされつつ、Foregroundに戻った瞬間に指定したコールバック関数が呼ばれ、Backgroundの状態での歩数が通知されます。

## [iOS]歩数の取得制限について

Expoの内部的な実装の話になりますが、iOSネイティブにおいて歩数を取得する際は下記のような2通りの方法があります。

* **HealthKit**から取得する
  
* **CMPedometer**を使う

HealthKitの場合はHealthStoreに永続化されたユーザーのデータを取得することができるのですが、ExpoのPedometerでは後者のCMPedometerを使用しています。
この場合、取得することができる過去の歩数データは**最大７日間まで**になっており、Expoにおいても同様の制限があります。

Expoでは現状HealthKitを使用できないため、もしPedometer（CMPedometer）以上の機能が必要な場合はEject後、HealthKitやGoogle Fitを使ったReactNative用パッケージを導入することを検討してください。

パッケージ例（こちらは未検証）
https://github.com/terrillo/rn-apple-healthkit
https://github.com/OvalMoney/react-native-fitness

CMPedometerの７日制限に関するドキュメントは[こちら](https://developer.apple.com/documentation/coremotion/cmpedometer/1613946-querypedometerdata#discussion)。
しかしこの７日という数字も怪しくて、Expoで検証したところ少なくとも私の場合は過去１０日間くらい取得可能でした。
それ以前の歩数の取得はエラーにはならず`{ steps: 0 }`が返ります。

## [iOS]権限確認ダイアログについて

iOSでは`isAvailableAsync`の際に権限の確認ダイアログが表示されます。
App Storeにリリースする際は、このダイアログに表示される説明文を適切に設定する必要があります。（この辺り、詳しくは[こちら](https://qiita.com/mildsummer/items/3b17cf6f80e04ad37578)）
`app.json`の`expo.ios.infoPlist.NSMotionUsageDescription`を追加してください。

Expo Clientでは反映されませんが、ビルドして確認するとこんな感じに説明文が表示されます。
<img width="375" alt="PNGイメージ 43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/1372ea63-e0f2-28e2-3c60-b3de0e1de35f.png">

```json:app.json（部分）
{
  "expo": {
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "バンドル名",
      "infoPlist": {
        "NSMotionUsageDescription": "[テスト文言]歩数を取得するためにモーション情報を使用します"
      }
    }
  }
}
```

## [Android]`getStepCountAsync`が使用できない

ExpoのSDK34以降、Androidにおいて`getStepCountAsync`が使用できない問題（というか変更）が発生しています。
Githubのissueだとこの辺り。

**[expo-sensors] Pedometer unable to query steps, documentation unclear/contradictory**
https://github.com/expo/expo/issues/4895

現時点では完全かつ安全に解決する方法は無いようですが、上記のissueでも提案されている通り、SDK33の`expo`パッケージからPedometerをimportして無理矢理使用することが可能です。
後述するGoogle OAuth Client IDの取得に関する検証の際にはこの方法で使用しています。

```json:package.json(部分)
"dependencies": {
  "expo": "~36.0.0",
  "expo-legacy": "npm:expo@33.0.0",
```

SDK33以前では`expo-sensors`に分離されていません。
このようにSDK33の`expo`を別名でインストールし、Pedometerをインポートします。

```js
import { Pedometer } from "expo-legacy";

const end = new Date();
const start = new Date();
start.setDate(end.getDate() - 1);

try {
  const { steps } = await Pedometer.getStepCountAsync(start, end);
  console.log(steps);
} catch (e) {
  console.log(e);
}
```

### なぜSDK34以降使用できなくなっているのか

SDK33→SDK34において、Androidでの歩数の取得方法が変更されているようです。
SDK33では**Google Fitness(Google Fit)**からユーザーのデータを取得しています。
このGoogle Fitでの実装に何かしら問題があったのかわかりませんが、SDK34以降のバージョンではAndroidのSensorEventを使用して歩数を`watch`のみしており、`getStepCountAsync`は実装されていません。

ソースを比べてみたい方はこちらをどうぞ。

SDK33
https://github.com/expo/expo/blob/4311483ed8d2f12a5401dc1420e1503821681013/android/versioned-abis/expoview-abi33_0_0/src/main/java/abi33_0_0/host/exp/exponent/modules/api/PedometerModule.java

SDK36(Latest)
https://github.com/expo/expo/blob/4311483ed8d2f12a5401dc1420e1503821681013/android/versioned-abis/expoview-abi36_0_0/src/main/java/abi36_0_0/expo/modules/sensors/modules/PedometerModule.java
`getStepCountAsync`は`Getting step count for date range is not supported on Android yet.`というエラーメッセージを返すようになっています。

**[2020/04/03追記]**
SDK37現在でも変わらずです。

## [Android]スタンドアロンアプリでの設定方法

上述した通りSDK33まではAndroidではGoogle Fitを使用してユーザーに紐づかれた歩数を取得しているわけですが、このデータの使用には[公式ドキュメント](https://docs.expo.io/versions/latest/sdk/pedometer/#standalone-applications)に書かれているとおり、Android OAuth Clientとしての登録が必要になります。
ここでは、Android OAuth Clientとしての設定方法について説明します。

なお、今現在はGoogle Fitを使わない仕様になっているので、このドキュメントに今もこの説明が入っているのは誤りです。
そもそも`getStepCountAsync`が使えないので嬉しくないですが、**通常この設定は必要ありません**。

上述した方法で無理矢理`getStepCountAsync`を使う場合や、将来的（希望的観測）にPedometerがGoogle Fitを使ってちゃんと実装し直された場合のために設定を検証してみます。

### 1. Expo Client上で確認

先ほどの方法でSDK33からPedometerをインポートし、ドキュメントのソースをちょっと変更して画面を作ります。

```jsx:App.js
import React from "react";
import { Pedometer } from "expo-legacy";
import { StyleSheet, Text, View, Button } from "react-native";

export default class App extends React.Component {
  state = {
    isPedometerAvailable: "checking",
    pastStepCount: 0,
    currentStepCount: 0
  };

  componentWillUnmount() {
    this._subscription && this._subscription.remove();
    delete this._subscription;
  }

  init = async () => {
    let isPedometerAvailable = false;
    try {
      isPedometerAvailable = await Pedometer.isAvailableAsync();
      this.setState({ isPedometerAvailable: String(isPedometerAvailable) });
    } catch ({ message }) {
      this.setState({ isPedometerAvailable: `Could not get isPedometerAvailable: ${message}` });
    }
    if (isPedometerAvailable) {
      this._subscription = Pedometer.watchStepCount(result => {
        this.setState({
          currentStepCount: result.steps
        });
      });
      const end = new Date();
      const start = new Date();
      start.setDate(end.getDate() - 1);
      try {
        const { steps } = await Pedometer.getStepCountAsync(start, end);
        this.setState({ pastStepCount: steps });
      } catch ({ message }) {
        this.setState({ pastStepCount: `Could not get stepCount: ${message}` });
      }
    }
  };

  render() {
    return (
      <View style={styles.container}>
        <Text>
          Pedometer.isAvailableAsync(): {this.state.isPedometerAvailable}
        </Text>
        <Text>
          Steps taken in the last 24 hours: {this.state.pastStepCount}
        </Text>
        <Text>Walk! And watch this go up: {this.state.currentStepCount}</Text>
        <Button title="INIT" onPress={this.init} />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    marginTop: 15,
    alignItems: "center",
    justifyContent: "center"
  }
});
```

Android端末のExpo Clientで確認。このような画面になります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/4df6ffa7-09e5-1d7b-66df-d1fd444a127a.png" alt="android" width="375" />
「INIT」ボタンを押し、`isAvailableAsync`が呼ばれると、
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/1c8b87ec-b466-f887-a0c9-d9b8416653f7.png" alt="android" width="375" />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/923af536-afe3-57db-3d51-2541a2ffd22a.png" alt="android" width="375" />
このようなGoogleの承認画面（「OAuth 同意画面」）が表示され、「Allow」ボタンで許可すると、歩数データの取得が可能になります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/64c3ab1d-17a8-a797-8c5c-680df792c0c6.png" alt="android" width="375" />

上の画面をみて推察できる通り、Expo Clientを使っている状態だとこの時のAndroid OAuth Client登録や同意画面の設定はExpo Clientのものが使われています。
これを実際にスタンドアロンアプリとしてビルドしPlay Storeに申請する際には各自での設定が必要になります。

ちなみにこの時点で、以下のような不具合があるのがわかりました。

* 同意画面をキャンセルした時に`isAvailableAsync`のPromise(返り値)が終了しない
* 故にキャンセルされたことが検知できず、再度`isAvailableAsync`を呼んでも何も起きない
* 端末の設定からExpoアプリの「Physical activity（ボディーセンサー）」権限を不許可にすると、同意画面の際にアプリがクラッシュする
* `watchStepCount`で取得できる歩数が差分の歩数ではなく累算された値になっている

おそらくSDK34以降にGoogle Fitnessでの実装を取りやめているのはこのような不安定さからでしょう。
繰り返しになってしまいますが`getStepCountAsync`が使えず内部的な実装方法が変わっているため、上記の問題はSDK36現在では発生しません。

### 2. スタンドアロンアプリでビルド

不具合は一旦置いておき、スタンドアロンアプリでビルドしてみます。
`app.json`でパッケージ名を設定してください。確認なので適当でいいですが実際は独自ドメインのものを使用します。

```json:app.json（部分）
{
  "expo": {
    "android": {
      "package": "パッケージ名"
    }
  }
}
```

```console
$ expo build:android
```
Expo Cliの流れに沿ってビルドします。

Google Fitを使用するためのAndroid OAuth Clientの登録には、アプリの証明書に含まれるSHA-1の[指紋（Fingerprint）](https://ja.wikipedia.org/wiki/%E6%8C%87%E7%B4%8B_(%E5%85%AC%E9%96%8B%E9%8D%B5%E6%9A%97%E5%8F%B7))情報が必要です。

公式ドキュメント：**Get an OAuth 2.0 Client ID**
https://developers.google.com/fit/android/get-api-key

Expoでのビルドの場合は証明書をお任せで作成してもらうことになるので、ビルド後にapkファイルの中から指紋を抽出するという流れになります。
apkファイルをダウンロードしたら、下記のコマンドを使用します。
JDKに含まれる`keytool`コマンドがない場合は適宜[インストール](https://www.oracle.com/technetwork/jp/java/javase/downloads/index.html)してください。

```console
$ cd ~/Downloads
$ keytool -list -printcert -jarfile <ダウンロードしたapkファイルのパス>.apk | grep SHA1 | awk '{ print $2 }'
```

```console
BB:0D:AC:74:D3:21:E1:43:67:71:9B:62:91:AF:A1:66:6E:44:5D:75
```

こんな感じの文字列が表示されたら、コピーしておきます。
証明書は一つのExpoプロジェクトをビルドする際に最初に作成され、その後の同アプリのビルドでは同じものが使われます。
指紋情報も同アプリで一度取得したら変更は無いはずです。

### 3. OAuth Clientの設定

あとは[上記のドキュメント](https://developers.google.com/fit/android/get-api-key)に沿って設定をすればOKです。
![Get_an_OAuth_2_0_Client_ID_ _ _Google_Fit_ _ _Google_Developers.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/7bc16762-1faa-49b6-2a5b-ee51677461c5.png)
Fitness APIを使用するための設定の流れを自動的に案内してくれるリンクがあります。これをクリックし、
Googleプロジェクトを選択します。この時点でアプリに使用するGoogleプロジェクトが存在していない場合は新たに作成してください。
Firebaseで作成したプロジェクトもここに表示され、選択することができます。
![API_を有効にする_-_Google_API_コンソール.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/668b8598-8032-8934-aecb-750991800f90.png)
![認証情報ウィザード_-_Test_Project_-_Google_API_コンソール.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/14bdaa18-906e-9ffa-78bc-5a0ed7221ce0.png)
「プロジェクトへの認証情報の追加」画面になったら、「Fitness API」を選択されているのを確認し、

* 「APIを呼び出す場所」 → 「Android」
* アクセスするデータの種類 → 「ユーザーデータ」

を選択して次に進みます。
この時点で、Googleプロジェクトに紐づかれたOAuth 同意画面が無い場合は設定が求められます。
![同意画面_-_Test_Project_-_Google_API_コンソール.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/2abbdf80-5499-b38c-a1dc-9054e468d44f.png)
![同意画面_-_Test_Project_-_Google_API_コンソール.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/21ff4310-ea30-5a82-6564-5ce95727eb97.png)

App Storeに申請する際はアイコン、連絡先、ホームページURL、プライバシーポリシーURL等の情報を入力する必要があるようです。その際はこの同意画面をGoogleへ「確認のため送信」します。

同意画面の設定が終わったら（検証のみであればアプリ名くらいでよいはず。「確認のため送信」はせず「保存」だけします。）、クライアントIDの作成画面に進みます。
![OAuth_クライアント_ID_の作成_-_Test_Project_-_Google_API_コンソール.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/6f9432d5-b229-aec8-f92b-1c68a3ef8244.png)
このような画面になったら、クライアントの名前（アプリ名等）、先ほど取得した指紋の文字列、`app.json`で設定したパッケージ名を入力して「作成」します。
![認証情報_–_API_とサービス_–_Test_Project_–_Google_API_Console.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/31538a6d-52f6-7471-ed31-361141ce7fb9.png)
同意画面の確認申請・公開までにログインが１００回までに制限されるとのことです。
![認証情報_–_API_とサービス_–_Test_Project_–_Google_API_Console.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/e6304f44-b881-49da-a29c-fe63d63608fe.png)
このようにプロジェクトの認証情報にOAuth 2.0 クライアントIDが追加されたら成功です。

このクライアントIDはExpoの[Google認証](https://docs.expo.io/versions/latest/sdk/google/)機能でも使用することになります。
Pedometer（SDK33以下）の場合はこのIDをアプリ側のソースでパラメータに使用するといったことはなく、パッケージ名で紐づかれていれば問題ありません。

### 4. スタンドアロンアプリで確認

端末をUSBで繋いで、ADBコマンドを使ってインストールしてみます。

```console
$ adb install <ダウンロードしたapkファイルのパス>.apk
```

起動し「INIT」ボタンを押して`isAvailableAsync`が呼ばれると、まずはGoogleアカウントの選択画面が表示されます。
端末の言語設定を英語にしているので英語になっていますが、あとで日本語にして確認してみます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/0ab8b203-de5d-e823-b02a-0b67131d26ec.png" alt="android" width="375" />
ここで`expo-pedometer-example`となっているのは`app.json`の`expo.name`で設定されたアプリ名（デフォルトではプロジェクトIDとイコール）です。
アプリアイコンは`assets`ディレクトリに入っているデフォルトのアイコンが表示されています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/c634c352-84c9-ce7b-4819-c98c0e03771f.png" alt="android" width="375" />
続いて権限を承認する画面になります。ここで`歩数計テスト`となっているのは、[「OAuth 同意画面」の設定](https://console.developers.google.com/apis/credentials/consent/edit)で入力された「アプリケーション名」です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/1dc70561-bcba-dfb0-a8d2-eb210bf6638e.png" alt="android" width="375" />
続いて最終確認の画面。「Allow」をタップすると、
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/8d675f1b-72a3-6db5-90a1-1ff76852c11b.png" alt="android" width="375" />
`getStepCountAsync`を使用して２４時間以内の歩数が取得できました。

#### アプリ名・アイコン・日本語設定で確認

`app.json`でアプリ名を設定（OAuth 同意画面の設定に合わせ「歩数計テスト」）し、アイコンを独自のものに変更、端末の言語設定を日本語にした状態で再度確認しました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/359caa2f-67c7-8a51-afc6-37ea80d8908c.png" alt="android" width="375" />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/84027c1d-22b2-2148-7b09-e92f3ad9bfdc.png" alt="android" width="375" />
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514229/6c5ae7cf-098e-9443-b2ec-0377297e5956.png" alt="android" width="375" />
設定は問題なく反映されています。
以上でOAuth Client登録について大体把握できました。

## まとめ

基本動作の他、iOSでは`NSMotionUsageDescription`の設定を、AndroidではGoogle OAuth Client登録の方法を確認しました。
現状、iOS・Android共に満足に使用できるのはアプリ起動中に歩数を監視する機能のみとなっていて、それ以上の機能を求める場合には難しいようでした。
ExpoアプリのGoogle OAuth Client登録についてについては基本的に必要ありませんが、SDK33と同様の機能をAndroidで実装するにはGoogle Fitを使うしかない（と思われる）ので、Expoの今後のバージョンアップによって登録が必要になる可能性があります。