• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.healthconnect.storage.request;
18 
19 import static android.health.connect.datatypes.AggregationType.AVG;
20 import static android.health.connect.datatypes.AggregationType.COUNT;
21 import static android.health.connect.datatypes.AggregationType.MAX;
22 import static android.health.connect.datatypes.AggregationType.MIN;
23 import static android.health.connect.datatypes.AggregationType.SUM;
24 
25 import static com.android.server.healthconnect.storage.datatypehelpers.RecordHelper.APP_INFO_ID_COLUMN_NAME;
26 
27 import android.annotation.NonNull;
28 import android.database.Cursor;
29 import android.health.connect.AggregateResult;
30 import android.health.connect.Constants;
31 import android.health.connect.LocalTimeRangeFilter;
32 import android.health.connect.TimeRangeFilter;
33 import android.health.connect.TimeRangeFilterHelper;
34 import android.health.connect.datatypes.AggregationType;
35 import android.util.ArrayMap;
36 import android.util.Pair;
37 import android.util.Slog;
38 
39 import com.android.server.healthconnect.storage.TransactionManager;
40 import com.android.server.healthconnect.storage.datatypehelpers.AppInfoHelper;
41 import com.android.server.healthconnect.storage.datatypehelpers.RecordHelper;
42 import com.android.server.healthconnect.storage.datatypehelpers.aggregation.PriorityRecordsAggregator;
43 import com.android.server.healthconnect.storage.utils.OrderByClause;
44 import com.android.server.healthconnect.storage.utils.SqlJoin;
45 import com.android.server.healthconnect.storage.utils.StorageUtils;
46 import com.android.server.healthconnect.storage.utils.WhereClauses;
47 
48 import java.time.Duration;
49 import java.time.LocalDateTime;
50 import java.time.Period;
51 import java.util.ArrayList;
52 import java.util.List;
53 import java.util.Map;
54 
55 /**
56  * A request for {@link TransactionManager} to query the DB for aggregation results
57  *
58  * @hide
59  */
60 public class AggregateTableRequest {
61     private static final String TAG = "HealthConnectAggregate";
62     private static final String GROUP_BY_COLUMN_NAME = "category";
63 
64     private final long DEFAULT_TIME = -1;
65     private final String mTableName;
66     private final List<String> mColumnNamesToAggregate;
67     private final AggregationType<?> mAggregationType;
68     private final RecordHelper<?> mRecordHelper;
69     private final Map<Integer, AggregateResult<?>> mAggregateResults = new ArrayMap<>();
70     private final String mTimeColumnName;
71     // Additional column used for time filtering. End time for interval records,
72     // null for other records.
73     private final String mEndTimeColumnName;
74     private final SqlJoin mSqlJoin;
75     private List<Long> mPackageFilters;
76     private long mStartTime = DEFAULT_TIME;
77     private long mEndTime = DEFAULT_TIME;
78     private String mPackageColumnName;
79     private String mGroupByColumnName;
80     private int mGroupBySize = 1;
81     private final List<String> mAdditionalColumnsToFetch;
82     private final AggregateParams.PriorityAggregationExtraParams mPriorityParams;
83     private final boolean mUseLocalTime;
84     private List<Long> mTimeSplits;
85 
AggregateTableRequest( AggregateParams params, AggregationType<?> aggregationType, RecordHelper<?> recordHelper, boolean useLocalTime)86     public AggregateTableRequest(
87             AggregateParams params,
88             AggregationType<?> aggregationType,
89             RecordHelper<?> recordHelper,
90             boolean useLocalTime) {
91         mTableName = params.getTableName();
92         mColumnNamesToAggregate = params.getColumnsToFetch();
93         mTimeColumnName = params.getTimeColumnName();
94         mAggregationType = aggregationType;
95         mRecordHelper = recordHelper;
96         mSqlJoin = params.getJoin();
97         mPriorityParams = params.getPriorityAggregationExtraParams();
98         mEndTimeColumnName = params.getExtraTimeColumnName();
99         mAdditionalColumnsToFetch = new ArrayList<>();
100         mAdditionalColumnsToFetch.add(params.getTimeOffsetColumnName());
101         mAdditionalColumnsToFetch.add(mTimeColumnName);
102         if (mEndTimeColumnName != null) {
103             mAdditionalColumnsToFetch.add(mEndTimeColumnName);
104         }
105         mUseLocalTime = useLocalTime;
106     }
107 
108     /**
109      * @return {@link AggregationType} for this request
110      */
getAggregationType()111     public AggregationType<?> getAggregationType() {
112         return mAggregationType;
113     }
114 
115     /**
116      * @return {@link RecordHelper} for this request
117      */
getRecordHelper()118     public RecordHelper<?> getRecordHelper() {
119         return mRecordHelper;
120     }
121 
122     /**
123      * @return results fetched after performing aggregate operation for this class.
124      *     <p>Note: Only available after the call to {@link
125      *     TransactionManager#populateWithAggregation} has been made
126      */
getAggregateResults()127     public List<AggregateResult<?>> getAggregateResults() {
128         List<AggregateResult<?>> aggregateResults = new ArrayList<>(mGroupBySize);
129         for (int i = 0; i < mGroupBySize; i++) {
130             aggregateResults.add(mAggregateResults.get(i));
131         }
132 
133         return aggregateResults;
134     }
135 
136     /** Returns SQL statement to get data origins for the aggregation operation */
getCommandToFetchAggregateMetadata()137     public String getCommandToFetchAggregateMetadata() {
138         final StringBuilder builder = new StringBuilder("SELECT DISTINCT ");
139         builder.append(APP_INFO_ID_COLUMN_NAME).append(", ");
140         return appendAggregateCommand(builder, /* isMetadata= */ true);
141     }
142 
143     /** Returns name of the main time column (start time for Interval, time for Instant records) */
getTimeColumnName()144     public String getTimeColumnName() {
145         return mTimeColumnName;
146     }
147 
148     /** Returns whether request is using local time instead of physical one. */
getUseLocalTime()149     public boolean getUseLocalTime() {
150         return mUseLocalTime;
151     }
152 
153     /** Returns SQL statement to perform aggregation operation */
154     @NonNull
getAggregationCommand()155     public String getAggregationCommand() {
156         final StringBuilder builder = new StringBuilder("SELECT ");
157         String aggCommand;
158         boolean usingPriority =
159                 StorageUtils.supportsPriority(
160                                 mRecordHelper.getRecordIdentifier(),
161                                 mAggregationType.getAggregateOperationType())
162                         || StorageUtils.isDerivedType(mRecordHelper.getRecordIdentifier());
163         if (usingPriority) {
164             for (String columnName : mColumnNamesToAggregate) {
165                 builder.append(columnName).append(", ");
166             }
167         } else {
168             aggCommand = getSqlCommandFor(mAggregationType.getAggregateOperationType());
169 
170             for (String columnName : mColumnNamesToAggregate) {
171                 builder.append(aggCommand)
172                         .append("(")
173                         .append(columnName)
174                         .append(")")
175                         .append(" as ")
176                         .append(columnName)
177                         .append(", ");
178             }
179         }
180 
181         if (mAdditionalColumnsToFetch != null) {
182             for (String additionalColumnToFetch : mAdditionalColumnsToFetch) {
183                 builder.append(additionalColumnToFetch).append(", ");
184             }
185         }
186 
187         return appendAggregateCommand(builder, usingPriority);
188     }
189 
setPackageFilter( List<Long> packageFilters, String packageColumnName)190     public AggregateTableRequest setPackageFilter(
191             List<Long> packageFilters, String packageColumnName) {
192         mPackageFilters = packageFilters;
193         mPackageColumnName = packageColumnName;
194 
195         return this;
196     }
197 
198     /** Sets time filter for table request. */
setTimeFilter(long startTime, long endTime)199     public AggregateTableRequest setTimeFilter(long startTime, long endTime) {
200         // Return if the params will result in no impact on the query
201         if (startTime < 0 || endTime < startTime) {
202             return this;
203         }
204 
205         mStartTime = startTime;
206         mEndTime = endTime;
207         mTimeSplits = List.of(mStartTime, mEndTime);
208         return this;
209     }
210 
211     /** Sets group by fields. */
setGroupBy( String columnName, Period period, Duration duration, TimeRangeFilter timeRangeFilter)212     public void setGroupBy(
213             String columnName, Period period, Duration duration, TimeRangeFilter timeRangeFilter) {
214         mGroupByColumnName = columnName;
215         if (period != null) {
216             mTimeSplits = getGroupSplitsForPeriod(timeRangeFilter, period);
217         } else if (duration != null) {
218             mTimeSplits = getGroupSplitsForDuration(timeRangeFilter, duration);
219         } else {
220             throw new IllegalArgumentException(
221                     "Either aggregation period or duration should be not null");
222         }
223         mGroupBySize = mTimeSplits.size() - 1;
224 
225         if (Constants.DEBUG) {
226             Slog.d(
227                     TAG,
228                     "Group aggregation splits: "
229                             + mTimeSplits
230                             + " number of groups: "
231                             + mGroupBySize);
232         }
233     }
234 
onResultsFetched(Cursor cursor, Cursor metaDataCursor)235     public void onResultsFetched(Cursor cursor, Cursor metaDataCursor) {
236         if (StorageUtils.isDerivedType(mRecordHelper.getRecordIdentifier())) {
237             deriveAggregate(cursor);
238         } else if (StorageUtils.supportsPriority(
239                 mRecordHelper.getRecordIdentifier(),
240                 mAggregationType.getAggregateOperationType())) {
241             processPriorityRequest(cursor);
242         } else {
243             processNoPrioritiesRequest(cursor);
244         }
245 
246         updateResultWithDataOriginPackageNames(metaDataCursor);
247     }
248 
processPriorityRequest(Cursor cursor)249     private void processPriorityRequest(Cursor cursor) {
250         List<Long> priorityList =
251                 StorageUtils.getAppIdPriorityList(mRecordHelper.getRecordIdentifier());
252         PriorityRecordsAggregator aggregator =
253                 new PriorityRecordsAggregator(
254                         mTimeSplits,
255                         priorityList,
256                         mAggregationType.getAggregationTypeIdentifier(),
257                         mPriorityParams,
258                         mUseLocalTime);
259         aggregator.calculateAggregation(cursor);
260         AggregateResult<?> result;
261         for (int groupNumber = 0; groupNumber < mGroupBySize; groupNumber++) {
262             if (aggregator.getResultForGroup(groupNumber) == null) {
263                 continue;
264             }
265 
266             if (mAggregationType.getAggregateResultClass() == Long.class) {
267                 result =
268                         new AggregateResult<>(
269                                 aggregator.getResultForGroup(groupNumber).longValue());
270             } else {
271                 result = new AggregateResult<>(aggregator.getResultForGroup(groupNumber));
272             }
273             mAggregateResults.put(
274                     groupNumber,
275                     result.setZoneOffset(aggregator.getZoneOffsetForGroup(groupNumber)));
276         }
277 
278         if (Constants.DEBUG) {
279             Slog.d(TAG, "Priority aggregation result: " + mAggregateResults);
280         }
281     }
282 
processNoPrioritiesRequest(Cursor cursor)283     private void processNoPrioritiesRequest(Cursor cursor) {
284         while (cursor.moveToNext()) {
285             mAggregateResults.put(
286                     StorageUtils.getCursorInt(cursor, GROUP_BY_COLUMN_NAME),
287                     mRecordHelper.getAggregateResult(cursor, mAggregationType));
288         }
289     }
290 
getSqlCommandFor(@ggregationType.AggregateOperationType int type)291     private static String getSqlCommandFor(@AggregationType.AggregateOperationType int type) {
292         return switch (type) {
293             case MAX -> "MAX";
294             case MIN -> "MIN";
295             case AVG -> "AVG";
296             case SUM -> "SUM";
297             case COUNT -> "COUNT";
298             default -> null;
299         };
300     }
301 
appendAggregateCommand(StringBuilder builder, boolean isMetadata)302     private String appendAggregateCommand(StringBuilder builder, boolean isMetadata) {
303         boolean useGroupBy = mGroupByColumnName != null && !isMetadata;
304         if (useGroupBy) {
305             builder.append(" CASE ");
306             int groupByIndex = 0;
307             for (int i = 0; i < mTimeSplits.size() - 1; i++) {
308                 builder.append(" WHEN ")
309                         .append(mTimeColumnName)
310                         .append(" >= ")
311                         .append(mTimeSplits.get(i))
312                         .append(" AND ")
313                         .append(mTimeColumnName)
314                         .append(" < ")
315                         .append(mTimeSplits.get(i + 1))
316                         .append(" THEN ")
317                         .append(groupByIndex++);
318             }
319             builder.append(" END " + GROUP_BY_COLUMN_NAME + " ");
320         } else {
321             builder.setLength(builder.length() - 2); // Remove the last 2 char i.e. ", "
322         }
323 
324         builder.append(" FROM ").append(mTableName);
325         if (mSqlJoin != null) {
326             builder.append(mSqlJoin.getJoinCommand());
327         }
328 
329         builder.append(buildAggregationWhereCondition());
330 
331         if (useGroupBy) {
332             builder.append(" GROUP BY " + GROUP_BY_COLUMN_NAME);
333         }
334 
335         OrderByClause orderByClause = new OrderByClause();
336         orderByClause.addOrderByClause(mTimeColumnName, true);
337         builder.append(orderByClause.getOrderBy());
338 
339         if (Constants.DEBUG) {
340             Slog.d(TAG, "Aggregation origin query: " + builder);
341         }
342 
343         return builder.toString();
344     }
345 
buildAggregationWhereCondition()346     private String buildAggregationWhereCondition() {
347         WhereClauses whereClauses = new WhereClauses();
348         whereClauses.addWhereInLongsClause(mPackageColumnName, mPackageFilters);
349 
350         if (mEndTimeColumnName != null) {
351             // Filter all records which overlap with time filter interval:
352             // recordStartTime < filterEndTime and recordEndTime >= filterStartTime
353             whereClauses.addWhereGreaterThanOrEqualClause(mEndTimeColumnName, mStartTime);
354         } else {
355             whereClauses.addWhereGreaterThanOrEqualClause(mTimeColumnName, mStartTime);
356         }
357         whereClauses.addWhereLessThanClause(mTimeColumnName, mEndTime);
358 
359         return whereClauses.get(/* withWhereKeyword= */ true);
360     }
361 
updateResultWithDataOriginPackageNames(Cursor metaDataCursor)362     private void updateResultWithDataOriginPackageNames(Cursor metaDataCursor) {
363         List<Long> packageIds = new ArrayList<>();
364         while (metaDataCursor.moveToNext()) {
365             packageIds.add(StorageUtils.getCursorLong(metaDataCursor, APP_INFO_ID_COLUMN_NAME));
366         }
367         List<String> packageNames = AppInfoHelper.getInstance().getPackageNames(packageIds);
368 
369         mAggregateResults.replaceAll(
370                 (n, v) -> mAggregateResults.get(n).setDataOrigins(packageNames));
371     }
372 
getGroupSplitIntervals()373     public List<Pair<Long, Long>> getGroupSplitIntervals() {
374         List<Pair<Long, Long>> groupIntervals = new ArrayList<>();
375         long previous = mTimeSplits.get(0);
376         for (int i = 1; i < mTimeSplits.size(); i++) {
377             Pair<Long, Long> pair = new Pair<>(previous, mTimeSplits.get(i));
378             groupIntervals.add(pair);
379             previous = mTimeSplits.get(i);
380         }
381 
382         return groupIntervals;
383     }
384 
getGroupSplitsForPeriod(TimeRangeFilter timeFilter, Period period)385     private List<Long> getGroupSplitsForPeriod(TimeRangeFilter timeFilter, Period period) {
386         LocalDateTime filterStart = ((LocalTimeRangeFilter) timeFilter).getStartTime();
387         LocalDateTime filterEnd = ((LocalTimeRangeFilter) timeFilter).getEndTime();
388 
389         List<Long> splits = new ArrayList<>();
390         splits.add(TimeRangeFilterHelper.getMillisOfLocalTime(filterStart));
391 
392         LocalDateTime currentEnd = filterStart.plus(period);
393         while (!currentEnd.isAfter(filterEnd)) {
394             splits.add(TimeRangeFilterHelper.getMillisOfLocalTime(currentEnd));
395             currentEnd = currentEnd.plus(period);
396         }
397 
398         // If the last group doesn't fit the rest of the window, we cut it up to filterEnd
399         if (splits.get(splits.size() - 1) < TimeRangeFilterHelper.getMillisOfLocalTime(filterEnd)) {
400             splits.add(TimeRangeFilterHelper.getMillisOfLocalTime(filterEnd));
401         }
402         return splits;
403     }
404 
getGroupSplitsForDuration( TimeRangeFilter timeRangeFilter, Duration duration)405     private List<Long> getGroupSplitsForDuration(
406             TimeRangeFilter timeRangeFilter, Duration duration) {
407         long groupByStart = TimeRangeFilterHelper.getFilterStartTimeMillis(timeRangeFilter);
408         long groupByEnd = TimeRangeFilterHelper.getFilterEndTimeMillis(timeRangeFilter);
409         long groupDurationMillis = duration.toMillis();
410 
411         List<Long> splits = new ArrayList<>();
412         splits.add(groupByStart);
413         long currentEnd = groupByStart + groupDurationMillis;
414         while (currentEnd <= groupByEnd) {
415             splits.add(currentEnd);
416             currentEnd += groupDurationMillis;
417         }
418 
419         // If the last group doesn't fit the rest of the window, we cut it up to filterEnd
420         if (splits.get(splits.size() - 1) < groupByEnd) {
421             splits.add(groupByEnd);
422         }
423         return splits;
424     }
425 
deriveAggregate(Cursor cursor)426     private void deriveAggregate(Cursor cursor) {
427         double[] derivedAggregateArray = mRecordHelper.deriveAggregate(cursor, this);
428         int index = 0;
429         cursor.moveToFirst();
430         for (double aggregate : derivedAggregateArray) {
431             mAggregateResults.put(
432                     index, mRecordHelper.getAggregateResult(cursor, mAggregationType, aggregate));
433             index++;
434         }
435     }
436 }
437