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