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