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