1 /**
2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * ```
8 * http://www.apache.org/licenses/LICENSE-2.0
9 * ```
10 *
11 * Unless required by applicable law or agreed to in writing, software distributed under the License
12 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
13 * or implied. See the License for the specific language governing permissions and limitations under
14 * the License.
15 */
16 package com.android.healthconnect.testapps.toolbox
17
18 import android.content.BroadcastReceiver
19 import android.content.Context
20 import android.content.Intent
21 import android.health.connect.HealthConnectManager
22 import android.health.connect.ReadRecordsRequestUsingFilters
23 import android.health.connect.TimeInstantRangeFilter
24 import android.health.connect.datatypes.DataOrigin
25 import android.health.connect.datatypes.Record
26 import androidx.work.ListenableWorker
27 import androidx.work.OneTimeWorkRequestBuilder
28 import androidx.work.OutOfQuotaPolicy
29 import androidx.work.WorkManager
30 import androidx.work.WorkerParameters
31 import androidx.work.workDataOf
32 import com.android.healthconnect.testapps.toolbox.utils.GeneralUtils.Companion.readRecords
33 import com.android.healthconnect.testapps.toolbox.utils.GeneralUtils.Companion.requireByteArrayExtra
34 import com.android.healthconnect.testapps.toolbox.utils.GeneralUtils.Companion.requireSerializable
35 import com.android.healthconnect.testapps.toolbox.utils.GeneralUtils.Companion.requireSystemService
36 import com.android.healthconnect.testapps.toolbox.utils.asString
37 import com.android.healthconnect.testapps.toolbox.utils.deserialize
38 import com.android.healthconnect.testapps.toolbox.utils.launchFuture
39 import com.android.healthconnect.testapps.toolbox.utils.serialize
40 import com.google.common.util.concurrent.ListenableFuture
41 import kotlinx.coroutines.flow.MutableSharedFlow
42 import kotlinx.coroutines.flow.first
43 import kotlinx.coroutines.withTimeoutOrNull
44 import java.io.Serializable
45 import java.time.Instant
46 import java.util.UUID
47 import kotlin.time.Duration.Companion.seconds
48
49 /**
50 * Calls the [ToolboxProxyReceiver] of the Toolbox app specified by the [packageName] argument
51 * and returns a response.
52 **/
callToolboxnull53 suspend fun callToolbox(
54 context: Context,
55 packageName: String,
56 payload: ToolboxProxyPayload,
57 ): String? {
58 val requestId = UUID.randomUUID().toString()
59
60 context.sendToolboxRequest(
61 toolboxPackageName = packageName,
62 request = ToolboxProxyRequest(
63 id = requestId,
64 callerPackageName = context.packageName,
65 payload = payload,
66 ),
67 )
68
69 return receiveToolboxResponse(requestId = requestId)
70 }
71
72 /** A payload to be sent to [ToolboxProxyReceiver] as part of a request. */
73 sealed interface ToolboxProxyPayload : Serializable {
74
75 /**
76 * Reads records of the specified [type] contributed by apps specified by the [packageNames]
77 * list, or by all apps if the [packageNames] list is empty.
78 */
79 data class ReadRecords(
80 val type: Class<out Record>,
81 val packageNames: List<String> = emptyList(),
82 ) : ToolboxProxyPayload
83 }
84
85 private data class ToolboxProxyRequest(
86 val id: String,
87 val callerPackageName: String,
88 val payload: ToolboxProxyPayload,
89 ) : Serializable
90
91 private data class ToolboxProxyResponse(
92 val requestId: String,
93 val response: String,
94 ) : Serializable
95
Contextnull96 private fun Context.sendToolboxRequest(toolboxPackageName: String, request: ToolboxProxyRequest) {
97 sendBroadcast(
98 Intent(ACTION_REQUEST)
99 .setClassName(toolboxPackageName, ToolboxProxyReceiver::class.java.name)
100 .putExtra(EXTRA_REQUEST, request.serialize())
101 )
102 }
103
Contextnull104 private fun Context.sendToolboxResponse(
105 toolboxPackageName: String,
106 response: ToolboxProxyResponse,
107 ) {
108 sendBroadcast(
109 Intent(ACTION_RESPONSE)
110 .setClassName(toolboxPackageName, ToolboxProxyReceiver::class.java.name)
111 .putExtra(EXTRA_RESPONSE, response)
112 )
113 }
114
115 const val PKG_TOOLBOX_2: String = "com.android.healthconnect.testapps.toolbox2"
116
117 private const val ACTION_REQUEST = "action.REQUEST"
118 private const val ACTION_RESPONSE = "action.RESPONSE"
119 private const val EXTRA_RESPONSE = "extra.RESPONSE"
120 private const val EXTRA_REQUEST = "extra.REQUEST"
121
122 private val responseFlow =
123 MutableSharedFlow<ToolboxProxyResponse>(extraBufferCapacity = Int.MAX_VALUE)
124
receiveToolboxResponsenull125 private suspend fun receiveToolboxResponse(requestId: String): String? =
126 withTimeoutOrNull(timeout = 5.seconds) {
127 responseFlow.first { it.requestId == requestId }.response
128 }
129
130 class ToolboxProxyReceiver : BroadcastReceiver() {
onReceivenull131 override fun onReceive(context: Context, intent: Intent) {
132 when (intent.action) {
133 ACTION_REQUEST -> {
134 enqueueWorker(
135 context = context,
136 requestBytes = intent.requireByteArrayExtra(EXTRA_REQUEST),
137 )
138 }
139
140 ACTION_RESPONSE -> {
141 responseFlow.tryEmit(
142 intent.requireSerializable<ToolboxProxyResponse>(EXTRA_RESPONSE)
143 )
144 }
145 }
146 }
147
enqueueWorkernull148 private fun enqueueWorker(context: Context, requestBytes: ByteArray) {
149 WorkManager.getInstance(context).enqueue(
150 OneTimeWorkRequestBuilder<ToolboxProxyWorker>()
151 .setInputData(workDataOf(EXTRA_REQUEST to requestBytes))
152 .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
153 .build()
154 )
155 }
156 }
157
158 class ToolboxProxyWorker(
159 private val context: Context,
160 params: WorkerParameters,
161 ) : ListenableWorker(context, params) {
162
163 private val manager = context.requireSystemService<HealthConnectManager>()
164
165 private val request =
166 requireNotNull(inputData.getByteArray(EXTRA_REQUEST)).deserialize<ToolboxProxyRequest>()
167
startWorknull168 override fun startWork(): ListenableFuture<Result> =
169 launchFuture {
170 try {
171 sendResponse(doWork())
172 Result.success()
173 } catch (e: Exception) {
174 sendResponse(e.toString())
175 Result.failure()
176 }
177 }
178
doWorknull179 private suspend fun doWork(): String =
180 when (val payload = request.payload) {
181 is ToolboxProxyPayload.ReadRecords -> readRecords(payload)
182 }
183
sendResponsenull184 private fun sendResponse(response: String) {
185 context.sendToolboxResponse(
186 toolboxPackageName = request.callerPackageName,
187 response = ToolboxProxyResponse(requestId = request.id, response = response),
188 )
189 }
190
readRecordsnull191 private suspend inline fun readRecords(payload: ToolboxProxyPayload.ReadRecords): String =
192 readRecords(
193 manager = manager,
194 request = ReadRecordsRequestUsingFilters.Builder(payload.type)
195 .setTimeRangeFilter(
196 TimeInstantRangeFilter.Builder()
197 .setStartTime(Instant.EPOCH)
198 .setEndTime(Instant.now())
199 .build()
200 )
201 .apply {
202 for (pkg in payload.packageNames) {
203 addDataOrigins(DataOrigin.Builder().setPackageName(pkg).build())
204 }
205 }
206 .build(),
207 ).joinToString(separator = "\n", transform = Record::asString)
208 }
209