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 android.health.connect.aidl;
18 
19 import static android.health.connect.Constants.DEFAULT_INT;
20 import static android.health.connect.Constants.DEFAULT_LONG;
21 import static android.health.connect.TimeRangeFilterHelper.getInstantFromLocalTime;
22 
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.health.connect.AggregateRecordsGroupedByDurationResponse;
26 import android.health.connect.AggregateRecordsGroupedByPeriodResponse;
27 import android.health.connect.AggregateRecordsResponse;
28 import android.health.connect.AggregateResult;
29 import android.health.connect.LocalTimeRangeFilter;
30 import android.health.connect.TimeInstantRangeFilter;
31 import android.health.connect.TimeRangeFilter;
32 import android.health.connect.TimeRangeFilterHelper;
33 import android.health.connect.datatypes.DataOrigin;
34 import android.health.connect.internal.datatypes.utils.AggregationTypeIdMapper;
35 import android.os.Parcel;
36 import android.os.Parcelable;
37 import android.util.ArrayMap;
38 
39 import java.time.Duration;
40 import java.time.Instant;
41 import java.time.LocalDateTime;
42 import java.time.Period;
43 import java.time.ZoneOffset;
44 import java.util.ArrayList;
45 import java.util.List;
46 import java.util.Map;
47 import java.util.Objects;
48 import java.util.Set;
49 
50 /** @hide */
51 public class AggregateDataResponseParcel implements Parcelable {
52     public static final Creator<AggregateDataResponseParcel> CREATOR =
53             new Creator<>() {
54                 @Override
55                 public AggregateDataResponseParcel createFromParcel(Parcel in) {
56                     return new AggregateDataResponseParcel(in);
57                 }
58 
59                 @Override
60                 public AggregateDataResponseParcel[] newArray(int size) {
61                     return new AggregateDataResponseParcel[size];
62                 }
63             };
64     private final List<AggregateRecordsResponse<?>> mAggregateRecordsResponses;
65     private Duration mDuration;
66     private Period mPeriod;
67     private TimeRangeFilter mTimeRangeFilter;
68 
69     @SuppressWarnings("NullAway.Init") // TODO(b/317029272): fix this suppression
AggregateDataResponseParcel(List<AggregateRecordsResponse<?>> aggregateRecordsResponse)70     public AggregateDataResponseParcel(List<AggregateRecordsResponse<?>> aggregateRecordsResponse) {
71         mAggregateRecordsResponses = aggregateRecordsResponse;
72     }
73 
74     @SuppressWarnings("NullAway.Init") // TODO(b/317029272): fix this suppression
AggregateDataResponseParcel(Parcel in)75     protected AggregateDataResponseParcel(Parcel in) {
76         final int size = in.readInt();
77         mAggregateRecordsResponses = new ArrayList<>(size);
78 
79         for (int i = 0; i < size; i++) {
80             final int mapSize = in.readInt();
81             Map<Integer, AggregateResult<?>> result = new ArrayMap<>(mapSize);
82 
83             for (int mapI = 0; mapI < mapSize; mapI++) {
84                 int id = in.readInt();
85                 boolean hasValue = in.readBoolean();
86                 if (hasValue) {
87                     result.put(
88                             id,
89                             AggregationTypeIdMapper.getInstance()
90                                     .getAggregateResultFor(id, in)
91                                     .setZoneOffset(parseZoneOffset(in))
92                                     .setDataOrigins(in.createStringArrayList()));
93                 } else {
94                     result.put(id, null);
95                 }
96             }
97 
98             mAggregateRecordsResponses.add(new AggregateRecordsResponse<>(result));
99         }
100 
101         int periodDays = in.readInt();
102         if (periodDays != DEFAULT_INT) {
103             int periodMonths = in.readInt();
104             int periodYears = in.readInt();
105             mPeriod = Period.of(periodYears, periodMonths, periodDays);
106         }
107 
108         long duration = in.readLong();
109         if (duration != DEFAULT_LONG) {
110             mDuration = Duration.ofMillis(duration);
111         }
112 
113         boolean isLocaltimeFilter = in.readBoolean();
114         long startTime = in.readLong();
115         long endTime = in.readLong();
116         if (startTime != DEFAULT_LONG && endTime != DEFAULT_LONG) {
117             if (isLocaltimeFilter) {
118                 mTimeRangeFilter =
119                         new LocalTimeRangeFilter.Builder()
120                                 .setStartTime(
121                                         TimeRangeFilterHelper.getLocalTimeFromMillis(startTime))
122                                 .setEndTime(TimeRangeFilterHelper.getLocalTimeFromMillis(endTime))
123                                 .build();
124             } else {
125                 mTimeRangeFilter =
126                         new TimeInstantRangeFilter.Builder()
127                                 .setStartTime(Instant.ofEpochMilli(startTime))
128                                 .setEndTime(Instant.ofEpochMilli(endTime))
129                                 .build();
130             }
131         }
132     }
133 
134     @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression
setDuration( @ullable Duration duration, @Nullable TimeRangeFilter timeRangeFilter)135     public AggregateDataResponseParcel setDuration(
136             @Nullable Duration duration, @Nullable TimeRangeFilter timeRangeFilter) {
137         mDuration = duration;
138         mTimeRangeFilter = timeRangeFilter;
139 
140         return this;
141     }
142 
143     @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression
setPeriod( @ullable Period period, @Nullable TimeRangeFilter timeRangeFilter)144     public AggregateDataResponseParcel setPeriod(
145             @Nullable Period period, @Nullable TimeRangeFilter timeRangeFilter) {
146         mPeriod = period;
147         mTimeRangeFilter = timeRangeFilter;
148 
149         return this;
150     }
151 
152     /**
153      * @return the first response from {@code mAggregateRecordsResponses}
154      */
getAggregateDataResponse()155     public AggregateRecordsResponse<?> getAggregateDataResponse() {
156         return mAggregateRecordsResponses.get(0);
157     }
158 
159     /**
160      * @return responses from {@code mAggregateRecordsResponses} grouped as per the {@code
161      *     mDuration}
162      */
163     @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression
164     public List<AggregateRecordsGroupedByDurationResponse<?>>
getAggregateDataResponseGroupedByDuration()165             getAggregateDataResponseGroupedByDuration() {
166         Objects.requireNonNull(mDuration);
167 
168         if (mAggregateRecordsResponses.isEmpty()) {
169             return List.of();
170         }
171 
172         if (mTimeRangeFilter instanceof LocalTimeRangeFilter timeFilter) {
173             return getAggregateDataResponseForLocalTimeGroupedByDuration(
174                     timeFilter.getStartTime(), timeFilter.getEndTime());
175         }
176 
177         if (mTimeRangeFilter instanceof TimeInstantRangeFilter timeFilter) {
178             return getAggregateDataResponseForInstantTimeGroupedByDuration(
179                     timeFilter.getStartTime(), timeFilter.getEndTime());
180         }
181 
182         throw new IllegalArgumentException(
183                 "Invalid time filter object. Object should be either TimeInstantRangeFilter or "
184                         + "LocalTimeRangeFilter.");
185     }
186 
187     private List<AggregateRecordsGroupedByDurationResponse<?>>
getAggregateDataResponseForLocalTimeGroupedByDuration( LocalDateTime startTime, LocalDateTime endTime)188             getAggregateDataResponseForLocalTimeGroupedByDuration(
189                     LocalDateTime startTime, LocalDateTime endTime) {
190         List<AggregateRecordsGroupedByDurationResponse<?>> responses = new ArrayList<>();
191         Duration bucketStartTimeOffset = Duration.ZERO;
192         for (AggregateRecordsResponse<?> response : mAggregateRecordsResponses) {
193             ZoneOffset zoneOffset = response.getFirstZoneOffset();
194             Instant endTimeInstant = getInstantFromLocalTime(endTime, zoneOffset);
195             Instant bucketStartTime =
196                     getInstantFromLocalTime(startTime, zoneOffset).plus(bucketStartTimeOffset);
197             Instant bucketEndTime = bucketStartTime.plus(mDuration);
198             if (bucketEndTime.isAfter(endTimeInstant)) {
199                 bucketEndTime = endTimeInstant;
200             }
201 
202             responses.add(
203                     new AggregateRecordsGroupedByDurationResponse<>(
204                             bucketStartTime, bucketEndTime, response.getAggregateResults()));
205             bucketStartTimeOffset = bucketStartTimeOffset.plus(mDuration);
206         }
207 
208         return responses;
209     }
210 
211     private List<AggregateRecordsGroupedByDurationResponse<?>>
getAggregateDataResponseForInstantTimeGroupedByDuration( Instant startTime, Instant endTime)212             getAggregateDataResponseForInstantTimeGroupedByDuration(
213                     Instant startTime, Instant endTime) {
214         List<AggregateRecordsGroupedByDurationResponse<?>> responses = new ArrayList<>();
215         Duration offsetDuration = Duration.ZERO;
216         for (AggregateRecordsResponse<?> response : mAggregateRecordsResponses) {
217             Instant buckedStartTime = startTime.plus(offsetDuration);
218             Instant buckedEndTime = buckedStartTime.plus(mDuration);
219             if (buckedEndTime.isAfter(endTime)) {
220                 buckedEndTime = endTime;
221             }
222 
223             responses.add(
224                     new AggregateRecordsGroupedByDurationResponse<>(
225                             buckedStartTime, buckedEndTime, response.getAggregateResults()));
226 
227             offsetDuration = offsetDuration.plus(mDuration);
228         }
229 
230         return responses;
231     }
232 
233     /**
234      * @return responses from {@code mAggregateRecordsResponses} grouped as per the {@code mPeriod}
235      */
236     @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression
237     public List<AggregateRecordsGroupedByPeriodResponse<?>>
getAggregateDataResponseGroupedByPeriod()238             getAggregateDataResponseGroupedByPeriod() {
239         Objects.requireNonNull(mPeriod);
240 
241         List<AggregateRecordsGroupedByPeriodResponse<?>> aggregateRecordsGroupedByPeriodResponses =
242                 new ArrayList<>();
243 
244         LocalDateTime groupBoundary = ((LocalTimeRangeFilter) mTimeRangeFilter).getStartTime();
245         for (AggregateRecordsResponse<?> aggregateRecordsResponse : mAggregateRecordsResponses) {
246             aggregateRecordsGroupedByPeriodResponses.add(
247                     new AggregateRecordsGroupedByPeriodResponse<>(
248                             groupBoundary,
249                             groupBoundary.plus(mPeriod),
250                             aggregateRecordsResponse.getAggregateResults()));
251             groupBoundary = groupBoundary.plus(mPeriod);
252         }
253 
254         if (!aggregateRecordsGroupedByPeriodResponses.isEmpty()) {
255             aggregateRecordsGroupedByPeriodResponses
256                     .get(aggregateRecordsGroupedByPeriodResponses.size() - 1)
257                     .setEndTime(getPeriodEndLocalDateTime(mTimeRangeFilter));
258         }
259 
260         return aggregateRecordsGroupedByPeriodResponses;
261     }
262 
263     @Override
describeContents()264     public int describeContents() {
265         return 0;
266     }
267 
268     @Override
writeToParcel(@onNull Parcel dest, int flags)269     public void writeToParcel(@NonNull Parcel dest, int flags) {
270         dest.writeInt(mAggregateRecordsResponses.size());
271         for (AggregateRecordsResponse<?> aggregateRecordsResponse : mAggregateRecordsResponses) {
272             dest.writeInt(aggregateRecordsResponse.getAggregateResults().size());
273             aggregateRecordsResponse
274                     .getAggregateResults()
275                     .forEach(
276                             (key, val) -> {
277                                 dest.writeInt(key);
278                                 // to represent if the value is present or not
279                                 dest.writeBoolean(val != null);
280                                 if (val != null) {
281                                     val.putToParcel(dest);
282                                     ZoneOffset zoneOffset = val.getZoneOffset();
283                                     if (zoneOffset != null) {
284                                         dest.writeInt(val.getZoneOffset().getTotalSeconds());
285                                     } else {
286                                         dest.writeInt(DEFAULT_INT);
287                                     }
288                                     Set<DataOrigin> dataOrigins = val.getDataOrigins();
289                                     List<String> packageNames = new ArrayList<>();
290                                     for (DataOrigin dataOrigin : dataOrigins) {
291                                         packageNames.add(dataOrigin.getPackageName());
292                                     }
293                                     dest.writeStringList(packageNames);
294                                 }
295                             });
296         }
297 
298         if (mPeriod != null) {
299             dest.writeInt(mPeriod.getDays());
300             dest.writeInt(mPeriod.getMonths());
301             dest.writeInt(mPeriod.getYears());
302         } else {
303             dest.writeInt(DEFAULT_INT);
304         }
305 
306         if (mDuration != null) {
307             dest.writeLong(mDuration.toMillis());
308         } else {
309             dest.writeLong(DEFAULT_LONG);
310         }
311 
312         if (mTimeRangeFilter != null) {
313             dest.writeBoolean(TimeRangeFilterHelper.isLocalTimeFilter(mTimeRangeFilter));
314             dest.writeLong(TimeRangeFilterHelper.getFilterStartTimeMillis(mTimeRangeFilter));
315             dest.writeLong(TimeRangeFilterHelper.getFilterEndTimeMillis(mTimeRangeFilter));
316         } else {
317             dest.writeBoolean(false);
318             dest.writeLong(DEFAULT_LONG);
319             dest.writeLong(DEFAULT_LONG);
320         }
321     }
322 
323     @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression
parseZoneOffset(Parcel in)324     private ZoneOffset parseZoneOffset(Parcel in) {
325         int zoneOffsetInSecs = in.readInt();
326         ZoneOffset zoneOffset = null;
327         if (zoneOffsetInSecs != DEFAULT_INT) {
328             zoneOffset = ZoneOffset.ofTotalSeconds(zoneOffsetInSecs);
329         }
330 
331         return zoneOffset;
332     }
333 
334     @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression
getPeriodEndLocalDateTime(TimeRangeFilter timeRangeFilter)335     private LocalDateTime getPeriodEndLocalDateTime(TimeRangeFilter timeRangeFilter) {
336         if (timeRangeFilter instanceof TimeInstantRangeFilter) {
337             return LocalDateTime.ofInstant(
338                     ((TimeInstantRangeFilter) timeRangeFilter).getEndTime(),
339                     ZoneOffset.systemDefault());
340         } else if (timeRangeFilter instanceof LocalTimeRangeFilter) {
341             return ((LocalTimeRangeFilter) timeRangeFilter).getEndTime();
342         } else {
343             throw new IllegalArgumentException(
344                     "Invalid time filter object. Object should be either "
345                             + "TimeInstantRangeFilter or LocalTimeRangeFilter.");
346         }
347     }
348 
getPeriodDeltaInDays(Period period)349     private long getPeriodDeltaInDays(Period period) {
350         return period.getDays();
351     }
352 
getDurationInstant(long duration)353     private Instant getDurationInstant(long duration) {
354         return Instant.ofEpochMilli(duration);
355     }
356 
getDurationDelta(Duration duration)357     private long getDurationDelta(Duration duration) {
358         return duration.toMillis();
359     }
360 }
361