Skip to content

Commit

Permalink
VIT-6034: Gracefully handle missing permission when generating Health…
Browse files Browse the repository at this point in the history
… Connect changes token (#102)
  • Loading branch information
andersio authored Mar 20, 2024
1 parent 85e5879 commit cd313a7
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ class DefaultSyncNotificationBuilder(
}

private fun createChannel(context: Context, content: DefaultSyncNotificationContent): String {
val importance = NotificationManager.IMPORTANCE_MIN
// We cannot use IMPORTANT_MIN for FGS, or else Android will coerce it to IMPORTANT_HIGH
val importance = NotificationManager.IMPORTANCE_LOW
val mChannel = NotificationChannel("VitalHealthConnectSync", content.channelName, importance)
mChannel.description = content.channelDescription
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ object UnSecurePrefKeys {
internal const val nextAlarmAtKey = "nextAlarmAt"
internal const val lastAutoSyncedAtKey = "lastAutoSyncedAt"
internal const val lastSeenWorkIdKey = "lastSeenWorkId"
internal const val typesMonitoredByChangesTokenKey = "typesMonitoredByChangesToken"
internal fun readResourceGrant(resource: VitalResource) = "resource.read.$resource"
internal fun writeResourceGrant(resource: WritableVitalResource) = "resource.write.$resource"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ fun VitalResource.recordTypeChangesToTriggerSync(): List<KClass<out Record>> = w
VitalResource.Water -> listOf(HydrationRecord::class)
VitalResource.Activity -> listOf(
ActiveCaloriesBurnedRecord::class,
TotalCaloriesBurnedRecord::class,
BasalMetabolicRateRecord::class,
StepsRecord::class,
DistanceRecord::class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package io.tryvital.vitalhealthconnect.workers
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import androidx.health.connect.client.permission.HealthPermission
import androidx.health.connect.client.records.Record
import androidx.health.connect.client.request.ChangesTokenRequest
import androidx.health.connect.client.response.ChangesResponse
import androidx.work.CoroutineWorker
Expand All @@ -16,6 +18,7 @@ import io.tryvital.client.VitalClient
import io.tryvital.client.services.data.DataStage
import io.tryvital.client.utils.VitalLogger
import io.tryvital.vitalhealthconnect.HealthConnectClientProvider
import io.tryvital.vitalhealthconnect.UnSecurePrefKeys
import io.tryvital.vitalhealthconnect.ext.toDate
import io.tryvital.vitalhealthconnect.model.VitalResource
import io.tryvital.vitalhealthconnect.model.processedresource.ProcessedResourceData
Expand All @@ -33,6 +36,7 @@ import java.time.ZonedDateTime
import java.time.temporal.ChronoUnit
import java.util.Date
import java.util.TimeZone
import kotlin.reflect.KClass

const val VITAL_SYNC_NOTIFICATION_ID = 123

Expand Down Expand Up @@ -174,6 +178,21 @@ internal class ResourceSyncWorker(appContext: Context, workerParams: WorkerParam
val userId = vitalClient.checkUserId()
val client = healthConnectClientProvider.getHealthConnectClient(applicationContext)

val recordTypesToMonitor = recordTypesToMonitor().toSimpleNameSet()
val monitoringTypes = monitoringRecordTypes()

// The types being monitored by the current `changesToken` no longer match the set
// we want to monitor, probably due to permission changes.
// Treat this as if the changesToken has expired.
if (recordTypesToMonitor != monitoringTypes) {
return genericBackfill(
stage = DataStage.Daily,
start = state.lastSync.toInstant(),
end = Instant.now(),
timeZone = timeZone,
)
}

var token = state.changesToken
var changes: ChangesResponse

Expand Down Expand Up @@ -218,11 +237,16 @@ internal class ResourceSyncWorker(appContext: Context, workerParams: WorkerParam
private suspend fun genericBackfill(stage: DataStage, start: Instant, end: Instant, timeZone: TimeZone) {
val userId = vitalClient.checkUserId()
val client = healthConnectClientProvider.getHealthConnectClient(applicationContext)
var token = client.getChangesToken(
ChangesTokenRequest(
recordTypes = input.resource.recordTypeChangesToTriggerSync().toSet(),

val recordTypesToMonitor = recordTypesToMonitor()
var token = client.getChangesToken(ChangesTokenRequest(recordTypes = recordTypesToMonitor))

sharedPreferences.edit()
.putStringSet(
UnSecurePrefKeys.typesMonitoredByChangesTokenKey,
recordTypesToMonitor.toSimpleNameSet()
)
)
.apply()

val (stageStart, stageEnd) = when (stage) {
// Historical stage must pass the same start ..< end throughout all the chunks.
Expand Down Expand Up @@ -274,6 +298,25 @@ internal class ResourceSyncWorker(appContext: Context, workerParams: WorkerParam
setIncremental(token = token)
}

private fun monitoringRecordTypes(): Set<String> {
return sharedPreferences.getStringSet(UnSecurePrefKeys.typesMonitoredByChangesTokenKey, null) ?: setOf()
}

/**
* Health Connect rejects the request if we include [Record] types we do not have permission
* for. So we need to proactively filter out [Record] types based on what read permissions we
* have at the moment.
*/
private suspend fun recordTypesToMonitor(): Set<KClass<out Record>> {
val client = healthConnectClientProvider.getHealthConnectClient(applicationContext)
val grantedPermissions = client.permissionController.getGrantedPermissions()

return input.resource.recordTypeChangesToTriggerSync()
.filterTo(mutableSetOf()) { recordType ->
HealthPermission.getReadPermission(recordType) in grantedPermissions
}
}

private fun setIncremental(token: String) {
val newState = ResourceSyncState.Incremental(token, lastSync = Date())

Expand All @@ -298,3 +341,7 @@ internal inline fun <reified T: Any> SharedPreferences.Editor.putJson(key: Strin
}

internal val VitalResource.syncStateKey get() = "sync-state.${this.name}"

// All Record types are public JVM types, so they must have a simple name.
private fun Set<KClass<out Record>>.toSimpleNameSet(): Set<String>
= mapTo(mutableSetOf()) { it.simpleName!! }

0 comments on commit cd313a7

Please sign in to comment.