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 package android.health.connect.datatypes;
17 
18 import static android.health.connect.datatypes.RecordTypeIdentifier.RECORD_TYPE_HYDRATION;
19 
20 import android.annotation.NonNull;
21 import android.health.connect.HealthConnectManager;
22 import android.health.connect.datatypes.units.Volume;
23 import android.health.connect.datatypes.validation.ValidationUtils;
24 import android.health.connect.internal.datatypes.HydrationRecordInternal;
25 
26 import java.time.Instant;
27 import java.time.ZoneOffset;
28 import java.util.Objects;
29 
30 /** Captures the amount of liquids user had in a single drink. */
31 @Identifier(recordIdentifier = RECORD_TYPE_HYDRATION)
32 public final class HydrationRecord extends IntervalRecord {
33     /**
34      * Metric identifier to get hydration total volume using aggregate APIs in {@link
35      * HealthConnectManager}
36      */
37     @NonNull
38     public static final AggregationType<Volume> VOLUME_TOTAL =
39             new AggregationType<>(
40                     AggregationType.AggregationTypeIdentifier.HYDRATION_RECORD_VOLUME_TOTAL,
41                     AggregationType.SUM,
42                     RECORD_TYPE_HYDRATION,
43                     Volume.class);
44 
45     private final Volume mVolume;
46 
47     /**
48      * @param metadata Metadata to be associated with the record. See {@link Metadata}.
49      * @param startTime Start time of this activity
50      * @param startZoneOffset Zone offset of the user when the activity started
51      * @param endTime End time of this activity
52      * @param endZoneOffset Zone offset of the user when the activity finished
53      * @param volume Volume of this activity
54      * @param skipValidation Boolean flag to skip validation of record values.
55      */
HydrationRecord( @onNull Metadata metadata, @NonNull Instant startTime, @NonNull ZoneOffset startZoneOffset, @NonNull Instant endTime, @NonNull ZoneOffset endZoneOffset, @NonNull Volume volume, boolean skipValidation)56     private HydrationRecord(
57             @NonNull Metadata metadata,
58             @NonNull Instant startTime,
59             @NonNull ZoneOffset startZoneOffset,
60             @NonNull Instant endTime,
61             @NonNull ZoneOffset endZoneOffset,
62             @NonNull Volume volume,
63             boolean skipValidation) {
64         super(
65                 metadata,
66                 startTime,
67                 startZoneOffset,
68                 endTime,
69                 endZoneOffset,
70                 skipValidation,
71                 /* enforceFutureTimeRestrictions= */ true);
72         Objects.requireNonNull(volume);
73         if (!skipValidation) {
74             ValidationUtils.requireInRange(volume.getInLiters(), 0.0, 100.0, "volume");
75         }
76         mVolume = volume;
77     }
78 
79     /**
80      * @return hydration volume
81      */
82     @NonNull
getVolume()83     public Volume getVolume() {
84         return mVolume;
85     }
86 
87     /**
88      * Indicates whether some other object is "equal to" this one.
89      *
90      * @param o the reference object with which to compare.
91      * @return {@code true} if this object is the same as the obj
92      */
93     @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression
94     @Override
equals(Object o)95     public boolean equals(Object o) {
96         if (this == o) return true;
97         if (!super.equals(o)) return false;
98         HydrationRecord that = (HydrationRecord) o;
99         return Objects.equals(getVolume(), that.getVolume());
100     }
101 
102     /**
103      * @return a hash code value for this object.
104      */
105     @Override
hashCode()106     public int hashCode() {
107         return Objects.hash(super.hashCode(), getVolume());
108     }
109 
110     /** Builder class for {@link HydrationRecord} */
111     public static final class Builder {
112         private final Metadata mMetadata;
113         private final Instant mStartTime;
114         private final Instant mEndTime;
115         private ZoneOffset mStartZoneOffset;
116         private ZoneOffset mEndZoneOffset;
117         private final Volume mVolume;
118 
119         /**
120          * @param metadata Metadata to be associated with the record. See {@link Metadata}.
121          * @param startTime Start time of this activity
122          * @param endTime End time of this activity
123          * @param volume Volume of the liquids in {@link Volume} unit. Required field. Valid range:
124          *     0-100 liters.
125          */
Builder( @onNull Metadata metadata, @NonNull Instant startTime, @NonNull Instant endTime, @NonNull Volume volume)126         public Builder(
127                 @NonNull Metadata metadata,
128                 @NonNull Instant startTime,
129                 @NonNull Instant endTime,
130                 @NonNull Volume volume) {
131             Objects.requireNonNull(metadata);
132             Objects.requireNonNull(startTime);
133             Objects.requireNonNull(endTime);
134             Objects.requireNonNull(volume);
135             mMetadata = metadata;
136             mStartTime = startTime;
137             mEndTime = endTime;
138             mVolume = volume;
139             mStartZoneOffset = ZoneOffset.systemDefault().getRules().getOffset(startTime);
140             mEndZoneOffset = ZoneOffset.systemDefault().getRules().getOffset(endTime);
141         }
142 
143         /** Sets the zone offset of the user when the activity started */
144         @NonNull
setStartZoneOffset(@onNull ZoneOffset startZoneOffset)145         public Builder setStartZoneOffset(@NonNull ZoneOffset startZoneOffset) {
146             Objects.requireNonNull(startZoneOffset);
147 
148             mStartZoneOffset = startZoneOffset;
149             return this;
150         }
151 
152         /** Sets the zone offset of the user when the activity ended */
153         @NonNull
setEndZoneOffset(@onNull ZoneOffset endZoneOffset)154         public Builder setEndZoneOffset(@NonNull ZoneOffset endZoneOffset) {
155             Objects.requireNonNull(endZoneOffset);
156 
157             mEndZoneOffset = endZoneOffset;
158             return this;
159         }
160 
161         /** Sets the start zone offset of this record to system default. */
162         @NonNull
clearStartZoneOffset()163         public Builder clearStartZoneOffset() {
164             mStartZoneOffset = RecordUtils.getDefaultZoneOffset();
165             return this;
166         }
167 
168         /** Sets the start zone offset of this record to system default. */
169         @NonNull
clearEndZoneOffset()170         public Builder clearEndZoneOffset() {
171             mEndZoneOffset = RecordUtils.getDefaultZoneOffset();
172             return this;
173         }
174 
175         /**
176          * @return Object of {@link HydrationRecord} without validating the values.
177          * @hide
178          */
179         @NonNull
buildWithoutValidation()180         public HydrationRecord buildWithoutValidation() {
181             return new HydrationRecord(
182                     mMetadata,
183                     mStartTime,
184                     mStartZoneOffset,
185                     mEndTime,
186                     mEndZoneOffset,
187                     mVolume,
188                     true);
189         }
190 
191         /**
192          * @return Object of {@link HydrationRecord}
193          */
194         @NonNull
build()195         public HydrationRecord build() {
196             return new HydrationRecord(
197                     mMetadata,
198                     mStartTime,
199                     mStartZoneOffset,
200                     mEndTime,
201                     mEndZoneOffset,
202                     mVolume,
203                     false);
204         }
205     }
206 
207     /** @hide */
208     @Override
toRecordInternal()209     public HydrationRecordInternal toRecordInternal() {
210         HydrationRecordInternal recordInternal =
211                 (HydrationRecordInternal)
212                         new HydrationRecordInternal()
213                                 .setUuid(getMetadata().getId())
214                                 .setPackageName(getMetadata().getDataOrigin().getPackageName())
215                                 .setLastModifiedTime(
216                                         getMetadata().getLastModifiedTime().toEpochMilli())
217                                 .setClientRecordId(getMetadata().getClientRecordId())
218                                 .setClientRecordVersion(getMetadata().getClientRecordVersion())
219                                 .setManufacturer(getMetadata().getDevice().getManufacturer())
220                                 .setModel(getMetadata().getDevice().getModel())
221                                 .setDeviceType(getMetadata().getDevice().getType())
222                                 .setRecordingMethod(getMetadata().getRecordingMethod());
223         recordInternal.setStartTime(getStartTime().toEpochMilli());
224         recordInternal.setEndTime(getEndTime().toEpochMilli());
225         recordInternal.setStartZoneOffset(getStartZoneOffset().getTotalSeconds());
226         recordInternal.setEndZoneOffset(getEndZoneOffset().getTotalSeconds());
227         recordInternal.setVolume(mVolume.getInLiters());
228         return recordInternal;
229     }
230 }
231