1 /* 2 * Copyright (C) 2024 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.datatypes.RecordTypeIdentifier.RECORD_TYPE_EXERCISE_SESSION; 20 21 import static com.android.server.healthconnect.storage.datatypehelpers.ExerciseSessionRecordHelper.EXERCISE_SESSION_RECORD_TABLE_NAME; 22 import static com.android.server.healthconnect.storage.datatypehelpers.ExerciseSessionRecordHelper.PLANNED_EXERCISE_SESSION_ID_COLUMN_NAME; 23 import static com.android.server.healthconnect.storage.utils.StorageUtils.BLOB; 24 import static com.android.server.healthconnect.storage.utils.StorageUtils.BLOB_NULL; 25 import static com.android.server.healthconnect.storage.utils.StorageUtils.BOOLEAN_FALSE_VALUE; 26 import static com.android.server.healthconnect.storage.utils.StorageUtils.BOOLEAN_TRUE_VALUE; 27 import static com.android.server.healthconnect.storage.utils.StorageUtils.INTEGER; 28 import static com.android.server.healthconnect.storage.utils.StorageUtils.INTEGER_NOT_NULL; 29 import static com.android.server.healthconnect.storage.utils.StorageUtils.PRIMARY_AUTOINCREMENT; 30 import static com.android.server.healthconnect.storage.utils.StorageUtils.TEXT_NULL; 31 import static com.android.server.healthconnect.storage.utils.StorageUtils.convertBytesToDouble; 32 import static com.android.server.healthconnect.storage.utils.StorageUtils.convertBytesToInt; 33 import static com.android.server.healthconnect.storage.utils.StorageUtils.convertBytesToLong; 34 import static com.android.server.healthconnect.storage.utils.StorageUtils.convertDoubleToBytes; 35 import static com.android.server.healthconnect.storage.utils.StorageUtils.convertIntToBytes; 36 import static com.android.server.healthconnect.storage.utils.StorageUtils.convertLongToBytes; 37 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorBlob; 38 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorInt; 39 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorString; 40 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorUUID; 41 42 import android.annotation.Nullable; 43 import android.content.ContentValues; 44 import android.database.Cursor; 45 import android.health.connect.datatypes.RecordTypeIdentifier; 46 import android.health.connect.datatypes.units.Energy; 47 import android.health.connect.datatypes.units.Length; 48 import android.health.connect.datatypes.units.Mass; 49 import android.health.connect.datatypes.units.Power; 50 import android.health.connect.datatypes.units.Velocity; 51 import android.health.connect.internal.datatypes.ExerciseCompletionGoalInternal; 52 import android.health.connect.internal.datatypes.ExerciseCompletionGoalInternal.ActiveCaloriesBurnedGoalInternal; 53 import android.health.connect.internal.datatypes.ExerciseCompletionGoalInternal.DistanceGoalInternal; 54 import android.health.connect.internal.datatypes.ExerciseCompletionGoalInternal.DistanceWithVariableRestGoalInternal; 55 import android.health.connect.internal.datatypes.ExerciseCompletionGoalInternal.DurationGoalInternal; 56 import android.health.connect.internal.datatypes.ExerciseCompletionGoalInternal.RepetitionsGoalInternal; 57 import android.health.connect.internal.datatypes.ExerciseCompletionGoalInternal.StepsGoalInternal; 58 import android.health.connect.internal.datatypes.ExerciseCompletionGoalInternal.TotalCaloriesBurnedGoalInternal; 59 import android.health.connect.internal.datatypes.ExerciseCompletionGoalInternal.UnspecifiedGoalInternal; 60 import android.health.connect.internal.datatypes.ExercisePerformanceGoalInternal; 61 import android.health.connect.internal.datatypes.ExercisePerformanceGoalInternal.AmrapGoalInternal; 62 import android.health.connect.internal.datatypes.ExercisePerformanceGoalInternal.CadenceGoalInternal; 63 import android.health.connect.internal.datatypes.ExercisePerformanceGoalInternal.HeartRateGoalInternal; 64 import android.health.connect.internal.datatypes.ExercisePerformanceGoalInternal.PowerGoalInternal; 65 import android.health.connect.internal.datatypes.ExercisePerformanceGoalInternal.RateOfPerceivedExertionGoalInternal; 66 import android.health.connect.internal.datatypes.ExercisePerformanceGoalInternal.SpeedGoalInternal; 67 import android.health.connect.internal.datatypes.ExercisePerformanceGoalInternal.WeightGoalInternal; 68 import android.health.connect.internal.datatypes.PlannedExerciseBlockInternal; 69 import android.health.connect.internal.datatypes.PlannedExerciseSessionRecordInternal; 70 import android.health.connect.internal.datatypes.PlannedExerciseStepInternal; 71 import android.util.ArrayMap; 72 import android.util.Pair; 73 74 import com.android.server.healthconnect.storage.request.AlterTableRequest; 75 import com.android.server.healthconnect.storage.request.CreateTableRequest; 76 import com.android.server.healthconnect.storage.request.ReadTableRequest; 77 import com.android.server.healthconnect.storage.request.UpsertTableRequest; 78 import com.android.server.healthconnect.storage.utils.InternalHealthConnectMappings; 79 import com.android.server.healthconnect.storage.utils.SqlJoin; 80 import com.android.server.healthconnect.storage.utils.StorageUtils; 81 import com.android.server.healthconnect.storage.utils.TableColumnPair; 82 import com.android.server.healthconnect.storage.utils.WhereClauses; 83 84 import java.time.Duration; 85 import java.util.ArrayList; 86 import java.util.Arrays; 87 import java.util.Collections; 88 import java.util.List; 89 import java.util.UUID; 90 91 /** 92 * Helper class for PlannedExerciseSessionRecord. 93 * 94 * @hide 95 */ 96 public final class PlannedExerciseSessionRecordHelper 97 extends IntervalRecordHelper<PlannedExerciseSessionRecordInternal> { 98 // Tables. 99 public static final String PLANNED_EXERCISE_SESSION_RECORD_TABLE_NAME = 100 "planned_exercise_session_record_table"; 101 private static final String PLANNED_EXERCISE_SESSION_BLOCKS_TABLE_NAME = 102 "planned_exercise_session_blocks_table"; 103 private static final String PLANNED_EXERCISE_SESSION_STEPS_TABLE_NAME = 104 "planned_exercise_session_steps_table"; 105 private static final String PLANNED_EXERCISE_SESSION_GOALS_TABLE_NAME = 106 "planned_exercise_session_goals_table"; 107 108 // Planned exercise record columns. 109 private static final String NOTES_COLUMN_NAME = "notes"; 110 private static final String EXERCISE_TYPE_COLUMN_NAME = "exercise_type"; 111 private static final String TITLE_COLUMN_NAME = "title"; 112 private static final String HAS_EXPLICIT_TIME_COLUMN_NAME = "has_explicit_time"; 113 public static final String COMPLETED_SESSION_ID_COLUMN_NAME = "completed_session_id"; 114 115 // Exercise block columns. 116 private static final String BLOCK_ROW_ID_COLUMN_NAME = "block_row_id"; 117 private static final String BLOCK_PARENT_ID_COLUMN_NAME = "block_parent_id"; 118 private static final String BLOCK_DESCRIPTION_COLUMN_NAME = "block_description"; 119 private static final String BLOCK_REPETITIONS_COLUMN_NAME = "repetitions"; 120 121 // Exercise step columns. 122 private static final String STEP_ROW_ID_COLUMN_NAME = "step_row_id"; 123 private static final String STEP_PARENT_ID_COLUMN_NAME = "step_parent_id"; 124 private static final String STEP_DESCRIPTION_COLUMN_NAME = "step_description"; 125 private static final String STEP_CATEGORY_COLUMN_NAME = "category"; 126 private static final String STEP_EXERCISE_TYPE_COLUMN_NAME = "step_exercise_type"; 127 128 // Exercise goal columns. 129 private static final String GOAL_ROW_ID_COLUMN_NAME = "goal_row_id"; 130 private static final String GOAL_PARENT_ID_COLUMN_NAME = "goal_parent_id"; 131 private static final String GOAL_TYPE_ID_COLUMN_NAME = "type_id"; 132 private static final String GOAL_MIN_COLUMN_NAME = "goal_min"; 133 private static final String GOAL_MAX_COLUMN_NAME = "goal_max"; 134 PlannedExerciseSessionRecordHelper()135 public PlannedExerciseSessionRecordHelper() { 136 super(RecordTypeIdentifier.RECORD_TYPE_PLANNED_EXERCISE_SESSION); 137 } 138 139 /** Returns the table name to be created corresponding to this helper */ 140 @Override getMainTableName()141 public String getMainTableName() { 142 return PLANNED_EXERCISE_SESSION_RECORD_TABLE_NAME; 143 } 144 145 @Override getIntervalRecordColumnInfo()146 protected List<Pair<String, String>> getIntervalRecordColumnInfo() { 147 return Arrays.asList( 148 new Pair<>(NOTES_COLUMN_NAME, TEXT_NULL), 149 new Pair<>(EXERCISE_TYPE_COLUMN_NAME, INTEGER), 150 new Pair<>(TITLE_COLUMN_NAME, TEXT_NULL), 151 new Pair<>(HAS_EXPLICIT_TIME_COLUMN_NAME, INTEGER)); 152 // We add the completed exercise session ID column separately as it has a foreign key 153 // relationship with a different table. 154 } 155 156 /** Adds the required table for planned exercise sessions. */ getAlterTableRequestForPlannedExerciseFeature()157 public AlterTableRequest getAlterTableRequestForPlannedExerciseFeature() { 158 List<Pair<String, String>> columnInfo = new ArrayList<>(); 159 columnInfo.add(new Pair<>(COMPLETED_SESSION_ID_COLUMN_NAME, BLOB_NULL)); 160 AlterTableRequest result = new AlterTableRequest(getMainTableName(), columnInfo); 161 result.addForeignKeyConstraint( 162 COMPLETED_SESSION_ID_COLUMN_NAME, 163 EXERCISE_SESSION_RECORD_TABLE_NAME, 164 UUID_COLUMN_NAME); 165 return result; 166 } 167 168 @Override getChildTableCreateRequests()169 List<CreateTableRequest> getChildTableCreateRequests() { 170 return Arrays.asList( 171 new CreateTableRequest( 172 PLANNED_EXERCISE_SESSION_BLOCKS_TABLE_NAME, 173 Arrays.asList( 174 new Pair<>(BLOCK_ROW_ID_COLUMN_NAME, PRIMARY_AUTOINCREMENT), 175 new Pair<>(BLOCK_PARENT_ID_COLUMN_NAME, INTEGER_NOT_NULL), 176 new Pair<>(BLOCK_DESCRIPTION_COLUMN_NAME, TEXT_NULL), 177 new Pair<>( 178 BLOCK_REPETITIONS_COLUMN_NAME, INTEGER_NOT_NULL))) 179 .addForeignKey( 180 PLANNED_EXERCISE_SESSION_RECORD_TABLE_NAME, 181 Collections.singletonList(BLOCK_PARENT_ID_COLUMN_NAME), 182 Collections.singletonList(PRIMARY_COLUMN_NAME)), 183 new CreateTableRequest( 184 PLANNED_EXERCISE_SESSION_STEPS_TABLE_NAME, 185 Arrays.asList( 186 new Pair<>(STEP_ROW_ID_COLUMN_NAME, PRIMARY_AUTOINCREMENT), 187 new Pair<>(STEP_PARENT_ID_COLUMN_NAME, INTEGER_NOT_NULL), 188 new Pair<>(STEP_DESCRIPTION_COLUMN_NAME, TEXT_NULL), 189 new Pair<>(STEP_CATEGORY_COLUMN_NAME, INTEGER_NOT_NULL), 190 new Pair<>( 191 STEP_EXERCISE_TYPE_COLUMN_NAME, INTEGER_NOT_NULL))) 192 .addForeignKey( 193 PLANNED_EXERCISE_SESSION_BLOCKS_TABLE_NAME, 194 Collections.singletonList(STEP_PARENT_ID_COLUMN_NAME), 195 Collections.singletonList(BLOCK_ROW_ID_COLUMN_NAME)), 196 new CreateTableRequest( 197 PLANNED_EXERCISE_SESSION_GOALS_TABLE_NAME, 198 Arrays.asList( 199 new Pair<>(GOAL_ROW_ID_COLUMN_NAME, PRIMARY_AUTOINCREMENT), 200 new Pair<>(GOAL_PARENT_ID_COLUMN_NAME, INTEGER_NOT_NULL), 201 new Pair<>(GOAL_TYPE_ID_COLUMN_NAME, INTEGER_NOT_NULL), 202 new Pair<>(GOAL_MIN_COLUMN_NAME, BLOB), 203 new Pair<>(GOAL_MAX_COLUMN_NAME, BLOB))) 204 .addForeignKey( 205 PLANNED_EXERCISE_SESSION_STEPS_TABLE_NAME, 206 Collections.singletonList(GOAL_PARENT_ID_COLUMN_NAME), 207 Collections.singletonList(STEP_ROW_ID_COLUMN_NAME))); 208 } 209 210 @Override getJoinForReadRequest()211 SqlJoin getJoinForReadRequest() { 212 return new SqlJoin( 213 PLANNED_EXERCISE_SESSION_RECORD_TABLE_NAME, 214 PLANNED_EXERCISE_SESSION_BLOCKS_TABLE_NAME, 215 PRIMARY_COLUMN_NAME, 216 BLOCK_PARENT_ID_COLUMN_NAME) 217 .setJoinType(SqlJoin.SQL_JOIN_LEFT) 218 .attachJoin( 219 new SqlJoin( 220 PLANNED_EXERCISE_SESSION_BLOCKS_TABLE_NAME, 221 PLANNED_EXERCISE_SESSION_STEPS_TABLE_NAME, 222 BLOCK_ROW_ID_COLUMN_NAME, 223 STEP_PARENT_ID_COLUMN_NAME) 224 .setJoinType(SqlJoin.SQL_JOIN_LEFT)) 225 .attachJoin( 226 new SqlJoin( 227 PLANNED_EXERCISE_SESSION_STEPS_TABLE_NAME, 228 PLANNED_EXERCISE_SESSION_GOALS_TABLE_NAME, 229 STEP_ROW_ID_COLUMN_NAME, 230 GOAL_PARENT_ID_COLUMN_NAME) 231 .setJoinType(SqlJoin.SQL_JOIN_LEFT)); 232 } 233 234 @Override populateSpecificRecordValue( Cursor cursor, PlannedExerciseSessionRecordInternal plannedExerciseSessionRecord)235 void populateSpecificRecordValue( 236 Cursor cursor, PlannedExerciseSessionRecordInternal plannedExerciseSessionRecord) { 237 plannedExerciseSessionRecord.setNotes(getCursorString(cursor, NOTES_COLUMN_NAME)); 238 plannedExerciseSessionRecord.setExerciseType( 239 getCursorInt(cursor, EXERCISE_TYPE_COLUMN_NAME)); 240 plannedExerciseSessionRecord.setTitle(getCursorString(cursor, TITLE_COLUMN_NAME)); 241 plannedExerciseSessionRecord.setHasExplicitTime( 242 getCursorInt(cursor, HAS_EXPLICIT_TIME_COLUMN_NAME) != BOOLEAN_FALSE_VALUE); 243 if (!StorageUtils.isNullValue(cursor, COMPLETED_SESSION_ID_COLUMN_NAME)) { 244 plannedExerciseSessionRecord.setCompletedExerciseSessionId( 245 getCursorUUID(cursor, COMPLETED_SESSION_ID_COLUMN_NAME)); 246 } 247 248 plannedExerciseSessionRecord.setExerciseBlocks(extractBlocks(cursor)); 249 } 250 extractBlocks(Cursor cursor)251 private List<PlannedExerciseBlockInternal> extractBlocks(Cursor cursor) { 252 // In the case where there are *no* blocks in a planned session, the joined columns from the 253 // blocks table will be null. 254 if (cursor.isNull(cursor.getColumnIndex(BLOCK_REPETITIONS_COLUMN_NAME))) { 255 return Collections.emptyList(); 256 } 257 List<PlannedExerciseBlockInternal> result = new ArrayList<>(); 258 UUID uuid = getCursorUUID(cursor, UUID_COLUMN_NAME); 259 do { 260 // Populate blocks from each row. 261 PlannedExerciseBlockInternal block = 262 new PlannedExerciseBlockInternal( 263 getCursorInt(cursor, BLOCK_REPETITIONS_COLUMN_NAME)); 264 block.setDescription(getCursorString(cursor, BLOCK_DESCRIPTION_COLUMN_NAME)); 265 block.setExerciseSteps(extractSteps(cursor)); 266 result.add(block); 267 } while (cursor.moveToNext() && uuid.equals(getCursorUUID(cursor, UUID_COLUMN_NAME))); 268 // In case we hit another record, move the cursor back to read next record in outer 269 // RecordHelper#getInternalRecords loop. 270 cursor.moveToPrevious(); 271 return result; 272 } 273 extractSteps(Cursor cursor)274 private List<PlannedExerciseStepInternal> extractSteps(Cursor cursor) { 275 // In the case where there are *no* steps in a block, the joined columns from the steps 276 // table will be null. 277 if (cursor.isNull(cursor.getColumnIndex(STEP_EXERCISE_TYPE_COLUMN_NAME))) { 278 return Collections.emptyList(); 279 } 280 List<PlannedExerciseStepInternal> result = new ArrayList<>(); 281 long currentBlockId = getCursorInt(cursor, BLOCK_ROW_ID_COLUMN_NAME); 282 do { 283 long currentStepId = getCursorInt(cursor, STEP_ROW_ID_COLUMN_NAME); 284 // Populate steps from each row. 285 PlannedExerciseStepInternal step = 286 new PlannedExerciseStepInternal( 287 getCursorInt(cursor, STEP_EXERCISE_TYPE_COLUMN_NAME), 288 getCursorInt(cursor, STEP_CATEGORY_COLUMN_NAME), 289 extractCompletionGoal(cursor)); 290 step.setDescription(getCursorString(cursor, STEP_DESCRIPTION_COLUMN_NAME)); 291 List<ExercisePerformanceGoalInternal> performanceGoals = new ArrayList<>(); 292 while (cursor.moveToNext() 293 && getCursorInt(cursor, STEP_ROW_ID_COLUMN_NAME) == currentStepId) { 294 performanceGoals.add(extractPerformanceGoal(cursor)); 295 } 296 step.setPerformanceGoals(performanceGoals); 297 cursor.moveToPrevious(); 298 result.add(step); 299 } while (cursor.moveToNext() 300 && currentBlockId == getCursorInt(cursor, STEP_PARENT_ID_COLUMN_NAME)); 301 // In case we hit another block, move the cursor back to current block. 302 cursor.moveToPrevious(); 303 return result; 304 } 305 extractCompletionGoal(Cursor cursor)306 private ExerciseCompletionGoalInternal extractCompletionGoal(Cursor cursor) { 307 int goalTypeId = getCursorInt(cursor, GOAL_TYPE_ID_COLUMN_NAME); 308 switch (goalTypeId) { 309 case UnspecifiedGoalInternal.UNSPECIFIED_GOAL_TYPE_ID: 310 return UnspecifiedGoalInternal.INSTANCE; 311 case DistanceGoalInternal.DISTANCE_GOAL_TYPE_ID: 312 return new DistanceGoalInternal( 313 Length.fromMeters( 314 convertBytesToDouble(getCursorBlob(cursor, GOAL_MIN_COLUMN_NAME)))); 315 case StepsGoalInternal.STEPS_GOAL_TYPE_ID: 316 return new StepsGoalInternal( 317 convertBytesToInt(getCursorBlob(cursor, GOAL_MIN_COLUMN_NAME))); 318 case DurationGoalInternal.DURATION_GOAL_TYPE_ID: 319 return new DurationGoalInternal( 320 Duration.ofMillis( 321 convertBytesToLong(getCursorBlob(cursor, GOAL_MIN_COLUMN_NAME)))); 322 case RepetitionsGoalInternal.REPETITIONS_GOAL_TYPE_ID: 323 return new RepetitionsGoalInternal( 324 convertBytesToInt(getCursorBlob(cursor, GOAL_MIN_COLUMN_NAME))); 325 case TotalCaloriesBurnedGoalInternal.TOTAL_CALORIES_BURNED_GOAL_TYPE_ID: 326 return new TotalCaloriesBurnedGoalInternal( 327 Energy.fromCalories( 328 convertBytesToDouble(getCursorBlob(cursor, GOAL_MIN_COLUMN_NAME)))); 329 case ActiveCaloriesBurnedGoalInternal.ACTIVE_CALORIES_BURNED_GOAL_TYPE_ID: 330 return new ActiveCaloriesBurnedGoalInternal( 331 Energy.fromCalories( 332 convertBytesToDouble(getCursorBlob(cursor, GOAL_MIN_COLUMN_NAME)))); 333 case DistanceWithVariableRestGoalInternal.DISTANCE_WITH_VARIABLE_REST_GOAL_TYPE_ID: 334 return extractDistanceWithVariableRestGoal(cursor); 335 case ExerciseCompletionGoalInternal.UnknownGoalInternal.UNKNOWN_GOAL_TYPE_ID: 336 // Fall through. 337 default: 338 return ExerciseCompletionGoalInternal.UnknownGoalInternal.INSTANCE; 339 } 340 } 341 extractPerformanceGoal(Cursor cursor)342 private ExercisePerformanceGoalInternal extractPerformanceGoal(Cursor cursor) { 343 int goalTypeId = getCursorInt(cursor, GOAL_TYPE_ID_COLUMN_NAME); 344 switch (goalTypeId) { 345 case PowerGoalInternal.POWER_GOAL_TYPE_ID: 346 return new PowerGoalInternal( 347 Power.fromWatts( 348 convertBytesToDouble(getCursorBlob(cursor, GOAL_MIN_COLUMN_NAME))), 349 Power.fromWatts( 350 convertBytesToDouble(getCursorBlob(cursor, GOAL_MAX_COLUMN_NAME)))); 351 case SpeedGoalInternal.SPEED_GOAL_TYPE_ID: 352 return new SpeedGoalInternal( 353 Velocity.fromMetersPerSecond( 354 convertBytesToDouble(getCursorBlob(cursor, GOAL_MIN_COLUMN_NAME))), 355 Velocity.fromMetersPerSecond( 356 convertBytesToDouble(getCursorBlob(cursor, GOAL_MAX_COLUMN_NAME)))); 357 case CadenceGoalInternal.CADENCE_GOAL_TYPE_ID: 358 return new CadenceGoalInternal( 359 convertBytesToDouble(getCursorBlob(cursor, GOAL_MIN_COLUMN_NAME)), 360 convertBytesToDouble(getCursorBlob(cursor, GOAL_MAX_COLUMN_NAME))); 361 case HeartRateGoalInternal.HEART_RATE_GOAL_TYPE_ID: 362 return new HeartRateGoalInternal( 363 convertBytesToInt(getCursorBlob(cursor, GOAL_MIN_COLUMN_NAME)), 364 convertBytesToInt(getCursorBlob(cursor, GOAL_MAX_COLUMN_NAME))); 365 case WeightGoalInternal.WEIGHT_GOAL_TYPE_ID: 366 return new WeightGoalInternal( 367 Mass.fromGrams( 368 convertBytesToDouble(getCursorBlob(cursor, GOAL_MIN_COLUMN_NAME)))); 369 case RateOfPerceivedExertionGoalInternal.RATE_OF_PERCEIVED_EXERTION_TYPE_ID: 370 return new RateOfPerceivedExertionGoalInternal( 371 convertBytesToInt(getCursorBlob(cursor, GOAL_MIN_COLUMN_NAME))); 372 case AmrapGoalInternal.AMRAP_GOAL_TYPE_ID: 373 return AmrapGoalInternal.INSTANCE; 374 case ExercisePerformanceGoalInternal.UnknownGoalInternal.UNKNOWN_GOAL_TYPE_ID: 375 // Fall through. 376 default: 377 return ExercisePerformanceGoalInternal.UnknownGoalInternal.INSTANCE; 378 } 379 } 380 381 @Override populateSpecificContentValues( ContentValues contentValues, PlannedExerciseSessionRecordInternal exerciseSessionRecord)382 void populateSpecificContentValues( 383 ContentValues contentValues, 384 PlannedExerciseSessionRecordInternal exerciseSessionRecord) { 385 contentValues.put(NOTES_COLUMN_NAME, exerciseSessionRecord.getNotes()); 386 contentValues.put(EXERCISE_TYPE_COLUMN_NAME, exerciseSessionRecord.getExerciseType()); 387 contentValues.put(TITLE_COLUMN_NAME, exerciseSessionRecord.getTitle()); 388 if (exerciseSessionRecord.getCompletedExerciseSessionId() != null) { 389 contentValues.put( 390 COMPLETED_SESSION_ID_COLUMN_NAME, 391 exerciseSessionRecord.getCompletedExerciseSessionId().toString()); 392 } 393 contentValues.put( 394 HAS_EXPLICIT_TIME_COLUMN_NAME, 395 exerciseSessionRecord.getHasExplicitTime() 396 ? BOOLEAN_TRUE_VALUE 397 : BOOLEAN_FALSE_VALUE); 398 } 399 400 @Override getChildTableUpsertRequests( PlannedExerciseSessionRecordInternal record)401 List<UpsertTableRequest> getChildTableUpsertRequests( 402 PlannedExerciseSessionRecordInternal record) { 403 List<UpsertTableRequest> blockUpsertRequests = new ArrayList<>(); 404 for (PlannedExerciseBlockInternal exerciseBlock : record.getExerciseBlocks()) { 405 blockUpsertRequests.add(getBlockUpsertRequest(exerciseBlock)); 406 } 407 408 return blockUpsertRequests; 409 } 410 getBlockUpsertRequest(PlannedExerciseBlockInternal exerciseBlock)411 private UpsertTableRequest getBlockUpsertRequest(PlannedExerciseBlockInternal exerciseBlock) { 412 ContentValues blockContentValues = new ContentValues(); 413 blockContentValues.put(BLOCK_REPETITIONS_COLUMN_NAME, exerciseBlock.getRepetitions()); 414 blockContentValues.put(BLOCK_DESCRIPTION_COLUMN_NAME, exerciseBlock.getDescription()); 415 UpsertTableRequest blockUpsertRequest = 416 new UpsertTableRequest( 417 PLANNED_EXERCISE_SESSION_BLOCKS_TABLE_NAME, blockContentValues) 418 .setParentColumnForChildTables(BLOCK_PARENT_ID_COLUMN_NAME); 419 420 List<UpsertTableRequest> stepUpsertRequests = new ArrayList<>(); 421 for (PlannedExerciseStepInternal exerciseStep : exerciseBlock.getExerciseSteps()) { 422 stepUpsertRequests.add(getStepUpsert(exerciseStep)); 423 } 424 425 blockUpsertRequest.setChildTableRequests(stepUpsertRequests); 426 return blockUpsertRequest; 427 } 428 getStepUpsert(PlannedExerciseStepInternal exerciseStep)429 private UpsertTableRequest getStepUpsert(PlannedExerciseStepInternal exerciseStep) { 430 ContentValues stepContentValues = new ContentValues(); 431 stepContentValues.put(STEP_DESCRIPTION_COLUMN_NAME, exerciseStep.getDescription()); 432 stepContentValues.put(STEP_CATEGORY_COLUMN_NAME, exerciseStep.getExerciseCategory()); 433 stepContentValues.put(STEP_EXERCISE_TYPE_COLUMN_NAME, exerciseStep.getExerciseType()); 434 435 UpsertTableRequest stepUpsertRequest = 436 new UpsertTableRequest(PLANNED_EXERCISE_SESSION_STEPS_TABLE_NAME, stepContentValues) 437 .setParentColumnForChildTables(STEP_PARENT_ID_COLUMN_NAME); 438 439 List<UpsertTableRequest> goalUpsertRequests = new ArrayList<>(); 440 goalUpsertRequests.add(getCompletionGoalUpsert(exerciseStep.getCompletionGoal())); 441 for (ExercisePerformanceGoalInternal performanceGoal : exerciseStep.getPerformanceGoals()) { 442 goalUpsertRequests.add(getPerformanceGoalUpsert(performanceGoal)); 443 } 444 stepUpsertRequest.setChildTableRequests(goalUpsertRequests); 445 return stepUpsertRequest; 446 } 447 getCompletionGoalUpsert( ExerciseCompletionGoalInternal completionGoal)448 private UpsertTableRequest getCompletionGoalUpsert( 449 ExerciseCompletionGoalInternal completionGoal) { 450 451 ContentValues completionGoalContentValues = new ContentValues(); 452 completionGoalContentValues.put(GOAL_TYPE_ID_COLUMN_NAME, completionGoal.getTypeId()); 453 if (completionGoal instanceof DistanceGoalInternal) { 454 completionGoalContentValues.put( 455 GOAL_MIN_COLUMN_NAME, 456 convertDoubleToBytes( 457 ((DistanceGoalInternal) completionGoal).getDistance().getInMeters())); 458 } else if (completionGoal instanceof StepsGoalInternal) { 459 completionGoalContentValues.put( 460 GOAL_MIN_COLUMN_NAME, 461 convertIntToBytes(((StepsGoalInternal) completionGoal).getSteps())); 462 463 } else if (completionGoal instanceof DurationGoalInternal) { 464 completionGoalContentValues.put( 465 GOAL_MIN_COLUMN_NAME, 466 convertLongToBytes( 467 ((DurationGoalInternal) completionGoal).getDuration().toMillis())); 468 } else if (completionGoal instanceof RepetitionsGoalInternal) { 469 completionGoalContentValues.put( 470 GOAL_MIN_COLUMN_NAME, 471 convertIntToBytes(((RepetitionsGoalInternal) completionGoal).getReps())); 472 } else if (completionGoal instanceof TotalCaloriesBurnedGoalInternal) { 473 completionGoalContentValues.put( 474 GOAL_MIN_COLUMN_NAME, 475 convertDoubleToBytes( 476 ((TotalCaloriesBurnedGoalInternal) completionGoal) 477 .getTotalCalories() 478 .getInCalories())); 479 } else if (completionGoal instanceof ActiveCaloriesBurnedGoalInternal) { 480 completionGoalContentValues.put( 481 GOAL_MIN_COLUMN_NAME, 482 convertDoubleToBytes( 483 ((ActiveCaloriesBurnedGoalInternal) completionGoal) 484 .getActiveCalories() 485 .getInCalories())); 486 } else if (completionGoal instanceof DistanceWithVariableRestGoalInternal) { 487 populateContentValuesForDistanceWithVariableRestGoal( 488 completionGoalContentValues, 489 (DistanceWithVariableRestGoalInternal) completionGoal); 490 } 491 return new UpsertTableRequest( 492 PLANNED_EXERCISE_SESSION_GOALS_TABLE_NAME, completionGoalContentValues) 493 .setParentColumnForChildTables(GOAL_PARENT_ID_COLUMN_NAME); 494 } 495 getPerformanceGoalUpsert( ExercisePerformanceGoalInternal performanceGoal)496 private UpsertTableRequest getPerformanceGoalUpsert( 497 ExercisePerformanceGoalInternal performanceGoal) { 498 ContentValues performanceGoalContentValues = new ContentValues(); 499 performanceGoalContentValues.put(GOAL_TYPE_ID_COLUMN_NAME, performanceGoal.getTypeId()); 500 if (performanceGoal instanceof PowerGoalInternal) { 501 performanceGoalContentValues.put( 502 GOAL_MIN_COLUMN_NAME, 503 convertDoubleToBytes( 504 ((PowerGoalInternal) performanceGoal).getMinPower().getInWatts())); 505 performanceGoalContentValues.put( 506 GOAL_MAX_COLUMN_NAME, 507 convertDoubleToBytes( 508 ((PowerGoalInternal) performanceGoal).getMaxPower().getInWatts())); 509 } else if (performanceGoal instanceof SpeedGoalInternal) { 510 performanceGoalContentValues.put( 511 GOAL_MIN_COLUMN_NAME, 512 convertDoubleToBytes( 513 ((SpeedGoalInternal) performanceGoal) 514 .getMinSpeed() 515 .getInMetersPerSecond())); 516 performanceGoalContentValues.put( 517 GOAL_MAX_COLUMN_NAME, 518 convertDoubleToBytes( 519 ((SpeedGoalInternal) performanceGoal) 520 .getMaxSpeed() 521 .getInMetersPerSecond())); 522 } else if (performanceGoal instanceof CadenceGoalInternal) { 523 performanceGoalContentValues.put( 524 GOAL_MIN_COLUMN_NAME, 525 convertDoubleToBytes(((CadenceGoalInternal) performanceGoal).getMinRpm())); 526 performanceGoalContentValues.put( 527 GOAL_MAX_COLUMN_NAME, 528 convertDoubleToBytes(((CadenceGoalInternal) performanceGoal).getMaxRpm())); 529 } else if (performanceGoal instanceof HeartRateGoalInternal) { 530 performanceGoalContentValues.put( 531 GOAL_MIN_COLUMN_NAME, 532 convertIntToBytes(((HeartRateGoalInternal) performanceGoal).getMinBpm())); 533 performanceGoalContentValues.put( 534 GOAL_MAX_COLUMN_NAME, 535 convertIntToBytes(((HeartRateGoalInternal) performanceGoal).getMaxBpm())); 536 } else if (performanceGoal instanceof WeightGoalInternal) { 537 performanceGoalContentValues.put( 538 GOAL_MIN_COLUMN_NAME, 539 convertDoubleToBytes( 540 ((WeightGoalInternal) performanceGoal).getMass().getInGrams())); 541 } else if (performanceGoal instanceof RateOfPerceivedExertionGoalInternal) { 542 performanceGoalContentValues.put( 543 GOAL_MIN_COLUMN_NAME, 544 convertIntToBytes( 545 ((RateOfPerceivedExertionGoalInternal) performanceGoal).getRpe())); 546 } 547 return new UpsertTableRequest( 548 PLANNED_EXERCISE_SESSION_GOALS_TABLE_NAME, performanceGoalContentValues) 549 .setParentColumnForChildTables(GOAL_PARENT_ID_COLUMN_NAME); 550 } 551 extractDistanceWithVariableRestGoal( Cursor cursor)552 private static DistanceWithVariableRestGoalInternal extractDistanceWithVariableRestGoal( 553 Cursor cursor) { 554 byte[] bytes = getCursorBlob(cursor, GOAL_MIN_COLUMN_NAME); 555 Length distance = 556 Length.fromMeters(convertBytesToDouble(Arrays.copyOfRange(bytes, 0, Double.BYTES))); 557 Duration duration = 558 Duration.ofMillis( 559 convertBytesToLong( 560 Arrays.copyOfRange( 561 bytes, Double.BYTES, Double.BYTES + Long.BYTES))); 562 return new DistanceWithVariableRestGoalInternal(distance, duration); 563 } 564 populateContentValuesForDistanceWithVariableRestGoal( ContentValues contentValues, DistanceWithVariableRestGoalInternal distanceWithVariableRestGoalInternal)565 private static void populateContentValuesForDistanceWithVariableRestGoal( 566 ContentValues contentValues, 567 DistanceWithVariableRestGoalInternal distanceWithVariableRestGoalInternal) { 568 byte[] distanceBytes = 569 convertDoubleToBytes( 570 distanceWithVariableRestGoalInternal.getDistance().getInMeters()); 571 byte[] durationBytes = 572 convertLongToBytes(distanceWithVariableRestGoalInternal.getDuration().toMillis()); 573 byte[] bytes = new byte[16]; 574 System.arraycopy(distanceBytes, 0, bytes, 0, Double.BYTES); 575 System.arraycopy(durationBytes, 0, bytes, Double.BYTES, Long.BYTES); 576 contentValues.put(GOAL_MIN_COLUMN_NAME, bytes); 577 } 578 579 @Override getChildTablesWithRowsToBeDeletedDuringUpdate( @ullable ArrayMap<String, Boolean> extraWritePermissionToState)580 public List<TableColumnPair> getChildTablesWithRowsToBeDeletedDuringUpdate( 581 @Nullable ArrayMap<String, Boolean> extraWritePermissionToState) { 582 // Children of the block table will get automatically deleted via cascades. 583 return Collections.singletonList( 584 new TableColumnPair( 585 PLANNED_EXERCISE_SESSION_BLOCKS_TABLE_NAME, BLOCK_PARENT_ID_COLUMN_NAME)); 586 } 587 588 @Override getReadRequestsForRecordsModifiedByDeletion( UUID deletedRecordUuid)589 public List<ReadTableRequest> getReadRequestsForRecordsModifiedByDeletion( 590 UUID deletedRecordUuid) { 591 ReadTableRequest affectedExerciseSessionsReadRequest = 592 new ReadTableRequest(EXERCISE_SESSION_RECORD_TABLE_NAME); 593 affectedExerciseSessionsReadRequest.setColumnNames( 594 Arrays.asList( 595 UUID_COLUMN_NAME, 596 APP_INFO_ID_COLUMN_NAME, 597 PLANNED_EXERCISE_SESSION_ID_COLUMN_NAME)); 598 WhereClauses whereStatement = new WhereClauses(WhereClauses.LogicalOperator.AND); 599 whereStatement.addWhereEqualsClause( 600 PLANNED_EXERCISE_SESSION_ID_COLUMN_NAME, 601 StorageUtils.getHexString(deletedRecordUuid)); 602 affectedExerciseSessionsReadRequest.setWhereClause(whereStatement); 603 affectedExerciseSessionsReadRequest.setRecordHelper( 604 InternalHealthConnectMappings.getInstance() 605 .getRecordHelper(RECORD_TYPE_EXERCISE_SESSION)); 606 return Collections.singletonList(affectedExerciseSessionsReadRequest); 607 } 608 } 609