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 50 import com.android.cts.install.lib.TestApp; 51 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 TEST_APP_RECEIVER_CLASS_NAME = 63 "android.healthconnect.cts.testhelper.TestAppReceiver"; 64 private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(30); 65 66 public static final TestAppProxy APP_WRITE_PERMS_ONLY = 67 TestAppProxy.forPackageName("android.healthconnect.cts.testapp.writePermsOnly"); 68 69 private final String mPackageName; 70 private final boolean mInBackground; 71 TestAppProxy(String packageName, boolean inBackground)72 private TestAppProxy(String packageName, boolean inBackground) { 73 mPackageName = packageName; 74 mInBackground = inBackground; 75 } 76 77 /** Create a new {@link TestAppProxy} for given test app. */ forApp(TestApp testApp)78 public static TestAppProxy forApp(TestApp testApp) { 79 return forPackageName(testApp.getPackageName()); 80 } 81 82 /** Create a new {@link TestAppProxy} for given package name. */ forPackageName(String packageName)83 public static TestAppProxy forPackageName(String packageName) { 84 return new TestAppProxy(packageName, false); 85 } 86 87 /** 88 * Create a new {@link TestAppProxy} for given package name which performs calls in the 89 * background. 90 */ forPackageNameInBackground(String packageName)91 public static TestAppProxy forPackageNameInBackground(String packageName) { 92 return new TestAppProxy(packageName, true); 93 } 94 95 /** Returns the package name of the app. */ getPackageName()96 public String getPackageName() { 97 return mPackageName; 98 } 99 100 /** Inserts a record to HC on behalf of the app. */ insertRecord(Record record)101 public String insertRecord(Record record) throws Exception { 102 return insertRecords(Collections.singletonList(record)).get(0); 103 } 104 105 /** Inserts records to HC on behalf of the app. */ insertRecords(Record... records)106 public List<String> insertRecords(Record... records) throws Exception { 107 return insertRecords(Arrays.asList(records)); 108 } 109 110 /** Inserts records to HC on behalf of the app. */ insertRecords(List<? extends Record> records)111 public List<String> insertRecords(List<? extends Record> records) throws Exception { 112 Bundle requestBundle = BundleHelper.fromInsertRecordsRequest(records); 113 Bundle responseBundle = getFromTestApp(requestBundle); 114 return BundleHelper.toInsertRecordsResponse(responseBundle); 115 } 116 117 /** Deletes records from HC on behalf of the app. */ deleteRecords(RecordIdFilter... recordIdFilters)118 public void deleteRecords(RecordIdFilter... recordIdFilters) throws Exception { 119 deleteRecords(Arrays.asList(recordIdFilters)); 120 } 121 122 /** Deletes records from HC on behalf of the app. */ deleteRecords(List<RecordIdFilter> recordIdFilters)123 public void deleteRecords(List<RecordIdFilter> recordIdFilters) throws Exception { 124 Bundle requestBundle = BundleHelper.fromDeleteRecordsByIdsRequest(recordIdFilters); 125 getFromTestApp(requestBundle); 126 } 127 128 /** Updates records in HC on behalf of the app. */ updateRecords(Record... records)129 public void updateRecords(Record... records) throws Exception { 130 updateRecords(Arrays.asList(records)); 131 } 132 133 /** Updates records in HC on behalf of the app. */ updateRecords(List<Record> records)134 public void updateRecords(List<Record> records) throws Exception { 135 Bundle requestBundle = BundleHelper.fromUpdateRecordsRequest(records); 136 getFromTestApp(requestBundle); 137 } 138 139 /** Read records from HC on behalf of the app. */ readRecords(ReadRecordsRequestUsingFilters<T> request)140 public <T extends Record> List<T> readRecords(ReadRecordsRequestUsingFilters<T> request) 141 throws Exception { 142 Bundle requestBundle = BundleHelper.fromReadRecordsRequestUsingFilters(request); 143 Bundle responseBundle = getFromTestApp(requestBundle); 144 return BundleHelper.toReadRecordsResponse(responseBundle); 145 } 146 147 /** Read records from HC on behalf of the app. */ readRecords(ReadRecordsRequestUsingIds<T> request)148 public <T extends Record> List<T> readRecords(ReadRecordsRequestUsingIds<T> request) 149 throws Exception { 150 Bundle requestBundle = BundleHelper.fromReadRecordsRequestUsingIds(request); 151 Bundle responseBundle = getFromTestApp(requestBundle); 152 return BundleHelper.toReadRecordsResponse(responseBundle); 153 } 154 155 /** Gets changelogs from HC on behalf of the app. */ getChangeLogs(ChangeLogsRequest request)156 public ChangeLogsResponse getChangeLogs(ChangeLogsRequest request) throws Exception { 157 Bundle requestBundle = BundleHelper.fromChangeLogsRequest(request); 158 Bundle responseBundle = getFromTestApp(requestBundle); 159 return BundleHelper.toChangeLogsResponse(responseBundle); 160 } 161 162 /** Gets a change log token from HC on behalf of the app. */ getChangeLogToken(ChangeLogTokenRequest request)163 public String getChangeLogToken(ChangeLogTokenRequest request) throws Exception { 164 Bundle requestBundle = BundleHelper.fromChangeLogTokenRequest(request); 165 Bundle responseBundle = getFromTestApp(requestBundle); 166 return BundleHelper.toChangeLogTokenResponse(responseBundle); 167 } 168 169 /** 170 * Inserts a Medical Data Source to HC on behalf of the app. 171 * 172 * @return the inserted data source 173 */ createMedicalDataSource(CreateMedicalDataSourceRequest request)174 public MedicalDataSource createMedicalDataSource(CreateMedicalDataSourceRequest request) 175 throws Exception { 176 Bundle requestBundle = BundleHelper.fromCreateMedicalDataSourceRequest(request); 177 Bundle responseBundle = getFromTestApp(requestBundle); 178 return BundleHelper.toMedicalDataSource(responseBundle); 179 } 180 181 /** Gets a list of {@link MedicalDataSource}s given a list of ids on behalf of the app. */ getMedicalDataSources(List<String> ids)182 public List<MedicalDataSource> getMedicalDataSources(List<String> ids) throws Exception { 183 Bundle requestBundle = BundleHelper.fromMedicalDataSourceIds(ids); 184 Bundle responseBundle = getFromTestApp(requestBundle); 185 return BundleHelper.toMedicalDataSources(responseBundle); 186 } 187 188 /** Gets a list of {@link MedicalDataSource}s given a {@link GetMedicalDataSourcesRequest}. */ getMedicalDataSources(GetMedicalDataSourcesRequest request)189 public List<MedicalDataSource> getMedicalDataSources(GetMedicalDataSourcesRequest request) 190 throws Exception { 191 Bundle requestBundle = BundleHelper.fromMedicalDataSourceRequest(request); 192 Bundle responseBundle = getFromTestApp(requestBundle); 193 return BundleHelper.toMedicalDataSources(responseBundle); 194 } 195 196 /** 197 * Upserts a Medical Resource to HC on behalf of the app. 198 * 199 * @return the inserted resource 200 */ upsertMedicalResource(String datasourceId, String data)201 public MedicalResource upsertMedicalResource(String datasourceId, String data) 202 throws Exception { 203 String R4VersionString = "4.0.1"; 204 UpsertMedicalResourceRequest request = 205 new UpsertMedicalResourceRequest.Builder( 206 datasourceId, parseFhirVersion(R4VersionString), data) 207 .build(); 208 Bundle requestBundle = BundleHelper.fromUpsertMedicalResourceRequests(List.of(request)); 209 Bundle responseBundle = getFromTestApp(requestBundle); 210 return BundleHelper.toMedicalResources(responseBundle).get(0); 211 } 212 213 /** 214 * Reads a list of {@link MedicalResource}s for the provided {@code request} on behalf of the 215 * app. 216 */ readMedicalResources(ReadMedicalResourcesRequest request)217 public ReadMedicalResourcesResponse readMedicalResources(ReadMedicalResourcesRequest request) 218 throws Exception { 219 Bundle requestBundle = BundleHelper.fromReadMedicalResourcesRequest(request); 220 Bundle responseBundle = getFromTestApp(requestBundle); 221 return BundleHelper.toReadMedicalResourcesResponse(responseBundle); 222 } 223 224 /** 225 * Reads a list of {@link MedicalResource}s for the provided {@code ids} on behalf of the app. 226 */ readMedicalResources(List<MedicalResourceId> ids)227 public List<MedicalResource> readMedicalResources(List<MedicalResourceId> ids) 228 throws Exception { 229 Bundle requestBundle = BundleHelper.fromMedicalResourceIdsForRead(ids); 230 Bundle responseBundle = getFromTestApp(requestBundle); 231 return BundleHelper.toMedicalResources(responseBundle); 232 } 233 234 /** Deletes Medical Resources from HC on behalf of the app for the given {@code ids}. */ deleteMedicalResources(List<MedicalResourceId> ids)235 public void deleteMedicalResources(List<MedicalResourceId> ids) throws Exception { 236 Bundle requestBundle = BundleHelper.fromMedicalResourceIdsForDelete(ids); 237 getFromTestApp(requestBundle); 238 } 239 240 /** Deletes Medical Resources from HC on behalf of the app for the given {@code request}. */ deleteMedicalResources(DeleteMedicalResourcesRequest request)241 public void deleteMedicalResources(DeleteMedicalResourcesRequest request) throws Exception { 242 Bundle requestBundle = BundleHelper.fromDeleteMedicalResourcesRequest(request); 243 getFromTestApp(requestBundle); 244 } 245 246 /** Deletes Medical Data Source with data for the provided {@code id} on behalf of the app. */ deleteMedicalDataSourceWithData(String id)247 public void deleteMedicalDataSourceWithData(String id) throws Exception { 248 Bundle requestBundle = BundleHelper.fromMedicalDataSourceId(id); 249 getFromTestApp(requestBundle); 250 } 251 252 /** Instructs the app to self-revokes the specified permission. */ selfRevokePermission(String permission)253 public void selfRevokePermission(String permission) throws Exception { 254 Bundle requestBundle = BundleHelper.forSelfRevokePermissionRequest(permission); 255 getFromTestApp(requestBundle); 256 } 257 258 /** Instructs the app to kill itself. */ kill()259 public void kill() throws Exception { 260 Bundle requestBundle = BundleHelper.forKillSelfRequest(); 261 getFromTestApp(requestBundle); 262 } 263 264 /** Starts an activity on behalf of the app and returns the result. */ startActivityForResult(Intent intent)265 public Instrumentation.ActivityResult startActivityForResult(Intent intent) throws Exception { 266 return startActivityForResult(intent, null); 267 } 268 269 /** 270 * Starts an activity on behalf of the app, executes the runnable and returns the result. 271 * 272 * <p>The corresponding test app must have the following activity declared in the Manifest. 273 * 274 * <pre>{@code 275 * <activity android:name="android.healthconnect.cts.utils.ProxyActivity" 276 * android:exported="true"> 277 * <intent-filter> 278 * <action android:name="android.healthconnect.cts.ACTION_START_ACTIVITY_FOR_RESULT"/> 279 * <category android:name="android.intent.category.DEFAULT"/> 280 * </intent-filter> 281 * </activity> 282 * }</pre> 283 */ startActivityForResult(Intent intent, Runnable runnable)284 public Instrumentation.ActivityResult startActivityForResult(Intent intent, Runnable runnable) 285 throws Exception { 286 Intent testAppIntent = new Intent(ProxyActivity.PROXY_ACTIVITY_ACTION); 287 testAppIntent.setPackage(mPackageName); 288 testAppIntent.putExtra(Intent.EXTRA_INTENT, intent); 289 290 return ProxyActivity.launchActivityForResult(testAppIntent, runnable); 291 } 292 getFromTestApp(Bundle bundleToCreateIntent)293 private Bundle getFromTestApp(Bundle bundleToCreateIntent) throws Exception { 294 final CountDownLatch latch = new CountDownLatch(1); 295 AtomicReference<Bundle> response = new AtomicReference<>(); 296 AtomicReference<Exception> exceptionAtomicReference = new AtomicReference<>(); 297 BroadcastReceiver broadcastReceiver = 298 new BroadcastReceiver() { 299 @Override 300 public void onReceive(Context context, Intent intent) { 301 if (intent.hasExtra(INTENT_EXCEPTION)) { 302 exceptionAtomicReference.set( 303 (Exception) (intent.getSerializableExtra(INTENT_EXCEPTION))); 304 } else { 305 response.set(intent.getExtras()); 306 } 307 latch.countDown(); 308 } 309 }; 310 311 launchTestApp(bundleToCreateIntent, broadcastReceiver, latch); 312 if (exceptionAtomicReference.get() != null) { 313 throw exceptionAtomicReference.get(); 314 } 315 return response.get(); 316 } 317 launchTestApp( Bundle bundleToCreateIntent, BroadcastReceiver broadcastReceiver, CountDownLatch latch)318 private void launchTestApp( 319 Bundle bundleToCreateIntent, BroadcastReceiver broadcastReceiver, CountDownLatch latch) 320 throws Exception { 321 322 // Register broadcast receiver 323 final IntentFilter intentFilter = new IntentFilter(); 324 intentFilter.addAction(bundleToCreateIntent.getString(QUERY_TYPE)); 325 intentFilter.addCategory(Intent.CATEGORY_DEFAULT); 326 getContext().registerReceiver(broadcastReceiver, intentFilter, Context.RECEIVER_EXPORTED); 327 328 // Launch the test app. 329 Intent intent; 330 331 if (mInBackground) { 332 intent = new Intent().setClassName(mPackageName, TEST_APP_RECEIVER_CLASS_NAME); 333 } else { 334 intent = new Intent(Intent.ACTION_MAIN); 335 intent.setPackage(mPackageName); 336 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 337 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 338 intent.addCategory(Intent.CATEGORY_LAUNCHER); 339 } 340 341 intent.putExtras(bundleToCreateIntent); 342 343 Thread.sleep(500); 344 345 if (mInBackground) { 346 getContext().sendBroadcast(intent); 347 } else { 348 getContext().startActivity(intent); 349 } 350 351 // We don't wait for responses to kill requests. These kill the app & there is no easy or 352 // reliable way for the app to return a broadcast before being killed. 353 boolean isKillRequest = 354 bundleToCreateIntent.getString(QUERY_TYPE).equals(KILL_SELF_REQUEST); 355 if (!isKillRequest && !latch.await(POLLING_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) { 356 final String errorMessage = 357 "Timed out while waiting to receive " 358 + bundleToCreateIntent.getString(QUERY_TYPE) 359 + " intent from " 360 + mPackageName; 361 throw new TimeoutException(errorMessage); 362 } 363 getContext().unregisterReceiver(broadcastReceiver); 364 } 365 } 366