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

Merged
merged 15 commits into from
Nov 10, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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())
}
}
matinzd marked this conversation as resolved.
Show resolved Hide resolved
}
}

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,
matinzd marked this conversation as resolved.
Show resolved Hide resolved
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
matinzd marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import androidx.health.connect.client.records.ExerciseRoute
import androidx.health.connect.client.records.ExerciseRouteResult
import androidx.health.connect.client.records.ExerciseSegment
import androidx.health.connect.client.records.ExerciseSessionRecord
import androidx.health.connect.client.records.PowerRecord
import androidx.health.connect.client.request.AggregateRequest
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
Expand Down Expand Up @@ -100,19 +99,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 @@ -153,4 +141,23 @@ class ReactExerciseSessionRecord : ReactHealthRecordImpl<ExerciseSessionRecord>
putArray("dataOrigins", convertDataOriginsToJsArray(record.dataOrigins))
}
}

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
56 changes: 30 additions & 26 deletions android/src/oldarch/HealthConnectSpec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,51 @@ package dev.matinzd.healthconnect
import com.facebook.react.bridge.*

abstract class HealthConnectSpec internal constructor(context: ReactApplicationContext) :
ReactContextBaseJavaModule(context) {
ReactContextBaseJavaModule(context) {

@ReactMethod
abstract fun getSdkStatus(providerPackageName: String, promise: Promise)
@ReactMethod abstract fun getSdkStatus(providerPackageName: String, promise: Promise)

@ReactMethod
abstract fun initialize(providerPackageName: String, promise: Promise);
@ReactMethod abstract fun initialize(providerPackageName: String, promise: Promise)

@ReactMethod
abstract fun openHealthConnectSettings();
@ReactMethod abstract fun openHealthConnectSettings()

@ReactMethod
abstract fun openHealthConnectDataManagement(providerPackageName: String?);
@ReactMethod 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 getGrantedPermissions(promise: Promise);
@ReactMethod abstract fun requestExerciseRoute(recordId: String, promise: Promise)

@ReactMethod
abstract fun revokeAllPermissions(promise: Promise);
@ReactMethod abstract fun getGrantedPermissions(promise: Promise)

@ReactMethod
abstract fun insertRecords(records: ReadableArray, promise: Promise);
@ReactMethod abstract fun revokeAllPermissions(promise: Promise)

@ReactMethod
abstract fun readRecords(recordType: String, options: ReadableMap, promise: Promise);
@ReactMethod abstract fun insertRecords(records: ReadableArray, promise: Promise)

@ReactMethod
abstract fun readRecord(recordType: String, recordId: String, promise: Promise);
@ReactMethod abstract fun readRecords(recordType: String, options: ReadableMap, promise: Promise)

@ReactMethod
abstract fun aggregateRecord(record: ReadableMap, promise: Promise);
@ReactMethod abstract fun readRecord(recordType: String, recordId: String, promise: Promise)

@ReactMethod
abstract fun getChanges(options: ReadableMap, promise: Promise);
@ReactMethod abstract fun aggregateRecord(record: ReadableMap, promise: Promise)

@ReactMethod abstract fun getChanges(options: ReadableMap, promise: Promise)

@ReactMethod
abstract fun deleteRecordsByUuids(recordType: String, recordIdsList: ReadableArray, clientRecordIdsList: ReadableArray, promise: Promise);
abstract fun deleteRecordsByUuids(
recordType: String,
recordIdsList: ReadableArray,
clientRecordIdsList: ReadableArray,
promise: Promise
)

@ReactMethod
abstract fun deleteRecordsByTimeRange(recordType: String, timeRangeFilter: ReadableMap, promise: Promise);
abstract fun deleteRecordsByTimeRange(
recordType: String,
timeRangeFilter: ReadableMap,
promise: Promise
)
}
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