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.validation.ValidationUtils.validateIntDefValue;
19 
20 import android.annotation.FloatRange;
21 import android.annotation.IntDef;
22 import android.annotation.NonNull;
23 import android.health.connect.datatypes.validation.ValidationUtils;
24 import android.health.connect.internal.datatypes.Vo2MaxRecordInternal;
25 
26 import java.lang.annotation.Retention;
27 import java.lang.annotation.RetentionPolicy;
28 import java.time.Instant;
29 import java.time.ZoneOffset;
30 import java.util.Objects;
31 import java.util.Set;
32 
33 /** Capture user's VO2 max score and optionally the measurement method. */
34 @Identifier(recordIdentifier = RecordTypeIdentifier.RECORD_TYPE_VO2_MAX)
35 public final class Vo2MaxRecord extends InstantRecord {
36 
37     private final int mMeasurementMethod;
38     private final double mVo2MillilitersPerMinuteKilogram;
39 
40     /**
41      * @param metadata Metadata to be associated with the record. See {@link Metadata}.
42      * @param time Start time of this activity
43      * @param zoneOffset Zone offset of the user when the activity started
44      * @param measurementMethod MeasurementMethod of this activity
45      * @param vo2MillilitersPerMinuteKilogram Vo2MillilitersPerMinuteKilogram of this activity
46      * @param skipValidation Boolean flag to skip validation of record values.
47      */
Vo2MaxRecord( @onNull Metadata metadata, @NonNull Instant time, @NonNull ZoneOffset zoneOffset, @Vo2MaxMeasurementMethod.Vo2MaxMeasurementMethodTypes int measurementMethod, double vo2MillilitersPerMinuteKilogram, boolean skipValidation)48     private Vo2MaxRecord(
49             @NonNull Metadata metadata,
50             @NonNull Instant time,
51             @NonNull ZoneOffset zoneOffset,
52             @Vo2MaxMeasurementMethod.Vo2MaxMeasurementMethodTypes int measurementMethod,
53             double vo2MillilitersPerMinuteKilogram,
54             boolean skipValidation) {
55         super(metadata, time, zoneOffset, skipValidation);
56         Objects.requireNonNull(metadata);
57         Objects.requireNonNull(time);
58         Objects.requireNonNull(zoneOffset);
59         validateIntDefValue(
60                 measurementMethod,
61                 Vo2MaxMeasurementMethod.VALID_TYPES,
62                 Vo2MaxMeasurementMethod.class.getSimpleName());
63         if (!skipValidation) {
64             ValidationUtils.requireInRange(
65                     vo2MillilitersPerMinuteKilogram, 0.0, 100.0, "vo2MillilitersPerMinuteKilogram");
66         }
67         mMeasurementMethod = measurementMethod;
68         mVo2MillilitersPerMinuteKilogram = vo2MillilitersPerMinuteKilogram;
69     }
70 
71     /**
72      * @return measurementMethod
73      */
74     @Vo2MaxMeasurementMethod.Vo2MaxMeasurementMethodTypes
getMeasurementMethod()75     public int getMeasurementMethod() {
76         return mMeasurementMethod;
77     }
78 
79     /**
80      * @return vo2MillilitersPerMinuteKilogram
81      */
getVo2MillilitersPerMinuteKilogram()82     public double getVo2MillilitersPerMinuteKilogram() {
83         return mVo2MillilitersPerMinuteKilogram;
84     }
85 
86     /** Identifier for V02 max measurement method */
87     public static final class Vo2MaxMeasurementMethod {
88         public static final int MEASUREMENT_METHOD_OTHER = 0;
89         public static final int MEASUREMENT_METHOD_METABOLIC_CART = 1;
90         public static final int MEASUREMENT_METHOD_HEART_RATE_RATIO = 2;
91         public static final int MEASUREMENT_METHOD_COOPER_TEST = 3;
92         public static final int MEASUREMENT_METHOD_MULTISTAGE_FITNESS_TEST = 4;
93         public static final int MEASUREMENT_METHOD_ROCKPORT_FITNESS_TEST = 5;
94 
95         /**
96          * Valid set of values for this IntDef. Update this set when add new type or deprecate
97          * existing type.
98          *
99          * @hide
100          */
101         public static final Set<Integer> VALID_TYPES =
102                 Set.of(
103                         MEASUREMENT_METHOD_OTHER,
104                         MEASUREMENT_METHOD_METABOLIC_CART,
105                         MEASUREMENT_METHOD_HEART_RATE_RATIO,
106                         MEASUREMENT_METHOD_COOPER_TEST,
107                         MEASUREMENT_METHOD_MULTISTAGE_FITNESS_TEST,
108                         MEASUREMENT_METHOD_ROCKPORT_FITNESS_TEST);
109 
Vo2MaxMeasurementMethod()110         Vo2MaxMeasurementMethod() {}
111 
112         /** @hide */
113         @IntDef({
114             MEASUREMENT_METHOD_OTHER,
115             MEASUREMENT_METHOD_METABOLIC_CART,
116             MEASUREMENT_METHOD_HEART_RATE_RATIO,
117             MEASUREMENT_METHOD_COOPER_TEST,
118             MEASUREMENT_METHOD_MULTISTAGE_FITNESS_TEST,
119             MEASUREMENT_METHOD_ROCKPORT_FITNESS_TEST
120         })
121         @Retention(RetentionPolicy.SOURCE)
122         public @interface Vo2MaxMeasurementMethodTypes {}
123     }
124 
125     /**
126      * Indicates whether some other object is "equal to" this one.
127      *
128      * @param o the reference object with which to compare.
129      * @return {@code true} if this object is the same as the obj
130      */
131     @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression
132     @Override
equals(Object o)133     public boolean equals(Object o) {
134         if (this == o) return true;
135         if (!super.equals(o)) return false;
136         Vo2MaxRecord that = (Vo2MaxRecord) o;
137         return getMeasurementMethod() == that.getMeasurementMethod()
138                 && Double.compare(
139                                 that.getVo2MillilitersPerMinuteKilogram(),
140                                 getVo2MillilitersPerMinuteKilogram())
141                         == 0;
142     }
143 
144     /** Returns a hash code value for the object. */
145     @Override
hashCode()146     public int hashCode() {
147         return Objects.hash(
148                 super.hashCode(), getMeasurementMethod(), getVo2MillilitersPerMinuteKilogram());
149     }
150 
151     /** Builder class for {@link Vo2MaxRecord} */
152     public static final class Builder {
153         private final Metadata mMetadata;
154         private final Instant mTime;
155         private ZoneOffset mZoneOffset;
156         private final int mMeasurementMethod;
157         private final double mVo2MillilitersPerMinuteKilogram;
158 
159         /**
160          * @param metadata Metadata to be associated with the record. See {@link Metadata}.
161          * @param time Start time of this activity
162          * @param measurementMethod VO2 max measurement method. Optional field. Allowed values:
163          *     {@link Vo2MaxMeasurementMethod}.
164          * @param vo2MillilitersPerMinuteKilogram Maximal aerobic capacity (VO2 max) in milliliters.
165          *     Required field. Valid range: 0-100.
166          */
Builder( @onNull Metadata metadata, @NonNull Instant time, @Vo2MaxMeasurementMethod.Vo2MaxMeasurementMethodTypes int measurementMethod, @FloatRange(from = 0, to = 100) double vo2MillilitersPerMinuteKilogram)167         public Builder(
168                 @NonNull Metadata metadata,
169                 @NonNull Instant time,
170                 @Vo2MaxMeasurementMethod.Vo2MaxMeasurementMethodTypes int measurementMethod,
171                 @FloatRange(from = 0, to = 100) double vo2MillilitersPerMinuteKilogram) {
172             Objects.requireNonNull(metadata);
173             Objects.requireNonNull(time);
174             mMetadata = metadata;
175             mTime = time;
176             mMeasurementMethod = measurementMethod;
177             mVo2MillilitersPerMinuteKilogram = vo2MillilitersPerMinuteKilogram;
178             mZoneOffset = ZoneOffset.systemDefault().getRules().getOffset(time);
179         }
180 
181         /** Sets the zone offset of the user when the activity happened */
182         @NonNull
setZoneOffset(@onNull ZoneOffset zoneOffset)183         public Builder setZoneOffset(@NonNull ZoneOffset zoneOffset) {
184             Objects.requireNonNull(zoneOffset);
185             mZoneOffset = zoneOffset;
186             return this;
187         }
188 
189         /** Sets the zone offset of this record to system default. */
190         @NonNull
clearZoneOffset()191         public Builder clearZoneOffset() {
192             mZoneOffset = RecordUtils.getDefaultZoneOffset();
193             return this;
194         }
195 
196         /**
197          * @return Object of {@link Vo2MaxRecord} without validating the values.
198          * @hide
199          */
200         @NonNull
buildWithoutValidation()201         public Vo2MaxRecord buildWithoutValidation() {
202             return new Vo2MaxRecord(
203                     mMetadata,
204                     mTime,
205                     mZoneOffset,
206                     mMeasurementMethod,
207                     mVo2MillilitersPerMinuteKilogram,
208                     true);
209         }
210 
211         /**
212          * @return Object of {@link Vo2MaxRecord}
213          */
214         @NonNull
build()215         public Vo2MaxRecord build() {
216             return new Vo2MaxRecord(
217                     mMetadata,
218                     mTime,
219                     mZoneOffset,
220                     mMeasurementMethod,
221                     mVo2MillilitersPerMinuteKilogram,
222                     false);
223         }
224     }
225 
226     /** @hide */
227     @Override
toRecordInternal()228     public Vo2MaxRecordInternal toRecordInternal() {
229         Vo2MaxRecordInternal recordInternal =
230                 (Vo2MaxRecordInternal)
231                         new Vo2MaxRecordInternal()
232                                 .setUuid(getMetadata().getId())
233                                 .setPackageName(getMetadata().getDataOrigin().getPackageName())
234                                 .setLastModifiedTime(
235                                         getMetadata().getLastModifiedTime().toEpochMilli())
236                                 .setClientRecordId(getMetadata().getClientRecordId())
237                                 .setClientRecordVersion(getMetadata().getClientRecordVersion())
238                                 .setManufacturer(getMetadata().getDevice().getManufacturer())
239                                 .setModel(getMetadata().getDevice().getModel())
240                                 .setDeviceType(getMetadata().getDevice().getType())
241                                 .setRecordingMethod(getMetadata().getRecordingMethod());
242         recordInternal.setTime(getTime().toEpochMilli());
243         recordInternal.setZoneOffset(getZoneOffset().getTotalSeconds());
244         recordInternal.setMeasurementMethod(mMeasurementMethod);
245         recordInternal.setVo2MillilitersPerMinuteKilogram(mVo2MillilitersPerMinuteKilogram);
246         return recordInternal;
247     }
248 }
249