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 com.android.server.healthconnect.permission; 18 19 import static com.android.server.healthconnect.TestUtils.getInternalBackgroundExecutorTaskCount; 20 import static com.android.server.healthconnect.TestUtils.waitForAllScheduledTasksToComplete; 21 import static com.android.server.healthconnect.permission.FirstGrantTimeDatastore.DATA_TYPE_CURRENT; 22 import static com.android.server.healthconnect.permission.FirstGrantTimeDatastore.DATA_TYPE_STAGED; 23 24 import static com.google.common.truth.Truth.assertThat; 25 import static com.google.common.truth.Truth8.assertThat; 26 27 import static org.mockito.ArgumentMatchers.any; 28 import static org.mockito.ArgumentMatchers.anyInt; 29 import static org.mockito.ArgumentMatchers.anyString; 30 import static org.mockito.ArgumentMatchers.eq; 31 import static org.mockito.Mockito.verify; 32 import static org.mockito.Mockito.when; 33 34 import android.app.UiAutomation; 35 import android.content.Context; 36 import android.content.pm.PackageInfo; 37 import android.content.pm.PackageManager; 38 import android.health.connect.HealthConnectException; 39 import android.health.connect.HealthConnectManager; 40 import android.health.connect.ReadRecordsRequest; 41 import android.health.connect.ReadRecordsRequestUsingFilters; 42 import android.health.connect.ReadRecordsResponse; 43 import android.health.connect.TimeInstantRangeFilter; 44 import android.health.connect.datatypes.Record; 45 import android.health.connect.datatypes.StepsRecord; 46 import android.os.OutcomeReceiver; 47 import android.os.Process; 48 import android.os.UserHandle; 49 import android.os.UserManager; 50 import android.util.Pair; 51 52 import androidx.test.InstrumentationRegistry; 53 54 import org.junit.After; 55 import org.junit.Before; 56 import org.junit.Test; 57 import org.mockito.ArgumentCaptor; 58 import org.mockito.ArgumentMatchers; 59 import org.mockito.Mock; 60 import org.mockito.MockitoAnnotations; 61 62 import java.time.Instant; 63 import java.util.ArrayList; 64 import java.util.Arrays; 65 import java.util.List; 66 import java.util.Optional; 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 // TODO(b/261432978): add test for sharedUser backup 74 public class FirstGrantTimeUnitTest { 75 76 private static final String SELF_PACKAGE_NAME = "com.android.healthconnect.unittests"; 77 private static final UserHandle CURRENT_USER = Process.myUserHandle(); 78 79 private static final int DEFAULT_VERSION = 1; 80 81 @Mock private HealthPermissionIntentAppsTracker mTracker; 82 @Mock private PackageManager mPackageManager; 83 @Mock private UserManager mUserManager; 84 @Mock private Context mContext; 85 @Mock private FirstGrantTimeDatastore mDatastore; 86 @Mock private PackageInfoUtils mPackageInfoUtils; 87 88 private FirstGrantTimeManager mGrantTimeManager; 89 90 private final UiAutomation mUiAutomation = 91 InstrumentationRegistry.getInstrumentation().getUiAutomation(); 92 93 @Before setUp()94 public void setUp() { 95 Context context = InstrumentationRegistry.getContext(); 96 MockitoAnnotations.initMocks(this); 97 when(mDatastore.readForUser(CURRENT_USER, DATA_TYPE_CURRENT)) 98 .thenReturn(new UserGrantTimeState(DEFAULT_VERSION)); 99 when(mDatastore.readForUser(CURRENT_USER, DATA_TYPE_STAGED)) 100 .thenReturn(new UserGrantTimeState(DEFAULT_VERSION)); 101 when(mTracker.supportsPermissionUsageIntent(SELF_PACKAGE_NAME, CURRENT_USER)) 102 .thenReturn(true); 103 when(mContext.createContextAsUser(any(), anyInt())).thenReturn(context); 104 when(mContext.getApplicationContext()).thenReturn(context); 105 when(mContext.getPackageManager()).thenReturn(mPackageManager); 106 when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager); 107 when(mUserManager.isUserUnlocked()).thenReturn(true); 108 109 mUiAutomation.adoptShellPermissionIdentity( 110 "android.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS"); 111 mGrantTimeManager = new FirstGrantTimeManager(mContext, mTracker, mDatastore); 112 } 113 114 @After tearDown()115 public void tearDown() throws Exception { 116 waitForAllScheduledTasksToComplete(); 117 PackageInfoUtils.clearInstance(); 118 } 119 120 @Test testSetFirstGrantTimeForAnApp_expectOtherAppsGrantTimesRemained()121 public void testSetFirstGrantTimeForAnApp_expectOtherAppsGrantTimesRemained() { 122 Instant instant1 = Instant.parse("2023-02-11T10:00:00Z"); 123 Instant instant2 = Instant.parse("2023-02-12T10:00:00Z"); 124 Instant instant3 = Instant.parse("2023-02-13T10:00:00Z"); 125 String anotherPackage = "another.package"; 126 // mock PackageInfoUtils 127 List<Pair<String, Integer>> packageNameAndUidPairs = 128 Arrays.asList(new Pair<>(SELF_PACKAGE_NAME, 0), new Pair<>(anotherPackage, 1)); 129 PackageInfoUtils.setInstanceForTest(mPackageInfoUtils); 130 List<PackageInfo> packageInfos = new ArrayList<>(); 131 for (Pair<String, Integer> pair : packageNameAndUidPairs) { 132 String packageName = pair.first; 133 int uid = pair.second; 134 PackageInfo packageInfo = new PackageInfo(); 135 packageInfo.packageName = packageName; 136 packageInfos.add(packageInfo); 137 when(mPackageInfoUtils.getPackageUid( 138 eq(packageName), any(UserHandle.class), any(Context.class))) 139 .thenReturn(uid); 140 when(mPackageInfoUtils.getPackageNameFromUid(eq(uid))) 141 .thenReturn(Optional.of(packageName)); 142 } 143 when(mPackageInfoUtils.getPackagesHoldingHealthPermissions( 144 any(UserHandle.class), any(Context.class))) 145 .thenReturn(packageInfos); 146 // mock initial storage 147 mGrantTimeManager = new FirstGrantTimeManager(mContext, mTracker, mDatastore); 148 UserGrantTimeState currentGrantTimeState = new UserGrantTimeState(DEFAULT_VERSION); 149 currentGrantTimeState.setPackageGrantTime(SELF_PACKAGE_NAME, instant1); 150 currentGrantTimeState.setPackageGrantTime(anotherPackage, instant2); 151 when(mDatastore.readForUser(CURRENT_USER, DATA_TYPE_CURRENT)) 152 .thenReturn(currentGrantTimeState); 153 // mock permission intent tracker 154 when(mTracker.supportsPermissionUsageIntent(anyString(), ArgumentMatchers.any())) 155 .thenReturn(true); 156 ArgumentCaptor<UserGrantTimeState> captor = 157 ArgumentCaptor.forClass(UserGrantTimeState.class); 158 159 assertThat(mGrantTimeManager.getFirstGrantTime(SELF_PACKAGE_NAME, CURRENT_USER)) 160 .hasValue(instant1); 161 assertThat(mGrantTimeManager.getFirstGrantTime(anotherPackage, CURRENT_USER)) 162 .hasValue(instant2); 163 164 mGrantTimeManager.setFirstGrantTime(SELF_PACKAGE_NAME, instant3, CURRENT_USER); 165 verify(mDatastore).writeForUser(captor.capture(), eq(CURRENT_USER), anyInt()); 166 167 UserGrantTimeState newUserGrantTimeState = captor.getValue(); 168 assertThat(newUserGrantTimeState.getPackageGrantTimes().keySet()).hasSize(2); 169 assertThat(newUserGrantTimeState.getPackageGrantTimes().get(SELF_PACKAGE_NAME)) 170 .isEqualTo(instant3); 171 assertThat(newUserGrantTimeState.getPackageGrantTimes().get(anotherPackage)) 172 .isEqualTo(instant2); 173 } 174 175 @Test(expected = IllegalArgumentException.class) testUnknownPackage_throwsException()176 public void testUnknownPackage_throwsException() { 177 mGrantTimeManager.getFirstGrantTime("android.unknown_package", CURRENT_USER); 178 } 179 180 @Test testCurrentPackage_intentNotSupported_grantTimeIsNull()181 public void testCurrentPackage_intentNotSupported_grantTimeIsNull() { 182 when(mTracker.supportsPermissionUsageIntent(SELF_PACKAGE_NAME, CURRENT_USER)) 183 .thenReturn(false); 184 assertThat(mGrantTimeManager.getFirstGrantTime(SELF_PACKAGE_NAME, CURRENT_USER)).isEmpty(); 185 } 186 187 @Test testOnPermissionsChangedCalledWhileDeviceIsLocked_getGrantTimeNotNullAfterUnlock()188 public void testOnPermissionsChangedCalledWhileDeviceIsLocked_getGrantTimeNotNullAfterUnlock() 189 throws TimeoutException { 190 // before device is unlocked 191 when(mUserManager.isUserUnlocked()).thenReturn(false); 192 when(mDatastore.readForUser(CURRENT_USER, DATA_TYPE_CURRENT)).thenReturn(null); 193 when(mDatastore.readForUser(CURRENT_USER, DATA_TYPE_STAGED)).thenReturn(null); 194 int uid = 123; 195 String[] packageNames = {"package.name"}; 196 when(mPackageManager.getPackagesForUid(uid)).thenReturn(packageNames); 197 when(mTracker.supportsPermissionUsageIntent(eq(packageNames[0]), ArgumentMatchers.any())) 198 .thenReturn(true); 199 mGrantTimeManager.onPermissionsChanged(uid); 200 waitForAllScheduledTasksToComplete(); 201 // after device is unlocked 202 when(mUserManager.isUserUnlocked()).thenReturn(true); 203 UserGrantTimeState currentGrantTimeState = new UserGrantTimeState(DEFAULT_VERSION); 204 Instant now = Instant.parse("2023-02-14T10:00:00Z"); 205 currentGrantTimeState.setPackageGrantTime(SELF_PACKAGE_NAME, now); 206 when(mDatastore.readForUser(CURRENT_USER, DATA_TYPE_CURRENT)) 207 .thenReturn(currentGrantTimeState); 208 209 assertThat(mGrantTimeManager.getFirstGrantTime(SELF_PACKAGE_NAME, CURRENT_USER)) 210 .hasValue(now); 211 } 212 213 @Test testOnPermissionsChangedCalled_withHealthPermissionsUid_expectBackgroundTaskAdded()214 public void testOnPermissionsChangedCalled_withHealthPermissionsUid_expectBackgroundTaskAdded() 215 throws TimeoutException { 216 long currentTaskCount = getInternalBackgroundExecutorTaskCount(); 217 waitForAllScheduledTasksToComplete(); 218 int uid = 123; 219 String[] packageNames = {"package.name"}; 220 when(mPackageManager.getPackagesForUid(uid)).thenReturn(packageNames); 221 when(mTracker.supportsPermissionUsageIntent(eq(packageNames[0]), ArgumentMatchers.any())) 222 .thenReturn(true); 223 224 mGrantTimeManager.onPermissionsChanged(uid); 225 waitForAllScheduledTasksToComplete(); 226 227 assertThat(getInternalBackgroundExecutorTaskCount()).isEqualTo(currentTaskCount + 1); 228 } 229 230 @Test 231 public void testOnPermissionsChangedCalled_withNoHealthPermissionsUid_expectNoBackgroundTaskAdded()232 testOnPermissionsChangedCalled_withNoHealthPermissionsUid_expectNoBackgroundTaskAdded() 233 throws TimeoutException { 234 long currentTaskCount = getInternalBackgroundExecutorTaskCount(); 235 waitForAllScheduledTasksToComplete(); 236 int uid = 123; 237 String[] packageNames = {"package.name"}; 238 when(mPackageManager.getPackagesForUid(uid)).thenReturn(packageNames); 239 when(mTracker.supportsPermissionUsageIntent(eq(packageNames[0]), ArgumentMatchers.any())) 240 .thenReturn(false); 241 242 mGrantTimeManager.onPermissionsChanged(uid); 243 waitForAllScheduledTasksToComplete(); 244 245 assertThat(getInternalBackgroundExecutorTaskCount()).isEqualTo(currentTaskCount); 246 } 247 248 @Test testCurrentPackage_intentSupported_grantTimeIsNotNull()249 public void testCurrentPackage_intentSupported_grantTimeIsNotNull() { 250 // Calling getFirstGrantTime will set grant time for the package 251 Optional<Instant> firstGrantTime = 252 mGrantTimeManager.getFirstGrantTime(SELF_PACKAGE_NAME, CURRENT_USER); 253 assertThat(firstGrantTime).isPresent(); 254 255 assertThat(firstGrantTime.get()).isGreaterThan(Instant.now().minusSeconds((long) 1e3)); 256 assertThat(firstGrantTime.get()).isLessThan(Instant.now().plusSeconds((long) 1e3)); 257 firstGrantTime.ifPresent( 258 grantTime -> { 259 assertThat(grantTime).isGreaterThan(Instant.now().minusSeconds((long) 1e3)); 260 assertThat(grantTime).isLessThan(Instant.now().plusSeconds((long) 1e3)); 261 }); 262 verify(mDatastore) 263 .writeForUser( 264 ArgumentMatchers.any(), 265 ArgumentMatchers.eq(CURRENT_USER), 266 ArgumentMatchers.eq(DATA_TYPE_CURRENT)); 267 verify(mDatastore) 268 .readForUser( 269 ArgumentMatchers.eq(CURRENT_USER), ArgumentMatchers.eq(DATA_TYPE_CURRENT)); 270 } 271 272 @Test testCurrentPackage_noGrantTimeBackupBecameAvailable_grantTimeEqualToStaged()273 public void testCurrentPackage_noGrantTimeBackupBecameAvailable_grantTimeEqualToStaged() { 274 assertThat(mGrantTimeManager.getFirstGrantTime(SELF_PACKAGE_NAME, CURRENT_USER)) 275 .isPresent(); 276 Instant backupTime = Instant.now().minusSeconds((long) 1e5); 277 UserGrantTimeState stagedState = setupGrantTimeState(null, backupTime); 278 mGrantTimeManager.applyAndStageGrantTimeStateForUser(CURRENT_USER, stagedState); 279 assertThat(mGrantTimeManager.getFirstGrantTime(SELF_PACKAGE_NAME, CURRENT_USER)) 280 .hasValue(backupTime); 281 } 282 283 @Test testCurrentPackage_noBackup_useRecordedTime()284 public void testCurrentPackage_noBackup_useRecordedTime() { 285 Instant stateTime = Instant.now().minusSeconds((long) 1e5); 286 UserGrantTimeState stagedState = setupGrantTimeState(stateTime, null); 287 288 assertThat(mGrantTimeManager.getFirstGrantTime(SELF_PACKAGE_NAME, CURRENT_USER)) 289 .hasValue(stateTime); 290 mGrantTimeManager.applyAndStageGrantTimeStateForUser(CURRENT_USER, stagedState); 291 assertThat(mGrantTimeManager.getFirstGrantTime(SELF_PACKAGE_NAME, CURRENT_USER)) 292 .hasValue(stateTime); 293 } 294 295 @Test testCurrentPackage_noBackup_grantTimeEqualToStaged()296 public void testCurrentPackage_noBackup_grantTimeEqualToStaged() { 297 Instant backupTime = Instant.now().minusSeconds((long) 1e5); 298 Instant stateTime = backupTime.plusSeconds(10); 299 UserGrantTimeState stagedState = setupGrantTimeState(stateTime, backupTime); 300 301 mGrantTimeManager.applyAndStageGrantTimeStateForUser(CURRENT_USER, stagedState); 302 assertThat(mGrantTimeManager.getFirstGrantTime(SELF_PACKAGE_NAME, CURRENT_USER)) 303 .hasValue(backupTime); 304 } 305 306 @Test testCurrentPackage_backupDataLater_stagedDataSkipped()307 public void testCurrentPackage_backupDataLater_stagedDataSkipped() { 308 Instant stateTime = Instant.now().minusSeconds((long) 1e5); 309 UserGrantTimeState stagedState = setupGrantTimeState(stateTime, stateTime.plusSeconds(1)); 310 311 mGrantTimeManager.applyAndStageGrantTimeStateForUser(CURRENT_USER, stagedState); 312 assertThat(mGrantTimeManager.getFirstGrantTime(SELF_PACKAGE_NAME, CURRENT_USER)) 313 .hasValue(stateTime); 314 } 315 316 @Test testWriteStagedData_getStagedStateForCurrentPackage_returnsCorrectState()317 public void testWriteStagedData_getStagedStateForCurrentPackage_returnsCorrectState() { 318 Instant stateTime = Instant.now().minusSeconds((long) 1e5); 319 setupGrantTimeState(stateTime, null); 320 321 UserGrantTimeState state = mGrantTimeManager.getGrantTimeStateForUser(CURRENT_USER); 322 assertThat(state.getSharedUserGrantTimes()).isEmpty(); 323 assertThat(state.getPackageGrantTimes().containsKey(SELF_PACKAGE_NAME)).isTrue(); 324 assertThat(state.getPackageGrantTimes().get(SELF_PACKAGE_NAME)).isEqualTo(stateTime); 325 } 326 327 @Test(expected = HealthConnectException.class) testReadRecords_withNoIntent_throwsException()328 public <T extends Record> void testReadRecords_withNoIntent_throwsException() 329 throws InterruptedException { 330 TimeInstantRangeFilter filter = 331 new TimeInstantRangeFilter.Builder() 332 .setStartTime(Instant.now()) 333 .setEndTime(Instant.now().plusMillis(3000)) 334 .build(); 335 ReadRecordsRequestUsingFilters<StepsRecord> request = 336 new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class) 337 .setTimeRangeFilter(filter) 338 .build(); 339 readRecords(request); 340 } 341 setupGrantTimeState(Instant currentTime, Instant stagedTime)342 private UserGrantTimeState setupGrantTimeState(Instant currentTime, Instant stagedTime) { 343 if (currentTime != null) { 344 UserGrantTimeState state = new UserGrantTimeState(DEFAULT_VERSION); 345 state.setPackageGrantTime(SELF_PACKAGE_NAME, currentTime); 346 when(mDatastore.readForUser(CURRENT_USER, DATA_TYPE_CURRENT)).thenReturn(state); 347 } 348 349 UserGrantTimeState backupState = new UserGrantTimeState(DEFAULT_VERSION); 350 if (stagedTime != null) { 351 backupState.setPackageGrantTime(SELF_PACKAGE_NAME, stagedTime); 352 } 353 when(mDatastore.readForUser(CURRENT_USER, DATA_TYPE_STAGED)).thenReturn(backupState); 354 return backupState; 355 } 356 readRecords(ReadRecordsRequest<T> request)357 private static <T extends Record> List<T> readRecords(ReadRecordsRequest<T> request) 358 throws InterruptedException { 359 Context context = InstrumentationRegistry.getInstrumentation().getContext(); 360 HealthConnectManager service = context.getSystemService(HealthConnectManager.class); 361 CountDownLatch latch = new CountDownLatch(1); 362 assertThat(service).isNotNull(); 363 assertThat(request.getRecordType()).isNotNull(); 364 AtomicReference<List<T>> response = new AtomicReference<>(); 365 AtomicReference<HealthConnectException> healthConnectExceptionAtomicReference = 366 new AtomicReference<>(); 367 service.readRecords( 368 request, 369 Executors.newSingleThreadExecutor(), 370 new OutcomeReceiver<>() { 371 @Override 372 public void onResult(ReadRecordsResponse<T> result) { 373 response.set(result.getRecords()); 374 latch.countDown(); 375 } 376 377 @Override 378 public void onError(HealthConnectException exception) { 379 healthConnectExceptionAtomicReference.set(exception); 380 latch.countDown(); 381 } 382 }); 383 assertThat(latch.await(3, TimeUnit.SECONDS)).isEqualTo(true); 384 if (healthConnectExceptionAtomicReference.get() != null) { 385 throw healthConnectExceptionAtomicReference.get(); 386 } 387 return response.get(); 388 } 389 } 390