ヘルスコネクトを Android 14 に対応するメモ

はじめに

この記事では、ヘルスコネクトを用いたアプリを Android 14 で利用するためのメモです。

以下よりメモ書きとなります。情報も少ないため、誤った方法である可能性もあります。ご了承ください。

背景

Android 14(API 34)よりヘルスコネクトがシステムに統合されている。(ヘルスコネクトと Android 14 の統合について)
2023年11月現在利用できるの一部のPixelとAndroid 14ベータ版をインストールしたユーザのみ。

既存のヘルスコネクトの実装をそのまま使おうとしたところ、ヘルスコネクトの認証画面が立ち上がらない。

今後のバージョン

ヘルスコネクトは、Android システムアプリとして Android 14 の一部に組み込まれます。

このバージョンのヘルスコネクトは、前のオープンベータ版と 100% 同等の機能を維持します。APK からフレームワーク モデルにスムーズに移行できるように、Android 14 のリリース前に、詳細なガイダンスを公開する予定です。

ヘルスコネクトのリリースについて

これまで使っていたSDK

これまでは androidx.health.connect:connect-client:1.0.0-alpha11を利用していた。

ヘルスコネクトを Android 13(APK)から Android 14(フレームワーク)に移行するでは移行の方法が書かれている。

記事内では、Jetpack SDK は、ヘルスコネクト APK とヘルスコネクト フレームワーク API の両方に共通の統合ポイントとして機能します。と書かれているため、Android 13でも14でも同じSDKで利用できるはずだ。

実際は以下のような問題が起きた。

  • 認証画面で、アプリのアップデートが必要という画面がでる
  • healthConnectClient.permissionController.getGrantedPermissions()が正しく機能していない
  • 認証しているにもかかわらず、lacks the following permissions でクラッシュする

これらの問題により、Jetpack SDK だけでは Android 14 では利用ができなかった。

問題1: 認証画面が出ない

先述したようにヘルスコネクトの認証画面が立ち上がらない。

この問題に関しては、Stackoverflow「Health Connect – programmatically open the app」より解決した。

val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
    Intent(HealthConnectManager.ACTION_MANAGE_HEALTH_PERMISSIONS)
        .putExtra(Intent.EXTRA_PACKAGE_NAME, BuildConfig.APPLICATION_ID)
} else {
    Intent(HealthConnectClient.ACTION_HEALTH_CONNECT_SETTINGS)
}
startActivity(intent)

このコードで、HealthConnectManager.ACTION_MANAGE_HEALTH_PERMISSIONSというものが出てくる。
HealthConnectManagerは、API 34から使えるandroid.health.connectのパッケージに含まれている。

そのためcompileSdk を 34 にアップデートすることでAndroid 14でヘルスコネクトの認証画面を出すことができる。

AndroidManifest にも追記が必要

アプリのプライバシー ポリシーのダイアログを表示するより、AndroidManifest.xmlファイルにも対応が必要。
この記載がないと、認証画面が出ない。(かつ特にエラーも出ないので注意)

...
<application>
  ...
  <!-- 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>

  <!-- Android 14対応 -->
  <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>
  ...
</application>
...

問題2: healthConnectClient.permissionController.getGrantedPermissions() が正しく機能していない

問題1が解決し、ヘルスコネクトの認証画面が出るようになっため、許可をした状態にしたあと、Jetpack SDK(androidx.health.connect.client)のgetGrantedPermissions()が正しく動作していないことに気づいた。

val granted = healthConnectClient.permissionController.getGrantedPermissions()
val permissions = setOf(
    HealthPermission.getReadPermission(StepsRecord::class),
)
if (granted.containsAll(permissions) {
    println("ヘルスコネクト歩数のパーミッション許可済み")
} else {
    println("許可されていない")
}

Android 14では許可にしていても、getGrantedPermissions()の中身は常に空だ。
そのため許可されていないという判定になる。

これは以下のようにすることで、正しい許可/未許可の判定を取得することができた。

val granted = context.checkSelfPermission(READ_STEPS)
if (granted == PERMISSION_GRANTED) {
    println("ヘルスコネクト歩数のパーミッション許可済み")
} else {
    println("許可されていない")
}

これはAndroid 14での変更点としてあげられていた写真と動画への部分的なアクセス権を付与するを参考に、同様の方法を試してみたところうまくいった。

問題3: 認証しているにもかかわらず、lacks the following permissions でクラッシュする

問題2にてヘルスコネクトのアクセス許可がされているのかどうかを取得することができた。
しかし、healthConnectClientを通して以下のようにヘルスコネクトより歩数を取得しようとエラーが起きる。

val response = healthConnectClient.readRecords(
    ReadRecordsRequest(StepsRecord::class, TimeRangeFilter.between(startTime, endTime))
)

java.lang.SecurityException: packageName lacks the following permissions: [android.permission.health.READ_STEPS]

許可をしているはずなのに、パーミッションが欠けているといわれてしまう。

最終的にこの問題は、Android 14では Jetpack SDK を使わないことで解決した。

API34から追加されているandroid.health.connectパッケージのみを利用することで以下のように歩数を取得することができた。
一部自作 extension(日付まわり)を利用している箇所もあるが、だいたい以下のような感じで、Builderを通して各リクエスト等を作成していく。

val manager = context.getSystemService(HEALTHCONNECT_SERVICE) as HealthConnectManager
val now = ZonedDateTime.now()
val startTime = now.startOfDay.toInstant()
val endTime = now.endOfDay.toInstant()
val timeRangeFilter = TimeInstantRangeFilter.Builder()
    .setStartTime(startTime)
    .setEndTime(endTime)
    .build()
val aggregationType = android.health.connect.datatypes.StepsRecord.STEPS_COUNT_TOTAL
val request = AggregateRecordsRequest.Builder<Long>(timeRangeFilter)
    .addAggregationType(aggregationType)
    .build()
val executor = Executors.newSingleThreadExecutor()
manager.aggregate(request, executor) { response ->
    val totalStepsCount = response.get(STEPS_COUNT_TOTAL)
    println(">> 歩数 ${totalStepsCount}")
}

パッケージ名を含め、変数等を Jetpack SDK とかなり近く、非常にやりにくい…

しかし、android.health.connectパッケージはAndroid 14未満では利用できないため、まだまだ条件分けをして実装する必要があるだろう…

まとめ

Android 14未満は androidx.health.connect(Jetpack SDK)
Android 14以上は android.health.connect(API Level 34からAndroid Platformに追加)

現状は上記のリファレンスぐらいしか参考がないが、いずれandroid.health.connectの方もサンプルコードやドキュメントが増えてくれるはずだ。
また、Jetpack SDK で動くっぽいことは書いてあるので、自分の環境ややり方が悪いだけの説もあると思う。

手探りなのでまだ正しい方法とも思えないが、同様に困っている人の役に立てれば幸いだ。