• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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