• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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