• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
23 import android.annotation.NonNull;
24 import android.content.ContentValues;
25 import android.database.Cursor;
26 import android.database.sqlite.SQLiteDatabase;
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 {
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     private static volatile ActivityDateHelper sActivityDateHelper;
60 
ActivityDateHelper()61     private ActivityDateHelper() {}
62 
63     /**
64      * Returns a requests representing the tables that should be created corresponding to this
65      * helper
66      */
67     @NonNull
getCreateTableRequest()68     public CreateTableRequest getCreateTableRequest() {
69         return new CreateTableRequest(TABLE_NAME, getColumnInfo())
70                 .addUniqueConstraints(List.of(EPOCH_DAYS_COLUMN_NAME, RECORD_TYPE_ID_COLUMN_NAME));
71     }
72 
73     /** Called on DB update. */
onUpgrade(int oldVersion, int newVersion, @NonNull SQLiteDatabase db)74     public void onUpgrade(int oldVersion, int newVersion, @NonNull SQLiteDatabase db) {}
75 
76     /** Deletes all entries from the database and clears the cache. */
clearData(TransactionManager transactionManager)77     public synchronized void clearData(TransactionManager transactionManager) {
78         transactionManager.delete(new DeleteTableRequest(TABLE_NAME));
79     }
80 
81     /** Insert a new activity dates for the given records */
82     @NonNull
insertRecordDate(@onNull List<RecordInternal<?>> recordInternals)83     public void insertRecordDate(@NonNull List<RecordInternal<?>> recordInternals) {
84         Objects.requireNonNull(recordInternals);
85 
86         final TransactionManager transactionManager = TransactionManager.getInitialisedInstance();
87 
88         List<UpsertTableRequest> upsertTableRequests = new ArrayList<>();
89         recordInternals.forEach(
90                 (recordInternal) -> upsertTableRequests.add(getUpsertTableRequest(recordInternal)));
91 
92         transactionManager.insertOrIgnoreOnConflict(upsertTableRequests);
93     }
94 
95     /** Returns a list of all dates with database writes for the given record types */
96     @NonNull
getActivityDates(@onNull List<Class<? extends Record>> recordTypes)97     public List<LocalDate> getActivityDates(@NonNull List<Class<? extends Record>> recordTypes) {
98         RecordMapper recordMapper = RecordMapper.getInstance();
99         List<Integer> recordTypeIds =
100                 recordTypes.stream().map(recordMapper::getRecordType).collect(Collectors.toList());
101 
102         return readDates(
103                 new ReadTableRequest(TABLE_NAME)
104                         .setWhereClause(
105                                 new WhereClauses()
106                                         .addWhereInIntsClause(
107                                                 RECORD_TYPE_ID_COLUMN_NAME, recordTypeIds))
108                         .setColumnNames(List.of(EPOCH_DAYS_COLUMN_NAME))
109                         .setDistinctClause(true));
110     }
111 
reSyncForAllRecords()112     public void reSyncForAllRecords() {
113         List<Integer> recordTypeIds =
114                 RecordMapper.getInstance().getRecordIdToExternalRecordClassMap().keySet().stream()
115                         .toList();
116 
117         reSyncByRecordTypeIds(recordTypeIds);
118     }
119 
reSyncByRecordTypeIds(List<Integer> recordTypeIds)120     public void reSyncByRecordTypeIds(List<Integer> recordTypeIds) {
121         List<UpsertTableRequest> upsertTableRequests = new ArrayList<>();
122         final TransactionManager transactionManager = TransactionManager.getInitialisedInstance();
123 
124         DeleteTableRequest deleteTableRequest =
125                 new DeleteTableRequest(TABLE_NAME)
126                         .setIds(
127                                 RECORD_TYPE_ID_COLUMN_NAME,
128                                 recordTypeIds.stream().map(String::valueOf).toList());
129 
130         // Fetch updated dates from respective record table and update the activity dates cache.
131         HashMap<Integer, List<Long>> recordTypeIdToEpochDays =
132                 fetchUpdatedDates(recordTypeIds, transactionManager);
133 
134         recordTypeIdToEpochDays.forEach(
135                 (recordTypeId, epochDays) ->
136                         epochDays.forEach(
137                                 (epochDay) ->
138                                         upsertTableRequests.add(
139                                                 getUpsertTableRequest(recordTypeId, epochDay))));
140 
141         transactionManager.runAsTransaction(
142                 db -> {
143                     db.execSQL(deleteTableRequest.getDeleteCommand());
144                     upsertTableRequests.forEach(
145                             upsertTableRequest ->
146                                     transactionManager.insertOrIgnore(db, upsertTableRequest));
147                 });
148     }
149 
150     @NonNull
getColumnInfo()151     List<Pair<String, String>> getColumnInfo() {
152         return Arrays.asList(
153                 new Pair<>(RecordHelper.PRIMARY_COLUMN_NAME, PRIMARY_AUTOINCREMENT),
154                 new Pair<>(EPOCH_DAYS_COLUMN_NAME, INTEGER_NOT_NULL),
155                 new Pair<>(RECORD_TYPE_ID_COLUMN_NAME, INTEGER_NOT_NULL));
156     }
157 
fetchUpdatedDates( List<Integer> recordTypeIds, TransactionManager transactionManager)158     private HashMap<Integer, List<Long>> fetchUpdatedDates(
159             List<Integer> recordTypeIds, TransactionManager transactionManager) {
160 
161         ReadTableRequest request;
162         RecordHelper<?> recordHelper;
163         HashMap<Integer, List<Long>> recordTypeIdToEpochDays = new HashMap<>();
164         for (int recordTypeId : recordTypeIds) {
165             recordHelper = RecordHelperProvider.getInstance().getRecordHelper(recordTypeId);
166             request =
167                     new ReadTableRequest(recordHelper.getMainTableName())
168                             .setColumnNames(List.of(recordHelper.getPeriodGroupByColumnName()))
169                             .setDistinctClause(true);
170             try (Cursor cursor = transactionManager.read(request)) {
171                 List<Long> distinctDates = new ArrayList<>();
172                 while (cursor.moveToNext()) {
173                     long epochDay =
174                             getCursorLong(cursor, recordHelper.getPeriodGroupByColumnName());
175                     distinctDates.add(epochDay);
176                 }
177                 recordTypeIdToEpochDays.put(recordTypeId, distinctDates);
178             }
179         }
180         return recordTypeIdToEpochDays;
181     }
182 
183     @NonNull
getContentValues(int recordTypeId, long epochDays)184     private ContentValues getContentValues(int recordTypeId, long epochDays) {
185         ContentValues contentValues = new ContentValues();
186         contentValues.put(EPOCH_DAYS_COLUMN_NAME, epochDays);
187         contentValues.put(RECORD_TYPE_ID_COLUMN_NAME, recordTypeId);
188 
189         return contentValues;
190     }
191 
192     /**
193      * Reads the dates stored in the HealthConnect database.
194      *
195      * @param request a read request.
196      * @return Cursor from table based on ids.
197      */
readDates(@onNull ReadTableRequest request)198     private List<LocalDate> readDates(@NonNull ReadTableRequest request) {
199         final TransactionManager transactionManager = TransactionManager.getInitialisedInstance();
200         try (Cursor cursor = transactionManager.read(request)) {
201             List<LocalDate> dates = new ArrayList<>();
202             while (cursor.moveToNext()) {
203                 long epochDay = getCursorLong(cursor, EPOCH_DAYS_COLUMN_NAME);
204                 dates.add(LocalDate.ofEpochDay(epochDay));
205             }
206             return dates;
207         }
208     }
209 
210     /** Returns an instance of this class */
getInstance()211     public static synchronized ActivityDateHelper getInstance() {
212         if (sActivityDateHelper == null) {
213             sActivityDateHelper = new ActivityDateHelper();
214         }
215 
216         return sActivityDateHelper;
217     }
218 
219     /** Creates UpsertTableRequest to insert into activity_date_table table. */
getUpsertTableRequest(int recordTypeId, long epochDays)220     public UpsertTableRequest getUpsertTableRequest(int recordTypeId, long epochDays) {
221         return new UpsertTableRequest(TABLE_NAME, getContentValues(recordTypeId, epochDays));
222     }
223 
224     /** Creates UpsertTableRequest to insert into activity_date_table table from recordInternal. */
getUpsertTableRequest(RecordInternal<?> recordInternal)225     public UpsertTableRequest getUpsertTableRequest(RecordInternal<?> recordInternal) {
226         return getUpsertTableRequest(
227                 recordInternal.getRecordType(),
228                 ChronoUnit.DAYS.between(LocalDate.EPOCH, recordInternal.getLocalDate()));
229     }
230 }
231