• 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 android.health.connect.Constants.DEFAULT_INT;
20 import static android.health.connect.Constants.DEFAULT_LONG;
21 import static android.health.connect.Constants.MAXIMUM_PAGE_SIZE;
22 
23 import static com.android.server.healthconnect.storage.datatypehelpers.IntervalRecordHelper.END_TIME_COLUMN_NAME;
24 import static com.android.server.healthconnect.storage.request.ReadTransactionRequest.TYPE_NOT_PRESENT_PACKAGE_NAME;
25 import static com.android.server.healthconnect.storage.utils.StorageUtils.BLOB_UNIQUE_NON_NULL;
26 import static com.android.server.healthconnect.storage.utils.StorageUtils.BLOB_UNIQUE_NULL;
27 import static com.android.server.healthconnect.storage.utils.StorageUtils.INTEGER;
28 import static com.android.server.healthconnect.storage.utils.StorageUtils.PRIMARY_AUTOINCREMENT;
29 import static com.android.server.healthconnect.storage.utils.StorageUtils.TEXT_NULL;
30 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorInt;
31 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorLong;
32 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorString;
33 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorUUID;
34 import static com.android.server.healthconnect.storage.utils.StorageUtils.getDedupeByteBuffer;
35 import static com.android.server.healthconnect.storage.utils.StorageUtils.supportsPriority;
36 
37 import android.annotation.NonNull;
38 import android.content.ContentValues;
39 import android.database.Cursor;
40 import android.database.sqlite.SQLiteDatabase;
41 import android.health.connect.AggregateResult;
42 import android.health.connect.aidl.ReadRecordsRequestParcel;
43 import android.health.connect.datatypes.AggregationType;
44 import android.health.connect.datatypes.RecordTypeIdentifier;
45 import android.health.connect.internal.datatypes.RecordInternal;
46 import android.health.connect.internal.datatypes.utils.RecordMapper;
47 import android.os.Trace;
48 import android.util.ArrayMap;
49 import android.util.Pair;
50 
51 import androidx.annotation.Nullable;
52 
53 import com.android.server.healthconnect.storage.request.AggregateParams;
54 import com.android.server.healthconnect.storage.request.AggregateTableRequest;
55 import com.android.server.healthconnect.storage.request.CreateTableRequest;
56 import com.android.server.healthconnect.storage.request.DeleteTableRequest;
57 import com.android.server.healthconnect.storage.request.ReadTableRequest;
58 import com.android.server.healthconnect.storage.request.UpsertTableRequest;
59 import com.android.server.healthconnect.storage.utils.OrderByClause;
60 import com.android.server.healthconnect.storage.utils.SqlJoin;
61 import com.android.server.healthconnect.storage.utils.StorageUtils;
62 import com.android.server.healthconnect.storage.utils.WhereClauses;
63 
64 import java.lang.reflect.InvocationTargetException;
65 import java.time.Instant;
66 import java.time.temporal.ChronoUnit;
67 import java.util.ArrayList;
68 import java.util.Arrays;
69 import java.util.Collections;
70 import java.util.List;
71 import java.util.Map;
72 import java.util.Objects;
73 import java.util.UUID;
74 import java.util.stream.Collectors;
75 
76 /**
77  * Parent class for all the helper classes for all the records
78  *
79  * @hide
80  */
81 public abstract class RecordHelper<T extends RecordInternal<?>> {
82     public static final String PRIMARY_COLUMN_NAME = "row_id";
83     public static final String UUID_COLUMN_NAME = "uuid";
84     public static final String CLIENT_RECORD_ID_COLUMN_NAME = "client_record_id";
85     public static final String APP_INFO_ID_COLUMN_NAME = "app_info_id";
86     public static final String LAST_MODIFIED_TIME_COLUMN_NAME = "last_modified_time";
87     private static final String CLIENT_RECORD_VERSION_COLUMN_NAME = "client_record_version";
88     private static final String DEVICE_INFO_ID_COLUMN_NAME = "device_info_id";
89     private static final String RECORDING_METHOD_COLUMN_NAME = "recording_method";
90     private static final String DEDUPE_HASH_COLUMN_NAME = "dedupe_hash";
91     private static final List<Pair<String, Integer>> UNIQUE_COLUMNS_INFO =
92             List.of(
93                     new Pair<>(DEDUPE_HASH_COLUMN_NAME, UpsertTableRequest.TYPE_BLOB),
94                     new Pair<>(UUID_COLUMN_NAME, UpsertTableRequest.TYPE_BLOB));
95     private static final String TAG_RECORD_HELPER = "HealthConnectRecordHelper";
96     private static final int TRACE_TAG_RECORD_HELPER = TAG_RECORD_HELPER.hashCode();
97     @RecordTypeIdentifier.RecordType private final int mRecordIdentifier;
98 
RecordHelper(@ecordTypeIdentifier.RecordType int recordIdentifier)99     RecordHelper(@RecordTypeIdentifier.RecordType int recordIdentifier) {
100         mRecordIdentifier = recordIdentifier;
101     }
102 
getDeleteRequestForAutoDelete(int recordAutoDeletePeriodInDays)103     public DeleteTableRequest getDeleteRequestForAutoDelete(int recordAutoDeletePeriodInDays) {
104         return new DeleteTableRequest(getMainTableName())
105                 .setTimeFilter(
106                         getStartTimeColumnName(),
107                         Instant.EPOCH.toEpochMilli(),
108                         Instant.now()
109                                 .minus(recordAutoDeletePeriodInDays, ChronoUnit.DAYS)
110                                 .toEpochMilli());
111     }
112 
113     @RecordTypeIdentifier.RecordType
getRecordIdentifier()114     public int getRecordIdentifier() {
115         return mRecordIdentifier;
116     }
117 
118     /**
119      * Called on DB update. Inheriting classes should implement this if they need to add new columns
120      * or tables.
121      */
onUpgrade(@onNull SQLiteDatabase db, int oldVersion, int newVersion)122     public void onUpgrade(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) {
123         // empty
124     }
125 
126     /**
127      * @return {@link AggregateTableRequest} corresponding to {@code aggregationType}
128      */
getAggregateTableRequest( AggregationType<?> aggregationType, List<String> packageFilter, long startTime, long endTime, boolean useLocalTime)129     public final AggregateTableRequest getAggregateTableRequest(
130             AggregationType<?> aggregationType,
131             List<String> packageFilter,
132             long startTime,
133             long endTime,
134             boolean useLocalTime) {
135         AggregateParams params = getAggregateParams(aggregationType);
136         params.setTimeColumnName(
137                 useLocalTime ? getLocalStartTimeColumnName() : getStartTimeColumnName());
138         params.setExtraTimeColumn(
139                 useLocalTime ? getLocalEndTimeColumnName() : getEndTimeColumnName());
140         params.setOffsetColumnToFetch(getZoneOffsetColumnName());
141 
142         if (supportsPriority(mRecordIdentifier, aggregationType.getAggregateOperationType())) {
143             List<String> columns =
144                     Arrays.asList(
145                             getStartTimeColumnName(),
146                             END_TIME_COLUMN_NAME,
147                             APP_INFO_ID_COLUMN_NAME,
148                             LAST_MODIFIED_TIME_COLUMN_NAME);
149             params.appendAdditionalColumns(columns);
150         }
151         if (StorageUtils.isDerivedType(mRecordIdentifier)) {
152             params.appendAdditionalColumns(Collections.singletonList(getStartTimeColumnName()));
153         }
154 
155         return new AggregateTableRequest(params, aggregationType, this, useLocalTime)
156                 .setPackageFilter(
157                         AppInfoHelper.getInstance().getAppInfoIds(packageFilter),
158                         APP_INFO_ID_COLUMN_NAME)
159                 .setTimeFilter(startTime, endTime);
160     }
161 
162     /**
163      * Used to get the Aggregate result for aggregate types
164      *
165      * @return {@link AggregateResult} for {@link AggregationType}
166      */
getAggregateResult( Cursor cursor, AggregationType<?> aggregationType)167     public AggregateResult<?> getAggregateResult(
168             Cursor cursor, AggregationType<?> aggregationType) {
169         return null;
170     }
171 
172     /**
173      * Used to get the Aggregate result for aggregate types where the priority of apps is to be
174      * considered for overlapping data for sleep and activity interval records
175      *
176      * @return {@link AggregateResult} for {@link AggregationType}
177      */
getAggregateResult( Cursor results, AggregationType<?> aggregationType, double total)178     public AggregateResult<?> getAggregateResult(
179             Cursor results, AggregationType<?> aggregationType, double total) {
180         return null;
181     }
182 
183     /**
184      * Used to calculate and get aggregate results for data types that support derived aggregates
185      */
deriveAggregate(Cursor cursor, AggregateTableRequest request)186     public double[] deriveAggregate(Cursor cursor, AggregateTableRequest request) {
187         return null;
188     }
189 
190     /**
191      * Returns a requests representing the tables that should be created corresponding to this
192      * helper
193      */
194     @NonNull
getCreateTableRequest()195     public final CreateTableRequest getCreateTableRequest() {
196         return new CreateTableRequest(getMainTableName(), getColumnInfo())
197                 .addForeignKey(
198                         DeviceInfoHelper.getInstance().getTableName(),
199                         Collections.singletonList(DEVICE_INFO_ID_COLUMN_NAME),
200                         Collections.singletonList(PRIMARY_COLUMN_NAME))
201                 .addForeignKey(
202                         AppInfoHelper.TABLE_NAME,
203                         Collections.singletonList(APP_INFO_ID_COLUMN_NAME),
204                         Collections.singletonList(PRIMARY_COLUMN_NAME))
205                 .setChildTableRequests(getChildTableCreateRequests())
206                 .setGeneratedColumnInfo(getGeneratedColumnInfo());
207     }
208 
getUpsertTableRequest(RecordInternal<?> recordInternal)209     public UpsertTableRequest getUpsertTableRequest(RecordInternal<?> recordInternal) {
210         return getUpsertTableRequest(recordInternal, null);
211     }
212 
213     @NonNull
214     @SuppressWarnings("unchecked")
getUpsertTableRequest( RecordInternal<?> recordInternal, ArrayMap<String, Boolean> extraWritePermissionToStateMap)215     public UpsertTableRequest getUpsertTableRequest(
216             RecordInternal<?> recordInternal,
217             ArrayMap<String, Boolean> extraWritePermissionToStateMap) {
218         Trace.traceBegin(
219                 TRACE_TAG_RECORD_HELPER, TAG_RECORD_HELPER.concat("GetUpsertTableRequest"));
220         ContentValues upsertValues = getContentValues((T) recordInternal);
221         updateUpsertValuesIfRequired(upsertValues, extraWritePermissionToStateMap);
222         UpsertTableRequest upsertTableRequest =
223                 new UpsertTableRequest(getMainTableName(), upsertValues, UNIQUE_COLUMNS_INFO)
224                         .setRequiresUpdateClause(
225                                 new UpsertTableRequest.IRequiresUpdate() {
226                                     @Override
227                                     public boolean requiresUpdate(
228                                             Cursor cursor,
229                                             ContentValues contentValues,
230                                             UpsertTableRequest request) {
231                                         final UUID newUUID =
232                                                 StorageUtils.convertBytesToUUID(
233                                                         contentValues.getAsByteArray(
234                                                                 UUID_COLUMN_NAME));
235                                         final UUID oldUUID =
236                                                 StorageUtils.getCursorUUID(
237                                                         cursor, UUID_COLUMN_NAME);
238 
239                                         if (!Objects.equals(newUUID, oldUUID)) {
240                                             // Use old UUID in case of conflicts on de-dupe.
241                                             contentValues.put(
242                                                     UUID_COLUMN_NAME,
243                                                     StorageUtils.convertUUIDToBytes(oldUUID));
244                                             request.getRecordInternal().setUuid(oldUUID);
245                                             // This means there was a duplication conflict, we want
246                                             // to update in this case.
247                                             return true;
248                                         }
249 
250                                         long clientRecordVersion =
251                                                 StorageUtils.getCursorLong(
252                                                         cursor, CLIENT_RECORD_VERSION_COLUMN_NAME);
253                                         long newClientRecordVersion =
254                                                 contentValues.getAsLong(
255                                                         CLIENT_RECORD_VERSION_COLUMN_NAME);
256 
257                                         return newClientRecordVersion >= clientRecordVersion;
258                                     }
259                                 })
260                         .setChildTableRequests(getChildTableUpsertRequests((T) recordInternal))
261                         .setHelper(this)
262                         .setExtraWritePermissionsStateMapping(extraWritePermissionToStateMap);
263         Trace.traceEnd(TRACE_TAG_RECORD_HELPER);
264         return upsertTableRequest;
265     }
266 
267     /* Updates upsert content values based on extra permissions state. */
updateUpsertValuesIfRequired( @onNull ContentValues values, @Nullable ArrayMap<String, Boolean> extraWritePermissionToStateMap)268     protected void updateUpsertValuesIfRequired(
269             @NonNull ContentValues values,
270             @Nullable ArrayMap<String, Boolean> extraWritePermissionToStateMap) {}
271 
getChildTablesToDeleteOnRecordUpsert( ArrayMap<String, Boolean> extraWritePermissionToState)272     public List<String> getChildTablesToDeleteOnRecordUpsert(
273             ArrayMap<String, Boolean> extraWritePermissionToState) {
274         return getAllChildTables();
275     }
276 
277     @NonNull
getAllChildTables()278     public List<String> getAllChildTables() {
279         List<String> childTables = new ArrayList<>();
280         for (CreateTableRequest childTableCreateRequest : getChildTableCreateRequests()) {
281             populateWithTablesNames(childTableCreateRequest, childTables);
282         }
283 
284         return childTables;
285     }
286 
287     @NonNull
getGeneratedColumnInfo()288     protected List<CreateTableRequest.GeneratedColumnInfo> getGeneratedColumnInfo() {
289         return Collections.emptyList();
290     }
291 
populateWithTablesNames( CreateTableRequest childTableCreateRequest, List<String> childTables)292     private void populateWithTablesNames(
293             CreateTableRequest childTableCreateRequest, List<String> childTables) {
294         childTables.add(childTableCreateRequest.getTableName());
295         for (CreateTableRequest childTableRequest :
296                 childTableCreateRequest.getChildTableRequests()) {
297             populateWithTablesNames(childTableRequest, childTables);
298         }
299     }
300 
301     /**
302      * Returns ReadSingleTableRequest for {@code request} and package name {@code packageName}
303      *
304      */
getReadTableRequest( ReadRecordsRequestParcel request, String packageName, boolean enforceSelfRead, long startDateAccess, Map<String, Boolean> extraPermsState)305     public ReadTableRequest getReadTableRequest(
306             ReadRecordsRequestParcel request,
307             String packageName,
308             boolean enforceSelfRead,
309             long startDateAccess,
310             Map<String, Boolean> extraPermsState) {
311         return new ReadTableRequest(getMainTableName())
312                 .setJoinClause(getJoinForReadRequest())
313                 .setWhereClause(
314                         getReadTableWhereClause(
315                                 request, packageName, enforceSelfRead, startDateAccess))
316                 .setOrderBy(getOrderByClause(request))
317                 .setLimit(getLimitSize(request))
318                 .setRecordHelper(this)
319                 .setExtraReadRequests(
320                         getExtraDataReadRequests(
321                                 request, packageName, startDateAccess, extraPermsState));
322     }
323 
324     /**
325      * Logs metrics specific to a record type's insertion/update.
326      *
327      * @param recordInternals List of records being inserted/updated
328      * @param packageName Caller package name
329      */
logUpsertMetrics( @onNull List<RecordInternal<?>> recordInternals, @NonNull String packageName)330     public void logUpsertMetrics(
331             @NonNull List<RecordInternal<?>> recordInternals, @NonNull String packageName) {
332         // Do nothing, implement in record specific helpers
333     }
334 
335     /**
336      * Logs metrics specific to a record type's read.
337      *
338      * @param recordInternals List of records being read
339      * @param packageName Caller package name
340      */
logReadMetrics( @onNull List<RecordInternal<?>> recordInternals, @NonNull String packageName)341     public void logReadMetrics(
342             @NonNull List<RecordInternal<?>> recordInternals, @NonNull String packageName) {
343         // Do nothing, implement in record specific helpers
344     }
345 
346     /** Returns ReadTableRequest for {@code uuids} */
getReadTableRequest(List<UUID> uuids, long startDateAccess)347     public ReadTableRequest getReadTableRequest(List<UUID> uuids, long startDateAccess) {
348         return new ReadTableRequest(getMainTableName())
349                 .setJoinClause(getJoinForReadRequest())
350                 .setWhereClause(
351                         new WhereClauses()
352                                 .addWhereInClauseWithoutQuotes(
353                                         UUID_COLUMN_NAME, StorageUtils.getListOfHexString(uuids))
354                                 .addWhereLaterThanTimeClause(
355                                         getStartTimeColumnName(), startDateAccess))
356                 .setRecordHelper(this)
357                 .setExtraReadRequests(getExtraDataReadRequests(uuids, startDateAccess));
358     }
359 
360     /**
361      * Returns a list of ReadSingleTableRequest for {@code request} and package name {@code
362      * packageName} to populate extra data. Called in database read requests.
363      */
getExtraDataReadRequests( ReadRecordsRequestParcel request, String packageName, long startDateAccess, Map<String, Boolean> extraPermsState)364     List<ReadTableRequest> getExtraDataReadRequests(
365             ReadRecordsRequestParcel request,
366             String packageName,
367             long startDateAccess,
368             Map<String, Boolean> extraPermsState) {
369         return Collections.emptyList();
370     }
371 
372     /**
373      * Returns list if ReadSingleTableRequest for {@code uuids} to populate extra data. Called in
374      * change logs read requests.
375      */
getExtraDataReadRequests(List<UUID> uuids, long startDateAccess)376     List<ReadTableRequest> getExtraDataReadRequests(List<UUID> uuids, long startDateAccess) {
377         return Collections.emptyList();
378     }
379 
380     /**
381      * Returns ReadTableRequest for the record corresponding to this helper with a distinct clause
382      * on the input column names.
383      */
getReadTableRequestWithDistinctAppInfoIds()384     public ReadTableRequest getReadTableRequestWithDistinctAppInfoIds() {
385         return new ReadTableRequest(getMainTableName())
386                 .setColumnNames(new ArrayList<>(List.of(APP_INFO_ID_COLUMN_NAME)))
387                 .setDistinctClause(true);
388     }
389 
390     /** Returns List of Internal records from the cursor */
391     @SuppressWarnings("unchecked")
getInternalRecords(Cursor cursor, int requestSize)392     public List<RecordInternal<?>> getInternalRecords(Cursor cursor, int requestSize) {
393         return getInternalRecords(cursor, requestSize, null);
394     }
395 
396     /** Returns List of Internal records from the cursor */
397     @SuppressWarnings("unchecked")
getInternalRecords( Cursor cursor, int requestSize, Map<Long, String> packageNamesByAppIds)398     public List<RecordInternal<?>> getInternalRecords(
399             Cursor cursor, int requestSize, Map<Long, String> packageNamesByAppIds) {
400         Trace.traceBegin(TRACE_TAG_RECORD_HELPER, TAG_RECORD_HELPER.concat("GetInternalRecords"));
401         List<RecordInternal<?>> recordInternalList = new ArrayList<>();
402 
403         int count = 0;
404         long prevStartTime = DEFAULT_LONG;
405         long currentStartTime = DEFAULT_LONG;
406         int tempCount = 0;
407         List<RecordInternal<?>> tempList = new ArrayList<>();
408         while (cursor.moveToNext()) {
409             try {
410                 T record =
411                         (T)
412                                 RecordMapper.getInstance()
413                                         .getRecordIdToInternalRecordClassMap()
414                                         .get(getRecordIdentifier())
415                                         .getConstructor()
416                                         .newInstance();
417                 record.setUuid(getCursorUUID(cursor, UUID_COLUMN_NAME));
418                 record.setLastModifiedTime(getCursorLong(cursor, LAST_MODIFIED_TIME_COLUMN_NAME));
419                 record.setClientRecordId(getCursorString(cursor, CLIENT_RECORD_ID_COLUMN_NAME));
420                 record.setClientRecordVersion(
421                         getCursorLong(cursor, CLIENT_RECORD_VERSION_COLUMN_NAME));
422                 record.setRecordingMethod(getCursorInt(cursor, RECORDING_METHOD_COLUMN_NAME));
423                 record.setRowId(getCursorInt(cursor, PRIMARY_COLUMN_NAME));
424                 long deviceInfoId = getCursorLong(cursor, DEVICE_INFO_ID_COLUMN_NAME);
425                 DeviceInfoHelper.getInstance().populateRecordWithValue(deviceInfoId, record);
426                 long appInfoId = getCursorLong(cursor, APP_INFO_ID_COLUMN_NAME);
427                 AppInfoHelper.getInstance()
428                         .populateRecordWithValue(appInfoId, record, packageNamesByAppIds);
429                 populateRecordValue(cursor, record);
430 
431                 prevStartTime = currentStartTime;
432                 currentStartTime = getCursorLong(cursor, getStartTimeColumnName());
433                 if (prevStartTime == DEFAULT_LONG || prevStartTime == currentStartTime) {
434                     // Fetch and add records with same startTime to tempList
435                     tempList.add(record);
436                     tempCount++;
437                 } else {
438                     if (count == 0) {
439                         // items in tempList having startTime same as the first record from cursor
440                         // is added to final list.
441                         // This makes sure that we return at least 1 record if the count of
442                         // records with startTime same as second record exceeds requestSize.
443                         recordInternalList.addAll(tempList);
444                         count = tempCount;
445                         tempList.clear();
446                         tempCount = 0;
447                         if (count >= requestSize) {
448                             // startTime of current record should be fetched for pageToken
449                             cursor.moveToPrevious();
450                             break;
451                         }
452                         tempList.add(record);
453                         tempCount = 1;
454                     } else if (tempCount + count <= requestSize) {
455                         // Makes sure after adding records in tempList with same starTime
456                         // the count does not exceed requestSize
457                         recordInternalList.addAll(tempList);
458                         count += tempCount;
459                         tempList.clear();
460                         tempCount = 0;
461                         if (count >= requestSize) {
462                             // After adding records if count is equal to requestSize then startTime
463                             // of current fetched record should be the next page token.
464                             cursor.moveToPrevious();
465                             break;
466                         }
467                         tempList.add(record);
468                         tempCount = 1;
469                     } else {
470                         // If adding records in tempList makes count > requestSize, then ignore temp
471                         // list and startTime of records in temp list should be the next page token.
472                         tempList.clear();
473                         int lastposition = cursor.getPosition();
474                         cursor.moveToPosition(lastposition - 2);
475                         break;
476                     }
477                 }
478             } catch (InstantiationException
479                     | IllegalAccessException
480                     | NoSuchMethodException
481                     | InvocationTargetException exception) {
482                 throw new IllegalArgumentException(exception);
483             }
484         }
485         if (!tempList.isEmpty()) {
486             if (tempCount + count <= requestSize) {
487                 // If reached end of cursor while fetching records then add it to final list
488                 recordInternalList.addAll(tempList);
489             } else {
490                 // If reached end of cursor while fetching and adding it will exceed requestSize
491                 // then ignore them,startTime of the last record will be pageToken for next read.
492                 cursor.moveToPosition(cursor.getCount() - 2);
493             }
494         }
495         Trace.traceEnd(TRACE_TAG_RECORD_HELPER);
496         return recordInternalList;
497     }
498 
499     /** Returns is the read of this record type is enabled */
isRecordOperationsEnabled()500     public boolean isRecordOperationsEnabled() {
501         return true;
502     }
503 
504     /** Populate internalRecords fields using extraDataCursor */
505     @SuppressWarnings("unchecked")
updateInternalRecordsWithExtraFields( List<RecordInternal<?>> internalRecords, Cursor cursorExtraData, String tableName)506     public void updateInternalRecordsWithExtraFields(
507             List<RecordInternal<?>> internalRecords, Cursor cursorExtraData, String tableName) {
508         readExtraData((List<T>) internalRecords, cursorExtraData, tableName);
509     }
510 
getDeleteTableRequest( List<String> packageFilters, long startTime, long endTime, boolean usesLocalTimeFilter)511     public DeleteTableRequest getDeleteTableRequest(
512             List<String> packageFilters,
513             long startTime,
514             long endTime,
515             boolean usesLocalTimeFilter) {
516         final String timeColumnName =
517                 usesLocalTimeFilter ? getLocalStartTimeColumnName() : getStartTimeColumnName();
518         return new DeleteTableRequest(getMainTableName(), getRecordIdentifier())
519                 .setTimeFilter(timeColumnName, startTime, endTime)
520                 .setPackageFilter(
521                         APP_INFO_ID_COLUMN_NAME,
522                         AppInfoHelper.getInstance().getAppInfoIds(packageFilters))
523                 .setRequiresUuId(UUID_COLUMN_NAME);
524     }
525 
getDeleteTableRequest(List<UUID> ids)526     public DeleteTableRequest getDeleteTableRequest(List<UUID> ids) {
527         return new DeleteTableRequest(getMainTableName(), getRecordIdentifier())
528                 .setIds(UUID_COLUMN_NAME, StorageUtils.getListOfHexString(ids))
529                 .setRequiresUuId(UUID_COLUMN_NAME)
530                 .setEnforcePackageCheck(APP_INFO_ID_COLUMN_NAME, UUID_COLUMN_NAME);
531     }
532 
getDurationGroupByColumnName()533     public abstract String getDurationGroupByColumnName();
534 
getPeriodGroupByColumnName()535     public abstract String getPeriodGroupByColumnName();
536 
getStartTimeColumnName()537     public abstract String getStartTimeColumnName();
538 
getLocalStartTimeColumnName()539     public abstract String getLocalStartTimeColumnName();
540 
getLocalEndTimeColumnName()541     public String getLocalEndTimeColumnName() {
542         return null;
543     }
544 
getEndTimeColumnName()545     public String getEndTimeColumnName() {
546         return null;
547     }
548 
549     /** Populate internalRecords with extra data. */
readExtraData(List<T> internalRecords, Cursor cursorExtraData, String tableName)550     void readExtraData(List<T> internalRecords, Cursor cursorExtraData, String tableName) {}
551 
552     /**
553      * Child classes should implement this if it wants to create additional tables, apart from the
554      * main table.
555      */
556     @NonNull
getChildTableCreateRequests()557     List<CreateTableRequest> getChildTableCreateRequests() {
558         return Collections.emptyList();
559     }
560 
561     /** Returns the table name to be created corresponding to this helper */
562     @NonNull
getMainTableName()563     abstract String getMainTableName();
564 
565     /** Returns the information required to perform aggregate operation. */
getAggregateParams(AggregationType<?> aggregateRequest)566     AggregateParams getAggregateParams(AggregationType<?> aggregateRequest) {
567         return null;
568     }
569 
570     /**
571      * This implementation should return the column names with which the table should be created.
572      *
573      * <p>NOTE: New columns can only be added via onUpgrade. Why? Consider what happens if a table
574      * already exists on the device
575      *
576      * <p>PLEASE DON'T USE THIS METHOD TO ADD NEW COLUMNS
577      */
578     @NonNull
getSpecificColumnInfo()579     abstract List<Pair<String, String>> getSpecificColumnInfo();
580 
581     /**
582      * Child classes implementation should add the values of {@code recordInternal} that needs to be
583      * populated in the DB to {@code contentValues}.
584      */
populateContentValues( @onNull ContentValues contentValues, @NonNull T recordInternal)585     abstract void populateContentValues(
586             @NonNull ContentValues contentValues, @NonNull T recordInternal);
587 
588     /**
589      * Child classes implementation should populate the values to the {@code record} using the
590      * cursor {@code cursor} queried from the DB .
591      */
populateRecordValue(@onNull Cursor cursor, @NonNull T recordInternal)592     abstract void populateRecordValue(@NonNull Cursor cursor, @NonNull T recordInternal);
593 
getChildTableUpsertRequests(T record)594     List<UpsertTableRequest> getChildTableUpsertRequests(T record) {
595         return Collections.emptyList();
596     }
597 
getJoinForReadRequest()598     SqlJoin getJoinForReadRequest() {
599         return null;
600     }
601 
getLimitSize(ReadRecordsRequestParcel request)602     private int getLimitSize(ReadRecordsRequestParcel request) {
603         if (request.getRecordIdFiltersParcel() == null) {
604             return request.getPageSize();
605         } else {
606             return MAXIMUM_PAGE_SIZE;
607         }
608     }
609 
getReadTableWhereClause( ReadRecordsRequestParcel request, String packageName, boolean enforceSelfRead, long startDateAccess)610     WhereClauses getReadTableWhereClause(
611             ReadRecordsRequestParcel request,
612             String packageName,
613             boolean enforceSelfRead,
614             long startDateAccess) {
615         if (request.getRecordIdFiltersParcel() == null) {
616             List<Long> appIds =
617                     AppInfoHelper.getInstance().getAppInfoIds(request.getPackageFilters()).stream()
618                             .distinct()
619                             .collect(Collectors.toList());
620             if (enforceSelfRead) {
621                 appIds =
622                         AppInfoHelper.getInstance()
623                                 .getAppInfoIds(Collections.singletonList(packageName));
624             }
625             if (appIds.size() == 1 && appIds.get(0) == DEFAULT_INT) {
626                 throw new TypeNotPresentException(TYPE_NOT_PRESENT_PACKAGE_NAME, new Throwable());
627             }
628 
629             WhereClauses clauses =
630                     new WhereClauses().addWhereInLongsClause(APP_INFO_ID_COLUMN_NAME, appIds);
631 
632             if (request.getPageToken() != DEFAULT_LONG) {
633                 // Since pageToken passed contains detail of sort order. Actual token value for read
634                 // is calculated back from the requested pageToken based on sort order.
635                 if (request.isAscending()) {
636                     clauses.addWhereGreaterThanOrEqualClause(
637                             getStartTimeColumnName(), request.getPageToken() / 2);
638                 } else {
639                     clauses.addWhereLessThanOrEqualClause(
640                             getStartTimeColumnName(), (request.getPageToken() - 1) / 2);
641                 }
642             }
643 
644             if (request.usesLocalTimeFilter()) {
645                 clauses.addWhereGreaterThanOrEqualClause(getStartTimeColumnName(), startDateAccess);
646                 clauses.addWhereBetweenClause(
647                         getLocalStartTimeColumnName(),
648                         request.getStartTime(),
649                         request.getEndTime());
650             } else {
651                 clauses.addWhereBetweenTimeClause(
652                         getStartTimeColumnName(), startDateAccess, request.getEndTime());
653             }
654 
655             return clauses;
656         }
657 
658         // Since for now we don't support mixing IDs and filters, we need to look for IDs now
659         List<UUID> ids =
660                 request.getRecordIdFiltersParcel().getRecordIdFilters().stream()
661                         .map(
662                                 (recordIdFilter) ->
663                                         StorageUtils.getUUIDFor(recordIdFilter, packageName))
664                         .collect(Collectors.toList());
665         WhereClauses whereClauses =
666                 new WhereClauses()
667                         .addWhereInClauseWithoutQuotes(
668                                 UUID_COLUMN_NAME, StorageUtils.getListOfHexString(ids));
669 
670         if (enforceSelfRead) {
671             long id = AppInfoHelper.getInstance().getAppInfoId(packageName);
672             if (id == DEFAULT_LONG) {
673                 throw new TypeNotPresentException(TYPE_NOT_PRESENT_PACKAGE_NAME, new Throwable());
674             }
675             whereClauses.addWhereInLongsClause(
676                     APP_INFO_ID_COLUMN_NAME, Collections.singletonList(id));
677             return whereClauses.addWhereLaterThanTimeClause(
678                     getStartTimeColumnName(), startDateAccess);
679         }
680         return whereClauses;
681     }
682 
getZoneOffsetColumnName()683     abstract String getZoneOffsetColumnName();
684 
getOrderByClause(ReadRecordsRequestParcel request)685     private OrderByClause getOrderByClause(ReadRecordsRequestParcel request) {
686         OrderByClause orderByClause = new OrderByClause();
687         if (request.getRecordIdFiltersParcel() == null) {
688             orderByClause.addOrderByClause(getStartTimeColumnName(), request.isAscending());
689         }
690         return orderByClause;
691     }
692 
693     @NonNull
getContentValues(@onNull T recordInternal)694     private ContentValues getContentValues(@NonNull T recordInternal) {
695         ContentValues recordContentValues = new ContentValues();
696 
697         recordContentValues.put(
698                 UUID_COLUMN_NAME, StorageUtils.convertUUIDToBytes(recordInternal.getUuid()));
699         recordContentValues.put(
700                 LAST_MODIFIED_TIME_COLUMN_NAME, recordInternal.getLastModifiedTime());
701         recordContentValues.put(CLIENT_RECORD_ID_COLUMN_NAME, recordInternal.getClientRecordId());
702         recordContentValues.put(
703                 CLIENT_RECORD_VERSION_COLUMN_NAME, recordInternal.getClientRecordVersion());
704         recordContentValues.put(RECORDING_METHOD_COLUMN_NAME, recordInternal.getRecordingMethod());
705         recordContentValues.put(DEVICE_INFO_ID_COLUMN_NAME, recordInternal.getDeviceInfoId());
706         recordContentValues.put(APP_INFO_ID_COLUMN_NAME, recordInternal.getAppInfoId());
707         recordContentValues.put(DEDUPE_HASH_COLUMN_NAME, getDedupeByteBuffer(recordInternal));
708 
709         populateContentValues(recordContentValues, recordInternal);
710 
711         return recordContentValues;
712     }
713 
714     /**
715      * This implementation should return the column names with which the table should be created.
716      *
717      * <p>NOTE: New columns can only be added via onUpgrade. Why? Consider what happens if a table
718      * already exists on the device
719      *
720      * <p>PLEASE DON'T USE THIS METHOD TO ADD NEW COLUMNS
721      */
722     @NonNull
getColumnInfo()723     private List<Pair<String, String>> getColumnInfo() {
724         ArrayList<Pair<String, String>> columnInfo = new ArrayList<>();
725         columnInfo.add(new Pair<>(PRIMARY_COLUMN_NAME, PRIMARY_AUTOINCREMENT));
726         columnInfo.add(new Pair<>(UUID_COLUMN_NAME, BLOB_UNIQUE_NON_NULL));
727         columnInfo.add(new Pair<>(LAST_MODIFIED_TIME_COLUMN_NAME, INTEGER));
728         columnInfo.add(new Pair<>(CLIENT_RECORD_ID_COLUMN_NAME, TEXT_NULL));
729         columnInfo.add(new Pair<>(CLIENT_RECORD_VERSION_COLUMN_NAME, TEXT_NULL));
730         columnInfo.add(new Pair<>(DEVICE_INFO_ID_COLUMN_NAME, INTEGER));
731         columnInfo.add(new Pair<>(APP_INFO_ID_COLUMN_NAME, INTEGER));
732         columnInfo.add(new Pair<>(RECORDING_METHOD_COLUMN_NAME, INTEGER));
733         columnInfo.add(new Pair<>(DEDUPE_HASH_COLUMN_NAME, BLOB_UNIQUE_NULL));
734 
735         columnInfo.addAll(getSpecificColumnInfo());
736 
737         return columnInfo;
738     }
739 
740     /** Checks that operation with current record type are supported. */
checkRecordOperationsAreEnabled(RecordInternal<?> recordInternal)741     public void checkRecordOperationsAreEnabled(RecordInternal<?> recordInternal) {}
742 
743     /** Returns permissions required to read extra record data. */
getExtraReadPermissions()744     public List<String> getExtraReadPermissions() {
745         return Collections.emptyList();
746     }
747 
748     /** Returns all extra permissions associated with current record type. */
getExtraWritePermissions()749     public List<String> getExtraWritePermissions() {
750         return Collections.emptyList();
751     }
752 
753     /** Returns extra permissions required to write given record. */
getRequiredExtraWritePermissions(RecordInternal<?> recordInternal)754     public List<String> getRequiredExtraWritePermissions(RecordInternal<?> recordInternal) {
755         return Collections.emptyList();
756     }
757 }
758