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_HEIGHT;
19 
20 import android.annotation.NonNull;
21 import android.health.connect.HealthConnectManager;
22 import android.health.connect.datatypes.units.Length;
23 import android.health.connect.datatypes.validation.ValidationUtils;
24 import android.health.connect.internal.datatypes.HeightRecordInternal;
25 
26 import java.time.Instant;
27 import java.time.ZoneOffset;
28 import java.util.Objects;
29 
30 /** Captures the user's height. */
31 @Identifier(recordIdentifier = RecordTypeIdentifier.RECORD_TYPE_HEIGHT)
32 public final class HeightRecord extends InstantRecord {
33 
34     private final Length mHeight;
35 
36     /**
37      * Metric identifier to get average height using aggregate APIs in {@link HealthConnectManager}
38      */
39     @android.annotation.NonNull
40     public static final AggregationType<Length> HEIGHT_AVG =
41             new AggregationType<>(
42                     AggregationType.AggregationTypeIdentifier.HEIGHT_RECORD_HEIGHT_AVG,
43                     AggregationType.AVG,
44                     RECORD_TYPE_HEIGHT,
45                     Length.class);
46 
47     /**
48      * Metric identifier to get minimum height using aggregate APIs in {@link HealthConnectManager}
49      */
50     @android.annotation.NonNull
51     public static final AggregationType<Length> HEIGHT_MIN =
52             new AggregationType<>(
53                     AggregationType.AggregationTypeIdentifier.HEIGHT_RECORD_HEIGHT_MIN,
54                     AggregationType.MIN,
55                     RECORD_TYPE_HEIGHT,
56                     Length.class);
57 
58     /**
59      * Metric identifier to get maximum height using aggregate APIs in {@link HealthConnectManager}
60      */
61     @android.annotation.NonNull
62     public static final AggregationType<Length> HEIGHT_MAX =
63             new AggregationType<>(
64                     AggregationType.AggregationTypeIdentifier.HEIGHT_RECORD_HEIGHT_MAX,
65                     AggregationType.MAX,
66                     RECORD_TYPE_HEIGHT,
67                     Length.class);
68 
69     /**
70      * @param metadata Metadata to be associated with the record. See {@link Metadata}.
71      * @param time Start time of this activity
72      * @param zoneOffset Zone offset of the user when the activity started
73      * @param height Height of this activity
74      * @param skipValidation Boolean flag to skip validation of record values.
75      */
HeightRecord( @onNull Metadata metadata, @NonNull Instant time, @NonNull ZoneOffset zoneOffset, @NonNull Length height, boolean skipValidation)76     private HeightRecord(
77             @NonNull Metadata metadata,
78             @NonNull Instant time,
79             @NonNull ZoneOffset zoneOffset,
80             @NonNull Length height,
81             boolean skipValidation) {
82         super(metadata, time, zoneOffset, skipValidation);
83         Objects.requireNonNull(metadata);
84         Objects.requireNonNull(time);
85         Objects.requireNonNull(zoneOffset);
86         Objects.requireNonNull(height);
87         if (!skipValidation) {
88             ValidationUtils.requireInRange(height.getInMeters(), 0.0, 3.0, "height");
89         }
90         mHeight = height;
91     }
92     /**
93      * @return height in {@link Length} unit.
94      */
95     @NonNull
getHeight()96     public Length getHeight() {
97         return mHeight;
98     }
99 
100     /**
101      * Indicates whether some other object is "equal to" this one.
102      *
103      * @param o the reference object with which to compare.
104      * @return {@code true} if this object is the same as the obj
105      */
106     @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression
107     @Override
equals(Object o)108     public boolean equals(Object o) {
109         if (this == o) return true;
110         if (!super.equals(o)) return false;
111         HeightRecord that = (HeightRecord) o;
112         return getHeight().equals(that.getHeight());
113     }
114 
115     /** Returns a hash code value for the object. */
116     @Override
hashCode()117     public int hashCode() {
118         return Objects.hash(super.hashCode(), getHeight());
119     }
120 
121     /** Builder class for {@link HeightRecord} */
122     public static final class Builder {
123         private final Metadata mMetadata;
124         private final Instant mTime;
125         private ZoneOffset mZoneOffset;
126         private final Length mHeight;
127 
128         /**
129          * @param metadata Metadata to be associated with the record. See {@link Metadata}.
130          * @param time Start time of this activity
131          * @param height Height in {@link Length} unit. Required field. Valid range: 0-3 meters.
132          */
Builder(@onNull Metadata metadata, @NonNull Instant time, @NonNull Length height)133         public Builder(@NonNull Metadata metadata, @NonNull Instant time, @NonNull Length height) {
134             Objects.requireNonNull(metadata);
135             Objects.requireNonNull(time);
136             Objects.requireNonNull(height);
137             mMetadata = metadata;
138             mTime = time;
139             mHeight = height;
140             mZoneOffset = ZoneOffset.systemDefault().getRules().getOffset(time);
141         }
142 
143         /** Sets the zone offset of the user when the activity happened */
144         @NonNull
setZoneOffset(@onNull ZoneOffset zoneOffset)145         public Builder setZoneOffset(@NonNull ZoneOffset zoneOffset) {
146             Objects.requireNonNull(zoneOffset);
147             mZoneOffset = zoneOffset;
148             return this;
149         }
150 
151         /** Sets the zone offset of this record to system default. */
152         @NonNull
clearZoneOffset()153         public Builder clearZoneOffset() {
154             mZoneOffset = RecordUtils.getDefaultZoneOffset();
155             return this;
156         }
157 
158         /**
159          * @return Object of {@link HeightRecord} without validating the values.
160          * @hide
161          */
162         @NonNull
buildWithoutValidation()163         public HeightRecord buildWithoutValidation() {
164             return new HeightRecord(mMetadata, mTime, mZoneOffset, mHeight, true);
165         }
166 
167         /**
168          * @return Object of {@link HeightRecord}
169          */
170         @NonNull
build()171         public HeightRecord build() {
172             return new HeightRecord(mMetadata, mTime, mZoneOffset, mHeight, false);
173         }
174     }
175 
176     /** @hide */
177     @Override
toRecordInternal()178     public HeightRecordInternal toRecordInternal() {
179         HeightRecordInternal recordInternal =
180                 (HeightRecordInternal)
181                         new HeightRecordInternal()
182                                 .setUuid(getMetadata().getId())
183                                 .setPackageName(getMetadata().getDataOrigin().getPackageName())
184                                 .setLastModifiedTime(
185                                         getMetadata().getLastModifiedTime().toEpochMilli())
186                                 .setClientRecordId(getMetadata().getClientRecordId())
187                                 .setClientRecordVersion(getMetadata().getClientRecordVersion())
188                                 .setManufacturer(getMetadata().getDevice().getManufacturer())
189                                 .setModel(getMetadata().getDevice().getModel())
190                                 .setDeviceType(getMetadata().getDevice().getType())
191                                 .setRecordingMethod(getMetadata().getRecordingMethod());
192         recordInternal.setTime(getTime().toEpochMilli());
193         recordInternal.setZoneOffset(getZoneOffset().getTotalSeconds());
194         recordInternal.setHeight(mHeight.getInMeters());
195         return recordInternal;
196     }
197 }
198