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