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