1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.healthconnect.cts.lib; 18 19 import static android.health.connect.datatypes.FhirVersion.parseFhirVersion; 20 import static android.healthconnect.cts.lib.BundleHelper.INTENT_EXCEPTION; 21 import static android.healthconnect.cts.lib.BundleHelper.KILL_SELF_REQUEST; 22 import static android.healthconnect.cts.lib.BundleHelper.QUERY_TYPE; 23 24 import static androidx.test.InstrumentationRegistry.getContext; 25 26 import android.app.Instrumentation; 27 import android.content.BroadcastReceiver; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.IntentFilter; 31 import android.health.connect.CreateMedicalDataSourceRequest; 32 import android.health.connect.DeleteMedicalResourcesRequest; 33 import android.health.connect.GetMedicalDataSourcesRequest; 34 import android.health.connect.MedicalResourceId; 35 import android.health.connect.ReadMedicalResourcesRequest; 36 import android.health.connect.ReadMedicalResourcesResponse; 37 import android.health.connect.ReadRecordsRequestUsingFilters; 38 import android.health.connect.ReadRecordsRequestUsingIds; 39 import android.health.connect.RecordIdFilter; 40 import android.health.connect.UpsertMedicalResourceRequest; 41 import android.health.connect.changelog.ChangeLogTokenRequest; 42 import android.health.connect.changelog.ChangeLogsRequest; 43 import android.health.connect.changelog.ChangeLogsResponse; 44 import android.health.connect.datatypes.MedicalDataSource; 45 import android.health.connect.datatypes.MedicalResource; 46 import android.health.connect.datatypes.Record; 47 import android.healthconnect.cts.utils.ProxyActivity; 48 import android.os.Bundle; 49 import android.util.Log; 50 51 import java.time.Instant; 52 import java.util.Arrays; 53 import java.util.Collections; 54 import java.util.List; 55 import java.util.concurrent.CountDownLatch; 56 import java.util.concurrent.TimeUnit; 57 import java.util.concurrent.TimeoutException; 58 import java.util.concurrent.atomic.AtomicReference; 59 60 /** Performs API calls to HC on behalf of test apps. */ 61 public class TestAppProxy { 62 private static final String TAG = "TestAppProxy"; 63 private static final String TEST_APP_RECEIVER_CLASS_NAME = 64 "android.healthconnect.cts.testhelper.TestAppReceiver"; 65 private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(55); 66 67 public static final TestAppProxy APP_WRITE_PERMS_ONLY = 68 TestAppProxy.forPackageName("android.healthconnect.cts.testapp.writePermsOnly"); 69 70 private final String mPackageName; 71 private final boolean mInBackground; 72 TestAppProxy(String packageName, boolean inBackground)73 private TestAppProxy(String packageName, boolean inBackground) { 74 mPackageName = packageName; 75 mInBackground = inBackground; 76 } 77 78 /** Create a new {@link TestAppProxy} for given package name. */ forPackageName(String packageName)79 public static TestAppProxy forPackageName(String packageName) { 80 return new TestAppProxy(packageName, false); 81 } 82 83 /** 84 * Create a new {@link TestAppProxy} for given package name which performs calls in the 85 * background. 86 */ forPackageNameInBackground(String packageName)87 public static TestAppProxy forPackageNameInBackground(String packageName) { 88 return new TestAppProxy(packageName, true); 89 } 90 91 /** Returns the package name of the app. */ getPackageName()92 public String getPackageName() { 93 return mPackageName; 94 } 95 96 /** Inserts a record to HC on behalf of the app. */ insertRecord(Record record)97 public String insertRecord(Record record) throws Exception { 98 return insertRecords(Collections.singletonList(record)).get(0); 99 } 100 101 /** Inserts records to HC on behalf of the app. */ insertRecords(Record... records)102 public List<String> insertRecords(Record... records) throws Exception { 103 return insertRecords(Arrays.asList(records)); 104 } 105 106 /** Inserts records to HC on behalf of the app. */ insertRecords(List<? extends Record> records)107 public List<String> insertRecords(List<? extends Record> records) throws Exception { 108 Bundle requestBundle = BundleHelper.fromInsertRecordsRequest(records); 109 Bundle responseBundle = getFromTestApp(requestBundle); 110 return BundleHelper.toInsertRecordsResponse(responseBundle); 111 } 112 113 /** Deletes records from HC on behalf of the app. */ deleteRecords(RecordIdFilter... recordIdFilters)114 public void deleteRecords(RecordIdFilter... recordIdFilters) throws Exception { 115 deleteRecords(Arrays.asList(recordIdFilters)); 116 } 117 118 /** Deletes records from HC on behalf of the app. */ deleteRecords(List<RecordIdFilter> recordIdFilters)119 public void deleteRecords(List<RecordIdFilter> recordIdFilters) throws Exception { 120 Bundle requestBundle = BundleHelper.fromDeleteRecordsByIdsRequest(recordIdFilters); 121 getFromTestApp(requestBundle); 122 } 123 124 /** Updates records in HC on behalf of the app. */ updateRecords(Record... records)125 public void updateRecords(Record... records) throws Exception { 126 updateRecords(Arrays.asList(records)); 127 } 128 129 /** Updates records in HC on behalf of the app. */ updateRecords(List<Record> records)130 public void updateRecords(List<Record> records) throws Exception { 131 Bundle requestBundle = BundleHelper.fromUpdateRecordsRequest(records); 132 getFromTestApp(requestBundle); 133 } 134 135 /** Read records from HC on behalf of the app. */ readRecords(ReadRecordsRequestUsingFilters<T> request)136 public <T extends Record> List<T> readRecords(ReadRecordsRequestUsingFilters<T> request) 137 throws Exception { 138 Bundle requestBundle = BundleHelper.fromReadRecordsRequestUsingFilters(request); 139 Bundle responseBundle = getFromTestApp(requestBundle); 140 return BundleHelper.toReadRecordsResponse(responseBundle); 141 } 142 143 /** Read records from HC on behalf of the app. */ readRecords(ReadRecordsRequestUsingIds<T> request)144 public <T extends Record> List<T> readRecords(ReadRecordsRequestUsingIds<T> request) 145 throws Exception { 146 Bundle requestBundle = BundleHelper.fromReadRecordsRequestUsingIds(request); 147 Bundle responseBundle = getFromTestApp(requestBundle); 148 return BundleHelper.toReadRecordsResponse(responseBundle); 149 } 150 151 /** Aggregate steps records from HC on behalf of the app. */ aggregateStepsCountTotal( Instant startTime, Instant endTime, List<String> packageNames)152 public Long aggregateStepsCountTotal( 153 Instant startTime, Instant endTime, List<String> packageNames) throws Exception { 154 Bundle requestBundle = 155 BundleHelper.fromAggregateStepsCountTotalRequest(startTime, endTime, packageNames); 156 Bundle responseBundle = getFromTestApp(requestBundle); 157 return BundleHelper.toAggregateStepsCountTotalResponse(responseBundle); 158 } 159 160 /** Gets changelogs from HC on behalf of the app. */ getChangeLogs(ChangeLogsRequest request)161 public ChangeLogsResponse getChangeLogs(ChangeLogsRequest request) throws Exception { 162 Bundle requestBundle = BundleHelper.fromChangeLogsRequest(request); 163 Bundle responseBundle = getFromTestApp(requestBundle); 164 return BundleHelper.toChangeLogsResponse(responseBundle); 165 } 166 167 /** Gets a change log token from HC on behalf of the app. */ getChangeLogToken(ChangeLogTokenRequest request)168 public String getChangeLogToken(ChangeLogTokenRequest request) throws Exception { 169 Bundle requestBundle = BundleHelper.fromChangeLogTokenRequest(request); 170 Bundle responseBundle = getFromTestApp(requestBundle); 171 return BundleHelper.toChangeLogTokenResponse(responseBundle); 172 } 173 174 /** 175 * Inserts a Medical Data Source to HC on behalf of the app. 176 * 177 * @return the inserted data source 178 */ createMedicalDataSource(CreateMedicalDataSourceRequest request)179 public MedicalDataSource createMedicalDataSource(CreateMedicalDataSourceRequest request) 180 throws Exception { 181 Bundle requestBundle = BundleHelper.fromCreateMedicalDataSourceRequest(request); 182 Bundle responseBundle = getFromTestApp(requestBundle); 183 return BundleHelper.toMedicalDataSource(responseBundle); 184 } 185 186 /** Gets a list of {@link MedicalDataSource}s given a list of ids on behalf of the app. */ getMedicalDataSources(List<String> ids)187 public List<MedicalDataSource> getMedicalDataSources(List<String> ids) throws Exception { 188 Bundle requestBundle = BundleHelper.fromMedicalDataSourceIds(ids); 189 Bundle responseBundle = getFromTestApp(requestBundle); 190 return BundleHelper.toMedicalDataSources(responseBundle); 191 } 192 193 /** Gets a list of {@link MedicalDataSource}s given a {@link GetMedicalDataSourcesRequest}. */ getMedicalDataSources(GetMedicalDataSourcesRequest request)194 public List<MedicalDataSource> getMedicalDataSources(GetMedicalDataSourcesRequest request) 195 throws Exception { 196 Bundle requestBundle = BundleHelper.fromMedicalDataSourceRequest(request); 197 Bundle responseBundle = getFromTestApp(requestBundle); 198 return BundleHelper.toMedicalDataSources(responseBundle); 199 } 200 201 /** 202 * Upserts a Medical Resource to HC on behalf of the app. 203 * 204 * @return the inserted resource 205 */ upsertMedicalResource(String datasourceId, String data)206 public MedicalResource upsertMedicalResource(String datasourceId, String data) 207 throws Exception { 208 String R4VersionString = "4.0.1"; 209 UpsertMedicalResourceRequest request = 210 new UpsertMedicalResourceRequest.Builder( 211 datasourceId, parseFhirVersion(R4VersionString), data) 212 .build(); 213 Bundle requestBundle = BundleHelper.fromUpsertMedicalResourceRequests(List.of(request)); 214 Bundle responseBundle = getFromTestApp(requestBundle); 215 return BundleHelper.toMedicalResources(responseBundle).get(0); 216 } 217 218 /** 219 * Reads a list of {@link MedicalResource}s for the provided {@code request} on behalf of the 220 * app. 221 */ readMedicalResources(ReadMedicalResourcesRequest request)222 public ReadMedicalResourcesResponse readMedicalResources(ReadMedicalResourcesRequest request) 223 throws Exception { 224 Bundle requestBundle = BundleHelper.fromReadMedicalResourcesRequest(request); 225 Bundle responseBundle = getFromTestApp(requestBundle); 226 return BundleHelper.toReadMedicalResourcesResponse(responseBundle); 227 } 228 229 /** 230 * Reads a list of {@link MedicalResource}s for the provided {@code ids} on behalf of the app. 231 */ readMedicalResources(List<MedicalResourceId> ids)232 public List<MedicalResource> readMedicalResources(List<MedicalResourceId> ids) 233 throws Exception { 234 Bundle requestBundle = BundleHelper.fromMedicalResourceIdsForRead(ids); 235 Bundle responseBundle = getFromTestApp(requestBundle); 236 return BundleHelper.toMedicalResources(responseBundle); 237 } 238 239 /** Deletes Medical Resources from HC on behalf of the app for the given {@code ids}. */ deleteMedicalResources(List<MedicalResourceId> ids)240 public void deleteMedicalResources(List<MedicalResourceId> ids) throws Exception { 241 Bundle requestBundle = BundleHelper.fromMedicalResourceIdsForDelete(ids); 242 getFromTestApp(requestBundle); 243 } 244 245 /** Deletes Medical Resources from HC on behalf of the app for the given {@code request}. */ deleteMedicalResources(DeleteMedicalResourcesRequest request)246 public void deleteMedicalResources(DeleteMedicalResourcesRequest request) throws Exception { 247 Bundle requestBundle = BundleHelper.fromDeleteMedicalResourcesRequest(request); 248 getFromTestApp(requestBundle); 249 } 250 251 /** Deletes Medical Data Source with data for the provided {@code id} on behalf of the app. */ deleteMedicalDataSourceWithData(String id)252 public void deleteMedicalDataSourceWithData(String id) throws Exception { 253 Bundle requestBundle = BundleHelper.fromMedicalDataSourceId(id); 254 getFromTestApp(requestBundle); 255 } 256 257 /** Instructs the app to self-revokes the specified permission. */ selfRevokePermission(String permission)258 public void selfRevokePermission(String permission) throws Exception { 259 Bundle requestBundle = BundleHelper.forSelfRevokePermissionRequest(permission); 260 getFromTestApp(requestBundle); 261 } 262 263 /** Instructs the app to kill itself. */ kill()264 public void kill() throws Exception { 265 Bundle requestBundle = BundleHelper.forKillSelfRequest(); 266 getFromTestApp(requestBundle); 267 } 268 269 /** Starts an activity on behalf of the app and returns the result. */ startActivityForResult(Intent intent)270 public Instrumentation.ActivityResult startActivityForResult(Intent intent) throws Exception { 271 return startActivityForResult(intent, null); 272 } 273 274 /** 275 * Starts an activity on behalf of the app, executes the runnable and returns the result. 276 * 277 * <p>The corresponding test app must have the following activity declared in the Manifest. 278 * 279 * <pre>{@code 280 * <activity android:name="android.healthconnect.cts.utils.ProxyActivity" 281 * android:exported="true"> 282 * <intent-filter> 283 * <action android:name="android.healthconnect.cts.ACTION_START_ACTIVITY_FOR_RESULT"/> 284 * <category android:name="android.intent.category.DEFAULT"/> 285 * </intent-filter> 286 * </activity> 287 * }</pre> 288 */ startActivityForResult(Intent intent, Runnable runnable)289 public Instrumentation.ActivityResult startActivityForResult(Intent intent, Runnable runnable) 290 throws Exception { 291 Intent testAppIntent = new Intent(ProxyActivity.PROXY_ACTIVITY_ACTION); 292 testAppIntent.setPackage(mPackageName); 293 testAppIntent.putExtra(Intent.EXTRA_INTENT, intent); 294 295 return ProxyActivity.launchActivityForResult(testAppIntent, runnable); 296 } 297 getFromTestApp(Bundle bundleToCreateIntent)298 private Bundle getFromTestApp(Bundle bundleToCreateIntent) throws Exception { 299 final CountDownLatch latch = new CountDownLatch(1); 300 AtomicReference<Bundle> response = new AtomicReference<>(); 301 AtomicReference<Exception> exceptionAtomicReference = new AtomicReference<>(); 302 BroadcastReceiver broadcastReceiver = 303 new BroadcastReceiver() { 304 @Override 305 public void onReceive(Context context, Intent intent) { 306 if (intent.hasExtra(INTENT_EXCEPTION)) { 307 exceptionAtomicReference.set( 308 (Exception) (intent.getSerializableExtra(INTENT_EXCEPTION))); 309 } else { 310 response.set(intent.getExtras()); 311 } 312 latch.countDown(); 313 } 314 }; 315 316 launchTestApp(bundleToCreateIntent, broadcastReceiver, latch); 317 if (exceptionAtomicReference.get() != null) { 318 throw exceptionAtomicReference.get(); 319 } 320 return response.get(); 321 } 322 launchTestApp( Bundle bundleToCreateIntent, BroadcastReceiver broadcastReceiver, CountDownLatch latch)323 private void launchTestApp( 324 Bundle bundleToCreateIntent, BroadcastReceiver broadcastReceiver, CountDownLatch latch) 325 throws Exception { 326 327 // Register broadcast receiver 328 final IntentFilter intentFilter = new IntentFilter(); 329 String action = bundleToCreateIntent.getString(QUERY_TYPE); 330 intentFilter.addAction(action); 331 intentFilter.addCategory(Intent.CATEGORY_DEFAULT); 332 getContext().registerReceiver(broadcastReceiver, intentFilter, Context.RECEIVER_EXPORTED); 333 334 // Launch the test app. 335 Intent intent; 336 337 Log.d(TAG, "launchTestApp(): action=" + action + " - inBackground=" + mInBackground); 338 if (mInBackground) { 339 intent = new Intent().setClassName(mPackageName, TEST_APP_RECEIVER_CLASS_NAME); 340 } else { 341 intent = new Intent(Intent.ACTION_MAIN); 342 intent.setPackage(mPackageName); 343 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 344 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 345 intent.addCategory(Intent.CATEGORY_LAUNCHER); 346 } 347 348 intent.putExtras(bundleToCreateIntent); 349 350 Thread.sleep(500); 351 352 if (mInBackground) { 353 getContext().sendBroadcast(intent); 354 } else { 355 getContext().startActivity(intent); 356 } 357 358 // We don't wait for responses to kill requests. These kill the app & there is no easy or 359 // reliable way for the app to return a broadcast before being killed. 360 boolean isKillRequest = 361 bundleToCreateIntent.getString(QUERY_TYPE).equals(KILL_SELF_REQUEST); 362 if (!isKillRequest && !latch.await(POLLING_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { 363 final String errorMessage = 364 "Timed out while waiting to receive " 365 + bundleToCreateIntent.getString(QUERY_TYPE) 366 + " intent from " 367 + mPackageName; 368 throw new TimeoutException(errorMessage); 369 } 370 getContext().unregisterReceiver(broadcastReceiver); 371 } 372 } 373