• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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.aggregation;
18 
19 import static android.health.connect.datatypes.AggregationType.AggregationTypeIdentifier.ACTIVE_CALORIES_BURNED_RECORD_ACTIVE_CALORIES_TOTAL;
20 import static android.health.connect.datatypes.AggregationType.AggregationTypeIdentifier.DISTANCE_RECORD_DISTANCE_TOTAL;
21 import static android.health.connect.datatypes.AggregationType.AggregationTypeIdentifier.ELEVATION_RECORD_ELEVATION_GAINED_TOTAL;
22 import static android.health.connect.datatypes.AggregationType.AggregationTypeIdentifier.EXERCISE_SESSION_DURATION_TOTAL;
23 import static android.health.connect.datatypes.AggregationType.AggregationTypeIdentifier.FLOORS_CLIMBED_RECORD_FLOORS_CLIMBED_TOTAL;
24 import static android.health.connect.datatypes.AggregationType.AggregationTypeIdentifier.SLEEP_SESSION_DURATION_TOTAL;
25 import static android.health.connect.datatypes.AggregationType.AggregationTypeIdentifier.STEPS_RECORD_COUNT_TOTAL;
26 import static android.health.connect.datatypes.AggregationType.AggregationTypeIdentifier.WHEEL_CHAIR_PUSHES_RECORD_COUNT_TOTAL;
27 
28 import android.database.Cursor;
29 import android.health.connect.Constants;
30 import android.health.connect.datatypes.AggregationType;
31 import android.util.ArrayMap;
32 import android.util.Slog;
33 
34 import com.android.internal.annotations.VisibleForTesting;
35 import com.android.server.healthconnect.storage.request.AggregateParams;
36 
37 import java.time.ZoneOffset;
38 import java.util.List;
39 import java.util.Map;
40 import java.util.TreeSet;
41 
42 /**
43  * Aggregates records with priorities.
44  *
45  * @hide
46  */
47 public class PriorityRecordsAggregator {
48     static final String TAG = "HealthPriorityRecordsAggregator";
49 
50     private final List<Long> mGroupSplits;
51     private final Map<Long, Integer> mAppIdToPriority;
52     private final Map<Integer, Double> mGroupToAggregationResult;
53     private final Map<Integer, ZoneOffset> mGroupToFirstZoneOffset;
54     private final int mNumberOfGroups;
55     private int mCurrentGroup = -1;
56     private long mLatestPopulatedStart = -1;
57     @AggregationType.AggregationTypeIdentifier private final int mAggregationType;
58 
59     private final TreeSet<AggregationTimestamp> mTimestampsBuffer;
60     private final TreeSet<AggregationRecordData> mOpenIntervals;
61 
62     private final AggregateParams.PriorityAggregationExtraParams mExtraParams;
63 
64     private final boolean mUseLocalTime;
65 
PriorityRecordsAggregator( List<Long> groupSplits, List<Long> appIdPriorityList, @AggregationType.AggregationTypeIdentifier int aggregationType, AggregateParams.PriorityAggregationExtraParams extraParams, boolean useLocalTime)66     public PriorityRecordsAggregator(
67             List<Long> groupSplits,
68             List<Long> appIdPriorityList,
69             @AggregationType.AggregationTypeIdentifier int aggregationType,
70             AggregateParams.PriorityAggregationExtraParams extraParams,
71             boolean useLocalTime) {
72         mGroupSplits = groupSplits;
73         mAggregationType = aggregationType;
74         mExtraParams = extraParams;
75         mAppIdToPriority = new ArrayMap<>();
76         for (int i = 0; i < appIdPriorityList.size(); i++) {
77             // Add to the map with -index, so app with higher priority has higher value in the map.
78             mAppIdToPriority.put(appIdPriorityList.get(i), appIdPriorityList.size() - i);
79         }
80         mUseLocalTime = useLocalTime;
81         mTimestampsBuffer = new TreeSet<>();
82         mNumberOfGroups = mGroupSplits.size() - 1;
83         mGroupToFirstZoneOffset = new ArrayMap<>(mNumberOfGroups);
84         mOpenIntervals = new TreeSet<>();
85         mGroupToAggregationResult = new ArrayMap<>(mGroupSplits.size());
86 
87         if (Constants.DEBUG) {
88             Slog.d(
89                     TAG,
90                     "Aggregation request for splits: "
91                             + mGroupSplits
92                             + " with priorities: "
93                             + appIdPriorityList);
94         }
95     }
96 
97     /** Calculates aggregation result for each group. */
calculateAggregation(Cursor cursor)98     public void calculateAggregation(Cursor cursor) {
99         initialiseTimestampsBuffer(cursor);
100         populateTimestampBuffer(cursor);
101         AggregationTimestamp scanPoint, nextPoint;
102         while (mTimestampsBuffer.size() > 1) {
103             scanPoint = mTimestampsBuffer.pollFirst();
104             nextPoint = mTimestampsBuffer.first();
105             if (scanPoint.getType() == AggregationTimestamp.GROUP_BORDER) {
106                 mCurrentGroup += 1;
107             } else if (scanPoint.getType() == AggregationTimestamp.INTERVAL_START) {
108                 mOpenIntervals.add(scanPoint.getParentData());
109             } else if (scanPoint.getType() == AggregationTimestamp.INTERVAL_END) {
110                 mOpenIntervals.remove(scanPoint.getParentData());
111             } else {
112                 throw new UnsupportedOperationException(
113                         "Unknown aggregation timestamp type: " + scanPoint.getType());
114             }
115             updateAggregationResult(scanPoint, nextPoint);
116             populateTimestampBuffer(cursor);
117         }
118 
119         if (Constants.DEBUG) {
120             Slog.d(TAG, "Aggregation result: " + mGroupToAggregationResult.toString());
121         }
122     }
123 
populateTimestampBuffer(Cursor cursor)124     private void populateTimestampBuffer(Cursor cursor) {
125         // Buffer populating strategy guarantees that at the moment we start to process the earliest
126         // record, we added to the buffer later overlapping records and the first non-overlapping
127         // record. It guarantees that the aggregation score can be calculated correctly for any
128         // timestamp within the earliest record interval.
129         if (mTimestampsBuffer.first().getType() != AggregationTimestamp.INTERVAL_START) {
130             return;
131         }
132 
133         // Add record timestamps to buffer until latest buffer record do not overlap with earliest
134         // buffer record.
135         long expansionBorder = mTimestampsBuffer.first().getParentData().getEndTime();
136         if (Constants.DEBUG) {
137             Slog.d(
138                     TAG,
139                     "Try to update buffer exp border: "
140                             + expansionBorder
141                             + " latest start: "
142                             + mLatestPopulatedStart);
143         }
144 
145         while (mLatestPopulatedStart <= expansionBorder && cursor.moveToNext()) {
146             AggregationRecordData data = readNewDataAndAddToBuffer(cursor);
147             mLatestPopulatedStart = data.getStartTime();
148 
149             if (Constants.DEBUG) {
150                 Slog.d(TAG, "Updated buffer with : " + data);
151             }
152         }
153 
154         if (Constants.DEBUG) {
155             Slog.d(TAG, "Timestamps buffer: " + mTimestampsBuffer);
156         }
157     }
158 
initialiseTimestampsBuffer(Cursor cursor)159     private void initialiseTimestampsBuffer(Cursor cursor) {
160         for (Long groupSplit : mGroupSplits) {
161             mTimestampsBuffer.add(
162                     new AggregationTimestamp(AggregationTimestamp.GROUP_BORDER, groupSplit));
163         }
164 
165         if (cursor.moveToNext()) {
166             readNewDataAndAddToBuffer(cursor);
167         }
168 
169         if (Constants.DEBUG) {
170             Slog.d(TAG, "Initialised aggregation buffer: " + mTimestampsBuffer);
171         }
172     }
173 
readNewDataAndAddToBuffer(Cursor cursor)174     private AggregationRecordData readNewDataAndAddToBuffer(Cursor cursor) {
175         AggregationRecordData data = readNewData(cursor);
176         mTimestampsBuffer.add(data.getStartTimestamp());
177         mTimestampsBuffer.add(data.getEndTimestamp());
178         return data;
179     }
180 
181     @VisibleForTesting
readNewData(Cursor cursor)182     AggregationRecordData readNewData(Cursor cursor) {
183         AggregationRecordData data = createAggregationRecordData();
184         data.populateAggregationData(cursor, mUseLocalTime, mAppIdToPriority);
185         return data;
186     }
187 
188     /** Returns result for the given group */
getResultForGroup(Integer groupNumber)189     public Double getResultForGroup(Integer groupNumber) {
190         return mGroupToAggregationResult.get(groupNumber);
191     }
192 
193     /** Returns start time zone offset for the given group */
getZoneOffsetForGroup(Integer groupNumber)194     public ZoneOffset getZoneOffsetForGroup(Integer groupNumber) {
195         return mGroupToFirstZoneOffset.get(groupNumber);
196     }
197 
createAggregationRecordData()198     private AggregationRecordData createAggregationRecordData() {
199         return switch (mAggregationType) {
200             case STEPS_RECORD_COUNT_TOTAL,
201                     ACTIVE_CALORIES_BURNED_RECORD_ACTIVE_CALORIES_TOTAL,
202                     DISTANCE_RECORD_DISTANCE_TOTAL,
203                     ELEVATION_RECORD_ELEVATION_GAINED_TOTAL,
204                     FLOORS_CLIMBED_RECORD_FLOORS_CLIMBED_TOTAL,
205                     WHEEL_CHAIR_PUSHES_RECORD_COUNT_TOTAL -> new ValueColumnAggregationData(
206                     mExtraParams.getColumnToAggregateName(),
207                     mExtraParams.getColumnToAggregateType());
208             case SLEEP_SESSION_DURATION_TOTAL,
209                     EXERCISE_SESSION_DURATION_TOTAL -> new SessionDurationAggregationData(
210                     mExtraParams.getExcludeIntervalStartColumnName(),
211                     mExtraParams.getExcludeIntervalEndColumnName());
212             default -> throw new UnsupportedOperationException(
213                     "Priority aggregation do not support type: " + mAggregationType);
214         };
215     }
216 
updateAggregationResult( AggregationTimestamp startPoint, AggregationTimestamp endPoint)217     private void updateAggregationResult(
218             AggregationTimestamp startPoint, AggregationTimestamp endPoint) {
219         if (Constants.DEBUG) {
220             Slog.d(
221                     TAG,
222                     "Updating result for group "
223                             + mCurrentGroup
224                             + " for interval: ("
225                             + startPoint.getTime()
226                             + ", "
227                             + endPoint.getTime()
228                             + ")");
229         }
230 
231         if (mOpenIntervals.isEmpty() || mCurrentGroup < 0 || mCurrentGroup >= mNumberOfGroups) {
232             if (Constants.DEBUG) {
233                 Slog.d(TAG, "No open intervals or current group: " + mCurrentGroup);
234             }
235             return;
236         }
237 
238         if (!mGroupToAggregationResult.containsKey(mCurrentGroup)) {
239             mGroupToAggregationResult.put(mCurrentGroup, 0.0d);
240         }
241 
242         if (Constants.DEBUG) {
243             Slog.d(TAG, "Update result with: " + mOpenIntervals.last());
244         }
245 
246         mGroupToAggregationResult.put(
247                 mCurrentGroup,
248                 mGroupToAggregationResult.get(mCurrentGroup)
249                         + mOpenIntervals
250                                 .last()
251                                 .getResultOnInterval(startPoint.getTime(), endPoint.getTime()));
252 
253         if (mCurrentGroup >= 0
254                 && !mGroupToFirstZoneOffset.containsKey(mCurrentGroup)
255                 && !mOpenIntervals.isEmpty()) {
256             mGroupToFirstZoneOffset.put(mCurrentGroup, getZoneOffsetOfEarliestOpenInterval());
257         }
258     }
259 
getZoneOffsetOfEarliestOpenInterval()260     private ZoneOffset getZoneOffsetOfEarliestOpenInterval() {
261         AggregationRecordData earliestInterval = mOpenIntervals.first();
262         for (AggregationRecordData data : mOpenIntervals) {
263             if (data.getStartTime() < earliestInterval.getStartTime()) {
264                 earliestInterval = data;
265             }
266         }
267         return earliestInterval.getStartTimeZoneOffset();
268     }
269 }
270