1 /*
2  * Copyright (C) 2024 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.healthconnect.cts.lib;
18 
19 import static android.health.connect.datatypes.Device.DEVICE_TYPE_RING;
20 import static android.health.connect.datatypes.Device.DEVICE_TYPE_WATCH;
21 import static android.health.connect.datatypes.Metadata.RECORDING_METHOD_AUTOMATICALLY_RECORDED;
22 import static android.health.connect.datatypes.Metadata.RECORDING_METHOD_MANUAL_ENTRY;
23 
24 import android.health.connect.datatypes.ActivityIntensityRecord;
25 import android.health.connect.datatypes.DataOrigin;
26 import android.health.connect.datatypes.Device;
27 import android.health.connect.datatypes.Metadata;
28 import android.health.connect.datatypes.MindfulnessSessionRecord;
29 import android.health.connect.datatypes.Record;
30 import android.os.Bundle;
31 
32 import androidx.annotation.Nullable;
33 
34 import java.time.Instant;
35 import java.time.LocalDate;
36 import java.time.LocalDateTime;
37 import java.time.LocalTime;
38 import java.time.ZoneId;
39 import java.time.ZoneOffset;
40 import java.time.ZonedDateTime;
41 import java.time.temporal.ChronoUnit;
42 
43 public abstract class RecordFactory<T extends Record> {
44 
45     public static final ZonedDateTime YESTERDAY_11AM =
46             LocalDate.now(ZoneId.systemDefault())
47                     .minusDays(1)
48                     .atTime(11, 0)
49                     .atZone(ZoneId.systemDefault());
50 
51     public static final ZonedDateTime MIDNIGHT_ONE_WEEK_AGO =
52             YESTERDAY_11AM.truncatedTo(ChronoUnit.DAYS).minusDays(7);
53 
54     public static final LocalDateTime YESTERDAY_10AM_LOCAL =
55             LocalDate.now(ZoneId.systemDefault()).minusDays(1).atTime(LocalTime.parse("10:00"));
56 
57     /**
58      * Returns a record with all possible fields of this data type set to non-default values.
59      *
60      * <p>Used to make sure all fields get successfully written and returned from Health Connect end
61      * to end.
62      */
newFullRecord(Metadata metadata, Instant startTime, Instant endTime)63     public abstract T newFullRecord(Metadata metadata, Instant startTime, Instant endTime);
64 
65     /**
66      * Returns a record with all possible fields of this data type set to non-default values.
67      *
68      * <p>This is similar to {@link #newFullRecord(Metadata, Instant, Instant)} however every field
69      * should have a different value. This is used to test that all fields successfully get updated
70      * on the server side.
71      */
anotherFullRecord(Metadata metadata, Instant startTime, Instant endTime)72     public abstract T anotherFullRecord(Metadata metadata, Instant startTime, Instant endTime);
73 
74     /**
75      * Returns a record with all optional fields of this data type unset or set to default values.
76      *
77      * <p>Used to make sure null/default values are handled correctly by the server side.
78      */
newEmptyRecord(Metadata metadata, Instant startTime, Instant endTime)79     public abstract T newEmptyRecord(Metadata metadata, Instant startTime, Instant endTime);
80 
81     /** Returns a copy of the given record with the metadata set to the provided one. */
recordWithMetadata(T record, Metadata metadata)82     protected abstract T recordWithMetadata(T record, Metadata metadata);
83 
84     /**
85      * Returns a bundle containing the values specific to this data type.
86      *
87      * <p>The returned bundle is then used by {@link #newRecordFromValuesBundle} to recreate the
88      * record.
89      *
90      * <p>Metadata, start & end time, start & end zone offset should not be populated as they are
91      * handled separately.
92      *
93      * <p>Used by multi-app tests to send the record across the test apps.
94      */
getValuesBundleForRecord(T record)95     protected abstract Bundle getValuesBundleForRecord(T record);
96 
97     /** Returns a bundle containing the values specific to this data type. */
getValuesBundle(Record record)98     public final Bundle getValuesBundle(Record record) {
99         return getValuesBundleForRecord((T) record);
100     }
101 
102     /** Recreates the record from the given values bundle and metadata. */
newRecordFromValuesBundle( Metadata metadata, Instant startTime, Instant endTime, ZoneOffset startZoneOffset, ZoneOffset endZoneOffset, Bundle bundle)103     public abstract T newRecordFromValuesBundle(
104             Metadata metadata,
105             Instant startTime,
106             Instant endTime,
107             ZoneOffset startZoneOffset,
108             ZoneOffset endZoneOffset,
109             Bundle bundle);
110 
111     /** Returns the record with id and package name overridden by the given ones. */
recordWithIdAndPackageName(Record record, String id, String packageName)112     public final T recordWithIdAndPackageName(Record record, String id, String packageName) {
113         return recordWithMetadata(
114                 (T) record,
115                 toBuilder(record.getMetadata())
116                         .setId(id)
117                         .setDataOrigin(new DataOrigin.Builder().setPackageName(packageName).build())
118                         .build());
119     }
120 
121     /** Returns a default instance of metadata with no fields populated. */
newEmptyMetadata()122     public static Metadata newEmptyMetadata() {
123         return new Metadata.Builder().build();
124     }
125 
126     /** Returns a default instance of metadata with no fields populated except the id. */
newEmptyMetadataWithId(String id)127     public static Metadata newEmptyMetadataWithId(String id) {
128         return new Metadata.Builder().setId(id).build();
129     }
130 
131     /** Returns a default instance of metadata with no fields populated except the client id. */
newEmptyMetadataWithClientId(String clientId)132     public static Metadata newEmptyMetadataWithClientId(String clientId) {
133         return new Metadata.Builder().setClientRecordId(clientId).build();
134     }
135 
136     /** Returns a default instance of metadata with no fields populated except id and client id. */
newEmptyMetadataWithIdClientIdAndVersion( String id, String clientId, int version)137     public static Metadata newEmptyMetadataWithIdClientIdAndVersion(
138             String id, String clientId, int version) {
139         return new Metadata.Builder()
140                 .setId(id)
141                 .setClientRecordId(clientId)
142                 .setClientRecordVersion(version)
143                 .build();
144     }
145 
146     /**
147      * Returns an instance of metadata with all possible fields populated except ids and version.
148      */
newFullMetadataWithoutIds()149     public static Metadata newFullMetadataWithoutIds() {
150         return newFullMetadataBuilderWithoutIds().build();
151     }
152 
153     /**
154      * Returns an instance of metadata builder with all possible fields populated except ids and
155      * version.
156      */
newFullMetadataBuilderWithoutIds()157     private static Metadata.Builder newFullMetadataBuilderWithoutIds() {
158         return new Metadata.Builder()
159                 .setDataOrigin(new DataOrigin.Builder().setPackageName("foo.package.name").build())
160                 .setRecordingMethod(RECORDING_METHOD_MANUAL_ENTRY)
161                 .setDevice(
162                         new Device.Builder()
163                                 .setType(DEVICE_TYPE_WATCH)
164                                 .setManufacturer("foo-manufacturer")
165                                 .setModel("foo-model")
166                                 .build())
167                 .setLastModifiedTime(Instant.now().minusSeconds(120));
168     }
169 
170     /**
171      * Returns an instance of metadata with all possible fields populated except client id and
172      * version.
173      */
newFullMetadataWithId(String id)174     public static Metadata newFullMetadataWithId(String id) {
175         return newFullMetadataBuilderWithoutIds().setId(id).build();
176     }
177 
178     /** Returns an instance of metadata with all possible fields populated except id and version. */
newFullMetadataWithClientId(String clientId)179     public static Metadata newFullMetadataWithClientId(String clientId) {
180         return newFullMetadataBuilderWithoutIds().setClientRecordId(clientId).build();
181     }
182 
183     /** Returns an instance of metadata with all possible fields populated except id. */
newFullMetadataWithClientIdAndVersion(String clientId, int version)184     public static Metadata newFullMetadataWithClientIdAndVersion(String clientId, int version) {
185         return newFullMetadataBuilderWithoutIds()
186                 .setClientRecordId(clientId)
187                 .setClientRecordVersion(version)
188                 .build();
189     }
190 
191     /**
192      * Same as {@link #newFullMetadataWithId(String)} but all fields are set to different values.
193      */
newAnotherFullMetadataWithId(String id)194     public static Metadata newAnotherFullMetadataWithId(String id) {
195         return newAnotherFullMetadataBuilderWithoutIds().setId(id).build();
196     }
197 
198     /**
199      * Same as {@link #newFullMetadataWithClientId(String)} but all fields are set to different
200      * values.
201      */
newAnotherFullMetadataWithClientId(String clientId)202     public static Metadata newAnotherFullMetadataWithClientId(String clientId) {
203         return newAnotherFullMetadataBuilderWithoutIds().setClientRecordId(clientId).build();
204     }
205 
206     /**
207      * Same as {@link #newFullMetadataWithClientIdAndVersion(String, int)} but all fields are set to
208      * different values.
209      */
newAnotherFullMetadataWithClientIdAndVersion( String clientId, int version)210     public static Metadata newAnotherFullMetadataWithClientIdAndVersion(
211             String clientId, int version) {
212         return newAnotherFullMetadataBuilderWithoutIds()
213                 .setClientRecordId(clientId)
214                 .setClientRecordVersion(version)
215                 .build();
216     }
217 
218     /**
219      * Same as {@link #newFullMetadataBuilderWithoutIds()} but all fields are set to different
220      * values.
221      */
newAnotherFullMetadataBuilderWithoutIds()222     private static Metadata.Builder newAnotherFullMetadataBuilderWithoutIds() {
223         return new Metadata.Builder()
224                 .setDataOrigin(new DataOrigin.Builder().setPackageName("bar.package.name").build())
225                 .setRecordingMethod(RECORDING_METHOD_AUTOMATICALLY_RECORDED)
226                 .setDevice(
227                         new Device.Builder()
228                                 .setType(DEVICE_TYPE_RING)
229                                 .setManufacturer("bar-manufacturer")
230                                 .setModel("bar-model")
231                                 .build())
232                 .setLastModifiedTime(Instant.now().minusSeconds(234));
233     }
234 
235     /** Converts the given metadata to the corresponding builder. */
toBuilder(Metadata metadata)236     private static Metadata.Builder toBuilder(Metadata metadata) {
237         return new Metadata.Builder()
238                 .setId(metadata.getId())
239                 .setLastModifiedTime(metadata.getLastModifiedTime())
240                 .setRecordingMethod(metadata.getRecordingMethod())
241                 .setDataOrigin(metadata.getDataOrigin())
242                 .setDevice(metadata.getDevice())
243                 .setClientRecordVersion(metadata.getClientRecordVersion())
244                 .setClientRecordId(metadata.getClientRecordId());
245     }
246 
247     /** Returns a record test helper for given data type. */
248     @Nullable
forDataType(Class<? extends Record> recordClass)249     public static RecordFactory<? extends Record> forDataType(Class<? extends Record> recordClass) {
250         if (recordClass.equals(ActivityIntensityRecord.class)) {
251             return new ActivityIntensityRecordFactory();
252         } else if (recordClass.equals(MindfulnessSessionRecord.class)) {
253             return new MindfulnessSessionRecordFactory();
254         }
255         return null;
256     }
257 }
258