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.health.connect; 18 19 import static com.android.healthfitness.flags.Flags.FLAG_PERSONAL_HEALTH_RECORD; 20 21 import static java.util.Objects.hash; 22 import static java.util.Objects.requireNonNull; 23 24 import android.annotation.FlaggedApi; 25 import android.annotation.NonNull; 26 import android.annotation.Nullable; 27 import android.health.connect.datatypes.FhirVersion; 28 import android.net.Uri; 29 import android.os.Parcel; 30 import android.os.Parcelable; 31 32 /** 33 * A create request for {@link HealthConnectManager#createMedicalDataSource}. 34 * 35 * <p>Medical data is represented using the <a href="https://hl7.org/fhir/">Fast Healthcare 36 * Interoperability Resources (FHIR)</a> standard. 37 */ 38 @FlaggedApi(FLAG_PERSONAL_HEALTH_RECORD) 39 public final class CreateMedicalDataSourceRequest implements Parcelable { 40 // The character limit for the {@code mDisplayName} 41 private static final int DISPLAY_NAME_CHARACTER_LIMIT = 90; 42 // The character limit for the {@code mFhirBaseUri} 43 private static final int FHIR_BASE_URI_CHARACTER_LIMIT = 2000; 44 45 @NonNull private final Uri mFhirBaseUri; 46 @NonNull private final String mDisplayName; 47 @NonNull private final FhirVersion mFhirVersion; 48 private long mDataSize; 49 50 @NonNull 51 public static final Creator<CreateMedicalDataSourceRequest> CREATOR = 52 new Creator<CreateMedicalDataSourceRequest>() { 53 @NonNull 54 @Override 55 /* 56 * @throws IllegalArgumentException if the {@code mFhirBaseUri} or 57 * {@code mDisplayName} exceed the character limits. 58 */ 59 public CreateMedicalDataSourceRequest createFromParcel(@NonNull Parcel in) { 60 return new CreateMedicalDataSourceRequest(in); 61 } 62 63 @NonNull 64 @Override 65 public CreateMedicalDataSourceRequest[] newArray(int size) { 66 return new CreateMedicalDataSourceRequest[size]; 67 } 68 }; 69 70 /** 71 * Creates a new instance of {@link CreateMedicalDataSourceRequest}. Please see {@link 72 * CreateMedicalDataSourceRequest.Builder} for more detailed parameters information. 73 */ CreateMedicalDataSourceRequest( @onNull Uri fhirBaseUri, @NonNull String displayName, @NonNull FhirVersion fhirVersion)74 private CreateMedicalDataSourceRequest( 75 @NonNull Uri fhirBaseUri, 76 @NonNull String displayName, 77 @NonNull FhirVersion fhirVersion) { 78 requireNonNull(fhirBaseUri); 79 requireNonNull(displayName); 80 requireNonNull(fhirVersion); 81 validateFhirBaseUriCharacterLimit(fhirBaseUri); 82 validateDisplayNameCharacterLimit(displayName); 83 validateFhirVersion(fhirVersion); 84 85 mFhirBaseUri = fhirBaseUri; 86 mDisplayName = displayName; 87 mFhirVersion = fhirVersion; 88 } 89 CreateMedicalDataSourceRequest(@onNull Parcel in)90 private CreateMedicalDataSourceRequest(@NonNull Parcel in) { 91 requireNonNull(in); 92 mDataSize = in.dataSize(); 93 94 mFhirBaseUri = requireNonNull(in.readParcelable(Uri.class.getClassLoader(), Uri.class)); 95 mDisplayName = requireNonNull(in.readString()); 96 mFhirVersion = 97 requireNonNull( 98 in.readParcelable(FhirVersion.class.getClassLoader(), FhirVersion.class)); 99 100 validateFhirBaseUriCharacterLimit(mFhirBaseUri); 101 validateDisplayNameCharacterLimit(mDisplayName); 102 validateFhirVersion(mFhirVersion); 103 } 104 105 /** 106 * Returns the FHIR base URI. For data coming from a FHIR server this is <a 107 * href="https://hl7.org/fhir/R4/http.html#root">the base URL</a>. 108 */ 109 @NonNull getFhirBaseUri()110 public Uri getFhirBaseUri() { 111 return mFhirBaseUri; 112 } 113 114 /** Returns the display name. For the request to succeed this must be unique per app. */ 115 @NonNull getDisplayName()116 public String getDisplayName() { 117 return mDisplayName; 118 } 119 120 /** 121 * Returns the FHIR version. For the request to succeeds this must be a version supported by 122 * Health Connect, as documented on the {@link FhirVersion}. 123 */ 124 @NonNull getFhirVersion()125 public FhirVersion getFhirVersion() { 126 return mFhirVersion; 127 } 128 129 /** 130 * Returns the size of the parcel when the class was created from Parcel. 131 * 132 * @hide 133 */ getDataSize()134 public long getDataSize() { 135 return mDataSize; 136 } 137 138 @Override describeContents()139 public int describeContents() { 140 return 0; 141 } 142 143 @Override writeToParcel(@onNull Parcel dest, int flags)144 public void writeToParcel(@NonNull Parcel dest, int flags) { 145 dest.writeParcelable(mFhirBaseUri, 0); 146 dest.writeString(mDisplayName); 147 dest.writeParcelable(mFhirVersion, 0); 148 } 149 150 @Override equals(@ullable Object o)151 public boolean equals(@Nullable Object o) { 152 if (this == o) return true; 153 if (!(o instanceof CreateMedicalDataSourceRequest that)) return false; 154 return getFhirBaseUri().equals(that.getFhirBaseUri()) 155 && getDisplayName().equals(that.getDisplayName()) 156 && getFhirVersion().equals(that.getFhirVersion()); 157 } 158 159 @Override hashCode()160 public int hashCode() { 161 return hash(getFhirBaseUri(), getDisplayName(), getFhirVersion()); 162 } 163 164 @Override toString()165 public String toString() { 166 StringBuilder sb = new StringBuilder(); 167 sb.append(this.getClass().getSimpleName()).append("{"); 168 sb.append("fhirBaseUri=").append(getFhirBaseUri()); 169 sb.append(",displayName=").append(getDisplayName()); 170 sb.append(",fhirVersion=").append(getFhirVersion()); 171 sb.append("}"); 172 return sb.toString(); 173 } 174 175 /** Builder class for {@link CreateMedicalDataSourceRequest}. */ 176 public static final class Builder { 177 @NonNull private Uri mFhirBaseUri; 178 @NonNull private String mDisplayName; 179 @NonNull private FhirVersion mFhirVersion; 180 181 /** 182 * Constructs a new {@link CreateMedicalDataSourceRequest.Builder} instance. 183 * 184 * @param fhirBaseUri The FHIR base URI of the data source. For data coming from a FHIR 185 * server this should be the <a href="https://hl7.org/fhir/R4/http.html#root">FHIR base 186 * URL</a> (e.g. `https://example.com/fhir`). If the data is generated by an app without 187 * a FHIR URL, this can be populated by a unique and understandable URI defined by the 188 * app (e.g. `myapp://..`) that points to the source of the data. We recommend not to 189 * use a domain name that you don't control. If your app supports <a 190 * href="https://developer.android.com/training/app-links/deep-linking">app deep 191 * linking</a>, this URI would ideally link to the source data. The maximum length for 192 * the URI is 2000 characters. 193 * @param displayName The display name that describes the data source. The maximum length 194 * for the display name is 90 characters. This must be unique per app. 195 * @param fhirVersion The FHIR version of the medical data that will be linked to this data 196 * source. This has to be a version supported by Health Connect, as documented on the 197 * {@link FhirVersion}. 198 */ Builder( @onNull Uri fhirBaseUri, @NonNull String displayName, @NonNull FhirVersion fhirVersion)199 public Builder( 200 @NonNull Uri fhirBaseUri, 201 @NonNull String displayName, 202 @NonNull FhirVersion fhirVersion) { 203 requireNonNull(fhirBaseUri); 204 requireNonNull(displayName); 205 requireNonNull(fhirVersion); 206 207 mFhirBaseUri = fhirBaseUri; 208 mDisplayName = displayName; 209 mFhirVersion = fhirVersion; 210 } 211 212 /** Constructs a clone of the other {@link CreateMedicalDataSourceRequest.Builder}. */ Builder(@onNull Builder other)213 public Builder(@NonNull Builder other) { 214 requireNonNull(other); 215 mFhirBaseUri = other.mFhirBaseUri; 216 mDisplayName = other.mDisplayName; 217 mFhirVersion = other.mFhirVersion; 218 } 219 220 /** Constructs a clone of the other {@link CreateMedicalDataSourceRequest} instance. */ Builder(@onNull CreateMedicalDataSourceRequest other)221 public Builder(@NonNull CreateMedicalDataSourceRequest other) { 222 requireNonNull(other); 223 mFhirBaseUri = other.getFhirBaseUri(); 224 mDisplayName = other.getDisplayName(); 225 mFhirVersion = other.getFhirVersion(); 226 } 227 228 /** 229 * Sets the FHIR base URI. For data coming from a FHIR server this should be the <a 230 * href="https://hl7.org/fhir/R4/http.html#root">FHIR base URL</a> (e.g. 231 * `https://example.com/fhir`). 232 * 233 * <p>If the data is generated by an app without a FHIR base URL, this can be populated by a 234 * URI defined by the app (e.g. `myapp://..`) that should: 235 * 236 * <ul> 237 * <li>Be a unique and understandable URI. 238 * <li>Point to the source of the data. We recommend not to use a domain name that you 239 * don't control. 240 * <li>Ideally allow linking to the data source if your app supports <a 241 * href="https://developer.android.com/training/app-links/deep-linking">app deep 242 * linking</a> to the data. 243 * </ul> 244 * 245 * <p>The URI may not exceed 2000 characters. 246 */ 247 @NonNull setFhirBaseUri(@onNull Uri fhirBaseUri)248 public Builder setFhirBaseUri(@NonNull Uri fhirBaseUri) { 249 requireNonNull(fhirBaseUri); 250 mFhirBaseUri = fhirBaseUri; 251 return this; 252 } 253 254 /** 255 * Sets the display name. For the request to succeed this must be unique per app. 256 * 257 * <p>The display name may not exceed 90 characters. 258 */ 259 @NonNull setDisplayName(@onNull String displayName)260 public Builder setDisplayName(@NonNull String displayName) { 261 requireNonNull(displayName); 262 mDisplayName = displayName; 263 return this; 264 } 265 266 /** 267 * Sets the FHIR version of data from this data source. 268 * 269 * <p>This has to be a version supported by Health Connect, as documented on the {@link 270 * FhirVersion}. 271 */ 272 @NonNull setFhirVersion(@onNull FhirVersion fhirVersion)273 public Builder setFhirVersion(@NonNull FhirVersion fhirVersion) { 274 requireNonNull(fhirVersion); 275 mFhirVersion = fhirVersion; 276 return this; 277 } 278 279 /** 280 * Returns a new instance of {@link CreateMedicalDataSourceRequest} with the specified 281 * parameters. 282 * 283 * @throws IllegalArgumentException if the {@code mFhirBaseUri} or {@code mDisplayName} 284 * exceed the character limits or if the {@code mFhirVersion} is not supported by Health 285 * Connect. 286 */ 287 @NonNull build()288 public CreateMedicalDataSourceRequest build() { 289 return new CreateMedicalDataSourceRequest(mFhirBaseUri, mDisplayName, mFhirVersion); 290 } 291 } 292 validateDisplayNameCharacterLimit(String displayName)293 private static void validateDisplayNameCharacterLimit(String displayName) { 294 if (displayName.isEmpty()) { 295 throw new IllegalArgumentException("Display name cannot be empty."); 296 } 297 if (displayName.length() > DISPLAY_NAME_CHARACTER_LIMIT) { 298 throw new IllegalArgumentException( 299 "Display name cannot be longer than " 300 + DISPLAY_NAME_CHARACTER_LIMIT 301 + " characters."); 302 } 303 } 304 validateFhirBaseUriCharacterLimit(Uri fhirBaseUri)305 private static void validateFhirBaseUriCharacterLimit(Uri fhirBaseUri) { 306 String fhirBaseUriString = fhirBaseUri.toString(); 307 if (fhirBaseUriString.isEmpty()) { 308 throw new IllegalArgumentException("FHIR base URI cannot be empty."); 309 } 310 if (fhirBaseUriString.length() > FHIR_BASE_URI_CHARACTER_LIMIT) { 311 throw new IllegalArgumentException( 312 "FHIR base URI cannot be longer than " 313 + FHIR_BASE_URI_CHARACTER_LIMIT 314 + " characters."); 315 } 316 } 317 validateFhirVersion(FhirVersion fhirVersion)318 private static void validateFhirVersion(FhirVersion fhirVersion) { 319 if (!fhirVersion.isSupportedFhirVersion()) { 320 throw new IllegalArgumentException("Unsupported FHIR version " + fhirVersion + "."); 321 } 322 } 323 } 324