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; 20 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorInt; 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.SQLException; 27 import android.database.sqlite.SQLiteDatabase; 28 import android.health.connect.datatypes.RecordTypeIdentifier; 29 import android.health.connect.internal.datatypes.IntervalRecordInternal; 30 import android.util.Pair; 31 32 import com.android.server.healthconnect.storage.request.AlterTableRequest; 33 import com.android.server.healthconnect.storage.request.CreateTableRequest; 34 import com.android.server.healthconnect.storage.utils.StorageUtils; 35 36 import java.time.Instant; 37 import java.time.LocalDate; 38 import java.time.ZoneOffset; 39 import java.time.temporal.ChronoUnit; 40 import java.util.ArrayList; 41 import java.util.Arrays; 42 import java.util.List; 43 44 /** 45 * Parent class for all the Interval type records 46 * 47 * @param <T> internal record for which this class is responsible. 48 * @hide 49 */ 50 public abstract class IntervalRecordHelper<T extends IntervalRecordInternal<?>> 51 extends RecordHelper<T> { 52 public static final String START_TIME_COLUMN_NAME = "start_time"; 53 public static final String START_ZONE_OFFSET_COLUMN_NAME = "start_zone_offset"; 54 private static final String START_LOCAL_DATE_TIME_EXPRESSION = 55 START_TIME_COLUMN_NAME + " + 1000 * " + START_ZONE_OFFSET_COLUMN_NAME; 56 public static final String END_TIME_COLUMN_NAME = "end_time"; 57 public static final String END_ZONE_OFFSET_COLUMN_NAME = "end_zone_offset"; 58 private static final String END_LOCAL_DATE_TIME_EXPRESSION = 59 END_TIME_COLUMN_NAME + " + 1000 * " + END_ZONE_OFFSET_COLUMN_NAME; 60 private static final String LOCAL_DATE_COLUMN_NAME = "local_date"; 61 public static final String LOCAL_DATE_TIME_START_TIME_COLUMN_NAME = 62 "local_date_time_start_time"; 63 public static final String LOCAL_DATE_TIME_END_TIME_COLUMN_NAME = "local_date_time_end_time"; 64 IntervalRecordHelper(@ecordTypeIdentifier.RecordType int recordIdentifier)65 IntervalRecordHelper(@RecordTypeIdentifier.RecordType int recordIdentifier) { 66 super(recordIdentifier); 67 } 68 69 @Override getStartTimeColumnName()70 public final String getStartTimeColumnName() { 71 return START_TIME_COLUMN_NAME; 72 } 73 74 @Override getLocalStartTimeColumnName()75 public final String getLocalStartTimeColumnName() { 76 return LOCAL_DATE_TIME_START_TIME_COLUMN_NAME; 77 } 78 79 @Override getEndTimeColumnName()80 public final String getEndTimeColumnName() { 81 return END_TIME_COLUMN_NAME; 82 } 83 84 @Override getLocalEndTimeColumnName()85 public final String getLocalEndTimeColumnName() { 86 return LOCAL_DATE_TIME_END_TIME_COLUMN_NAME; 87 } 88 89 @Override applyGeneratedLocalTimeUpgrade(@onNull SQLiteDatabase db)90 public final void applyGeneratedLocalTimeUpgrade(@NonNull SQLiteDatabase db) { 91 try { 92 db.execSQL( 93 AlterTableRequest.getAlterTableCommandToAddGeneratedColumn( 94 getMainTableName(), 95 new CreateTableRequest.GeneratedColumnInfo( 96 LOCAL_DATE_TIME_START_TIME_COLUMN_NAME, 97 INTEGER, 98 START_LOCAL_DATE_TIME_EXPRESSION))); 99 db.execSQL( 100 AlterTableRequest.getAlterTableCommandToAddGeneratedColumn( 101 getMainTableName(), 102 new CreateTableRequest.GeneratedColumnInfo( 103 LOCAL_DATE_TIME_END_TIME_COLUMN_NAME, 104 INTEGER, 105 END_LOCAL_DATE_TIME_EXPRESSION))); 106 } catch (SQLException sqlException) { 107 // Ignore this means the field exists. This is possible via module rollback followed by 108 // an upgrade 109 } 110 } 111 112 113 @Override 114 @NonNull getGeneratedColumnInfo()115 protected List<CreateTableRequest.GeneratedColumnInfo> getGeneratedColumnInfo() { 116 return List.of( 117 new CreateTableRequest.GeneratedColumnInfo( 118 LOCAL_DATE_TIME_START_TIME_COLUMN_NAME, 119 INTEGER, 120 START_LOCAL_DATE_TIME_EXPRESSION), 121 new CreateTableRequest.GeneratedColumnInfo( 122 LOCAL_DATE_TIME_END_TIME_COLUMN_NAME, 123 INTEGER, 124 END_LOCAL_DATE_TIME_EXPRESSION)); 125 } 126 127 @Override getDurationGroupByColumnName()128 public final String getDurationGroupByColumnName() { 129 return START_TIME_COLUMN_NAME; 130 } 131 132 @Override getPeriodGroupByColumnName()133 public final String getPeriodGroupByColumnName() { 134 return LOCAL_DATE_COLUMN_NAME; 135 } 136 137 @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression getZoneOffset(Cursor cursor)138 final ZoneOffset getZoneOffset(Cursor cursor) { 139 ZoneOffset zoneOffset = null; 140 if (cursor.getCount() > 0 && cursor.getColumnIndex(START_ZONE_OFFSET_COLUMN_NAME) != -1) { 141 zoneOffset = 142 ZoneOffset.ofTotalSeconds( 143 StorageUtils.getCursorInt(cursor, START_ZONE_OFFSET_COLUMN_NAME)); 144 } 145 146 return zoneOffset; 147 } 148 149 /** 150 * This implementation should return the column names with which the table should be created. 151 * 152 * <p>NOTE: New columns can only be added via onUpgrade. Why? Consider what happens if a table 153 * already exists on the device 154 * 155 * <p>PLEASE DON'T USE THIS METHOD TO ADD NEW COLUMNS 156 */ 157 @Override 158 @NonNull getSpecificColumnInfo()159 final List<Pair<String, String>> getSpecificColumnInfo() { 160 ArrayList<Pair<String, String>> columnInfo = new ArrayList<>(); 161 columnInfo.add(new Pair<>(START_TIME_COLUMN_NAME, INTEGER)); 162 columnInfo.add(new Pair<>(START_ZONE_OFFSET_COLUMN_NAME, INTEGER)); 163 columnInfo.add(new Pair<>(END_TIME_COLUMN_NAME, INTEGER)); 164 columnInfo.add(new Pair<>(END_ZONE_OFFSET_COLUMN_NAME, INTEGER)); 165 columnInfo.add(new Pair<>(LOCAL_DATE_COLUMN_NAME, INTEGER)); 166 167 columnInfo.addAll(getIntervalRecordColumnInfo()); 168 169 return columnInfo; 170 } 171 172 @Override populateContentValues( @onNull ContentValues contentValues, @NonNull T intervalRecord)173 final void populateContentValues( 174 @NonNull ContentValues contentValues, @NonNull T intervalRecord) { 175 contentValues.put(START_TIME_COLUMN_NAME, intervalRecord.getStartTimeInMillis()); 176 contentValues.put( 177 START_ZONE_OFFSET_COLUMN_NAME, intervalRecord.getStartZoneOffsetInSeconds()); 178 contentValues.put(END_TIME_COLUMN_NAME, intervalRecord.getEndTimeInMillis()); 179 contentValues.put(END_ZONE_OFFSET_COLUMN_NAME, intervalRecord.getEndZoneOffsetInSeconds()); 180 contentValues.put( 181 LOCAL_DATE_COLUMN_NAME, 182 ChronoUnit.DAYS.between( 183 LocalDate.ofEpochDay(0), 184 LocalDate.ofInstant( 185 Instant.ofEpochMilli(intervalRecord.getStartTimeInMillis()), 186 ZoneOffset.ofTotalSeconds( 187 intervalRecord.getStartZoneOffsetInSeconds())))); 188 189 populateSpecificContentValues(contentValues, intervalRecord); 190 } 191 192 @Override populateRecordValue(@onNull Cursor cursor, @NonNull T recordInternal)193 final void populateRecordValue(@NonNull Cursor cursor, @NonNull T recordInternal) { 194 recordInternal.setStartTime(getCursorLong(cursor, START_TIME_COLUMN_NAME)); 195 recordInternal.setStartZoneOffset(getCursorInt(cursor, START_ZONE_OFFSET_COLUMN_NAME)); 196 recordInternal.setEndTime(getCursorLong(cursor, END_TIME_COLUMN_NAME)); 197 recordInternal.setEndZoneOffset(getCursorInt(cursor, END_ZONE_OFFSET_COLUMN_NAME)); 198 populateSpecificRecordValue(cursor, recordInternal); 199 } 200 201 /** This implementation should populate record with datatype specific values from the table. */ populateSpecificRecordValue(@onNull Cursor cursor, @NonNull T recordInternal)202 abstract void populateSpecificRecordValue(@NonNull Cursor cursor, @NonNull T recordInternal); 203 204 @Override getZoneOffsetColumnName()205 final String getZoneOffsetColumnName() { 206 return START_ZONE_OFFSET_COLUMN_NAME; 207 } 208 populateSpecificContentValues( @onNull ContentValues contentValues, @NonNull T intervalRecordInternal)209 abstract void populateSpecificContentValues( 210 @NonNull ContentValues contentValues, @NonNull T intervalRecordInternal); 211 212 /** 213 * This implementation should return the column names with which the table should be created. 214 * 215 * <p>NOTE: New columns can only be added via onUpgrade. Why? Consider what happens if a table 216 * already exists on the device 217 * 218 * <p>PLEASE DON'T USE THIS METHOD TO ADD NEW COLUMNS 219 */ 220 @NonNull getIntervalRecordColumnInfo()221 abstract List<Pair<String, String>> getIntervalRecordColumnInfo(); 222 223 /** Outputs list of columns needed for interval priority aggregations. */ getPriorityAggregationColumnNames()224 List<String> getPriorityAggregationColumnNames() { 225 return Arrays.asList( 226 APP_INFO_ID_COLUMN_NAME, 227 LAST_MODIFIED_TIME_COLUMN_NAME, 228 UUID_COLUMN_NAME, 229 START_ZONE_OFFSET_COLUMN_NAME); 230 } 231 } 232