• 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 androidx.test.InstrumentationRegistry.getContext;
20 
21 import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
22 
23 import static com.google.common.truth.Truth.assertThat;
24 
25 import android.content.BroadcastReceiver;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.IntentFilter;
29 import android.health.connect.HealthConnectException;
30 import android.health.connect.HealthConnectManager;
31 import android.health.connect.InsertRecordsResponse;
32 import android.health.connect.ReadRecordsRequest;
33 import android.health.connect.ReadRecordsResponse;
34 import android.health.connect.RecordIdFilter;
35 import android.health.connect.changelog.ChangeLogTokenRequest;
36 import android.health.connect.changelog.ChangeLogTokenResponse;
37 import android.health.connect.changelog.ChangeLogsRequest;
38 import android.health.connect.changelog.ChangeLogsResponse;
39 import android.health.connect.datatypes.BasalMetabolicRateRecord;
40 import android.health.connect.datatypes.DataOrigin;
41 import android.health.connect.datatypes.Device;
42 import android.health.connect.datatypes.ExerciseRoute;
43 import android.health.connect.datatypes.ExerciseSessionRecord;
44 import android.health.connect.datatypes.ExerciseSessionType;
45 import android.health.connect.datatypes.HeartRateRecord;
46 import android.health.connect.datatypes.Metadata;
47 import android.health.connect.datatypes.Record;
48 import android.health.connect.datatypes.StepsRecord;
49 import android.health.connect.datatypes.units.Power;
50 import android.os.Bundle;
51 import android.os.OutcomeReceiver;
52 import android.util.Log;
53 
54 import androidx.test.core.app.ApplicationProvider;
55 
56 import com.android.cts.install.lib.TestApp;
57 
58 import java.io.Serializable;
59 import java.time.Instant;
60 import java.time.ZoneOffset;
61 import java.util.ArrayList;
62 import java.util.Arrays;
63 import java.util.Collections;
64 import java.util.HashMap;
65 import java.util.List;
66 import java.util.Map;
67 import java.util.concurrent.CountDownLatch;
68 import java.util.concurrent.Executors;
69 import java.util.concurrent.TimeUnit;
70 import java.util.concurrent.TimeoutException;
71 import java.util.concurrent.atomic.AtomicReference;
72 
73 public class TestUtils {
74     static final String TAG = "HealthConnectTest";
75     public static final String QUERY_TYPE = "android.healthconnect.cts.queryType";
76     public static final String INTENT_EXTRA_CALLING_PKG = "android.healthconnect.cts.calling_pkg";
77     public static final String APP_PKG_NAME_USED_IN_DATA_ORIGIN =
78             "android.healthconnect.cts.pkg.usedInDataOrigin";
79     public static final String INSERT_RECORD_QUERY = "android.healthconnect.cts.insertRecord";
80     public static final String READ_RECORDS_QUERY = "android.healthconnect.cts.readRecords";
81     public static final String READ_RECORDS_SIZE = "android.healthconnect.cts.readRecordsNumber";
82     public static final String READ_USING_DATA_ORIGIN_FILTERS =
83             "android.healthconnect.cts.readUsingDataOriginFilters";
84     public static final String READ_RECORD_CLASS_NAME =
85             "android.healthconnect.cts.readRecordsClass";
86     public static final String READ_CHANGE_LOGS_QUERY = "android.healthconnect.cts.readChangeLogs";
87     public static final String CHANGE_LOGS_RESPONSE =
88             "android.healthconnect.cts.changeLogsResponse";
89     public static final String CHANGE_LOG_TOKEN = "android.healthconnect.cts.changeLogToken";
90     public static final String SUCCESS = "android.healthconnect.cts.success";
91     public static final String CLIENT_ID = "android.healthconnect.cts.clientId";
92     public static final String RECORD_IDS = "android.healthconnect.cts.records";
93     public static final String DELETE_RECORDS_QUERY = "android.healthconnect.cts.deleteRecords";
94     public static final String UPDATE_RECORDS_QUERY = "android.healthconnect.cts.updateRecords";
95     public static final String UPDATE_EXERCISE_ROUTE = "android.healthconnect.cts.updateRoute";
96 
97     public static final String UPSERT_EXERCISE_ROUTE = "android.healthconnect.cts.upsertRoute";
98     public static final String GET_CHANGE_LOG_TOKEN_QUERY =
99             "android.healthconnect.cts.getChangeLogToken";
100     public static final String INTENT_EXCEPTION = "android.healthconnect.cts.exception";
101     private static final long POLLING_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(20);
102 
103     public static class RecordTypeAndRecordIds implements Serializable {
104         private String mRecordType;
105         private List<String> mRecordIds;
106 
RecordTypeAndRecordIds(String recordType, List<String> ids)107         public RecordTypeAndRecordIds(String recordType, List<String> ids) {
108             mRecordType = recordType;
109             mRecordIds = ids;
110         }
111 
getRecordType()112         public String getRecordType() {
113             return mRecordType;
114         }
115 
getRecordIds()116         public List<String> getRecordIds() {
117             return mRecordIds;
118         }
119     }
120 
insertRecordAs(TestApp testApp)121     public static Bundle insertRecordAs(TestApp testApp) throws Exception {
122         Bundle bundle = new Bundle();
123         bundle.putString(QUERY_TYPE, INSERT_RECORD_QUERY);
124 
125         return getFromTestApp(testApp, bundle);
126     }
127 
deleteRecordsAs( TestApp testApp, List<RecordTypeAndRecordIds> listOfRecordIdsAndClass)128     public static Bundle deleteRecordsAs(
129             TestApp testApp, List<RecordTypeAndRecordIds> listOfRecordIdsAndClass)
130             throws Exception {
131         Bundle bundle = new Bundle();
132         bundle.putString(QUERY_TYPE, DELETE_RECORDS_QUERY);
133         bundle.putSerializable(RECORD_IDS, (Serializable) listOfRecordIdsAndClass);
134 
135         return getFromTestApp(testApp, bundle);
136     }
137 
updateRecordsAs( TestApp testAppToUpdateData, List<RecordTypeAndRecordIds> listOfRecordIdsAndClass)138     public static Bundle updateRecordsAs(
139             TestApp testAppToUpdateData, List<RecordTypeAndRecordIds> listOfRecordIdsAndClass)
140             throws Exception {
141         Bundle bundle = new Bundle();
142         bundle.putString(QUERY_TYPE, UPDATE_RECORDS_QUERY);
143         bundle.putSerializable(RECORD_IDS, (Serializable) listOfRecordIdsAndClass);
144 
145         return getFromTestApp(testAppToUpdateData, bundle);
146     }
147 
updateRouteAs(TestApp testAppToUpdateData)148     public static Bundle updateRouteAs(TestApp testAppToUpdateData) throws Exception {
149         Bundle bundle = new Bundle();
150         bundle.putString(QUERY_TYPE, UPDATE_EXERCISE_ROUTE);
151         return getFromTestApp(testAppToUpdateData, bundle);
152     }
153 
insertSessionNoRouteAs(TestApp testAppToUpdateData)154     public static Bundle insertSessionNoRouteAs(TestApp testAppToUpdateData) throws Exception {
155         Bundle bundle = new Bundle();
156         bundle.putString(QUERY_TYPE, UPSERT_EXERCISE_ROUTE);
157         return getFromTestApp(testAppToUpdateData, bundle);
158     }
159 
insertRecordWithAnotherAppPackageName( TestApp testAppToInsertData, TestApp testAppPkgNameUsed)160     public static Bundle insertRecordWithAnotherAppPackageName(
161             TestApp testAppToInsertData, TestApp testAppPkgNameUsed) throws Exception {
162         Bundle bundle = new Bundle();
163         bundle.putString(QUERY_TYPE, INSERT_RECORD_QUERY);
164         bundle.putString(APP_PKG_NAME_USED_IN_DATA_ORIGIN, testAppPkgNameUsed.getPackageName());
165 
166         return getFromTestApp(testAppToInsertData, bundle);
167     }
168 
readRecordsAs(TestApp testApp, ArrayList<String> recordClassesToRead)169     public static Bundle readRecordsAs(TestApp testApp, ArrayList<String> recordClassesToRead)
170             throws Exception {
171         Bundle bundle = new Bundle();
172         bundle.putString(QUERY_TYPE, READ_RECORDS_QUERY);
173         bundle.putStringArrayList(READ_RECORD_CLASS_NAME, recordClassesToRead);
174 
175         return getFromTestApp(testApp, bundle);
176     }
177 
insertRecordWithGivenClientId(TestApp testApp, double clientId)178     public static Bundle insertRecordWithGivenClientId(TestApp testApp, double clientId)
179             throws Exception {
180         Bundle bundle = new Bundle();
181         bundle.putString(QUERY_TYPE, INSERT_RECORD_QUERY);
182         bundle.putDouble(CLIENT_ID, clientId);
183 
184         return getFromTestApp(testApp, bundle);
185     }
186 
readRecordsUsingDataOriginFiltersAs( TestApp testApp, ArrayList<String> recordClassesToRead)187     public static Bundle readRecordsUsingDataOriginFiltersAs(
188             TestApp testApp, ArrayList<String> recordClassesToRead) throws Exception {
189         Bundle bundle = new Bundle();
190         bundle.putString(QUERY_TYPE, READ_RECORDS_QUERY);
191         bundle.putStringArrayList(READ_RECORD_CLASS_NAME, recordClassesToRead);
192         bundle.putBoolean(READ_USING_DATA_ORIGIN_FILTERS, true);
193 
194         return getFromTestApp(testApp, bundle);
195     }
196 
readChangeLogsUsingDataOriginFiltersAs( TestApp testApp, String changeLogToken)197     public static Bundle readChangeLogsUsingDataOriginFiltersAs(
198             TestApp testApp, String changeLogToken) throws Exception {
199         Bundle bundle = new Bundle();
200         bundle.putString(QUERY_TYPE, READ_CHANGE_LOGS_QUERY);
201         bundle.putString(CHANGE_LOG_TOKEN, changeLogToken);
202         bundle.putBoolean(READ_USING_DATA_ORIGIN_FILTERS, true);
203 
204         return getFromTestApp(testApp, bundle);
205     }
206 
getChangeLogTokenAs(TestApp testApp, String pkgName)207     public static Bundle getChangeLogTokenAs(TestApp testApp, String pkgName) throws Exception {
208         Bundle bundle = new Bundle();
209         bundle.putString(QUERY_TYPE, GET_CHANGE_LOG_TOKEN_QUERY);
210         bundle.putString(APP_PKG_NAME_USED_IN_DATA_ORIGIN, pkgName);
211 
212         return getFromTestApp(testApp, bundle);
213     }
214 
getFromTestApp(TestApp testApp, Bundle bundleToCreateIntent)215     private static Bundle getFromTestApp(TestApp testApp, Bundle bundleToCreateIntent)
216             throws Exception {
217         final CountDownLatch latch = new CountDownLatch(1);
218         AtomicReference<Bundle> response = new AtomicReference<>();
219         AtomicReference<Exception> exceptionAtomicReference = new AtomicReference<>();
220         BroadcastReceiver broadcastReceiver =
221                 new BroadcastReceiver() {
222                     @Override
223                     public void onReceive(Context context, Intent intent) {
224                         if (intent.hasExtra(INTENT_EXCEPTION)) {
225                             exceptionAtomicReference.set(
226                                     (Exception) (intent.getSerializableExtra(INTENT_EXCEPTION)));
227                         } else {
228                             response.set(intent.getExtras());
229                         }
230                         latch.countDown();
231                     }
232                 };
233 
234         launchTestApp(testApp, bundleToCreateIntent, broadcastReceiver, latch);
235         if (exceptionAtomicReference.get() != null) {
236             throw exceptionAtomicReference.get();
237         }
238         return response.get();
239     }
240 
launchTestApp( TestApp testApp, Bundle bundleToCreateIntent, BroadcastReceiver broadcastReceiver, CountDownLatch latch)241     private static void launchTestApp(
242             TestApp testApp,
243             Bundle bundleToCreateIntent,
244             BroadcastReceiver broadcastReceiver,
245             CountDownLatch latch)
246             throws Exception {
247 
248         // Register broadcast receiver
249         final IntentFilter intentFilter = new IntentFilter();
250         intentFilter.addAction(bundleToCreateIntent.getString(QUERY_TYPE));
251         intentFilter.addCategory(Intent.CATEGORY_DEFAULT);
252         getContext().registerReceiver(broadcastReceiver, intentFilter, Context.RECEIVER_EXPORTED);
253 
254         // Launch the test app.
255         final Intent intent = new Intent(Intent.ACTION_MAIN);
256         intent.setPackage(testApp.getPackageName());
257         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
258         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
259         intent.putExtra(INTENT_EXTRA_CALLING_PKG, getContext().getPackageName());
260         intent.putExtras(bundleToCreateIntent);
261         intent.addCategory(Intent.CATEGORY_LAUNCHER);
262         intent.putExtras(bundleToCreateIntent);
263 
264         Thread.sleep(500);
265         getContext().startActivity(intent);
266         if (!latch.await(POLLING_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
267             final String errorMessage =
268                     "Timed out while waiting to receive "
269                             + bundleToCreateIntent.getString(QUERY_TYPE)
270                             + " intent from "
271                             + testApp.getPackageName();
272             throw new TimeoutException(errorMessage);
273         }
274         getContext().unregisterReceiver(broadcastReceiver);
275     }
276 
insertRecordAndGetId(Record record, Context context)277     public static String insertRecordAndGetId(Record record, Context context)
278             throws InterruptedException {
279         return insertRecords(Collections.singletonList(record), context)
280                 .get(0)
281                 .getMetadata()
282                 .getId();
283     }
284 
insertRecordsAndGetIds( List<Record> records, Context context)285     public static List<RecordTypeAndRecordIds> insertRecordsAndGetIds(
286             List<Record> records, Context context) throws InterruptedException {
287         List<Record> insertedRecords = insertRecords(records, context);
288 
289         Map<String, List<String>> recordTypeToRecordIdsMap = new HashMap<>();
290         for (Record record : insertedRecords) {
291             recordTypeToRecordIdsMap.putIfAbsent(record.getClass().getName(), new ArrayList<>());
292             recordTypeToRecordIdsMap
293                     .get(record.getClass().getName())
294                     .add(record.getMetadata().getId());
295         }
296 
297         List<RecordTypeAndRecordIds> recordTypeAndRecordIdsList = new ArrayList<>();
298         for (String recordType : recordTypeToRecordIdsMap.keySet()) {
299             recordTypeAndRecordIdsList.add(
300                     new RecordTypeAndRecordIds(
301                             recordType, recordTypeToRecordIdsMap.get(recordType)));
302         }
303 
304         return recordTypeAndRecordIdsList;
305     }
306 
insertRecords(List<Record> records, Context context)307     public static List<Record> insertRecords(List<Record> records, Context context)
308             throws InterruptedException {
309         CountDownLatch latch = new CountDownLatch(1);
310         HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
311         assertThat(service).isNotNull();
312         AtomicReference<List<Record>> response = new AtomicReference<>();
313         AtomicReference<HealthConnectException> exceptionAtomicReference = new AtomicReference<>();
314         service.insertRecords(
315                 records,
316                 Executors.newSingleThreadExecutor(),
317                 new OutcomeReceiver<>() {
318                     @Override
319                     public void onResult(InsertRecordsResponse result) {
320                         response.set(result.getRecords());
321                         latch.countDown();
322                     }
323 
324                     @Override
325                     public void onError(HealthConnectException exception) {
326                         Log.e(TAG, exception.getMessage());
327                         exceptionAtomicReference.set(exception);
328                         latch.countDown();
329                     }
330                 });
331         assertThat(latch.await(3, TimeUnit.SECONDS)).isTrue();
332         if (exceptionAtomicReference.get() != null) {
333             throw exceptionAtomicReference.get();
334         }
335         assertThat(response.get()).hasSize(records.size());
336 
337         return response.get();
338     }
339 
getTestRecords(String packageName)340     public static List<Record> getTestRecords(String packageName) {
341         double clientId = Math.random();
342         return getTestRecords(packageName, clientId);
343     }
344 
getTestRecords(String packageName, Double clientId)345     public static List<Record> getTestRecords(String packageName, Double clientId) {
346         return Arrays.asList(
347                 getExerciseSessionRecord(packageName, clientId, /* withRoute= */ true),
348                 getStepsRecord(packageName, clientId),
349                 getHeartRateRecord(packageName, clientId),
350                 getBasalMetabolicRateRecord(packageName, clientId));
351     }
352 
getExerciseSessionRecord( String packageName, double clientId, boolean withRoute)353     public static ExerciseSessionRecord getExerciseSessionRecord(
354             String packageName, double clientId, boolean withRoute) {
355         Instant startTime = Instant.now().minusSeconds(3000);
356         Instant endTime = Instant.now();
357         ExerciseSessionRecord.Builder builder =
358                 new ExerciseSessionRecord.Builder(
359                                 buildSessionMetadata(packageName, clientId),
360                                 startTime,
361                                 endTime,
362                                 ExerciseSessionType.EXERCISE_SESSION_TYPE_OTHER_WORKOUT)
363                         .setEndZoneOffset(ZoneOffset.MAX)
364                         .setStartZoneOffset(ZoneOffset.MIN)
365                         .setNotes("notes")
366                         .setTitle("title");
367 
368         if (withRoute) {
369             builder.setRoute(
370                     new ExerciseRoute(
371                             List.of(
372                                     new ExerciseRoute.Location.Builder(startTime, 50., 50.).build(),
373                                     new ExerciseRoute.Location.Builder(
374                                                     startTime.plusSeconds(2), 51., 51.)
375                                             .build())));
376         }
377         return builder.build();
378     }
379 
getStepsRecord(String packageName, double clientId)380     public static StepsRecord getStepsRecord(String packageName, double clientId) {
381         Device device =
382                 new Device.Builder().setManufacturer("google").setModel("Pixel").setType(1).build();
383         DataOrigin dataOrigin = new DataOrigin.Builder().setPackageName(packageName).build();
384         return new StepsRecord.Builder(
385                         new Metadata.Builder()
386                                 .setDevice(device)
387                                 .setDataOrigin(dataOrigin)
388                                 .setClientRecordId("SR" + clientId)
389                                 .build(),
390                         Instant.now(),
391                         Instant.now().plusMillis(1000),
392                         10)
393                 .build();
394     }
395 
getHeartRateRecord(String packageName, double clientId)396     public static HeartRateRecord getHeartRateRecord(String packageName, double clientId) {
397         HeartRateRecord.HeartRateSample heartRateSample =
398                 new HeartRateRecord.HeartRateSample(72, Instant.now().plusMillis(100));
399         ArrayList<HeartRateRecord.HeartRateSample> heartRateSamples = new ArrayList<>();
400         heartRateSamples.add(heartRateSample);
401         heartRateSamples.add(heartRateSample);
402         Device device =
403                 new Device.Builder().setManufacturer("google").setModel("Pixel").setType(1).build();
404         DataOrigin dataOrigin = new DataOrigin.Builder().setPackageName(packageName).build();
405 
406         return new HeartRateRecord.Builder(
407                         new Metadata.Builder()
408                                 .setDevice(device)
409                                 .setDataOrigin(dataOrigin)
410                                 .setClientRecordId("HR" + clientId)
411                                 .build(),
412                         Instant.now(),
413                         Instant.now().plusMillis(500),
414                         heartRateSamples)
415                 .build();
416     }
417 
getBasalMetabolicRateRecord( String packageName, double clientId)418     public static BasalMetabolicRateRecord getBasalMetabolicRateRecord(
419             String packageName, double clientId) {
420         Device device =
421                 new Device.Builder()
422                         .setManufacturer("google")
423                         .setModel("Pixel4a")
424                         .setType(2)
425                         .build();
426         DataOrigin dataOrigin = new DataOrigin.Builder().setPackageName(packageName).build();
427         return new BasalMetabolicRateRecord.Builder(
428                         new Metadata.Builder()
429                                 .setDevice(device)
430                                 .setDataOrigin(dataOrigin)
431                                 .setClientRecordId("BMR" + clientId)
432                                 .build(),
433                         Instant.now(),
434                         Power.fromWatts(100.0))
435                 .build();
436     }
437 
verifyDeleteRecords(List<RecordIdFilter> request, Context context)438     public static void verifyDeleteRecords(List<RecordIdFilter> request, Context context)
439             throws InterruptedException {
440         HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
441         CountDownLatch latch = new CountDownLatch(1);
442         AtomicReference<HealthConnectException> exceptionAtomicReference = new AtomicReference<>();
443         assertThat(service).isNotNull();
444         service.deleteRecords(
445                 request,
446                 Executors.newSingleThreadExecutor(),
447                 new OutcomeReceiver<>() {
448                     @Override
449                     public void onResult(Void result) {
450                         latch.countDown();
451                     }
452 
453                     @Override
454                     public void onError(HealthConnectException healthConnectException) {
455                         exceptionAtomicReference.set(healthConnectException);
456                         latch.countDown();
457                     }
458                 });
459         assertThat(latch.await(3, TimeUnit.SECONDS)).isEqualTo(true);
460         if (exceptionAtomicReference.get() != null) {
461             throw exceptionAtomicReference.get();
462         }
463     }
464 
readRecords( ReadRecordsRequest<T> request, Context context)465     public static <T extends Record> List<T> readRecords(
466             ReadRecordsRequest<T> request, Context context) throws InterruptedException {
467         HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
468         CountDownLatch latch = new CountDownLatch(1);
469         assertThat(service).isNotNull();
470         assertThat(request.getRecordType()).isNotNull();
471         AtomicReference<List<T>> response = new AtomicReference<>();
472         AtomicReference<HealthConnectException> healthConnectExceptionAtomicReference =
473                 new AtomicReference<>();
474         service.readRecords(
475                 request,
476                 Executors.newSingleThreadExecutor(),
477                 new OutcomeReceiver<>() {
478                     @Override
479                     public void onResult(ReadRecordsResponse<T> result) {
480                         response.set(result.getRecords());
481                         latch.countDown();
482                     }
483 
484                     @Override
485                     public void onError(HealthConnectException exception) {
486                         Log.e(TAG, exception.getMessage());
487                         healthConnectExceptionAtomicReference.set(exception);
488                         latch.countDown();
489                     }
490                 });
491         assertThat(latch.await(3, TimeUnit.SECONDS)).isEqualTo(true);
492         if (healthConnectExceptionAtomicReference.get() != null) {
493             throw healthConnectExceptionAtomicReference.get();
494         }
495         return response.get();
496     }
497 
updateRecords(List<Record> records, Context context)498     public static void updateRecords(List<Record> records, Context context)
499             throws InterruptedException {
500         CountDownLatch latch = new CountDownLatch(1);
501         HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
502         assertThat(service).isNotNull();
503         AtomicReference<HealthConnectException> exceptionAtomicReference = new AtomicReference<>();
504 
505         service.updateRecords(
506                 records,
507                 Executors.newSingleThreadExecutor(),
508                 new OutcomeReceiver<>() {
509                     @Override
510                     public void onResult(Void result) {
511                         latch.countDown();
512                     }
513 
514                     @Override
515                     public void onError(HealthConnectException exception) {
516                         exceptionAtomicReference.set(exception);
517                         latch.countDown();
518                     }
519                 });
520         assertThat(latch.await(3, TimeUnit.SECONDS)).isTrue();
521         if (exceptionAtomicReference.get() != null) {
522             throw exceptionAtomicReference.get();
523         }
524     }
525 
readRecords(ReadRecordsRequest<T> request)526     public static <T extends Record> List<T> readRecords(ReadRecordsRequest<T> request)
527             throws InterruptedException {
528         Context context = ApplicationProvider.getApplicationContext();
529         HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
530         CountDownLatch latch = new CountDownLatch(1);
531         assertThat(service).isNotNull();
532         assertThat(request.getRecordType()).isNotNull();
533         AtomicReference<List<T>> response = new AtomicReference<>();
534         AtomicReference<HealthConnectException> healthConnectExceptionAtomicReference =
535                 new AtomicReference<>();
536         service.readRecords(
537                 request,
538                 Executors.newSingleThreadExecutor(),
539                 new OutcomeReceiver<>() {
540                     @Override
541                     public void onResult(ReadRecordsResponse<T> result) {
542                         response.set(result.getRecords());
543                         latch.countDown();
544                     }
545 
546                     @Override
547                     public void onError(HealthConnectException exception) {
548                         Log.e(TAG, exception.getMessage());
549                         healthConnectExceptionAtomicReference.set(exception);
550                         latch.countDown();
551                     }
552                 });
553         assertThat(latch.await(3, TimeUnit.SECONDS)).isEqualTo(true);
554         if (healthConnectExceptionAtomicReference.get() != null) {
555             throw healthConnectExceptionAtomicReference.get();
556         }
557         return response.get();
558     }
559 
getChangeLogToken( ChangeLogTokenRequest request, Context context)560     public static ChangeLogTokenResponse getChangeLogToken(
561             ChangeLogTokenRequest request, Context context) throws InterruptedException {
562         HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
563         assertThat(service).isNotNull();
564         CountDownLatch latch = new CountDownLatch(1);
565         AtomicReference<ChangeLogTokenResponse> response = new AtomicReference<>();
566         AtomicReference<HealthConnectException> exceptionAtomicReference = new AtomicReference<>();
567         service.getChangeLogToken(
568                 request,
569                 Executors.newSingleThreadExecutor(),
570                 new OutcomeReceiver<>() {
571                     @Override
572                     public void onResult(ChangeLogTokenResponse result) {
573                         response.set(result);
574                         latch.countDown();
575                     }
576 
577                     @Override
578                     public void onError(HealthConnectException exception) {
579                         Log.e(TAG, exception.getMessage());
580                         exceptionAtomicReference.set(exception);
581                         latch.countDown();
582                     }
583                 });
584         assertThat(latch.await(3, TimeUnit.SECONDS)).isTrue();
585         if (exceptionAtomicReference.get() != null) {
586             throw exceptionAtomicReference.get();
587         }
588         return response.get();
589     }
590 
getChangeLogs( ChangeLogsRequest changeLogsRequest, Context context)591     public static ChangeLogsResponse getChangeLogs(
592             ChangeLogsRequest changeLogsRequest, Context context) throws InterruptedException {
593         HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
594         assertThat(service).isNotNull();
595 
596         CountDownLatch latch = new CountDownLatch(1);
597         AtomicReference<ChangeLogsResponse> response = new AtomicReference<>();
598         AtomicReference<HealthConnectException> healthConnectExceptionAtomicReference =
599                 new AtomicReference<>();
600         service.getChangeLogs(
601                 changeLogsRequest,
602                 Executors.newSingleThreadExecutor(),
603                 new OutcomeReceiver<>() {
604                     @Override
605                     public void onResult(ChangeLogsResponse result) {
606                         response.set(result);
607                         latch.countDown();
608                     }
609 
610                     @Override
611                     public void onError(HealthConnectException exception) {
612                         healthConnectExceptionAtomicReference.set(exception);
613                         latch.countDown();
614                     }
615                 });
616         assertThat(latch.await(3, TimeUnit.SECONDS)).isEqualTo(true);
617         if (healthConnectExceptionAtomicReference.get() != null) {
618             throw healthConnectExceptionAtomicReference.get();
619         }
620 
621         return response.get();
622     }
623 
deleteAllStagedRemoteData()624     public static void deleteAllStagedRemoteData() {
625         Context context = ApplicationProvider.getApplicationContext();
626         HealthConnectManager service = context.getSystemService(HealthConnectManager.class);
627         assertThat(service).isNotNull();
628         runWithShellPermissionIdentity(
629                 () ->
630                         // TODO(b/241542162): Avoid reflection once TestApi can be called from CTS
631                         service.getClass().getMethod("deleteAllStagedRemoteData").invoke(service),
632                 "android.permission.DELETE_STAGED_HEALTH_CONNECT_REMOTE_DATA");
633     }
634 
buildSessionMetadata(String packageName, double clientId)635     private static Metadata buildSessionMetadata(String packageName, double clientId) {
636         Device device =
637                 new Device.Builder().setManufacturer("google").setModel("Pixel").setType(1).build();
638         DataOrigin dataOrigin = new DataOrigin.Builder().setPackageName(packageName).build();
639         return new Metadata.Builder()
640                 .setDevice(device)
641                 .setDataOrigin(dataOrigin)
642                 .setClientRecordId(String.valueOf(clientId))
643                 .build();
644     }
645 }
646