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