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 android.health.connect;
18 
19 import static android.health.connect.Constants.DEFAULT_LONG;
20 
21 import static java.lang.Integer.min;
22 
23 import java.util.Objects;
24 
25 /**
26  * A wrapper object contains information encoded in the {@code long} page token.
27  *
28  * @hide
29  */
30 public final class PageTokenWrapper {
31     /**
32      * This constant represents an empty token returned by the last read request, meaning no more
33      * pages are available.
34      *
35      * <p>We do not use this for read requests where page token is passed. The API design is
36      * asymmetry, when page token is passed in, it always contains {@code isAscending} information;
37      * the information is not available when it's returned.
38      */
39     public static final PageTokenWrapper EMPTY_PAGE_TOKEN = new PageTokenWrapper();
40 
41     private static final long MAX_ALLOWED_TIME_MILLIS = (1L << 44) - 1;
42     private static final long MAX_ALLOWED_OFFSET = (1 << 18) - 1;
43     private static final int OFFSET_START_BIT = 45;
44     private static final int TIMESTAMP_START_BIT = 1;
45 
46     private final boolean mIsAscending;
47     private final long mTimeMillis;
48     private final int mOffset;
49     private final boolean mIsTimestampSet;
50     private final boolean mIsEmpty;
51 
52     /** isAscending stored in the page token. */
isAscending()53     public boolean isAscending() {
54         return mIsAscending;
55     }
56 
57     /** Timestamp stored in the page token. */
timeMillis()58     public long timeMillis() {
59         return mTimeMillis;
60     }
61 
62     /** Offset stored in the page token. */
offset()63     public int offset() {
64         return mOffset;
65     }
66 
67     /** Whether or not the timestamp is set. */
isTimestampSet()68     public boolean isTimestampSet() {
69         return mIsTimestampSet;
70     }
71 
72     /** Whether or not the page token contains meaningful values. */
isEmpty()73     public boolean isEmpty() {
74         return mIsEmpty;
75     }
76 
77     /**
78      * Both {@code timeMillis} and {@code offset} have to be non-negative; {@code timeMillis} cannot
79      * exceed 2^44-1.
80      *
81      * <p>Note that due to space constraints, {@code offset} cannot exceed 2^18-1 (262143). If the
82      * {@code offset} parameter exceeds the maximum allowed value, it'll fallback to the max value.
83      *
84      * <p>More details see go/hc-page-token
85      */
of(boolean isAscending, long timeMillis, int offset)86     public static PageTokenWrapper of(boolean isAscending, long timeMillis, int offset) {
87         checkArgument(timeMillis >= 0, "timestamp can not be negative");
88         checkArgument(timeMillis <= MAX_ALLOWED_TIME_MILLIS, "timestamp too large");
89         checkArgument(offset >= 0, "offset can not be negative");
90         int boundedOffset = min((int) MAX_ALLOWED_OFFSET, offset);
91         return new PageTokenWrapper(isAscending, timeMillis, boundedOffset);
92     }
93 
94     /**
95      * Generate a page token that contains only {@code isAscending} information. Timestamp and
96      * offset are not set.
97      */
ofAscending(boolean isAscending)98     public static PageTokenWrapper ofAscending(boolean isAscending) {
99         return new PageTokenWrapper(isAscending);
100     }
101 
102     /**
103      * Construct a {@link PageTokenWrapper} from {@code pageToken} and {@code defaultIsAscending}.
104      *
105      * <p>When {@code pageToken} is not set, in which case we can not get {@code isAscending} from
106      * the token, it falls back to {@code defaultIsAscending}.
107      *
108      * <p>{@code pageToken} must be a non-negative long number (except for using the sentinel value
109      * {@code DEFAULT_LONG}, whose current value is {@code -1}, which represents page token not set)
110      */
from(long pageToken, boolean defaultIsAscending)111     public static PageTokenWrapper from(long pageToken, boolean defaultIsAscending) {
112         if (pageToken == DEFAULT_LONG) {
113             return PageTokenWrapper.ofAscending(defaultIsAscending);
114         }
115         checkArgument(pageToken >= 0, "pageToken cannot be negative");
116         return PageTokenWrapper.of(
117                 getIsAscending(pageToken), getTimestamp(pageToken), getOffset(pageToken));
118     }
119 
120     /**
121      * Take the least significant bit in the given {@code pageToken} to retrieve isAscending
122      * information.
123      *
124      * <p>If the last bit of the token is 1, isAscending is false; otherwise isAscending is true.
125      */
getIsAscending(long pageToken)126     private static boolean getIsAscending(long pageToken) {
127         return (pageToken & 1) == 0;
128     }
129 
130     /** Shifts bits in the given {@code pageToken} to retrieve timestamp information. */
getTimestamp(long pageToken)131     private static long getTimestamp(long pageToken) {
132         long mask = MAX_ALLOWED_TIME_MILLIS << TIMESTAMP_START_BIT;
133         return (pageToken & mask) >> TIMESTAMP_START_BIT;
134     }
135 
136     /** Shifts bits in the given {@code pageToken} to retrieve offset information. */
getOffset(long pageToken)137     private static int getOffset(long pageToken) {
138         return (int) (pageToken >> OFFSET_START_BIT);
139     }
140 
checkArgument(boolean expression, String errorMsg)141     private static void checkArgument(boolean expression, String errorMsg) {
142         if (!expression) {
143             throw new IllegalArgumentException(errorMsg);
144         }
145     }
146 
147     /**
148      * Encodes a {@link PageTokenWrapper} to a long value.
149      *
150      * <p>Page token is structured as following from right (least significant bit) to left (most
151      * significant bit):
152      * <li>Least significant bit: 0 = isAscending true, 1 = isAscending false
153      * <li>Next 44 bits: timestamp, represents epoch time millis
154      * <li>Next 18 bits: offset, represents number of records processed in the previous page
155      * <li>Sign bit: not used for encoding, page token is a signed long
156      */
encode()157     public long encode() {
158         return mIsTimestampSet
159                 ? ((long) mOffset << OFFSET_START_BIT)
160                         | (mTimeMillis << TIMESTAMP_START_BIT)
161                         | (mIsAscending ? 0 : 1)
162                 : DEFAULT_LONG;
163     }
164 
165     @Override
toString()166     public String toString() {
167         if (mIsEmpty) {
168             return "PageTokenWrapper{}";
169         }
170         StringBuilder builder = new StringBuilder("PageTokenWrapper{");
171         builder.append("isAscending = ").append(mIsAscending);
172         if (mIsTimestampSet) {
173             builder.append(", timeMillis = ").append(mTimeMillis);
174             builder.append(", offset = ").append(mOffset);
175         }
176         return builder.append("}").toString();
177     }
178 
179     @Override
equals(Object o)180     public boolean equals(Object o) {
181         if (this == o) return true;
182         if (!(o instanceof PageTokenWrapper that)) return false;
183         return mIsAscending == that.mIsAscending
184                 && mTimeMillis == that.mTimeMillis
185                 && mOffset == that.mOffset
186                 && mIsTimestampSet == that.mIsTimestampSet
187                 && mIsEmpty == that.mIsEmpty;
188     }
189 
190     @Override
hashCode()191     public int hashCode() {
192         return Objects.hash(mIsAscending, mOffset, mTimeMillis, mIsTimestampSet, mIsEmpty);
193     }
194 
PageTokenWrapper(boolean isAscending, long timeMillis, int offset)195     private PageTokenWrapper(boolean isAscending, long timeMillis, int offset) {
196         this.mIsAscending = isAscending;
197         this.mTimeMillis = timeMillis;
198         this.mOffset = offset;
199         this.mIsTimestampSet = true;
200         this.mIsEmpty = false;
201     }
202 
PageTokenWrapper(boolean isAscending)203     private PageTokenWrapper(boolean isAscending) {
204         this.mIsAscending = isAscending;
205         this.mTimeMillis = 0;
206         this.mOffset = 0;
207         this.mIsTimestampSet = false;
208         this.mIsEmpty = false;
209     }
210 
PageTokenWrapper()211     private PageTokenWrapper() {
212         this.mIsAscending = true;
213         this.mTimeMillis = 0;
214         this.mOffset = 0;
215         this.mIsTimestampSet = false;
216         this.mIsEmpty = true;
217     }
218 }
219