Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds a new method for requesting exercise permissions #167

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import com.facebook.react.bridge.WritableNativeArray
import com.facebook.react.bridge.WritableNativeMap
import dev.matinzd.healthconnect.permissions.HealthConnectPermissionDelegate
import dev.matinzd.healthconnect.permissions.PermissionUtils
import dev.matinzd.healthconnect.records.ReactExerciseSessionRecord
import dev.matinzd.healthconnect.records.ReactHealthRecord
import dev.matinzd.healthconnect.utils.ClientNotInitialized
import dev.matinzd.healthconnect.utils.ExerciseRouteAccessDenied
import dev.matinzd.healthconnect.utils.convertChangesTokenRequestOptionsFromJS
import dev.matinzd.healthconnect.utils.getTimeRangeFilter
import dev.matinzd.healthconnect.utils.reactRecordTypeToClassMap
Expand Down Expand Up @@ -62,16 +64,34 @@ class HealthConnectManager(private val applicationContext: ReactApplicationConte
}

fun requestPermission(
reactPermissions: ReadableArray, providerPackageName: String, promise: Promise
reactPermissions: ReadableArray,
includeRoute: Boolean?,
promise: Promise
) {
throwUnlessClientIsAvailable(promise) {
coroutineScope.launch {
val granted = HealthConnectPermissionDelegate.launch(PermissionUtils.parsePermissions(reactPermissions))
val granted = HealthConnectPermissionDelegate.launchPermissionsDialog(PermissionUtils.parsePermissions(reactPermissions, includeRoute ?: false))
promise.resolve(PermissionUtils.mapPermissionResult(granted))
}
}
}

fun requestExerciseRoute(
recordId: String, promise: Promise
) {
throwUnlessClientIsAvailable(promise) {
coroutineScope.launch {
val exerciseRoute = HealthConnectPermissionDelegate.launchExerciseRouteAccessRequestDialog(recordId)
if (exerciseRoute != null) {
promise.resolve(ReactExerciseSessionRecord.parseExerciseRoute(exerciseRoute))
}
else{
promise.rejectWithException(ExerciseRouteAccessDenied())
}
}
Comment on lines +84 to +91
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm getting some flaky results with exercises, everything returns permissions denied as in exerciseRoute is null and as is there is no way to know for certain if that's because there is no data or something is wrong with my patch/permissions etc. Maybe we should fetch the exercise record here and switch on ExerciseRouteResult to determine whether to request permission or reject with "NoDataException". Could go hand in hand with this.

}
}

fun revokeAllPermissions(promise: Promise) {
throwUnlessClientIsAvailable(promise) {
coroutineScope.launch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,21 @@ class HealthConnectModule internal constructor(context: ReactApplicationContext)
@ReactMethod
override fun requestPermission(
permissions: ReadableArray,
providerPackageName: String,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't used so replaced it.

includeRoute: Boolean?,
promise: Promise
) {
return manager.requestPermission(permissions, providerPackageName, promise)
return manager.requestPermission(permissions, includeRoute, promise)
}

@ReactMethod
override fun requestExerciseRoute(
recordId: String,
promise: Promise
) {
return manager.requestExerciseRoute(recordId, promise)
}


@ReactMethod
override fun getGrantedPermissions(promise: Promise) {
return manager.getGrantedPermissions(promise)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,51 @@ package dev.matinzd.healthconnect.permissions
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResultLauncher
import androidx.health.connect.client.PermissionController
import androidx.health.connect.client.contracts.ExerciseRouteRequestContract
import androidx.health.connect.client.records.ExerciseRoute
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch

object HealthConnectPermissionDelegate {
private lateinit var requestPermission: ActivityResultLauncher<Set<String>>
private val channel = Channel<Set<String>>()
private val coroutineScope = CoroutineScope(Dispatchers.IO)
private val permissionsChannel = Channel<Set<String>>()
private val exerciseRouteChannel = Channel<ExerciseRoute?>()

private lateinit var requestPermission: ActivityResultLauncher<Set<String>>
private lateinit var requestRoutePermission: ActivityResultLauncher<String>

fun setPermissionDelegate(
activity: ComponentActivity,
providerPackageName: String = "com.google.android.apps.healthdata"
) {
val contract = PermissionController.createRequestPermissionResultContract(providerPackageName)
val exerciseRouteRequestContract = ExerciseRouteRequestContract()

requestPermission = activity.registerForActivityResult(contract) {
coroutineScope.launch {
channel.send(it)
permissionsChannel.send(it)
coroutineContext.cancel()
}
}

requestRoutePermission = activity.registerForActivityResult(exerciseRouteRequestContract) {
coroutineScope.launch {
exerciseRouteChannel.send(it)
coroutineContext.cancel()
}
}
}

suspend fun launch(permissions: Set<String>): Set<String> {
suspend fun launchPermissionsDialog(permissions: Set<String>): Set<String> {
requestPermission.launch(permissions)
return channel.receive()
return permissionsChannel.receive()
}

suspend fun launchExerciseRouteAccessRequestDialog(recordId: String): ExerciseRoute? {
requestRoutePermission.launch(recordId)
return exerciseRouteChannel.receive()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ package dev.matinzd.healthconnect.permissions

import androidx.health.connect.client.PermissionController
import androidx.health.connect.client.permission.HealthPermission
import androidx.health.connect.client.records.ExerciseSessionRecord
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.WritableNativeArray
import dev.matinzd.healthconnect.utils.InvalidRecordType
import dev.matinzd.healthconnect.utils.reactRecordTypeToClassMap

class PermissionUtils {
companion object {
fun parsePermissions(reactPermissions: ReadableArray): Set<String> {
return reactPermissions.toArrayList().mapNotNull {
fun parsePermissions(reactPermissions: ReadableArray, includeExerciseRoute: Boolean): Set<String> {
val setOfPermissions = reactPermissions.toArrayList().mapNotNull {
it as HashMap<*, *>
val recordType = it["recordType"]
val recordClass = reactRecordTypeToClassMap[recordType]
Expand All @@ -22,6 +23,14 @@ class PermissionUtils {
else -> null
}
}.toSet()

val containsExercise = setOfPermissions.contains(HealthPermission.getWritePermission(ExerciseSessionRecord::class))

return if (containsExercise && includeExerciseRoute) {
setOfPermissions.plus(HealthPermission.PERMISSION_WRITE_EXERCISE_ROUTE)
} else {
setOfPermissions
}
}

suspend fun getGrantedPermissions(permissionController: PermissionController): WritableNativeArray {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,19 +103,8 @@ class ReactExerciseSessionRecord : ReactHealthRecordImpl<ExerciseSessionRecord>

when (record.exerciseRouteResult) {
is ExerciseRouteResult.Data -> {
val exerciseRouteMap = WritableNativeMap()
exerciseRouteMap.putArray("route", WritableNativeArray().apply {
(record.exerciseRouteResult as ExerciseRouteResult.Data).exerciseRoute.route.map {
val map = WritableNativeMap()
map.putString("time", it.time.toString())
map.putDouble("latitude", it.latitude)
map.putDouble("longitude", it.longitude)
map.putMap("horizontalAccuracy", lengthToJsMap(it.horizontalAccuracy))
map.putMap("verticalAccuracy", lengthToJsMap(it.verticalAccuracy))
map.putMap("altitude", lengthToJsMap(it.altitude))
this.pushMap(map)
}
})
val exerciseRoute: ExerciseRoute = (record.exerciseRouteResult as ExerciseRouteResult.Data).exerciseRoute
val exerciseRouteMap = parseExerciseRoute(exerciseRoute)
putMap("exerciseRoute", exerciseRouteMap)
}

Expand Down Expand Up @@ -178,4 +167,23 @@ class ReactExerciseSessionRecord : ReactHealthRecordImpl<ExerciseSessionRecord>
}
}
}

companion object {
fun parseExerciseRoute(exerciseRoute: ExerciseRoute): WritableNativeMap {
return WritableNativeMap().apply {
putArray("route", WritableNativeArray().apply {
exerciseRoute.route.map {
val map = WritableNativeMap()
map.putString("time", it.time.toString())
map.putDouble("latitude", it.latitude)
map.putDouble("longitude", it.longitude)
map.putMap("horizontalAccuracy", lengthToJsMap(it.horizontalAccuracy))
map.putMap("verticalAccuracy", lengthToJsMap(it.verticalAccuracy))
map.putMap("altitude", lengthToJsMap(it.altitude))
this.pushMap(map)
}
})
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.os.RemoteException
import com.facebook.react.bridge.Promise
import okio.IOException

class ExerciseRouteAccessDenied : Exception("Request to access exercise route denied")
class ClientNotInitialized : Exception("Health Connect client is not initialized")
class InvalidRecordType : Exception("Record type is not valid")
class InvalidTemperature : Exception("Temperature is not valid")
Expand Down
5 changes: 4 additions & 1 deletion android/src/oldarch/HealthConnectSpec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ abstract class HealthConnectSpec internal constructor(context: ReactApplicationC
abstract fun openHealthConnectDataManagement(providerPackageName: String?);

@ReactMethod
abstract fun requestPermission(permissions: ReadableArray, providerPackageName: String, promise: Promise);
abstract fun requestPermission(permissions: ReadableArray, includeRoute: Boolean?, promise: Promise);

@ReactMethod
abstract fun requestExerciseRoute(recordId: String, promise: Promise);

@ReactMethod
abstract fun getGrantedPermissions(promise: Promise);
Expand Down
2 changes: 2 additions & 0 deletions example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<uses-permission android:name="android.permission.health.WRITE_STEPS" />
<uses-permission android:name="android.permission.health.READ_EXERCISE"/>
<uses-permission android:name="android.permission.health.WRITE_EXERCISE"/>
<uses-permission android:name="android.permission.health.READ_EXERCISE_ROUTES"/>
<uses-permission android:name="android.permission.health.WRITE_EXERCISE_ROUTE"/>

<application
android:name=".MainApplication"
Expand Down
Loading
Loading