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