1 /*
2  * Copyright (C) 2022 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.nopermission;
18 
19 import static android.health.connect.HealthPermissions.READ_DISTANCE;
20 import static android.health.connect.HealthPermissions.READ_EXERCISE;
21 import static android.health.connect.HealthPermissions.READ_HEART_RATE;
22 import static android.health.connect.HealthPermissions.READ_SLEEP;
23 import static android.health.connect.HealthPermissions.READ_STEPS;
24 import static android.health.connect.HealthPermissions.READ_TOTAL_CALORIES_BURNED;
25 import static android.health.connect.datatypes.DistanceRecord.DISTANCE_TOTAL;
26 import static android.health.connect.datatypes.ExerciseSessionRecord.EXERCISE_DURATION_TOTAL;
27 import static android.health.connect.datatypes.HeartRateRecord.BPM_MAX;
28 import static android.health.connect.datatypes.SleepSessionRecord.SLEEP_DURATION_TOTAL;
29 import static android.health.connect.datatypes.StepsRecord.STEPS_COUNT_TOTAL;
30 import static android.health.connect.datatypes.TotalCaloriesBurnedRecord.ENERGY_TOTAL;
31 import static android.healthconnect.cts.utils.DataFactory.NOW;
32 import static android.healthconnect.cts.utils.DataFactory.buildExerciseSession;
33 import static android.healthconnect.cts.utils.DataFactory.buildSleepSession;
34 import static android.healthconnect.cts.utils.DataFactory.getDistanceRecord;
35 import static android.healthconnect.cts.utils.DataFactory.getDistanceRecordWithNonEmptyId;
36 import static android.healthconnect.cts.utils.DataFactory.getHeartRateRecord;
37 import static android.healthconnect.cts.utils.DataFactory.getStepsRecord;
38 import static android.healthconnect.cts.utils.DataFactory.getTotalCaloriesBurnedRecord;
39 import static android.healthconnect.cts.utils.DataFactory.getTotalCaloriesBurnedRecordWithEmptyMetadata;
40 import static android.healthconnect.cts.utils.PermissionHelper.grantPermission;
41 import static android.healthconnect.cts.utils.PermissionHelper.revokeAllPermissions;
42 import static android.healthconnect.cts.utils.TestUtils.deleteRecords;
43 import static android.healthconnect.cts.utils.TestUtils.getAggregateResponse;
44 import static android.healthconnect.cts.utils.TestUtils.getAggregateResponseGroupByDuration;
45 import static android.healthconnect.cts.utils.TestUtils.getAggregateResponseGroupByPeriod;
46 import static android.healthconnect.cts.utils.TestUtils.getChangeLogToken;
47 import static android.healthconnect.cts.utils.TestUtils.insertRecords;
48 import static android.healthconnect.cts.utils.TestUtils.readRecords;
49 import static android.healthconnect.cts.utils.TestUtils.updateRecords;
50 import static android.healthconnect.cts.utils.TestUtils.verifyDeleteRecords;
51 
52 import static com.google.common.truth.Truth.assertThat;
53 
54 import static java.time.temporal.ChronoUnit.DAYS;
55 
56 import android.health.connect.AggregateRecordsRequest;
57 import android.health.connect.HealthConnectException;
58 import android.health.connect.LocalTimeRangeFilter;
59 import android.health.connect.ReadRecordsRequestUsingFilters;
60 import android.health.connect.ReadRecordsRequestUsingIds;
61 import android.health.connect.TimeInstantRangeFilter;
62 import android.health.connect.changelog.ChangeLogTokenRequest;
63 import android.health.connect.changelog.ChangeLogsRequest;
64 import android.health.connect.datatypes.AggregationType;
65 import android.health.connect.datatypes.DataOrigin;
66 import android.health.connect.datatypes.DistanceRecord;
67 import android.health.connect.datatypes.ExerciseSessionRecord;
68 import android.health.connect.datatypes.HeartRateRecord;
69 import android.health.connect.datatypes.Record;
70 import android.health.connect.datatypes.SleepSessionRecord;
71 import android.health.connect.datatypes.StepsRecord;
72 import android.health.connect.datatypes.TotalCaloriesBurnedRecord;
73 import android.healthconnect.cts.lib.TestAppProxy;
74 import android.healthconnect.cts.utils.AssumptionCheckerRule;
75 import android.healthconnect.cts.utils.TestUtils;
76 import android.platform.test.annotations.AppModeFull;
77 import android.util.Pair;
78 
79 import androidx.test.runner.AndroidJUnit4;
80 
81 import org.junit.Assert;
82 import org.junit.Rule;
83 import org.junit.Test;
84 import org.junit.runner.RunWith;
85 
86 import java.time.Duration;
87 import java.time.Instant;
88 import java.time.LocalDateTime;
89 import java.time.Period;
90 import java.time.ZoneOffset;
91 import java.util.Arrays;
92 import java.util.Collections;
93 import java.util.List;
94 
95 /** These test run under an environment which has no HC permissions */
96 @AppModeFull(reason = "HealthConnectManager is not accessible to instant apps")
97 @RunWith(AndroidJUnit4.class)
98 public class HealthConnectManagerNoPermissionsGrantedTest {
99     private static final TestAppProxy APP_A_WITH_READ_WRITE_PERMS =
100             TestAppProxy.forPackageName("android.healthconnect.cts.testapp.readWritePerms.A");
101 
102     @Rule
103     public AssumptionCheckerRule mSupportedHardwareRule =
104             new AssumptionCheckerRule(
105                     TestUtils::isHardwareSupported, "Tests should run on supported hardware only.");
106 
107     @Test
testInsert_noPermissions_expectError()108     public void testInsert_noPermissions_expectError() throws InterruptedException {
109         for (Record testRecord : getTestRecords()) {
110             try {
111                 insertRecords(Collections.singletonList(testRecord));
112                 Assert.fail("Insert must be not allowed without right HC permission");
113             } catch (HealthConnectException healthConnectException) {
114                 assertThat(healthConnectException.getErrorCode())
115                         .isEqualTo(HealthConnectException.ERROR_SECURITY);
116             }
117         }
118     }
119 
120     @Test
testUpdate_noPermissions_expectError()121     public void testUpdate_noPermissions_expectError() throws InterruptedException {
122         for (Record testRecord : getTestRecords()) {
123             try {
124                 updateRecords(Collections.singletonList(testRecord));
125                 Assert.fail("Update must be not allowed without right HC permission");
126             } catch (HealthConnectException healthConnectException) {
127                 assertThat(healthConnectException.getErrorCode())
128                         .isEqualTo(HealthConnectException.ERROR_SECURITY);
129             }
130         }
131     }
132 
133     @Test
testDeleteUsingId_noPermissions_expectError()134     public void testDeleteUsingId_noPermissions_expectError() throws InterruptedException {
135         for (Record testRecord : getTestRecords()) {
136             try {
137                 deleteRecords(Collections.singletonList(testRecord));
138                 Assert.fail("Delete using ids must be not allowed without right HC permission");
139             } catch (HealthConnectException healthConnectException) {
140                 assertThat(healthConnectException.getErrorCode())
141                         .isEqualTo(HealthConnectException.ERROR_SECURITY);
142             }
143         }
144     }
145 
146     @Test
testDeleteUsingFilter_noPermissions_expectError()147     public void testDeleteUsingFilter_noPermissions_expectError() throws InterruptedException {
148         for (Record testRecord : getTestRecords()) {
149             try {
150                 verifyDeleteRecords(
151                         testRecord.getClass(),
152                         new TimeInstantRangeFilter.Builder()
153                                 .setStartTime(Instant.now())
154                                 .setEndTime(Instant.now().plusMillis(1000))
155                                 .build());
156                 Assert.fail("Delete using filters must be not allowed without right HC permission");
157             } catch (HealthConnectException healthConnectException) {
158                 assertThat(healthConnectException.getErrorCode())
159                         .isEqualTo(HealthConnectException.ERROR_SECURITY);
160             }
161         }
162     }
163 
164     @Test
testChangeLogsToken_noPermissions_expectError()165     public void testChangeLogsToken_noPermissions_expectError() throws InterruptedException {
166         for (Record testRecord : getTestRecords()) {
167             try {
168                 getChangeLogToken(
169                         new ChangeLogTokenRequest.Builder()
170                                 .addRecordType(testRecord.getClass())
171                                 .build());
172                 Assert.fail(
173                         "Getting change log token must be not allowed without right HC permission");
174             } catch (HealthConnectException healthConnectException) {
175                 assertThat(healthConnectException.getErrorCode())
176                         .isEqualTo(HealthConnectException.ERROR_SECURITY);
177             }
178         }
179     }
180 
181     @Test
testGetChangeLogs_noPermissions_expectError()182     public void testGetChangeLogs_noPermissions_expectError() throws Exception {
183         TestAppProxy testApp = APP_A_WITH_READ_WRITE_PERMS;
184         String packageName = testApp.getPackageName();
185         revokeAllPermissions(packageName, /* reason= */ "for test");
186         List<Pair<String, Class<? extends Record>>> permissionAndRecordClassPairs =
187                 List.of(
188                         new Pair<>(READ_STEPS, StepsRecord.class),
189                         new Pair<>(READ_DISTANCE, DistanceRecord.class),
190                         new Pair<>(READ_HEART_RATE, HeartRateRecord.class),
191                         new Pair<>(READ_SLEEP, SleepSessionRecord.class),
192                         new Pair<>(READ_EXERCISE, ExerciseSessionRecord.class),
193                         new Pair<>(READ_TOTAL_CALORIES_BURNED, TotalCaloriesBurnedRecord.class));
194 
195         for (var permissionAndRecordClass : permissionAndRecordClassPairs) {
196             String permission = permissionAndRecordClass.first;
197             Class<? extends Record> recordClass = permissionAndRecordClass.second;
198             grantPermission(packageName, permission);
199             String token =
200                     testApp.getChangeLogToken(
201                             new ChangeLogTokenRequest.Builder().addRecordType(recordClass).build());
202             revokeAllPermissions(packageName, /* reason= */ "for test");
203 
204             try {
205                 testApp.getChangeLogs(new ChangeLogsRequest.Builder(token).build());
206 
207                 Assert.fail(
208                         String.format(
209                                 "Getting change logs for %s must be not allowed with %s permission",
210                                 recordClass.getSimpleName(), permission));
211             } catch (HealthConnectException healthConnectException) {
212                 assertThat(healthConnectException.getErrorCode())
213                         .isEqualTo(HealthConnectException.ERROR_SECURITY);
214             }
215         }
216     }
217 
218     @Test
testReadByFilters_noPermissions_expectError()219     public void testReadByFilters_noPermissions_expectError() throws InterruptedException {
220         for (Record testRecord : getTestRecords()) {
221             try {
222                 readRecords(
223                         new ReadRecordsRequestUsingFilters.Builder<>(testRecord.getClass())
224                                 .build());
225                 Assert.fail(
226                         "Read records by filters must be not allowed without right HC permission");
227             } catch (HealthConnectException healthConnectException) {
228                 assertThat(healthConnectException.getErrorCode())
229                         .isEqualTo(HealthConnectException.ERROR_SECURITY);
230             }
231         }
232     }
233 
234     @Test
testReadByRecordIds_noPermissions_expectError()235     public void testReadByRecordIds_noPermissions_expectError() throws InterruptedException {
236         for (Record testRecord : getTestRecords()) {
237             try {
238                 readRecords(
239                         new ReadRecordsRequestUsingIds.Builder<>(testRecord.getClass())
240                                 .addId("id")
241                                 .build());
242                 Assert.fail(
243                         "Read records by record ids must be not allowed without right HC "
244                                 + "permission");
245             } catch (HealthConnectException healthConnectException) {
246                 assertThat(healthConnectException.getErrorCode())
247                         .isEqualTo(HealthConnectException.ERROR_SECURITY);
248             }
249         }
250     }
251 
252     @Test
testReadByClientIds_noPermissions_expectError()253     public void testReadByClientIds_noPermissions_expectError() throws InterruptedException {
254         for (Record testRecord : getTestRecords()) {
255             try {
256                 readRecords(
257                         new ReadRecordsRequestUsingIds.Builder<>(testRecord.getClass())
258                                 .addClientRecordId("client_id")
259                                 .build());
260                 Assert.fail(
261                         "Read records by client ids must be not allowed without right HC "
262                                 + "permission");
263             } catch (HealthConnectException healthConnectException) {
264                 assertThat(healthConnectException.getErrorCode())
265                         .isEqualTo(HealthConnectException.ERROR_SECURITY);
266             }
267         }
268     }
269 
270     @Test
testAggregate_noPermissions_expectError()271     public void testAggregate_noPermissions_expectError() throws InterruptedException {
272         List<Pair<Record, AggregationType<?>>> recordAndAggregationTypePairs =
273                 List.of(
274                         new Pair<>(getHeartRateRecord(), BPM_MAX),
275                         new Pair<>(getStepsRecord(), STEPS_COUNT_TOTAL),
276                         new Pair<>(getDistanceRecord(), DISTANCE_TOTAL),
277                         new Pair<>(getTotalCaloriesBurnedRecordWithEmptyMetadata(), ENERGY_TOTAL),
278                         new Pair<>(buildSleepSession(), SLEEP_DURATION_TOTAL),
279                         new Pair<>(buildExerciseSession(), EXERCISE_DURATION_TOTAL));
280         for (var recordAndAggregationType : recordAndAggregationTypePairs) {
281             try {
282                 List<Record> records = List.of(recordAndAggregationType.first);
283                 AggregationType<?> aggregationType = recordAndAggregationType.second;
284                 TimeInstantRangeFilter timeInstantRangeFilter =
285                         new TimeInstantRangeFilter.Builder()
286                                 .setStartTime(Instant.ofEpochMilli(0))
287                                 .setEndTime(NOW.plus(1000, DAYS))
288                                 .build();
289                 getAggregateResponse(
290                         new AggregateRecordsRequest.Builder<>(timeInstantRangeFilter)
291                                 .addAggregationType((AggregationType<Object>) aggregationType)
292                                 .addDataOriginsFilter(
293                                         new DataOrigin.Builder().setPackageName("abc").build())
294                                 .build(),
295                         records);
296                 Assert.fail("Get Aggregations must be not allowed without right HC permission");
297             } catch (HealthConnectException healthConnectException) {
298                 assertThat(healthConnectException.getErrorCode())
299                         .isEqualTo(HealthConnectException.ERROR_SECURITY);
300             }
301         }
302     }
303 
304     @Test
testAggregateGroupByDuration_noPermissions_expectError()305     public void testAggregateGroupByDuration_noPermissions_expectError()
306             throws InterruptedException {
307         List<AggregationType<?>> aggregationTypes =
308                 List.of(
309                         BPM_MAX,
310                         STEPS_COUNT_TOTAL,
311                         DISTANCE_TOTAL,
312                         ENERGY_TOTAL,
313                         SLEEP_DURATION_TOTAL,
314                         EXERCISE_DURATION_TOTAL);
315         for (var aggregationType : aggregationTypes) {
316             try {
317                 TimeInstantRangeFilter timeInstantRangeFilter =
318                         new TimeInstantRangeFilter.Builder()
319                                 .setStartTime(NOW.minusMillis(500))
320                                 .setEndTime(NOW.plusMillis(2500))
321                                 .build();
322                 getAggregateResponseGroupByDuration(
323                         new AggregateRecordsRequest.Builder<>(timeInstantRangeFilter)
324                                 .addAggregationType((AggregationType<Object>) aggregationType)
325                                 .addDataOriginsFilter(
326                                         new DataOrigin.Builder().setPackageName("abc").build())
327                                 .build(),
328                         Duration.ofSeconds(1));
329                 Assert.fail(
330                         "Aggregations group by duration must be not allowed without right HC"
331                                 + " permission");
332             } catch (HealthConnectException healthConnectException) {
333                 assertThat(healthConnectException.getErrorCode())
334                         .isEqualTo(HealthConnectException.ERROR_SECURITY);
335             }
336         }
337     }
338 
339     @Test
testAggregateGroupByPeriod_noPermissions_expectError()340     public void testAggregateGroupByPeriod_noPermissions_expectError() throws InterruptedException {
341         List<AggregationType<?>> aggregationTypes =
342                 List.of(
343                         BPM_MAX,
344                         STEPS_COUNT_TOTAL,
345                         DISTANCE_TOTAL,
346                         ENERGY_TOTAL,
347                         SLEEP_DURATION_TOTAL,
348                         EXERCISE_DURATION_TOTAL);
349         for (var aggregationType : aggregationTypes) {
350             try {
351                 Instant start = NOW.minus(3, DAYS);
352                 Instant end = start.plus(3, DAYS);
353                 LocalTimeRangeFilter localTimeRangeFilter =
354                         new LocalTimeRangeFilter.Builder()
355                                 .setStartTime(LocalDateTime.ofInstant(start, ZoneOffset.UTC))
356                                 .setEndTime(LocalDateTime.ofInstant(end, ZoneOffset.UTC))
357                                 .build();
358                 getAggregateResponseGroupByPeriod(
359                         new AggregateRecordsRequest.Builder<>(localTimeRangeFilter)
360                                 .addAggregationType((AggregationType<Object>) aggregationType)
361                                 .build(),
362                         Period.ofDays(1));
363                 Assert.fail(
364                         "Aggregation group by period must be not allowed without right HC"
365                                 + " permission");
366             } catch (HealthConnectException healthConnectException) {
367                 assertThat(healthConnectException.getErrorCode())
368                         .isEqualTo(HealthConnectException.ERROR_SECURITY);
369             }
370         }
371     }
372 
getTestRecords()373     private static List<Record> getTestRecords() {
374         return Arrays.asList(
375                 getStepsRecord(),
376                 getHeartRateRecord(),
377                 buildSleepSession(),
378                 getDistanceRecordWithNonEmptyId(),
379                 getTotalCaloriesBurnedRecord("client_id"),
380                 buildExerciseSession());
381     }
382 }
383