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.logging;
18 
19 import static android.healthconnect.cts.HostSideTestUtil.isHardwareSupported;
20 
21 import static com.google.common.truth.Truth.assertThat;
22 
23 import android.cts.statsdatom.lib.AtomTestUtils;
24 import android.cts.statsdatom.lib.ConfigUtils;
25 import android.cts.statsdatom.lib.DeviceUtils;
26 import android.cts.statsdatom.lib.ReportUtils;
27 import android.healthconnect.cts.HostSideTestUtil;
28 
29 import com.android.os.StatsLog;
30 import com.android.os.healthfitness.api.ApiExtensionAtoms;
31 import com.android.os.healthfitness.api.HealthConnectStorageStats;
32 import com.android.os.healthfitness.api.HealthConnectUsageStats;
33 import com.android.tradefed.build.IBuildInfo;
34 import com.android.tradefed.device.DeviceNotAvailableException;
35 import com.android.tradefed.testtype.DeviceTestCase;
36 import com.android.tradefed.testtype.IBuildReceiver;
37 import com.android.tradefed.util.CommandStatus;
38 import com.android.tradefed.util.RunUtil;
39 
40 import com.google.protobuf.ExtensionRegistry;
41 
42 import java.time.Duration;
43 import java.time.Instant;
44 import java.time.temporal.ChronoUnit;
45 import java.util.Date;
46 import java.util.List;
47 
48 public class HealthConnectDailyLogsStatsTests extends DeviceTestCase implements IBuildReceiver {
49 
50     public static final String TEST_APP_PKG_NAME = "android.healthconnect.cts.testhelper";
51     private static final int NUMBER_OF_RETRIES = 10;
52     private static final String DAILY_LOG_TESTS_ACTIVITY = ".DailyLogsTests";
53     private static final String HEALTH_CONNECT_SERVICE_LOG_TESTS_ACTIVITY =
54             ".HealthConnectServiceLogsTests";
55     private IBuildInfo mCtsBuild;
56     private Instant mTestStartTime;
57     private Instant mTestStartTimeOnDevice;
58 
59     @Override
setUp()60     protected void setUp() throws Exception {
61         if (!isHardwareSupported(getDevice())) {
62             return;
63         }
64         super.setUp();
65         assertThat(mCtsBuild).isNotNull();
66         assertThat(isHardwareSupported(getDevice())).isTrue();
67         // TODO(b/313055175): Do not disable rate limiting once b/300238889 is resolved.
68         HostSideTestUtil.setupRateLimitingFeatureFlag(getDevice());
69         mTestStartTime = Instant.now();
70         mTestStartTimeOnDevice = Instant.ofEpochMilli(getDevice().getDeviceDate());
71         ConfigUtils.removeConfig(getDevice());
72         ReportUtils.clearReports(getDevice());
73         clearData();
74         // Doing this to avoid any access log entries which might make the test flaky.
75         increaseDeviceTimeByDays(/* numberOfDays= */ 31);
76     }
77 
78     @Override
tearDown()79     protected void tearDown() throws Exception {
80         if (!isHardwareSupported(getDevice())) {
81             return;
82         }
83         ConfigUtils.removeConfig(getDevice());
84         ReportUtils.clearReports(getDevice());
85         clearData();
86         // TODO(b/313055175): Do not disable rate limiting once b/300238889 is resolved.
87         HostSideTestUtil.restoreRateLimitingFeatureFlag(getDevice());
88         resetTime();
89         super.tearDown();
90     }
91 
92     @Override
setBuild(IBuildInfo buildInfo)93     public void setBuild(IBuildInfo buildInfo) {
94         mCtsBuild = buildInfo;
95     }
96 
testConnectedApps()97     public void testConnectedApps() throws Exception {
98         if (!isHardwareSupported(getDevice())) {
99             return;
100         }
101         ConfigUtils.uploadConfigForPushedAtoms(
102                 getDevice(),
103                 TEST_APP_PKG_NAME,
104                 new int[] {ApiExtensionAtoms.HEALTH_CONNECT_USAGE_STATS_FIELD_NUMBER});
105 
106         List<StatsLog.EventMetricData> data =
107                 getEventMetricDataList(/* testName= */ null, NUMBER_OF_RETRIES);
108         assertThat(data.size()).isAtLeast(1);
109         HealthConnectUsageStats atom =
110                 data.get(data.size() - 1)
111                         .getAtom()
112                         .getExtension(ApiExtensionAtoms.healthConnectUsageStats);
113 
114         assertThat(atom.getConnectedAppsCount()).isGreaterThan(0);
115         assertThat(atom.getAvailableAppsCount()).isGreaterThan(0);
116     }
117 
testDatabaseStats()118     public void testDatabaseStats() throws Exception {
119         if (!isHardwareSupported(getDevice())) {
120             return;
121         }
122         ConfigUtils.uploadConfigForPushedAtoms(
123                 getDevice(),
124                 TEST_APP_PKG_NAME,
125                 new int[] {ApiExtensionAtoms.HEALTH_CONNECT_STORAGE_STATS_FIELD_NUMBER});
126 
127         List<StatsLog.EventMetricData> data =
128                 getEventMetricDataList("testInsertRecordsSucceed", NUMBER_OF_RETRIES);
129         assertThat(data.size()).isAtLeast(1);
130         HealthConnectStorageStats atom =
131                 data.get(data.size() - 1)
132                         .getAtom()
133                         .getExtension(ApiExtensionAtoms.healthConnectStorageStats);
134 
135         assertThat(atom.getDatabaseSize()).isGreaterThan(0);
136         assertThat(atom.getInstantDataCount()).isEqualTo(1);
137         assertThat(atom.getIntervalDataCount()).isEqualTo(1);
138         assertThat(atom.getSeriesDataCount()).isEqualTo(1);
139         assertThat(atom.getChangelogCount()).isGreaterThan(2);
140     }
141 
testIsUserActive_insertRecord_userMonthlyActiveNextDay()142     public void testIsUserActive_insertRecord_userMonthlyActiveNextDay() throws Exception {
143         if (!isHardwareSupported(getDevice())) {
144             return;
145         }
146 
147         ConfigUtils.uploadConfigForPushedAtoms(
148                 getDevice(),
149                 TEST_APP_PKG_NAME,
150                 new int[] {ApiExtensionAtoms.HEALTH_CONNECT_USAGE_STATS_FIELD_NUMBER});
151         triggerTestInTestApp(
152                 HEALTH_CONNECT_SERVICE_LOG_TESTS_ACTIVITY, "testHealthConnectInsertRecords");
153         increaseDeviceTimeByDays(/* numberOfDays= */ 1);
154 
155         List<StatsLog.EventMetricData> data =
156                 getEventMetricDataList(/* testName= */ null, NUMBER_OF_RETRIES);
157         assertThat(data.size()).isAtLeast(1);
158         HealthConnectUsageStats atom =
159                 data.get(data.size() - 1)
160                         .getAtom()
161                         .getExtension(ApiExtensionAtoms.healthConnectUsageStats);
162 
163         assertThat(atom.getIsMonthlyActiveUser()).isTrue();
164     }
165 
testIsUserActive_insertRecord_userMonthlyActiveBetween7To30days()166     public void testIsUserActive_insertRecord_userMonthlyActiveBetween7To30days() throws Exception {
167         if (!isHardwareSupported(getDevice())) {
168             return;
169         }
170 
171         ConfigUtils.uploadConfigForPushedAtoms(
172                 getDevice(),
173                 TEST_APP_PKG_NAME,
174                 new int[] {ApiExtensionAtoms.HEALTH_CONNECT_USAGE_STATS_FIELD_NUMBER});
175         triggerTestInTestApp(
176                 HEALTH_CONNECT_SERVICE_LOG_TESTS_ACTIVITY, "testHealthConnectInsertRecords");
177         increaseDeviceTimeByDays(/* numberOfDays= */ 10);
178 
179         List<StatsLog.EventMetricData> data =
180                 getEventMetricDataList(/* testName= */ null, NUMBER_OF_RETRIES);
181         assertThat(data.size()).isAtLeast(1);
182         HealthConnectUsageStats atom =
183                 data.get(data.size() - 1)
184                         .getAtom()
185                         .getExtension(ApiExtensionAtoms.healthConnectUsageStats);
186 
187         assertThat(atom.getIsMonthlyActiveUser()).isTrue();
188     }
189 
testIsUserActive_insertRecord_userNotMonthlyActiveAfter30Days()190     public void testIsUserActive_insertRecord_userNotMonthlyActiveAfter30Days() throws Exception {
191         if (!isHardwareSupported(getDevice())) {
192             return;
193         }
194 
195         ConfigUtils.uploadConfigForPushedAtoms(
196                 getDevice(),
197                 TEST_APP_PKG_NAME,
198                 new int[] {ApiExtensionAtoms.HEALTH_CONNECT_USAGE_STATS_FIELD_NUMBER});
199         triggerTestInTestApp(
200                 HEALTH_CONNECT_SERVICE_LOG_TESTS_ACTIVITY, "testHealthConnectInsertRecords");
201         increaseDeviceTimeByDays(/* numberOfDays= */ 35);
202 
203         List<StatsLog.EventMetricData> data =
204                 getEventMetricDataList(/* testName= */ null, NUMBER_OF_RETRIES);
205         assertThat(data.size()).isAtLeast(1);
206         HealthConnectUsageStats atom =
207                 data.get(data.size() - 1)
208                         .getAtom()
209                         .getExtension(ApiExtensionAtoms.healthConnectUsageStats);
210 
211         assertThat(atom.getIsMonthlyActiveUser()).isFalse();
212     }
213 
testIsUserActive_readRecord_userMonthlyActiveNextDay()214     public void testIsUserActive_readRecord_userMonthlyActiveNextDay() throws Exception {
215         if (!isHardwareSupported(getDevice())) {
216             return;
217         }
218 
219         ConfigUtils.uploadConfigForPushedAtoms(
220                 getDevice(),
221                 TEST_APP_PKG_NAME,
222                 new int[] {ApiExtensionAtoms.HEALTH_CONNECT_USAGE_STATS_FIELD_NUMBER});
223         triggerTestInTestApp(
224                 HEALTH_CONNECT_SERVICE_LOG_TESTS_ACTIVITY, "testHealthConnectReadRecords");
225         increaseDeviceTimeByDays(/* numberOfDays= */ 1);
226 
227         List<StatsLog.EventMetricData> data =
228                 getEventMetricDataList(/* testName= */ null, NUMBER_OF_RETRIES);
229         assertThat(data.size()).isAtLeast(1);
230         HealthConnectUsageStats atom =
231                 data.get(data.size() - 1)
232                         .getAtom()
233                         .getExtension(ApiExtensionAtoms.healthConnectUsageStats);
234 
235         assertThat(atom.getIsMonthlyActiveUser()).isTrue();
236     }
237 
testIsUserActive_readRecord_userMonthlyActiveBetween7To30days()238     public void testIsUserActive_readRecord_userMonthlyActiveBetween7To30days() throws Exception {
239         if (!isHardwareSupported(getDevice())) {
240             return;
241         }
242 
243         ConfigUtils.uploadConfigForPushedAtoms(
244                 getDevice(),
245                 TEST_APP_PKG_NAME,
246                 new int[] {ApiExtensionAtoms.HEALTH_CONNECT_USAGE_STATS_FIELD_NUMBER});
247         triggerTestInTestApp(
248                 HEALTH_CONNECT_SERVICE_LOG_TESTS_ACTIVITY, "testHealthConnectReadRecords");
249         increaseDeviceTimeByDays(/* numberOfDays= */ 10);
250 
251         List<StatsLog.EventMetricData> data =
252                 getEventMetricDataList(/* testName= */ null, NUMBER_OF_RETRIES);
253         assertThat(data.size()).isAtLeast(1);
254         HealthConnectUsageStats atom =
255                 data.get(data.size() - 1)
256                         .getAtom()
257                         .getExtension(ApiExtensionAtoms.healthConnectUsageStats);
258 
259         assertThat(atom.getIsMonthlyActiveUser()).isTrue();
260     }
261 
testIsUserActive_readRecord_userNotMonthlyActiveAfter30Days()262     public void testIsUserActive_readRecord_userNotMonthlyActiveAfter30Days() throws Exception {
263         if (!isHardwareSupported(getDevice())) {
264             return;
265         }
266 
267         ConfigUtils.uploadConfigForPushedAtoms(
268                 getDevice(),
269                 TEST_APP_PKG_NAME,
270                 new int[] {ApiExtensionAtoms.HEALTH_CONNECT_USAGE_STATS_FIELD_NUMBER});
271         triggerTestInTestApp(
272                 HEALTH_CONNECT_SERVICE_LOG_TESTS_ACTIVITY, "testHealthConnectReadRecords");
273         increaseDeviceTimeByDays(/* numberOfDays= */ 35);
274 
275         List<StatsLog.EventMetricData> data =
276                 getEventMetricDataList(/* testName= */ null, NUMBER_OF_RETRIES);
277         assertThat(data.size()).isAtLeast(1);
278         HealthConnectUsageStats atom =
279                 data.get(data.size() - 1)
280                         .getAtom()
281                         .getExtension(ApiExtensionAtoms.healthConnectUsageStats);
282 
283         assertThat(atom.getIsMonthlyActiveUser()).isFalse();
284     }
285 
getEventMetricDataList(String testName, int retryCount)286     private List<StatsLog.EventMetricData> getEventMetricDataList(String testName, int retryCount)
287             throws Exception {
288         if (retryCount == 0) {
289             throw new RuntimeException("Could not collect metrics.");
290         }
291 
292         ExtensionRegistry extensionRegistry =
293                 triggerTestInTestApp(DAILY_LOG_TESTS_ACTIVITY, testName);
294         triggerDailyJob(); // This will run the job which calls DailyLogger to log some metrics.
295         List<StatsLog.EventMetricData> data =
296                 ReportUtils.getEventMetricDataList(getDevice(), extensionRegistry);
297 
298         if (data.size() == 0) {
299             return getEventMetricDataList(testName, retryCount - 1);
300         }
301         return data;
302     }
303 
clearData()304     private void clearData() throws Exception {
305         triggerTestInTestApp(DAILY_LOG_TESTS_ACTIVITY, "deleteAllRecordsAddedForTest");
306         // Next two lines will delete newly added Access Logs as all access logs over 7 days are
307         // deleted by the AutoDeleteService which is run by the daily job.
308         increaseDeviceTimeByDays(10);
309     }
310 
triggerTestInTestApp(String className, String testName)311     private ExtensionRegistry triggerTestInTestApp(String className, String testName)
312             throws Exception {
313 
314         if (testName != null) {
315             DeviceUtils.runDeviceTests(getDevice(), TEST_APP_PKG_NAME, className, testName);
316         }
317 
318         ExtensionRegistry registry = ExtensionRegistry.newInstance();
319         ApiExtensionAtoms.registerAllExtensions(registry);
320         return registry;
321     }
322 
triggerDailyJob()323     private void triggerDailyJob() throws Exception {
324 
325         // There are multiple instances of HealthConnectDailyService. This command finds the one
326         // that needs to be triggered for this test using the job param 'hc_daily_job'.
327         String output =
328                 getDevice()
329                         .executeShellCommand(
330                                 "dumpsys jobscheduler | grep -m1 -A0 -B10 \"hc_daily_job\"");
331         int indexOfStart = output.indexOf("/") + 1;
332         String jobId = output.substring(indexOfStart, output.indexOf(":", indexOfStart));
333         String jobExecutionCommand =
334                 "cmd jobscheduler run --namespace HEALTH_CONNECT_DAILY_JOB -f android " + jobId;
335 
336         executeLoggingJob(jobExecutionCommand, NUMBER_OF_RETRIES);
337         RunUtil.getDefault().sleep(AtomTestUtils.WAIT_TIME_LONG);
338     }
339 
executeLoggingJob(String jobExecutionCommand, int retry)340     private void executeLoggingJob(String jobExecutionCommand, int retry)
341             throws DeviceNotAvailableException, RuntimeException {
342         if (retry == 0) {
343             throw new RuntimeException("Could not execute job");
344         }
345         if (getDevice().executeShellV2Command(jobExecutionCommand).getStatus()
346                 != CommandStatus.SUCCESS) {
347             executeLoggingJob(jobExecutionCommand, retry - 1);
348         }
349     }
350 
increaseDeviceTimeByDays(int numberOfDays)351     private void increaseDeviceTimeByDays(int numberOfDays) throws Exception {
352         // This is to ensure that the essential daily functions run before increasing device time.
353         triggerDailyJob();
354 
355         Instant deviceDate = Instant.ofEpochMilli(getDevice().getDeviceDate());
356         getDevice().setDate(Date.from(deviceDate.plus(numberOfDays, ChronoUnit.DAYS)));
357         getDevice()
358                 .executeShellCommand(
359                         "cmd time_detector set_time_state_for_tests --unix_epoch_time "
360                                 + deviceDate.plus(numberOfDays, ChronoUnit.DAYS).toEpochMilli()
361                                 + " --user_should_confirm_time false --elapsed_realtime 0");
362 
363         getDevice().executeShellCommand("am broadcast -a android.intent.action.TIME_SET");
364 
365         // This is to ensure that the essential daily functions run after increasing device time.
366         triggerDailyJob();
367     }
368 
resetTime()369     private void resetTime() throws DeviceNotAvailableException {
370         long timeDiff = Duration.between(mTestStartTime, Instant.now()).toMillis();
371 
372         getDevice()
373                 .executeShellCommand(
374                         "cmd time_detector set_time_state_for_tests --unix_epoch_time "
375                                 + mTestStartTimeOnDevice.plusMillis(timeDiff).toEpochMilli()
376                                 + " --user_should_confirm_time false --elapsed_realtime 0");
377         getDevice().executeShellCommand("am broadcast -a android.intent.action.TIME_SET");
378     }
379 }
380