Skip to content

Commit 6bcf4d8

Browse files
committed
[Health 13.1.1] Late initialization error on android when calling getHealthConnectSdkStatus()
Fixes #1250
1 parent 9c8890c commit 6bcf4d8

1 file changed

Lines changed: 108 additions & 85 deletions

File tree

  • packages/health/android/src/main/kotlin/cachet/plugins/health

packages/health/android/src/main/kotlin/cachet/plugins/health/HealthPlugin.kt

Lines changed: 108 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ import io.flutter.plugin.common.PluginRegistry.ActivityResultListener
2222
import kotlinx.coroutines.*
2323

2424
/**
25-
* Main Flutter plugin class for Health Connect integration.
26-
* Manages plugin lifecycle, method channel communication, permission handling,
27-
* and coordinates between Flutter and Android Health Connect APIs.
25+
* Main Flutter plugin class for Health Connect integration. Manages plugin lifecycle, method
26+
* channel communication, permission handling, and coordinates between Flutter and Android Health
27+
* Connect APIs.
2828
*/
2929
class HealthPlugin(private var channel: MethodChannel? = null) :
30-
MethodCallHandler, ActivityResultListener, Result, ActivityAware, FlutterPlugin {
31-
30+
MethodCallHandler, ActivityResultListener, Result, ActivityAware, FlutterPlugin {
31+
3232
private var mResult: Result? = null
3333
private var handler: Handler? = null
3434
private var activity: Activity? = null
@@ -53,33 +53,32 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
5353
}
5454

5555
/**
56-
* Initializes the plugin when attached to the Flutter engine.
57-
* Sets up method channel, checks Health Connect availability, and initializes helper classes.
58-
*
56+
* Initializes the plugin when attached to the Flutter engine. Sets up method channel, checks
57+
* Health Connect availability, and initializes helper classes.
58+
*
5959
* @param flutterPluginBinding Plugin binding providing access to Flutter engine resources
6060
*/
6161
override fun onAttachedToEngine(
62-
@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding
62+
@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding
6363
) {
6464
scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
6565
channel = MethodChannel(flutterPluginBinding.binaryMessenger, CHANNEL_NAME)
6666
channel?.setMethodCallHandler(this)
6767
context = flutterPluginBinding.applicationContext
6868
handler = Handler(context!!.mainLooper)
69-
69+
7070
checkAvailability()
7171
if (healthConnectAvailable) {
72-
healthConnectClient = HealthConnectClient.getOrCreate(
73-
flutterPluginBinding.applicationContext
74-
)
72+
healthConnectClient =
73+
HealthConnectClient.getOrCreate(flutterPluginBinding.applicationContext)
7574
initializeHelpers()
7675
}
7776
}
7877

7978
/**
80-
* Cleans up resources when plugin is detached from Flutter engine.
81-
* Cancels coroutines and nullifies references to prevent memory leaks.
82-
*
79+
* Cleans up resources when plugin is detached from Flutter engine. Cancels coroutines and
80+
* nullifies references to prevent memory leaks.
81+
*
8382
* @param binding Plugin binding (unused in cleanup)
8483
*/
8584
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
@@ -97,74 +96,86 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
9796
}
9897

9998
override fun error(
100-
errorCode: String,
101-
errorMessage: String?,
102-
errorDetails: Any?,
99+
errorCode: String,
100+
errorMessage: String?,
101+
errorDetails: Any?,
103102
) {
104103
handler?.post { mResult?.error(errorCode, errorMessage, errorDetails) }
105104
}
106-
105+
107106
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
108107
return false
109108
}
110109

111110
/**
112-
* Handles method calls from Flutter and routes them to appropriate handler classes.
113-
* Central dispatcher for all Health Connect operations including permissions,
114-
* data reading, writing, and deletion.
115-
*
111+
* Handles method calls from Flutter and routes them to appropriate handler classes. Central
112+
* dispatcher for all Health Connect operations including permissions, data reading, writing,
113+
* and deletion.
114+
*
116115
* @param call Method call from Flutter containing method name and arguments
117116
* @param result Result callback to return data or status to Flutter
118117
*/
119118
override fun onMethodCall(call: MethodCall, result: Result) {
120119
when (call.method) {
121120
// SDK and Installation
122121
"installHealthConnect" -> installHealthConnect(call, result)
123-
"getHealthConnectSdkStatus" -> dataOperations.getHealthConnectSdkStatus(call, result)
124-
122+
"getHealthConnectSdkStatus" -> {
123+
checkAvailability()
124+
if (healthConnectAvailable && !(this::dataOperations.isInitialized)) {
125+
healthConnectClient = HealthConnectClient.getOrCreate(context!!)
126+
initializeHelpers()
127+
}
128+
result.success(healthConnectStatus)
129+
}
130+
125131
// Permissions
126132
"hasPermissions" -> dataOperations.hasPermissions(call, result)
127133
"requestAuthorization" -> requestAuthorization(call, result)
128134
"revokePermissions" -> dataOperations.revokePermissions(call, result)
129-
135+
130136
// History permissions
131-
"isHealthDataHistoryAvailable" -> dataOperations.isHealthDataHistoryAvailable(call, result)
132-
"isHealthDataHistoryAuthorized" -> dataOperations.isHealthDataHistoryAuthorized(call, result)
133-
"requestHealthDataHistoryAuthorization" -> requestHealthDataHistoryAuthorization(call, result)
134-
137+
"isHealthDataHistoryAvailable" ->
138+
dataOperations.isHealthDataHistoryAvailable(call, result)
139+
"isHealthDataHistoryAuthorized" ->
140+
dataOperations.isHealthDataHistoryAuthorized(call, result)
141+
"requestHealthDataHistoryAuthorization" ->
142+
requestHealthDataHistoryAuthorization(call, result)
143+
135144
// Background permissions
136-
"isHealthDataInBackgroundAvailable" -> dataOperations.isHealthDataInBackgroundAvailable(call, result)
137-
"isHealthDataInBackgroundAuthorized" -> dataOperations.isHealthDataInBackgroundAuthorized(call, result)
138-
"requestHealthDataInBackgroundAuthorization" -> requestHealthDataInBackgroundAuthorization(call, result)
139-
145+
"isHealthDataInBackgroundAvailable" ->
146+
dataOperations.isHealthDataInBackgroundAvailable(call, result)
147+
"isHealthDataInBackgroundAuthorized" ->
148+
dataOperations.isHealthDataInBackgroundAuthorized(call, result)
149+
"requestHealthDataInBackgroundAuthorization" ->
150+
requestHealthDataInBackgroundAuthorization(call, result)
151+
140152
// Reading data
141153
"getData" -> dataReader.getData(call, result)
142154
"getIntervalData" -> dataReader.getIntervalData(call, result)
143155
"getAggregateData" -> dataReader.getAggregateData(call, result)
144156
"getTotalStepsInInterval" -> dataReader.getTotalStepsInInterval(call, result)
145-
157+
146158
// Writing data
147159
"writeData" -> dataWriter.writeData(call, result)
148160
"writeWorkoutData" -> dataWriter.writeWorkoutData(call, result)
149161
"writeBloodPressure" -> dataWriter.writeBloodPressure(call, result)
150162
"writeBloodOxygen" -> dataWriter.writeBloodOxygen(call, result)
151163
"writeMenstruationFlow" -> dataWriter.writeMenstruationFlow(call, result)
152164
"writeMeal" -> dataWriter.writeMeal(call, result)
153-
// TODO: Add support for multiple speed for iOS as well
165+
// TODO: Add support for multiple speed for iOS as well
154166
// "writeMultipleSpeed" -> dataWriter.writeMultipleSpeedData(call, result)
155-
167+
156168
// Deleting data
157169
"delete" -> dataOperations.deleteData(call, result)
158170
"deleteByUUID" -> dataOperations.deleteByUUID(call, result)
159-
160171
else -> result.notImplemented()
161172
}
162173
}
163174

164175
/**
165-
* Called when activity is attached to the plugin.
166-
* Sets up permission request launcher and activity result handling.
167-
*
176+
* Called when activity is attached to the plugin. Sets up permission request launcher and
177+
* activity result handling.
178+
*
168179
* @param binding Activity plugin binding providing activity context
169180
*/
170181
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
@@ -175,14 +186,14 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
175186
activity = binding.activity
176187

177188
val requestPermissionActivityContract =
178-
PermissionController.createRequestPermissionResultContract()
189+
PermissionController.createRequestPermissionResultContract()
179190

180191
healthConnectRequestPermissionsLauncher =
181-
(activity as ComponentActivity).registerForActivityResult(
182-
requestPermissionActivityContract
183-
) { granted -> onHealthConnectPermissionCallback(granted) }
192+
(activity as ComponentActivity).registerForActivityResult(
193+
requestPermissionActivityContract
194+
) { granted -> onHealthConnectPermissionCallback(granted) }
184195
}
185-
196+
186197
override fun onDetachedFromActivityForConfigChanges() {
187198
onDetachedFromActivity()
188199
}
@@ -192,8 +203,8 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
192203
}
193204

194205
/**
195-
* Called when activity is detached from plugin.
196-
* Cleans up activity-specific resources and permission launchers.
206+
* Called when activity is detached from plugin. Cleans up activity-specific resources and
207+
* permission launchers.
197208
*/
198209
override fun onDetachedFromActivity() {
199210
if (channel == null) {
@@ -204,71 +215,83 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
204215
}
205216

206217
/**
207-
* Checks Health Connect availability and SDK status on the current device.
208-
* Determines if Health Connect is installed and accessible.
218+
* Checks Health Connect availability and SDK status on the current device. Determines if Health
219+
* Connect is installed and accessible.
209220
*/
210221
private fun checkAvailability() {
211222
healthConnectStatus = HealthConnectClient.getSdkStatus(context!!)
212223
healthConnectAvailable = healthConnectStatus == HealthConnectClient.SDK_AVAILABLE
213224
}
214225

215226
/**
216-
* Initializes helper classes for data operations after Health Connect client is ready.
217-
* Creates instances of reader, writer, operations, and converter classes.
227+
* Initializes helper classes for data operations after Health Connect client is ready. Creates
228+
* instances of reader, writer, operations, and converter classes.
218229
*/
219230
private fun initializeHelpers() {
220231
dataConverter = HealthDataConverter()
221232
dataReader = HealthDataReader(healthConnectClient, scope, context!!, dataConverter)
222233
dataWriter = HealthDataWriter(healthConnectClient, scope)
223-
dataOperations = HealthDataOperations(healthConnectClient, scope, healthConnectStatus, healthConnectAvailable)
234+
dataOperations =
235+
HealthDataOperations(
236+
healthConnectClient,
237+
scope,
238+
healthConnectStatus,
239+
healthConnectAvailable
240+
)
224241
}
225242

226243
/**
227-
* Launches Health Connect installation flow via Google Play Store.
228-
* Directs users to install Health Connect when it's not available.
229-
*
244+
* Launches Health Connect installation flow via Google Play Store. Directs users to install
245+
* Health Connect when it's not available.
246+
*
230247
* @param call Method call from Flutter (unused)
231248
* @param result Flutter result callback
232249
*/
233250
private fun installHealthConnect(call: MethodCall, result: Result) {
234251
val uriString =
235-
"market://details?id=com.google.android.apps.healthdata&url=healthconnect%3A%2F%2Fonboarding"
252+
"market://details?id=com.google.android.apps.healthdata&url=healthconnect%3A%2F%2Fonboarding"
236253
context!!.startActivity(
237-
Intent(Intent.ACTION_VIEW).apply {
238-
setPackage("com.android.vending")
239-
data = android.net.Uri.parse(uriString)
240-
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
241-
putExtra("overlay", true)
242-
putExtra("callerId", context!!.packageName)
243-
}
254+
Intent(Intent.ACTION_VIEW).apply {
255+
setPackage("com.android.vending")
256+
data = android.net.Uri.parse(uriString)
257+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
258+
putExtra("overlay", true)
259+
putExtra("callerId", context!!.packageName)
260+
}
244261
)
245262
result.success(null)
246263
}
247264

248265
/**
249-
* Handles permission request results from Health Connect permission dialog.
250-
* Called when user responds to permission request, updates Flutter with result.
251-
*
266+
* Handles permission request results from Health Connect permission dialog. Called when user
267+
* responds to permission request, updates Flutter with result.
268+
*
252269
* @param permissionGranted Set of permission strings that were granted
253270
*/
254271
private fun onHealthConnectPermissionCallback(permissionGranted: Set<String>) {
255272
if (!isReplySubmitted) {
256273
if (permissionGranted.isEmpty()) {
257274
mResult?.success(false)
258-
Log.i("FLUTTER_HEALTH", "Health Connect permissions were not granted! Make sure to declare the required permissions in the AndroidManifest.xml file.")
275+
Log.i(
276+
"FLUTTER_HEALTH",
277+
"Health Connect permissions were not granted! Make sure to declare the required permissions in the AndroidManifest.xml file."
278+
)
259279
} else {
260280
mResult?.success(true)
261-
Log.i("FLUTTER_HEALTH", "${permissionGranted.size} Health Connect permissions were granted!")
262-
Log.i("FLUTTER_HEALTH", "Permissions granted: $permissionGranted")
281+
Log.i(
282+
"FLUTTER_HEALTH",
283+
"${permissionGranted.size} Health Connect permissions were granted!"
284+
)
285+
Log.i("FLUTTER_HEALTH", "Permissions granted: $permissionGranted")
263286
}
264287
isReplySubmitted = true
265288
}
266289
}
267290

268291
/**
269-
* Initiates Health Connect permission request flow.
270-
* Prepares permission list and launches system permission dialog.
271-
*
292+
* Initiates Health Connect permission request flow. Prepares permission list and launches
293+
* system permission dialog.
294+
*
272295
* @param call Method call containing permission types and access levels
273296
* @param result Flutter result callback for permission request outcome
274297
*/
@@ -287,20 +310,20 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
287310
// Store the result to be called in onHealthConnectPermissionCallback
288311
mResult = result
289312
isReplySubmitted = false
290-
313+
291314
val permList = dataOperations.preparePermissionsList(call)
292315
if (permList == null) {
293316
result.success(false)
294317
return
295318
}
296-
319+
297320
healthConnectRequestPermissionsLauncher!!.launch(permList.toSet())
298321
}
299322

300323
/**
301-
* Requests specific permission for accessing health data history.
302-
* Launches permission dialog for historical data access capability.
303-
*
324+
* Requests specific permission for accessing health data history. Launches permission dialog
325+
* for historical data access capability.
326+
*
304327
* @param call Method call from Flutter (unused)
305328
* @param result Flutter result callback for permission request outcome
306329
*/
@@ -314,14 +337,14 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
314337
mResult = result
315338
isReplySubmitted = false
316339
healthConnectRequestPermissionsLauncher!!.launch(
317-
setOf(HealthPermission.PERMISSION_READ_HEALTH_DATA_HISTORY)
340+
setOf(HealthPermission.PERMISSION_READ_HEALTH_DATA_HISTORY)
318341
)
319342
}
320343

321344
/**
322-
* Requests specific permission for background health data access.
323-
* Launches permission dialog for background data reading capability.
324-
*
345+
* Requests specific permission for background health data access. Launches permission dialog
346+
* for background data reading capability.
347+
*
325348
* @param call Method call from Flutter (unused)
326349
* @param result Flutter result callback for permission request outcome
327350
*/
@@ -335,7 +358,7 @@ class HealthPlugin(private var channel: MethodChannel? = null) :
335358
mResult = result
336359
isReplySubmitted = false
337360
healthConnectRequestPermissionsLauncher!!.launch(
338-
setOf(HealthPermission.PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND)
361+
setOf(HealthPermission.PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND)
339362
)
340363
}
341-
}
364+
}

0 commit comments

Comments
 (0)