• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.server.healthconnect.phr;
18 
19 import static android.health.connect.Constants.DEFAULT_LONG;
20 import static android.health.connect.datatypes.MedicalDataSource.validateMedicalDataSourceIds;
21 import static android.health.connect.datatypes.MedicalResource.validateMedicalResourceType;
22 
23 import static java.util.Objects.hash;
24 import static java.util.stream.Collectors.toSet;
25 
26 import android.annotation.Nullable;
27 import android.health.connect.ReadMedicalResourcesInitialRequest;
28 import android.health.connect.aidl.ReadMedicalResourcesRequestParcel;
29 
30 import com.android.internal.annotations.VisibleForTesting;
31 
32 import java.util.Arrays;
33 import java.util.Base64;
34 import java.util.Objects;
35 import java.util.Set;
36 
37 /**
38  * Wrapper class for generating a PHR pageToken.
39  *
40  * @hide
41  */
42 public class PhrPageTokenWrapper {
43     public static final PhrPageTokenWrapper EMPTY_PAGE_TOKEN = new PhrPageTokenWrapper();
44     private static final String DELIMITER = ",";
45     private static final String INNER_DELIMITER = ";";
46     // This is used for when we just encode the mLastRowId without using filters.
47     private static final int NUM_OF_ENCODED_FIELDS_WITHOUT_REQUEST_FILTERS = 1;
48     // We currently encode mLastRowId, mRequest.getMedicalResourceType(), and
49     // mRequest.getDataSourceIds(). As we add more filters and need to update the encoding logic, we
50     // need to update this as well.
51     private static final int NUM_OF_ENCODED_FIELDS_WITH_REQUEST_FILTERS = 3;
52     // These are the indices at which we store and retrieve each field used for creating the
53     // pageToken string.
54     private static final int LAST_ROW_ID_INDEX = 0;
55     private static final int MEDICAL_RESOURCE_TYPE_INDEX = 1;
56     private static final int MEDICAL_DATA_SOURCE_IDS_INDEX = 2;
57 
58     @Nullable private final ReadMedicalResourcesInitialRequest mRequest;
59     private long mLastRowId = DEFAULT_LONG;
60 
61     /**
62      * Creates a {@link PhrPageTokenWrapper} from the given {@code lastRowId}.
63      *
64      * <p>This is currently only used in D2D merge logic where we want to read all data out instead
65      * of using filters. So using this, we can build a {@link PhrPageTokenWrapper} from only {@code
66      * lastRowId} specified.
67      */
from(long lastRowId)68     public static PhrPageTokenWrapper from(long lastRowId) {
69         return new PhrPageTokenWrapper(lastRowId);
70     }
71 
72     /**
73      * Creates a {@link PhrPageTokenWrapper} from the given {@link
74      * ReadMedicalResourcesRequestParcel}.
75      *
76      * <p>If {@code pageToken} in the request is {@code null}, the default {@code mLastRowId} is
77      * {@link DEFAULT_LONG}, meaning it's the initial request.
78      *
79      * @throws IllegalArgumentException if {@code pageToken} is empty, or not in valid Base64
80      *     scheme; or if the decoded {@code lastRowId} is negative; or if the decoded {@code
81      *     medicalResourceType} is not supported; or there are invalid IDs in the decoded {@code
82      *     dataSourceId}s.
83      * @throws NumberFormatException if the decoded {@code pageToken} does not contain a parsable
84      *     integer.
85      */
from(ReadMedicalResourcesRequestParcel request)86     public static PhrPageTokenWrapper from(ReadMedicalResourcesRequestParcel request) {
87         if (request.getPageToken() == null) {
88             // We create a new request and only populate the read filters. The pageSize will be
89             // unset as we don't use it for encoding/decoding.
90             ReadMedicalResourcesInitialRequest requestWithFiltersOnly =
91                     new ReadMedicalResourcesInitialRequest.Builder(request.getMedicalResourceType())
92                             .addDataSourceIds(request.getDataSourceIds())
93                             .build();
94             return new PhrPageTokenWrapper(requestWithFiltersOnly);
95         }
96         return from(request.getPageToken());
97     }
98 
99     /**
100      * Creates a {@link PhrPageTokenWrapper} from the given {@code pageToken}. Returns {@link
101      * #EMPTY_PAGE_TOKEN} if {@code pageToken} is empty or null.
102      */
fromPageTokenAllowingNull(@ullable String pageToken)103     public static PhrPageTokenWrapper fromPageTokenAllowingNull(@Nullable String pageToken) {
104         if (pageToken == null || pageToken.isEmpty()) {
105             return EMPTY_PAGE_TOKEN;
106         }
107 
108         return from(pageToken);
109     }
110 
111     /** Creates a {@link PhrPageTokenWrapper} from the given {@code pageToken}. */
112     @VisibleForTesting
from(@ullable String pageToken)113     static PhrPageTokenWrapper from(@Nullable String pageToken) {
114         Base64.Decoder decoder = Base64.getDecoder();
115         String decodedPageToken = new String(decoder.decode(pageToken));
116         String[] pageTokenSplit = decodedPageToken.split(DELIMITER, /* limit= */ -1);
117 
118         // If the pageToken was built from PhrPageTokenWrapper with request being
119         // null, this will only include the lastRowId.
120         if (pageTokenSplit.length == NUM_OF_ENCODED_FIELDS_WITHOUT_REQUEST_FILTERS) {
121             long lastRowId = Long.parseLong(decodedPageToken);
122             if (lastRowId < 0) {
123                 throw new IllegalArgumentException("Invalid pageToken");
124             }
125             return new PhrPageTokenWrapper(lastRowId);
126         }
127 
128         if (pageTokenSplit.length != NUM_OF_ENCODED_FIELDS_WITH_REQUEST_FILTERS) {
129             throw new IllegalArgumentException("Invalid pageToken");
130         }
131 
132         long lastRowId = Long.parseLong(pageTokenSplit[LAST_ROW_ID_INDEX]);
133         if (lastRowId < 0) {
134             throw new IllegalArgumentException("Invalid pageToken");
135         }
136 
137         int medicalResourceType = Integer.parseInt(pageTokenSplit[MEDICAL_RESOURCE_TYPE_INDEX]);
138         try {
139             validateMedicalResourceType(medicalResourceType);
140         } catch (IllegalArgumentException e) {
141             throw new IllegalArgumentException("Invalid pageToken");
142         }
143 
144         // We create a new request and only populate the read filters. The pageSize will be unset as
145         // we don't use it for encoding/decoding.
146         ReadMedicalResourcesInitialRequest.Builder requestWithFiltersOnly =
147                 new ReadMedicalResourcesInitialRequest.Builder(medicalResourceType);
148 
149         String medicalDataSourceIdsString = pageTokenSplit[MEDICAL_DATA_SOURCE_IDS_INDEX];
150         if (medicalDataSourceIdsString.isEmpty()) {
151             return new PhrPageTokenWrapper(requestWithFiltersOnly.build(), lastRowId);
152         }
153 
154         Set<String> medicalDataSourceIds =
155                 Arrays.stream(medicalDataSourceIdsString.split(INNER_DELIMITER)).collect(toSet());
156         try {
157             validateMedicalDataSourceIds(medicalDataSourceIds);
158         } catch (IllegalArgumentException e) {
159             throw new IllegalArgumentException("Invalid pageToken");
160         }
161 
162         return new PhrPageTokenWrapper(
163                 requestWithFiltersOnly.addDataSourceIds(medicalDataSourceIds).build(), lastRowId);
164     }
165 
166     /**
167      * Returns a pageToken string encoded from this {@link PhrPageTokenWrapper}.
168      *
169      * @throws IllegalStateException if {@code mLastRowId} is negative.
170      */
encode()171     public String encode() {
172         if (mLastRowId < 0) {
173             throw new IllegalStateException("cannot encode when mLastRowId is negative");
174         }
175         Base64.Encoder encoder = Base64.getEncoder();
176         return encoder.encodeToString(toReadableTokenString().getBytes());
177     }
178 
179     /**
180      * Converts this token to a readable string which will be used in {@link
181      * PhrPageTokenWrapper#encode()}.
182      */
toReadableTokenString()183     private String toReadableTokenString() {
184         String lastRowId = String.valueOf(mLastRowId);
185         if (mRequest == null) {
186             return lastRowId;
187         }
188         return String.join(
189                 DELIMITER,
190                 lastRowId,
191                 String.valueOf(mRequest.getMedicalResourceType()),
192                 String.join(INNER_DELIMITER, mRequest.getDataSourceIds()));
193     }
194 
195     /** Creates a String representation of this {@link PhrPageTokenWrapper}. */
toString()196     public String toString() {
197         return toReadableTokenString();
198     }
199 
200     /**
201      * Returns the last read row_id for the current {@code mRequest}. Default is {@link
202      * DEFAULT_LONG} for the initial request.
203      */
getLastRowId()204     public long getLastRowId() {
205         return mLastRowId;
206     }
207 
208     /**
209      * Sets the last row id.
210      *
211      * @throws IllegalStateException if {@code lastRowId} is negative.
212      */
cloneWithNewLastRowId(long lastRowId)213     public PhrPageTokenWrapper cloneWithNewLastRowId(long lastRowId) {
214         if (lastRowId < 0) {
215             throw new IllegalStateException("cannot set mLastRowId to negative");
216         }
217         return new PhrPageTokenWrapper(mRequest, lastRowId);
218     }
219 
220     /** Returns the initial request from which the {@link PhrPageTokenWrapper} is created from. */
221     @Nullable
getRequest()222     public ReadMedicalResourcesInitialRequest getRequest() {
223         return mRequest;
224     }
225 
226     /** Indicates whether some other object is "equal to" this one. */
227     @Override
equals(Object o)228     public boolean equals(Object o) {
229         if (this == o) return true;
230         if (!(o instanceof PhrPageTokenWrapper that)) return false;
231         return mLastRowId == that.mLastRowId && Objects.equals(mRequest, that.mRequest);
232     }
233 
234     /** Returns a hash code value for the object. */
235     @Override
hashCode()236     public int hashCode() {
237         return hash(getLastRowId(), getRequest());
238     }
239 
PhrPageTokenWrapper()240     private PhrPageTokenWrapper() {
241         this.mRequest = null;
242     }
243 
PhrPageTokenWrapper(long lastRowId)244     private PhrPageTokenWrapper(long lastRowId) {
245         this.mRequest = null;
246         this.mLastRowId = lastRowId;
247     }
248 
PhrPageTokenWrapper(@ullable ReadMedicalResourcesInitialRequest request)249     private PhrPageTokenWrapper(@Nullable ReadMedicalResourcesInitialRequest request) {
250         this.mRequest = request;
251     }
252 
PhrPageTokenWrapper( @ullable ReadMedicalResourcesInitialRequest request, long lastRowId)253     private PhrPageTokenWrapper(
254             @Nullable ReadMedicalResourcesInitialRequest request, long lastRowId) {
255         this.mRequest = request;
256         this.mLastRowId = lastRowId;
257     }
258 }
259