Skip to content

Commit

Permalink
feat: android 14 permission support (#60)
Browse files Browse the repository at this point in the history
* feat: android 14 permission support

BREAKING CHANGE: You will need to make some changes to your `MainActivity.kt` file

* fix: force unwrap

* chore: upgrade example to 0.73.3

* chore: update docs

* chore: update docs
  • Loading branch information
matinzd authored Feb 22, 2024
1 parent 5755396 commit 1dbe8f3
Show file tree
Hide file tree
Showing 12 changed files with 442 additions and 497 deletions.
39 changes: 36 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,46 @@ This library is a wrapper around Health Connect for react native. Health Connect

## Installation

Install `react-native-health-connect` by running:
To install react-native-health-connect, use the following command:

```bash
yarn add react-native-health-connect@latest
yarn add react-native-health-connect
```

Since this module is Android-only, you do not need to run `pod install`.
For version 2 onwards, please add the following code into your `MainActivity.kt` within the `onCreate` method:

```diff
package com.healthconnectexample

+ import android.os.Bundle
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
+ import dev.matinzd.healthconnect.permissions.HealthConnectPermissionDelegate

class MainActivity : ReactActivity() {
/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
*/
override fun getMainComponentName(): String = "HealthConnectExample"

+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ // In order to handle permission contract results, we need to set the permission delegate.
+ HealthConnectPermissionDelegate.setPermissionDelegate(this)
+ }

/**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
*/
override fun createReactActivityDelegate(): ReactActivityDelegate =
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
}

```


## Expo installation
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package dev.matinzd.healthconnect

import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.health.connect.client.HealthConnectClient
import com.facebook.react.bridge.ActivityEventListener
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import dev.matinzd.healthconnect.permissions.HCPermissionManager
import dev.matinzd.healthconnect.permissions.HealthConnectPermissionDelegate
import dev.matinzd.healthconnect.permissions.PermissionUtils
import dev.matinzd.healthconnect.records.ReactHealthRecord
import dev.matinzd.healthconnect.utils.ClientNotInitialized
import dev.matinzd.healthconnect.utils.getTimeRangeFilter
Expand All @@ -19,12 +17,9 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class HealthConnectManager(private val applicationContext: ReactApplicationContext) :
ActivityEventListener {
class HealthConnectManager(private val applicationContext: ReactApplicationContext) {
private lateinit var healthConnectClient: HealthConnectClient
private val coroutineScope = CoroutineScope(Dispatchers.IO)
private var pendingPromise: Promise? = null
private var latestPermissions: Set<String>? = null

private val isInitialized get() = this::healthConnectClient.isInitialized

Expand All @@ -35,16 +30,6 @@ class HealthConnectManager(private val applicationContext: ReactApplicationConte
block()
}

override fun onActivityResult(
activity: Activity?, requestCode: Int, resultCode: Int, intent: Intent?
) {
if (requestCode == REQUEST_CODE) {
HCPermissionManager.parseOnActivityResult(resultCode, intent, pendingPromise)
}
}

override fun onNewIntent(intent: Intent?) {}

fun openHealthConnectSettings() {
val intent = Intent(HealthConnectClient.ACTION_HEALTH_CONNECT_SETTINGS)
applicationContext.currentActivity?.startActivity(intent)
Expand Down Expand Up @@ -75,22 +60,10 @@ class HealthConnectManager(private val applicationContext: ReactApplicationConte
reactPermissions: ReadableArray, providerPackageName: String, promise: Promise
) {
throwUnlessClientIsAvailable(promise) {
this.pendingPromise = promise
this.latestPermissions = HCPermissionManager.parsePermissions(reactPermissions)

val bundle = Bundle().apply {
putString("providerPackageName", providerPackageName)
coroutineScope.launch {
val granted = HealthConnectPermissionDelegate.launch(PermissionUtils.parsePermissions(reactPermissions))
promise.resolve(PermissionUtils.mapPermissionResult(granted))
}

val intent = HCPermissionManager(providerPackageName).healthPermissionContract.createIntent(
applicationContext, latestPermissions!!
)

applicationContext.currentActivity?.startActivityForResult(
intent,
HealthConnectManager.REQUEST_CODE,
bundle
)
}
}

Expand All @@ -105,7 +78,7 @@ class HealthConnectManager(private val applicationContext: ReactApplicationConte
fun getGrantedPermissions(promise: Promise) {
throwUnlessClientIsAvailable(promise) {
coroutineScope.launch {
promise.resolve(HCPermissionManager.getGrantedPermissions(healthConnectClient.permissionController))
promise.resolve(PermissionUtils.getGrantedPermissions(healthConnectClient.permissionController))
}
}
}
Expand Down Expand Up @@ -205,13 +178,5 @@ class HealthConnectManager(private val applicationContext: ReactApplicationConte
}
}
}

companion object {
const val REQUEST_CODE = 4235
}

init {
applicationContext.addActivityEventListener(this)
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package dev.matinzd.healthconnect.permissions

import androidx.activity.result.ActivityResultLauncher
import androidx.health.connect.client.PermissionController
import com.facebook.react.ReactActivity
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)

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

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

suspend fun launch(permissions: Set<String>): Set<String> {
requestPermission.launch(permissions)
return channel.receive()
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
package dev.matinzd.healthconnect.permissions

import android.content.Intent
import androidx.health.connect.client.PermissionController
import androidx.health.connect.client.permission.HealthPermission
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.WritableNativeArray
import com.facebook.react.bridge.WritableNativeMap
import dev.matinzd.healthconnect.utils.InvalidRecordType
import dev.matinzd.healthconnect.utils.reactRecordTypeToClassMap

class HCPermissionManager(providerPackageName: String) {
val healthPermissionContract =
PermissionController.createRequestPermissionResultContract(providerPackageName)

class PermissionUtils {
companion object {
private const val DEFAULT_PROVIDER_PACKAGE_NAME = "com.google.android.apps.healthdata"

fun parsePermissions(reactPermissions: ReadableArray): Set<String> {
return reactPermissions.toArrayList().mapNotNull {
it as HashMap<*, *>
Expand All @@ -32,27 +25,11 @@ class HCPermissionManager(providerPackageName: String) {
}.toSet()
}

fun parseOnActivityResult(
resultCode: Int,
intent: Intent?,
pendingPromise: Promise?
) {
val providerPackageName =
intent?.getStringExtra("providerPackageName") ?: DEFAULT_PROVIDER_PACKAGE_NAME
val contract = HCPermissionManager(providerPackageName).healthPermissionContract
val result = contract.parseResult(
resultCode,
intent
)

pendingPromise?.resolve(mapPermissionResult(result))
}

suspend fun getGrantedPermissions(permissionController: PermissionController): WritableNativeArray {
return mapPermissionResult(permissionController.getGrantedPermissions())
}

private fun mapPermissionResult(grantedPermissions: Set<String>): WritableNativeArray {
fun mapPermissionResult(grantedPermissions: Set<String>): WritableNativeArray {
return WritableNativeArray().apply {
grantedPermissions.forEach {
val map = WritableNativeMap()
Expand Down
42 changes: 39 additions & 3 deletions docs/docs/get-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,47 @@ Health Connect requires the user to have screen lock enabled with a PIN, pattern

## Installation

1. Install react-native-health-connect by running:
To install react-native-health-connect, use the following command:

```bash
yarn add react-native-health-connect@latest
yarn add react-native-health-connect
```

For version 2 onwards, please add the following code into your `MainActivity.kt` within the `onCreate` method:

```diff
package com.healthconnectexample

+ import android.os.Bundle
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
+ import dev.matinzd.healthconnect.permissions.HealthConnectPermissionDelegate

class MainActivity : ReactActivity() {
/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
*/
override fun getMainComponentName(): String = "HealthConnectExample"

+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ // In order to handle permission contract results, we need to set the permission delegate.
+ HealthConnectPermissionDelegate.setPermissionDelegate(this)
+ }

/**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
*/
override fun createReactActivityDelegate(): ReactActivityDelegate =
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
}

```
Since this module is Android-only, you do not need to run `pod install`.


## Expo installation

Expand Down
20 changes: 20 additions & 0 deletions example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,29 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<!-- 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>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
package com.healthconnectexample

import android.os.Bundle
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
import dev.matinzd.healthconnect.permissions.HealthConnectPermissionDelegate

class MainActivity : ReactActivity() {

/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
*/
override fun getMainComponentName(): String = "HealthConnectExample"

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// In order to handle contract results, we need to set the permission delegate.
HealthConnectPermissionDelegate.setPermissionDelegate(this)
}

/**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
*/
override fun createReactActivityDelegate(): ReactActivityDelegate =
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
}
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ class MainApplication : Application(), ReactApplication {

override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> {
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
return PackageList(this).packages
}
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
}

override fun getJSMainModuleName(): String = "index"

Expand All @@ -40,4 +40,4 @@ class MainApplication : Application(), ReactApplication {
load()
}
}
}
}
Loading

0 comments on commit 1dbe8f3

Please sign in to comment.