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.health.connect.datatypes.MedicalDataSource; 27 import android.os.Parcel; 28 import android.os.Parcelable; 29 import android.util.ArraySet; 30 31 import java.util.ArrayList; 32 import java.util.HashSet; 33 import java.util.Objects; 34 import java.util.Set; 35 import java.util.regex.Matcher; 36 import java.util.regex.Pattern; 37 38 /** 39 * A create request for {@link HealthConnectManager#getMedicalDataSources}. 40 * 41 * <p>If no {@link GetMedicalDataSourcesRequest#getPackageNames() package names} are set, requests 42 * all {@link MedicalDataSource}s from all packages. Otherwise the request is limited to the 43 * requested package names. 44 */ 45 @FlaggedApi(FLAG_PERSONAL_HEALTH_RECORD) 46 public final class GetMedicalDataSourcesRequest implements Parcelable { 47 @NonNull private final Set<String> mPackageNames; 48 49 // A full Java-language-style package name for the Android app can contain uppercase 50 // or lowercase letters, numbers, and underscores ('_'). It must have at least two segments (one 51 // or more dots), and individual package name parts can only start with letters. See the 52 // <a 53 // href="https://developer.android.com/guide/topics/manifest/manifest-element.html#package">Android developer doc</a>. 54 private static final String PACKAGE_NAME_REGEX = 55 "^([A-Za-z][a-zA-Z0-9_]*\\.)+[A-Za-z][a-zA-Z0-9_]*$"; 56 57 @NonNull 58 public static final Creator<GetMedicalDataSourcesRequest> CREATOR = 59 new Creator<>() { 60 @Override 61 public GetMedicalDataSourcesRequest createFromParcel(Parcel in) { 62 return new GetMedicalDataSourcesRequest(in); 63 } 64 65 @Override 66 public GetMedicalDataSourcesRequest[] newArray(int size) { 67 return new GetMedicalDataSourcesRequest[size]; 68 } 69 }; 70 71 /** 72 * Creates a new instance of {@link GetMedicalDataSourcesRequest}. Please see {@link 73 * GetMedicalDataSourcesRequest.Builder} for more detailed parameters information. 74 */ GetMedicalDataSourcesRequest(@onNull Set<String> packageNames)75 private GetMedicalDataSourcesRequest(@NonNull Set<String> packageNames) { 76 Objects.requireNonNull(packageNames); 77 validatePackageNames(packageNames); 78 mPackageNames = packageNames; 79 } 80 81 /** 82 * Constructs this object with the data present in {@code parcel}. It should be in the same 83 * order as {@link #writeToParcel}. 84 */ GetMedicalDataSourcesRequest(@onNull Parcel in)85 private GetMedicalDataSourcesRequest(@NonNull Parcel in) { 86 Objects.requireNonNull(in); 87 mPackageNames = new ArraySet<>(requireNonNull(in.createStringArrayList())); 88 validatePackageNames(mPackageNames); 89 } 90 91 /** 92 * Returns the package names for which {@link MedicalDataSource}s are being requested, or an 93 * empty set for no filter. 94 */ 95 @NonNull getPackageNames()96 public Set<String> getPackageNames() { 97 return new ArraySet<>(mPackageNames); 98 } 99 100 /** 101 * Validates all of the provided {@code packageNames} are valid, which matches with the {@link 102 * #PACKAGE_NAME_REGEX}. 103 * 104 * @throws IllegalArgumentException with all invalid package names if not all {@code 105 * packageNames} are valid. 106 */ validatePackageNames(Set<String> packageNames)107 private static void validatePackageNames(Set<String> packageNames) { 108 Pattern pattern = Pattern.compile(PACKAGE_NAME_REGEX); 109 110 Set<String> invalidPackageNames = new HashSet<>(); 111 for (String packageName : packageNames) { 112 Matcher matcher = pattern.matcher(packageName); 113 if (!matcher.matches()) { 114 invalidPackageNames.add(packageName); 115 } 116 } 117 if (!invalidPackageNames.isEmpty()) { 118 throw new IllegalArgumentException("Invalid package name(s): " + invalidPackageNames); 119 } 120 } 121 122 @Override describeContents()123 public int describeContents() { 124 return 0; 125 } 126 127 @Override writeToParcel(@onNull Parcel dest, int flags)128 public void writeToParcel(@NonNull Parcel dest, int flags) { 129 dest.writeStringList(new ArrayList<>(mPackageNames)); 130 } 131 132 @Override equals(Object o)133 public boolean equals(Object o) { 134 if (this == o) return true; 135 if (!(o instanceof GetMedicalDataSourcesRequest that)) return false; 136 return mPackageNames.equals(that.mPackageNames); 137 } 138 139 @Override hashCode()140 public int hashCode() { 141 return hash(mPackageNames); 142 } 143 144 @Override toString()145 public String toString() { 146 StringBuilder sb = new StringBuilder(); 147 sb.append(this.getClass().getSimpleName()).append("{"); 148 sb.append("packageNames=").append(mPackageNames); 149 sb.append("}"); 150 return sb.toString(); 151 } 152 153 /** Builder class for {@link GetMedicalDataSourcesRequest}. */ 154 public static final class Builder { 155 private final Set<String> mPackageNames = new ArraySet<>(); 156 157 /** Constructs a new {@link GetMedicalDataSourcesRequest.Builder} with no filters set. */ Builder()158 public Builder() {} 159 160 /** Constructs a clone of the other {@link GetMedicalDataSourcesRequest.Builder}. */ Builder(@onNull Builder other)161 public Builder(@NonNull Builder other) { 162 requireNonNull(other); 163 mPackageNames.addAll(other.mPackageNames); 164 } 165 166 /** Constructs a clone of the other {@link GetMedicalDataSourcesRequest} instance. */ Builder(@onNull GetMedicalDataSourcesRequest other)167 public Builder(@NonNull GetMedicalDataSourcesRequest other) { 168 requireNonNull(other); 169 mPackageNames.addAll(other.getPackageNames()); 170 } 171 172 /** 173 * Adds a package name to limit this request to. 174 * 175 * <p>If the list of package names is empty, {@link MedicalDataSource}s for all packages 176 * will be requested. Otherwise only those for the added package names are requested. 177 * 178 * @throws IllegalArgumentException if the provided {@code packageName} is not valid. 179 */ 180 @NonNull addPackageName(@onNull String packageName)181 public Builder addPackageName(@NonNull String packageName) { 182 Objects.requireNonNull(packageName); 183 validatePackageNames(Set.of(packageName)); 184 mPackageNames.add(packageName); 185 return this; 186 } 187 188 /** Clears all package names. */ 189 @NonNull clearPackageNames()190 public Builder clearPackageNames() { 191 mPackageNames.clear(); 192 return this; 193 } 194 195 /** 196 * Returns a new instance of {@link GetMedicalDataSourcesRequest} with the specified 197 * parameters. 198 */ 199 @NonNull build()200 public GetMedicalDataSourcesRequest build() { 201 return new GetMedicalDataSourcesRequest(mPackageNames); 202 } 203 } 204 } 205