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