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 com.android.server.healthconnect.storage.datatypehelpers;
18 
19 import static com.android.server.healthconnect.storage.utils.StorageUtils.INTEGER_NOT_NULL;
20 import static com.android.server.healthconnect.storage.utils.StorageUtils.PRIMARY_AUTOINCREMENT;
21 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorLong;
22 import static com.android.server.healthconnect.storage.utils.WhereClauses.LogicalOperator.AND;
23 
24 import android.annotation.NonNull;
25 import android.content.ContentValues;
26 import android.database.Cursor;
27 import android.health.connect.datatypes.Record;
28 import android.health.connect.internal.datatypes.RecordInternal;
29 import android.health.connect.internal.datatypes.utils.RecordMapper;
30 import android.util.Pair;
31 
32 import com.android.server.healthconnect.storage.TransactionManager;
33 import com.android.server.healthconnect.storage.request.CreateTableRequest;
34 import com.android.server.healthconnect.storage.request.DeleteTableRequest;
35 import com.android.server.healthconnect.storage.request.ReadTableRequest;
36 import com.android.server.healthconnect.storage.request.UpsertTableRequest;
37 import com.android.server.healthconnect.storage.utils.RecordHelperProvider;
38 import com.android.server.healthconnect.storage.utils.WhereClauses;
39 
40 import java.time.LocalDate;
41 import java.time.temporal.ChronoUnit;
42 import java.util.ArrayList;
43 import java.util.Arrays;
44 import java.util.HashMap;
45 import java.util.List;
46 import java.util.Objects;
47 import java.util.stream.Collectors;
48 
49 /**
50  * Helper for Activity Date Table. The table maps a record to a date on which there was a db write
51  * for that record
52  *
53  * @hide
54  */
55 public final class ActivityDateHelper extends DatabaseHelper {
56     private static final String TABLE_NAME = "activity_date_table";
57     private static final String EPOCH_DAYS_COLUMN_NAME = "epoch_days";
58     private static final String RECORD_TYPE_ID_COLUMN_NAME = "record_type_id";
59 
60     /**
61      * Returns a requests representing the tables that should be created corresponding to this
62      * helper
63      */
64     @NonNull
getCreateTableRequest()65     public static CreateTableRequest getCreateTableRequest() {
66         return new CreateTableRequest(TABLE_NAME, getColumnInfo())
67                 .addUniqueConstraints(List.of(EPOCH_DAYS_COLUMN_NAME, RECORD_TYPE_ID_COLUMN_NAME));
68     }
69 
70     @Override
getMainTableName()71     protected String getMainTableName() {
72         return TABLE_NAME;
73     }
74 
75     /** Insert a new activity dates for the given records */
76     @NonNull
insertRecordDate(@onNull List<RecordInternal<?>> recordInternals)77     public static void insertRecordDate(@NonNull List<RecordInternal<?>> recordInternals) {
78         Objects.requireNonNull(recordInternals);
79 
80         final TransactionManager transactionManager = TransactionManager.getInitialisedInstance();
81 
82         List<UpsertTableRequest> upsertTableRequests = new ArrayList<>();
83         recordInternals.forEach(
84                 (recordInternal) -> upsertTableRequests.add(getUpsertTableRequest(recordInternal)));
85 
86         transactionManager.insertOrIgnoreOnConflict(upsertTableRequests);
87     }
88 
89     /** Returns a list of all dates with database writes for the given record types */
90     @NonNull
getActivityDates( @onNull List<Class<? extends Record>> recordTypes)91     public static List<LocalDate> getActivityDates(
92             @NonNull List<Class<? extends Record>> recordTypes) {
93         RecordMapper recordMapper = RecordMapper.getInstance();
94         List<Integer> recordTypeIds =
95                 recordTypes.stream().map(recordMapper::getRecordType).collect(Collectors.toList());
96 
97         return readDates(
98                 new ReadTableRequest(TABLE_NAME)
99                         .setWhereClause(
100                                 new WhereClauses(AND)
101                                         .addWhereInIntsClause(
102                                                 RECORD_TYPE_ID_COLUMN_NAME, recordTypeIds))
103                         .setColumnNames(List.of(EPOCH_DAYS_COLUMN_NAME))
104                         .setDistinctClause(true));
105     }
106 
107     /** Updates the activity dates cache for all records */
reSyncForAllRecords()108     public static void reSyncForAllRecords() {
109         List<Integer> recordTypeIds =
110                 RecordMapper.getInstance().getRecordIdToExternalRecordClassMap().keySet().stream()
111                         .toList();
112 
113         reSyncByRecordTypeIds(recordTypeIds);
114     }
115 
116     /** Updates the activity dates cache for the given record IDs */
reSyncByRecordTypeIds(List<Integer> recordTypeIds)117     public static void reSyncByRecordTypeIds(List<Integer> recordTypeIds) {
118         List<UpsertTableRequest> upsertTableRequests = new ArrayList<>();
119         final TransactionManager transactionManager = TransactionManager.getInitialisedInstance();
120 
121         DeleteTableRequest deleteTableRequest =
122                 new DeleteTableRequest(TABLE_NAME)
123                         .setIds(
124                                 RECORD_TYPE_ID_COLUMN_NAME,
125                                 recordTypeIds.stream().map(String::valueOf).toList());
126 
127         // Fetch updated dates from respective record table and update the activity dates cache.
128         HashMap<Integer, List<Long>> recordTypeIdToEpochDays =
129                 fetchUpdatedDates(recordTypeIds, transactionManager);
130 
131         recordTypeIdToEpochDays.forEach(
132                 (recordTypeId, epochDays) ->
133                         epochDays.forEach(
134                                 (epochDay) ->
135                                         upsertTableRequests.add(
136                                                 getUpsertTableRequest(recordTypeId, epochDay))));
137 
138         transactionManager.runAsTransaction(
139                 db -> {
140                     db.execSQL(deleteTableRequest.getDeleteCommand());
141                     upsertTableRequests.forEach(
142                             upsertTableRequest ->
143                                     transactionManager.insertOrIgnore(db, upsertTableRequest));
144                 });
145     }
146 
147     @NonNull
getColumnInfo()148     private static List<Pair<String, String>> getColumnInfo() {
149         return Arrays.asList(
150                 new Pair<>(RecordHelper.PRIMARY_COLUMN_NAME, PRIMARY_AUTOINCREMENT),
151                 new Pair<>(EPOCH_DAYS_COLUMN_NAME, INTEGER_NOT_NULL),
152                 new Pair<>(RECORD_TYPE_ID_COLUMN_NAME, INTEGER_NOT_NULL));
153     }
154 
fetchUpdatedDates( List<Integer> recordTypeIds, TransactionManager transactionManager)155     private static HashMap<Integer, List<Long>> fetchUpdatedDates(
156             List<Integer> recordTypeIds, TransactionManager transactionManager) {
157 
158         ReadTableRequest request;
159         RecordHelper<?> recordHelper;
160         HashMap<Integer, List<Long>> recordTypeIdToEpochDays = new HashMap<>();
161         for (int recordTypeId : recordTypeIds) {
162             recordHelper = RecordHelperProvider.getRecordHelper(recordTypeId);
163             request =
164                     new ReadTableRequest(recordHelper.getMainTableName())
165                             .setColumnNames(List.of(recordHelper.getPeriodGroupByColumnName()))
166                             .setDistinctClause(true);
167             try (Cursor cursor = transactionManager.read(request)) {
168                 List<Long> distinctDates = new ArrayList<>();
169                 while (cursor.moveToNext()) {
170                     long epochDay =
171                             getCursorLong(cursor, recordHelper.getPeriodGroupByColumnName());
172                     distinctDates.add(epochDay);
173                 }
174                 recordTypeIdToEpochDays.put(recordTypeId, distinctDates);
175             }
176         }
177         return recordTypeIdToEpochDays;
178     }
179 
180     @NonNull
getContentValues(int recordTypeId, long epochDays)181     private static ContentValues getContentValues(int recordTypeId, long epochDays) {
182         ContentValues contentValues = new ContentValues();
183         contentValues.put(EPOCH_DAYS_COLUMN_NAME, epochDays);
184         contentValues.put(RECORD_TYPE_ID_COLUMN_NAME, recordTypeId);
185 
186         return contentValues;
187     }
188 
189     /**
190      * Reads the dates stored in the HealthConnect database.
191      *
192      * @param request a read request.
193      * @return Cursor from table based on ids.
194      */
readDates(@onNull ReadTableRequest request)195     private static List<LocalDate> readDates(@NonNull ReadTableRequest request) {
196         final TransactionManager transactionManager = TransactionManager.getInitialisedInstance();
197         try (Cursor cursor = transactionManager.read(request)) {
198             List<LocalDate> dates = new ArrayList<>();
199             while (cursor.moveToNext()) {
200                 long epochDay = getCursorLong(cursor, EPOCH_DAYS_COLUMN_NAME);
201                 dates.add(LocalDate.ofEpochDay(epochDay));
202             }
203             return dates;
204         }
205     }
206 
207     /** Creates UpsertTableRequest to insert into activity_date_table table. */
getUpsertTableRequest(int recordTypeId, long epochDays)208     public static UpsertTableRequest getUpsertTableRequest(int recordTypeId, long epochDays) {
209         return new UpsertTableRequest(TABLE_NAME, getContentValues(recordTypeId, epochDays));
210     }
211 
212     /** Creates UpsertTableRequest to insert into activity_date_table table from recordInternal. */
getUpsertTableRequest(RecordInternal<?> recordInternal)213     public static UpsertTableRequest getUpsertTableRequest(RecordInternal<?> recordInternal) {
214         return getUpsertTableRequest(
215                 recordInternal.getRecordType(),
216                 ChronoUnit.DAYS.between(LocalDate.EPOCH, recordInternal.getLocalDate()));
217     }
218 }
219