Open48

Androidで歩数計実装を考える

てべすてんてべすてん

方針:

Foreground Serviceで歩数カウントする。

  • STEP_DETECTOR センサーで起動する。
  • 端末起動時に起動する
てべすてんてべすてん

調べてると、Google Fitとかから取る方法が出てくるけど、Sensorでできるならそれでええやろの顔をしてる。何かトラップがあるのかな。

てべすてんてべすてん

Foreground Serviceの調査

てべすてんてべすてん

Foreground Serivce

SerivceはActivityが落ちても実行され続けられるやつ。(デフォだとプロセスは同じになるらしい)Foreground ServiceとBackground Serviceがある。

その中でもForeground Serviceはユーザが見えやすいタイプのService。よく見るのだとダウンロード処理とか音楽の再生とかが実装されがち。

Background Serviceは通知がないためユーザがその存在に気づきにくい。そのため最近制限が厳しくなった記憶がある。

なんかカウントしてくれてる感が出るのとカウントされてるされてないのデバッグがしやすいとかあるので今回はForeground Serviceを使いたい。

てべすてんてべすてん

ドキュメントにForeground Serviceで今回実装したいのが例が紹介されていた。

ユーザーから許可を得た後、フォアグラウンド サービスでユーザーのランニングを記録するフィットネス アプリ。通知には、現在のフィットネス セッション中にユーザーが移動した距離が表示される場合があります。

てべすてんてべすてん

多くのユースケースでは、フォアグラウンドサービスの代わりに使用できる専用のプラットフォームやJetpack APIがあります。そのようなAPIがある場合、ほとんどの場合、フォアグラウンドサービスの代わりにそれを使用することが望ましいです。詳細については、フォアグラウンドサービスの代わりに専用のAPIを使用するを参照してください。
https://developer.android.com/develop/background-work/services/foreground-services

「大体のAPIがあるならそれを使ったほうがええで」とのこと。
Foreground Serviceの種類と代替のAPIは以下に載ってる。
https://developer.android.com/develop/background-work/services/fg-service-types

今回は種類で言ったらHealthだと思うが、Health にはAlternatives はないので、今回は手作りせねばいけなそう。

てべすてんてべすてん

ユーザーはデフォルトで通知を解除できる

ユーザが通知を解除できないようにするには、Notification.Builderを使用して通知を作成するときに、setOngoing()メソッドにtrueを渡します。

デフォだと削除できるらしい。今回だと (設計にもよるが) setOngoing(true) しないといけなさそう...?

てべすてんてべすてん

作り方

1. AndroidManifest.xmlに定義

<manifest xmlns:android="http://schemas.android.com/apk/res/android" ...>
  <application ...>
    <service
        android:name=".MyMediaPlaybackService"
        android:foregroundServiceType="mediaPlayback"
        android:exported="false">
    </service>

2. 権限の付与

<manifest xmlns:android="http://schemas.android.com/apk/res/android" ...>
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

実行時の権限リクエスト は要らない。

3. 権限等の準備

Android 14(APIレベル34)以降、フォアグラウンド・サービスを起動すると、サービスの種類に応じて特定の前提条件があるかどうかがチェックされます。

必要な権限はForeground Serviceを起動する前にチェック・取得しておこう。

4. Foreground Service スタート

val intent = Intent(...) // Build the intent for the service
context.startForegroundService(intent)

サービス内部で、通常はonStartCommand()の中で、サービスをフォアグラウンドで実行するように要求できます。そのためには、ServiceCompat.startForeground() を呼び出します(androidx-core 1.12以降で使用可能)。このメソッドは以下のパラメータを取る:

  • サービス
  • ステータスバーで通知を一意に識別する正の整数
  • 通知オブジェクト自体
  • サービスによって行われる作業を識別するフォアグラウンドサービスタイプ

5. サービスを実装

val notification = NotificationCompat.Builder(this, "CHANNEL_ID")
  // Create the notification to display while the service is running
  .build()
ServiceCompat.startForeground(
  /* service = */ this,
  /* id = */ 100, // Cannot be 0
  /* notification = */ notification,
  /* foregroundServiceType = */
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
    ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
  } else {
    0
  },
)
  • NotificationChannelって作らなくていいのかな。あとで検証しよう。

6. 終了する

サービスをフォアグラウンドから削除するには、stopForeground() を呼び出します。このメソッドは、ステータス・バー通知も削除するかどうかを示すブール値を取ります。サービスは実行され続けることに注意してください。

サービスがフォアグラウンドで実行されている間にサービスを停止すると、その通知は削除されます。
https://developer.android.com/develop/background-work/services/foreground-services#remove-from-foreground

てべすてんてべすてん

Foreground Serviceはユーザが止めれるから注意してね

コールバックもないから終わっても受け取れないらしい。(諸々がメモリから削除されるからしゃーないみたいな顔してそう)ApplicationExitInfoを使って再起動した時に理由をチェックすると良さげとのこと。

ユーザーが 停止REASON_USER_REQUESTEDボタンをタップしても、システムはアプリにコールバックを送信しません。アプリが再起動したら、API の一部である理由を確認すると役立ちます ApplicationExitInfo。
https://developer.android.com/develop/background-work/services/foreground-services#handle-user-initiated-stop

てべすてんてべすてん

📝
ServiceのCoroutineScopeとかは androidx.lifecycle:lifecycle-service についてくるLifecycleServiceを使ってServiceを定義することで取得できる。 と思ったらdeprecatedになってた。
多分 androidx.lifecycle:lifecycle-service を使うと良さそう。

てべすてんてべすてん

Android 13(API レベル 33)以降では、ユーザーが通知権限を拒否した場合、フォアグラウンド サービスに関する通知はタスク マネージャーに引き続き表示されますが、通知ドロワーには表示されません。

とのことなので実行時の権限もいるらしいわ。めんどいな。
とりあえず表示できたわ。

てべすてんてべすてん

つづいてはセンサーの話。

てべすてんてべすてん

基本の流れは以下の通りの認識。

// sensorManager, sensorを取得
val sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
val sensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)

// sensorListenerを登録して歩数が増えたときに何かしらの処理を行う。
val listener: SensorEventListener by lazy {
  object : SensorEventListener {
    override fun onSensorChanged(event: SensorEvent?) {
      if (event == null) return

      val stepsSinceLastReboot = event.values[0].toLong()
      Log.d(TAG, "Steps since last reboot: $stepsSinceLastReboot")
    }

    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
      Log.d(TAG, "Accuracy changed to: $accuracy")
    }
  }
}

sensorManager.registerListener(listener,
                sensor, SensorManager.SENSOR_DELAY_UI)
てべすてんてべすてん

ヘルスコネクト使った方がいいよと代表は言うが、ユーザビリティ最悪なんだよな、あれ。

てべすてんてべすてん

取れるデータを見てみて、「これ使ってこんな機能作れそう」とか話してもらうと良さそう。

てべすてんてべすてん

どんな機能を作りたいかは彼らに一任すべきだと思う。多分以下のいずれかになるとは思うが。

  • いろんな情報を取りたいのでヘルスコネクトにしてみる。
  • 一旦FGS/STEP_COUNTER Sensorで作って、必要が出てきたらヘルスコネクトにしてみる
  • 歩数以外要らなそうだが将来的な幅を持たせておきたいので、ヘルスコネクトで実装
  • 歩数以外要らなそうだから実装簡単/シンプル/センサーの勉強になるので、FGS/STEP_COUNTER Sensor なの方が良さそう
  • どっちでも作ってみて良さそうな方を採用する
てべすてんてべすてん

health connect

FGS / Sensor

比較表

項目 health connect FGS/Sensor
機能性 ⭕️。一度セットアップさえしてしまえば、色んなアプリから取得したデータを簡単に取り込めそうなので将来的に色んな機能を作り込むことができそう。 ❌。新しい機能を作るためには、都度センサー等から取得する必要がある。他アプリや他のデバイスから取得したいみたいな要件(ex: スマホとWearOS繋ぐとか) があると場合によってはツムツム。
保守性[1] ⭕️? 現状 Googleが推してるっぽい。Android14以降OSに組み込まれてるのでGoogle Fit APIみたいに廃れることはなさそうかな...? △。今後、Googleがhelth connectを推進するために普通のアプリがセンサーからは取得できないようにしてもおかしくない。
実装難易度 高。Qiita等の記事が少なめ。 比較的 低。昔からある方法なので 記事は多め。FGSやセンサーの勉強にもなるかも。
ユーザ視点の使いやすさ ❌。ユーザ側で権限の設定等 色々やることがある。 ⭕️。FGSを開始すれば取得できる。
脚注
  1. Googleの匙加減次第で振り回される可能性はありそうという意味ではどちらも変わらない気はするけども。 ↩︎

てべすてんてべすてん

これが原因の香り


<!-- For supported versions through Android 13, create an activity to show the rationale
       of Health Connect permissions once users click the privacy policy link. -->
  <activity
      android:name=".PermissionsRationaleActivity"
      android:exported="true">
    <intent-filter>
      <action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
    </intent-filter>
  </activity>

  <!-- For versions starting Android 14, create an activity alias to show the rationale
       of Health Connect permissions once users click the privacy policy link. -->
  <activity-alias
      android:name="ViewPermissionUsageActivity"
      android:exported="true"
      android:targetActivity=".PermissionsRationaleActivity"
      android:permission="android.permission.START_VIEW_PERMISSION_USAGE">
    <intent-filter>
      <action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
      <category android:name="android.intent.category.HEALTH_PERMISSIONS" />
    </intent-filter>
  </activity-alias>

https://developer.android.com/health-and-fitness/guides/health-connect/develop/get-started#show-privacy-policy

てべすてんてべすてん

そうだ
フォーム 書かないといけない んだった

パーミッションの宣言
健康とフィットネスのデータへのアクセスは機密です。Health Connectは、読み取りと書き込みの操作にセキュリティ層を実装し、ユーザーの信頼を維持します。

必要なデータタイプに基づいて、AndroidManifest.xmlファイルで読み取りと書き込みの権限を宣言します。フォームに記入した後、アクセスを要求したパーミッションのセットを使用していることを確認してください。

Health Connectは、標準のAndroid権限宣言形式を使用します。<uses-permission>タグでパーミッションを割り当てます。<manifest>タグ内にネストします。
https://developer.android.com/health-and-fitness/guides/health-connect/develop/get-started#declare-permissions

てべすてんてべすてん

リクエストを提出する前に、サードパーティの開発者は Play ストアでアプリを公開する 必要 があります。アプリがまだ開発中であっても、リクエストを提出する必要がある場合は、アプリを公開することをお勧めします。可能な場合は、サードパーティの開発者は、 リクエストの処理がスムーズに行われるように、アプリの Play ストア ページにアプリのプライバシー ポリシーも掲載する必要があります。
https://docs.google.com/forms/d/1LFjbq1MOCZySpP5eIVkoyzXTanpcGTYQH26lKcrQUJo/viewform?edit_requested=true

詰んじゃうくね?

てべすてんてべすてん

この要請は、Playストアで公開されているアプリにのみ適用されることに注意してください。ただし、サードパーティの開発者は引き続きローカルビルドでデータ型にアクセスできるため、この期間中にローカル環境での開発、統合、テストが制限されたり妨げられたりすることはありません。

とあるので詰んでるわけではなさそう。
多分ローカル環境で実行してることを何らかの方法でヘルスコネクトに教えないといけない

てべすてんてべすてん

一応フォームに「アプリケーションは現在 Play ストアで公開されていますか?」という欄があるので、変なアプリじゃなければ審査 通るんかなぁシランケド