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 com.android.server.healthconnect.storage.datatypehelpers.IntervalRecordHelper.END_TIME_COLUMN_NAME;
20 import static com.android.server.healthconnect.storage.datatypehelpers.IntervalRecordHelper.LOCAL_DATE_TIME_END_TIME_COLUMN_NAME;
21 import static com.android.server.healthconnect.storage.datatypehelpers.IntervalRecordHelper.LOCAL_DATE_TIME_START_TIME_COLUMN_NAME;
22 import static com.android.server.healthconnect.storage.datatypehelpers.IntervalRecordHelper.START_TIME_COLUMN_NAME;
23 import static com.android.server.healthconnect.storage.datatypehelpers.IntervalRecordHelper.START_ZONE_OFFSET_COLUMN_NAME;
24 import static com.android.server.healthconnect.storage.datatypehelpers.RecordHelper.APP_INFO_ID_COLUMN_NAME;
25 import static com.android.server.healthconnect.storage.datatypehelpers.RecordHelper.LAST_MODIFIED_TIME_COLUMN_NAME;
26 import static com.android.server.healthconnect.storage.datatypehelpers.RecordHelper.UUID_COLUMN_NAME;
27 
28 import android.database.Cursor;
29 
30 import com.android.internal.annotations.VisibleForTesting;
31 import com.android.server.healthconnect.storage.utils.StorageUtils;
32 
33 import java.time.ZoneOffset;
34 import java.util.Map;
35 import java.util.UUID;
36 
37 /**
38  * Represents priority aggregation data.
39  *
40  * @hide
41  */
42 public abstract class AggregationRecordData implements Comparable<AggregationRecordData> {
43     private long mRecordStartTime;
44     private long mRecordEndTime;
45     private int mPriority;
46     private long mLastModifiedTime;
47 
48     @SuppressWarnings("NullAway.Init") // TODO(b/317029272): fix this suppression
49     private ZoneOffset mStartTimeZoneOffset;
50 
getStartTime()51     long getStartTime() {
52         return mRecordStartTime;
53     }
54 
getEndTime()55     long getEndTime() {
56         return mRecordEndTime;
57     }
58 
getPriority()59     int getPriority() {
60         return mPriority;
61     }
62 
getLastModifiedTime()63     long getLastModifiedTime() {
64         return mLastModifiedTime;
65     }
66 
getStartTimeZoneOffset()67     ZoneOffset getStartTimeZoneOffset() {
68         return mStartTimeZoneOffset;
69     }
70 
readUuid(Cursor cursor)71     protected UUID readUuid(Cursor cursor) {
72         return StorageUtils.getCursorUUID(cursor, UUID_COLUMN_NAME);
73     }
74 
populateAggregationData( Cursor cursor, boolean useLocalTime, Map<Long, Integer> appIdToPriority)75     void populateAggregationData(
76             Cursor cursor, boolean useLocalTime, Map<Long, Integer> appIdToPriority) {
77         mRecordStartTime =
78                 StorageUtils.getCursorLong(
79                         cursor,
80                         useLocalTime
81                                 ? LOCAL_DATE_TIME_START_TIME_COLUMN_NAME
82                                 : START_TIME_COLUMN_NAME);
83         mRecordEndTime =
84                 StorageUtils.getCursorLong(
85                         cursor,
86                         useLocalTime ? LOCAL_DATE_TIME_END_TIME_COLUMN_NAME : END_TIME_COLUMN_NAME);
87         mLastModifiedTime = StorageUtils.getCursorLong(cursor, LAST_MODIFIED_TIME_COLUMN_NAME);
88         mStartTimeZoneOffset = StorageUtils.getZoneOffset(cursor, START_ZONE_OFFSET_COLUMN_NAME);
89         mPriority =
90                 appIdToPriority.getOrDefault(
91                         StorageUtils.getCursorLong(cursor, APP_INFO_ID_COLUMN_NAME),
92                         Integer.MIN_VALUE);
93         populateSpecificAggregationData(cursor, useLocalTime);
94     }
95 
getStartTimestamp()96     AggregationTimestamp getStartTimestamp() {
97         return new AggregationTimestamp(AggregationTimestamp.INTERVAL_START, getStartTime())
98                 .setParentData(this);
99     }
100 
getEndTimestamp()101     AggregationTimestamp getEndTimestamp() {
102         return new AggregationTimestamp(AggregationTimestamp.INTERVAL_END, getEndTime())
103                 .setParentData(this);
104     }
105 
106     @VisibleForTesting
setData( long startTime, long endTime, int priority, long lastModifiedTime)107     AggregationRecordData setData(
108             long startTime, long endTime, int priority, long lastModifiedTime) {
109         mRecordStartTime = startTime;
110         mRecordEndTime = endTime;
111         mPriority = priority;
112         mLastModifiedTime = lastModifiedTime;
113         return this;
114     }
115 
116     /**
117      * Calculates aggregation result given start and end time of the target interval. Implementation
118      * may assume that it's will be called with non overlapping intervals. So (start time, end time)
119      * input intervals of all calls will not overlap.
120      */
getResultOnInterval( AggregationTimestamp startPoint, AggregationTimestamp endPoint)121     abstract double getResultOnInterval(
122             AggregationTimestamp startPoint, AggregationTimestamp endPoint);
123 
populateSpecificAggregationData(Cursor cursor, boolean useLocalTime)124     abstract void populateSpecificAggregationData(Cursor cursor, boolean useLocalTime);
125 
126     @Override
toString()127     public String toString() {
128         return "AggregData{startTime=" + mRecordStartTime + ", endTime=" + mRecordEndTime + "}";
129     }
130 
131     /** Calculates overlap between two intervals */
calculateIntervalOverlapDuration( long intervalStart1, long intervalStart2, long intervalEnd1, long intervalEnd2)132     static long calculateIntervalOverlapDuration(
133             long intervalStart1, long intervalStart2, long intervalEnd1, long intervalEnd2) {
134         return Math.max(
135                 Math.min(intervalEnd1, intervalEnd2) - Math.max(intervalStart1, intervalStart2), 0);
136     }
137 
138     @Override
compareTo(AggregationRecordData o)139     public int compareTo(AggregationRecordData o) {
140         if (this.equals(o)) {
141             return 0;
142         }
143 
144         if (mPriority != o.getPriority()) {
145             return Integer.compare(mPriority, o.getPriority());
146         }
147 
148         // The later the last modified time, the higher priority this record has.
149         if (getLastModifiedTime() != o.getLastModifiedTime()) {
150             return Long.compare(getLastModifiedTime(), o.getLastModifiedTime());
151         }
152 
153         if (getStartTime() != o.getStartTime()) {
154             return Long.compare(getStartTime(), o.getStartTime());
155         }
156 
157         if (getEndTime() != o.getEndTime()) {
158             return Long.compare(getEndTime(), o.getEndTime());
159         }
160 
161         return Double.compare(
162                 getResultOnInterval(getStartTimestamp(), getEndTimestamp()),
163                 o.getResultOnInterval(o.getStartTimestamp(), o.getEndTimestamp()));
164     }
165 }
166