Automatic sources dropoff on 2020-06-10 18:32:38.095721
The change is generated with prebuilt drop tool.
Change-Id: I24cbf6ba6db262a1ae1445db1427a08fee35b3b4
diff --git a/android/util/AndroidException.java b/android/util/AndroidException.java
new file mode 100644
index 0000000..1345ddf
--- /dev/null
+++ b/android/util/AndroidException.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * Base class for all checked exceptions thrown by the Android frameworks.
+ */
+public class AndroidException extends Exception {
+ public AndroidException() {
+ }
+
+ public AndroidException(String name) {
+ super(name);
+ }
+
+ public AndroidException(String name, Throwable cause) {
+ super(name, cause);
+ }
+
+ public AndroidException(Exception cause) {
+ super(cause);
+ }
+
+ /** @hide */
+ protected AndroidException(String message, Throwable cause, boolean enableSuppression,
+ boolean writableStackTrace) {
+ super(message, cause, enableSuppression, writableStackTrace);
+ }
+};
+
diff --git a/android/util/AndroidRuntimeException.java b/android/util/AndroidRuntimeException.java
new file mode 100644
index 0000000..2b824bf
--- /dev/null
+++ b/android/util/AndroidRuntimeException.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * Base class for all unchecked exceptions thrown by the Android frameworks.
+ */
+public class AndroidRuntimeException extends RuntimeException {
+ public AndroidRuntimeException() {
+ }
+
+ public AndroidRuntimeException(String name) {
+ super(name);
+ }
+
+ public AndroidRuntimeException(String name, Throwable cause) {
+ super(name, cause);
+ }
+
+ public AndroidRuntimeException(Exception cause) {
+ super(cause);
+ }
+};
+
diff --git a/android/util/ArrayMap.java b/android/util/ArrayMap.java
new file mode 100644
index 0000000..4cf0a36
--- /dev/null
+++ b/android/util/ArrayMap.java
@@ -0,0 +1,1056 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.compat.annotation.UnsupportedAppUsage;
+
+import com.android.internal.util.ArrayUtils;
+
+import libcore.util.EmptyArray;
+
+import java.util.Collection;
+import java.util.ConcurrentModificationException;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * ArrayMap is a generic key->value mapping data structure that is
+ * designed to be more memory efficient than a traditional {@link java.util.HashMap}.
+ * It keeps its mappings in an array data structure -- an integer array of hash
+ * codes for each item, and an Object array of the key/value pairs. This allows it to
+ * avoid having to create an extra object for every entry put in to the map, and it
+ * also tries to control the growth of the size of these arrays more aggressively
+ * (since growing them only requires copying the entries in the array, not rebuilding
+ * a hash map).
+ *
+ * <p>Note that this implementation is not intended to be appropriate for data structures
+ * that may contain large numbers of items. It is generally slower than a traditional
+ * HashMap, since lookups require a binary search and adds and removes require inserting
+ * and deleting entries in the array. For containers holding up to hundreds of items,
+ * the performance difference is not significant, less than 50%.</p>
+ *
+ * <p>Because this container is intended to better balance memory use, unlike most other
+ * standard Java containers it will shrink its array as items are removed from it. Currently
+ * you have no control over this shrinking -- if you set a capacity and then remove an
+ * item, it may reduce the capacity to better match the current size. In the future an
+ * explicit call to set the capacity should turn off this aggressive shrinking behavior.</p>
+ *
+ * <p>This structure is <b>NOT</b> thread-safe.</p>
+ */
+public final class ArrayMap<K, V> implements Map<K, V> {
+ private static final boolean DEBUG = false;
+ private static final String TAG = "ArrayMap";
+
+ /**
+ * Attempt to spot concurrent modifications to this data structure.
+ *
+ * It's best-effort, but any time we can throw something more diagnostic than an
+ * ArrayIndexOutOfBoundsException deep in the ArrayMap internals it's going to
+ * save a lot of development time.
+ *
+ * Good times to look for CME include after any allocArrays() call and at the end of
+ * functions that change mSize (put/remove/clear).
+ */
+ private static final boolean CONCURRENT_MODIFICATION_EXCEPTIONS = true;
+
+ /**
+ * The minimum amount by which the capacity of a ArrayMap will increase.
+ * This is tuned to be relatively space-efficient.
+ */
+ private static final int BASE_SIZE = 4;
+
+ /**
+ * Maximum number of entries to have in array caches.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Allocations are an implementation detail.
+ private static final int CACHE_SIZE = 10;
+
+ /**
+ * Special hash array value that indicates the container is immutable.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Allocations are an implementation detail.
+ static final int[] EMPTY_IMMUTABLE_INTS = new int[0];
+
+ /**
+ * @hide Special immutable empty ArrayMap.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Use your own singleton empty map.
+ public static final ArrayMap EMPTY = new ArrayMap<>(-1);
+
+ /**
+ * Caches of small array objects to avoid spamming garbage. The cache
+ * Object[] variable is a pointer to a linked list of array objects.
+ * The first entry in the array is a pointer to the next array in the
+ * list; the second entry is a pointer to the int[] hash code array for it.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Allocations are an implementation detail.
+ static Object[] mBaseCache;
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Allocations are an implementation detail.
+ static int mBaseCacheSize;
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Allocations are an implementation detail.
+ static Object[] mTwiceBaseCache;
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Allocations are an implementation detail.
+ static int mTwiceBaseCacheSize;
+ /**
+ * Separate locks for each cache since each can be accessed independently of the other without
+ * risk of a deadlock.
+ */
+ private static final Object sBaseCacheLock = new Object();
+ private static final Object sTwiceBaseCacheLock = new Object();
+
+ private final boolean mIdentityHashCode;
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Hashes are an implementation detail. Use public key/value API.
+ int[] mHashes;
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Storage is an implementation detail. Use public key/value API.
+ Object[] mArray;
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Use size()
+ int mSize;
+ private MapCollections<K, V> mCollections;
+
+ private static int binarySearchHashes(int[] hashes, int N, int hash) {
+ try {
+ return ContainerHelpers.binarySearch(hashes, N, hash);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ if (CONCURRENT_MODIFICATION_EXCEPTIONS) {
+ throw new ConcurrentModificationException();
+ } else {
+ throw e; // the cache is poisoned at this point, there's not much we can do
+ }
+ }
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Hashes are an implementation detail. Use indexOfKey(Object).
+ int indexOf(Object key, int hash) {
+ final int N = mSize;
+
+ // Important fast case: if nothing is in here, nothing to look for.
+ if (N == 0) {
+ return ~0;
+ }
+
+ int index = binarySearchHashes(mHashes, N, hash);
+
+ // If the hash code wasn't found, then we have no entry for this key.
+ if (index < 0) {
+ return index;
+ }
+
+ // If the key at the returned index matches, that's what we want.
+ if (key.equals(mArray[index<<1])) {
+ return index;
+ }
+
+ // Search for a matching key after the index.
+ int end;
+ for (end = index + 1; end < N && mHashes[end] == hash; end++) {
+ if (key.equals(mArray[end << 1])) return end;
+ }
+
+ // Search for a matching key before the index.
+ for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
+ if (key.equals(mArray[i << 1])) return i;
+ }
+
+ // Key not found -- return negative value indicating where a
+ // new entry for this key should go. We use the end of the
+ // hash chain to reduce the number of array entries that will
+ // need to be copied when inserting.
+ return ~end;
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Use indexOf(null)
+ int indexOfNull() {
+ final int N = mSize;
+
+ // Important fast case: if nothing is in here, nothing to look for.
+ if (N == 0) {
+ return ~0;
+ }
+
+ int index = binarySearchHashes(mHashes, N, 0);
+
+ // If the hash code wasn't found, then we have no entry for this key.
+ if (index < 0) {
+ return index;
+ }
+
+ // If the key at the returned index matches, that's what we want.
+ if (null == mArray[index<<1]) {
+ return index;
+ }
+
+ // Search for a matching key after the index.
+ int end;
+ for (end = index + 1; end < N && mHashes[end] == 0; end++) {
+ if (null == mArray[end << 1]) return end;
+ }
+
+ // Search for a matching key before the index.
+ for (int i = index - 1; i >= 0 && mHashes[i] == 0; i--) {
+ if (null == mArray[i << 1]) return i;
+ }
+
+ // Key not found -- return negative value indicating where a
+ // new entry for this key should go. We use the end of the
+ // hash chain to reduce the number of array entries that will
+ // need to be copied when inserting.
+ return ~end;
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Allocations are an implementation detail.
+ private void allocArrays(final int size) {
+ if (mHashes == EMPTY_IMMUTABLE_INTS) {
+ throw new UnsupportedOperationException("ArrayMap is immutable");
+ }
+ if (size == (BASE_SIZE*2)) {
+ synchronized (sTwiceBaseCacheLock) {
+ if (mTwiceBaseCache != null) {
+ final Object[] array = mTwiceBaseCache;
+ mArray = array;
+ try {
+ mTwiceBaseCache = (Object[]) array[0];
+ mHashes = (int[]) array[1];
+ if (mHashes != null) {
+ array[0] = array[1] = null;
+ mTwiceBaseCacheSize--;
+ if (DEBUG) {
+ Log.d(TAG, "Retrieving 2x cache " + mHashes
+ + " now have " + mTwiceBaseCacheSize + " entries");
+ }
+ return;
+ }
+ } catch (ClassCastException e) {
+ }
+ // Whoops! Someone trampled the array (probably due to not protecting
+ // their access with a lock). Our cache is corrupt; report and give up.
+ Slog.wtf(TAG, "Found corrupt ArrayMap cache: [0]=" + array[0]
+ + " [1]=" + array[1]);
+ mTwiceBaseCache = null;
+ mTwiceBaseCacheSize = 0;
+ }
+ }
+ } else if (size == BASE_SIZE) {
+ synchronized (sBaseCacheLock) {
+ if (mBaseCache != null) {
+ final Object[] array = mBaseCache;
+ mArray = array;
+ try {
+ mBaseCache = (Object[]) array[0];
+ mHashes = (int[]) array[1];
+ if (mHashes != null) {
+ array[0] = array[1] = null;
+ mBaseCacheSize--;
+ if (DEBUG) {
+ Log.d(TAG, "Retrieving 1x cache " + mHashes
+ + " now have " + mBaseCacheSize + " entries");
+ }
+ return;
+ }
+ } catch (ClassCastException e) {
+ }
+ // Whoops! Someone trampled the array (probably due to not protecting
+ // their access with a lock). Our cache is corrupt; report and give up.
+ Slog.wtf(TAG, "Found corrupt ArrayMap cache: [0]=" + array[0]
+ + " [1]=" + array[1]);
+ mBaseCache = null;
+ mBaseCacheSize = 0;
+ }
+ }
+ }
+
+ mHashes = new int[size];
+ mArray = new Object[size<<1];
+ }
+
+ /**
+ * Make sure <b>NOT</b> to call this method with arrays that can still be modified. In other
+ * words, don't pass mHashes or mArray in directly.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Allocations are an implementation detail.
+ private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
+ if (hashes.length == (BASE_SIZE*2)) {
+ synchronized (sTwiceBaseCacheLock) {
+ if (mTwiceBaseCacheSize < CACHE_SIZE) {
+ array[0] = mTwiceBaseCache;
+ array[1] = hashes;
+ for (int i=(size<<1)-1; i>=2; i--) {
+ array[i] = null;
+ }
+ mTwiceBaseCache = array;
+ mTwiceBaseCacheSize++;
+ if (DEBUG) Log.d(TAG, "Storing 2x cache " + array
+ + " now have " + mTwiceBaseCacheSize + " entries");
+ }
+ }
+ } else if (hashes.length == BASE_SIZE) {
+ synchronized (sBaseCacheLock) {
+ if (mBaseCacheSize < CACHE_SIZE) {
+ array[0] = mBaseCache;
+ array[1] = hashes;
+ for (int i=(size<<1)-1; i>=2; i--) {
+ array[i] = null;
+ }
+ mBaseCache = array;
+ mBaseCacheSize++;
+ if (DEBUG) Log.d(TAG, "Storing 1x cache " + array
+ + " now have " + mBaseCacheSize + " entries");
+ }
+ }
+ }
+ }
+
+ /**
+ * Create a new empty ArrayMap. The default capacity of an array map is 0, and
+ * will grow once items are added to it.
+ */
+ public ArrayMap() {
+ this(0, false);
+ }
+
+ /**
+ * Create a new ArrayMap with a given initial capacity.
+ */
+ public ArrayMap(int capacity) {
+ this(capacity, false);
+ }
+
+ /** {@hide} */
+ public ArrayMap(int capacity, boolean identityHashCode) {
+ mIdentityHashCode = identityHashCode;
+
+ // If this is immutable, use the sentinal EMPTY_IMMUTABLE_INTS
+ // instance instead of the usual EmptyArray.INT. The reference
+ // is checked later to see if the array is allowed to grow.
+ if (capacity < 0) {
+ mHashes = EMPTY_IMMUTABLE_INTS;
+ mArray = EmptyArray.OBJECT;
+ } else if (capacity == 0) {
+ mHashes = EmptyArray.INT;
+ mArray = EmptyArray.OBJECT;
+ } else {
+ allocArrays(capacity);
+ }
+ mSize = 0;
+ }
+
+ /**
+ * Create a new ArrayMap with the mappings from the given ArrayMap.
+ */
+ public ArrayMap(ArrayMap<K, V> map) {
+ this();
+ if (map != null) {
+ putAll(map);
+ }
+ }
+
+ /**
+ * Make the array map empty. All storage is released.
+ */
+ @Override
+ public void clear() {
+ if (mSize > 0) {
+ final int[] ohashes = mHashes;
+ final Object[] oarray = mArray;
+ final int osize = mSize;
+ mHashes = EmptyArray.INT;
+ mArray = EmptyArray.OBJECT;
+ mSize = 0;
+ freeArrays(ohashes, oarray, osize);
+ }
+ if (CONCURRENT_MODIFICATION_EXCEPTIONS && mSize > 0) {
+ throw new ConcurrentModificationException();
+ }
+ }
+
+ /**
+ * @hide
+ * Like {@link #clear}, but doesn't reduce the capacity of the ArrayMap.
+ */
+ public void erase() {
+ if (mSize > 0) {
+ final int N = mSize<<1;
+ final Object[] array = mArray;
+ for (int i=0; i<N; i++) {
+ array[i] = null;
+ }
+ mSize = 0;
+ }
+ }
+
+ /**
+ * Ensure the array map can hold at least <var>minimumCapacity</var>
+ * items.
+ */
+ public void ensureCapacity(int minimumCapacity) {
+ final int osize = mSize;
+ if (mHashes.length < minimumCapacity) {
+ final int[] ohashes = mHashes;
+ final Object[] oarray = mArray;
+ allocArrays(minimumCapacity);
+ if (mSize > 0) {
+ System.arraycopy(ohashes, 0, mHashes, 0, osize);
+ System.arraycopy(oarray, 0, mArray, 0, osize<<1);
+ }
+ freeArrays(ohashes, oarray, osize);
+ }
+ if (CONCURRENT_MODIFICATION_EXCEPTIONS && mSize != osize) {
+ throw new ConcurrentModificationException();
+ }
+ }
+
+ /**
+ * Check whether a key exists in the array.
+ *
+ * @param key The key to search for.
+ * @return Returns true if the key exists, else false.
+ */
+ @Override
+ public boolean containsKey(Object key) {
+ return indexOfKey(key) >= 0;
+ }
+
+ /**
+ * Returns the index of a key in the set.
+ *
+ * @param key The key to search for.
+ * @return Returns the index of the key if it exists, else a negative integer.
+ */
+ public int indexOfKey(Object key) {
+ return key == null ? indexOfNull()
+ : indexOf(key, mIdentityHashCode ? System.identityHashCode(key) : key.hashCode());
+ }
+
+ /**
+ * Returns an index for which {@link #valueAt} would return the
+ * specified value, or a negative number if no keys map to the
+ * specified value.
+ * Beware that this is a linear search, unlike lookups by key,
+ * and that multiple keys can map to the same value and this will
+ * find only one of them.
+ */
+ public int indexOfValue(Object value) {
+ final int N = mSize*2;
+ final Object[] array = mArray;
+ if (value == null) {
+ for (int i=1; i<N; i+=2) {
+ if (array[i] == null) {
+ return i>>1;
+ }
+ }
+ } else {
+ for (int i=1; i<N; i+=2) {
+ if (value.equals(array[i])) {
+ return i>>1;
+ }
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Check whether a value exists in the array. This requires a linear search
+ * through the entire array.
+ *
+ * @param value The value to search for.
+ * @return Returns true if the value exists, else false.
+ */
+ @Override
+ public boolean containsValue(Object value) {
+ return indexOfValue(value) >= 0;
+ }
+
+ /**
+ * Retrieve a value from the array.
+ * @param key The key of the value to retrieve.
+ * @return Returns the value associated with the given key,
+ * or null if there is no such key.
+ */
+ @Override
+ public V get(Object key) {
+ final int index = indexOfKey(key);
+ return index >= 0 ? (V)mArray[(index<<1)+1] : null;
+ }
+
+ /**
+ * Return the key at the given index in the array.
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ *
+ * @param index The desired index, must be between 0 and {@link #size()}-1.
+ * @return Returns the key stored at the given index.
+ */
+ public K keyAt(int index) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ return (K)mArray[index << 1];
+ }
+
+ /**
+ * Return the value at the given index in the array.
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ *
+ * @param index The desired index, must be between 0 and {@link #size()}-1.
+ * @return Returns the value stored at the given index.
+ */
+ public V valueAt(int index) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ return (V)mArray[(index << 1) + 1];
+ }
+
+ /**
+ * Set the value at a given index in the array.
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ *
+ * @param index The desired index, must be between 0 and {@link #size()}-1.
+ * @param value The new value to store at this index.
+ * @return Returns the previous value at the given index.
+ */
+ public V setValueAt(int index, V value) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ index = (index << 1) + 1;
+ V old = (V)mArray[index];
+ mArray[index] = value;
+ return old;
+ }
+
+ /**
+ * Return true if the array map contains no items.
+ */
+ @Override
+ public boolean isEmpty() {
+ return mSize <= 0;
+ }
+
+ /**
+ * Add a new value to the array map.
+ * @param key The key under which to store the value. If
+ * this key already exists in the array, its value will be replaced.
+ * @param value The value to store for the given key.
+ * @return Returns the old value that was stored for the given key, or null if there
+ * was no such key.
+ */
+ @Override
+ public V put(K key, V value) {
+ final int osize = mSize;
+ final int hash;
+ int index;
+ if (key == null) {
+ hash = 0;
+ index = indexOfNull();
+ } else {
+ hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode();
+ index = indexOf(key, hash);
+ }
+ if (index >= 0) {
+ index = (index<<1) + 1;
+ final V old = (V)mArray[index];
+ mArray[index] = value;
+ return old;
+ }
+
+ index = ~index;
+ if (osize >= mHashes.length) {
+ final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1))
+ : (osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);
+
+ if (DEBUG) Log.d(TAG, "put: grow from " + mHashes.length + " to " + n);
+
+ final int[] ohashes = mHashes;
+ final Object[] oarray = mArray;
+ allocArrays(n);
+
+ if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
+ throw new ConcurrentModificationException();
+ }
+
+ if (mHashes.length > 0) {
+ if (DEBUG) Log.d(TAG, "put: copy 0-" + osize + " to 0");
+ System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
+ System.arraycopy(oarray, 0, mArray, 0, oarray.length);
+ }
+
+ freeArrays(ohashes, oarray, osize);
+ }
+
+ if (index < osize) {
+ if (DEBUG) Log.d(TAG, "put: move " + index + "-" + (osize-index)
+ + " to " + (index+1));
+ System.arraycopy(mHashes, index, mHashes, index + 1, osize - index);
+ System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);
+ }
+
+ if (CONCURRENT_MODIFICATION_EXCEPTIONS) {
+ if (osize != mSize || index >= mHashes.length) {
+ throw new ConcurrentModificationException();
+ }
+ }
+ mHashes[index] = hash;
+ mArray[index<<1] = key;
+ mArray[(index<<1)+1] = value;
+ mSize++;
+ return null;
+ }
+
+ /**
+ * Special fast path for appending items to the end of the array without validation.
+ * The array must already be large enough to contain the item.
+ * @hide
+ */
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Storage is an implementation detail. Use put(K, V).
+ public void append(K key, V value) {
+ int index = mSize;
+ final int hash = key == null ? 0
+ : (mIdentityHashCode ? System.identityHashCode(key) : key.hashCode());
+ if (index >= mHashes.length) {
+ throw new IllegalStateException("Array is full");
+ }
+ if (index > 0 && mHashes[index-1] > hash) {
+ RuntimeException e = new RuntimeException("here");
+ e.fillInStackTrace();
+ Log.w(TAG, "New hash " + hash
+ + " is before end of array hash " + mHashes[index-1]
+ + " at index " + index + " key " + key, e);
+ put(key, value);
+ return;
+ }
+ mSize = index+1;
+ mHashes[index] = hash;
+ index <<= 1;
+ mArray[index] = key;
+ mArray[index+1] = value;
+ }
+
+ /**
+ * The use of the {@link #append} function can result in invalid array maps, in particular
+ * an array map where the same key appears multiple times. This function verifies that
+ * the array map is valid, throwing IllegalArgumentException if a problem is found. The
+ * main use for this method is validating an array map after unpacking from an IPC, to
+ * protect against malicious callers.
+ * @hide
+ */
+ public void validate() {
+ final int N = mSize;
+ if (N <= 1) {
+ // There can't be dups.
+ return;
+ }
+ int basehash = mHashes[0];
+ int basei = 0;
+ for (int i=1; i<N; i++) {
+ int hash = mHashes[i];
+ if (hash != basehash) {
+ basehash = hash;
+ basei = i;
+ continue;
+ }
+ // We are in a run of entries with the same hash code. Go backwards through
+ // the array to see if any keys are the same.
+ final Object cur = mArray[i<<1];
+ for (int j=i-1; j>=basei; j--) {
+ final Object prev = mArray[j<<1];
+ if (cur == prev) {
+ throw new IllegalArgumentException("Duplicate key in ArrayMap: " + cur);
+ }
+ if (cur != null && prev != null && cur.equals(prev)) {
+ throw new IllegalArgumentException("Duplicate key in ArrayMap: " + cur);
+ }
+ }
+ }
+ }
+
+ /**
+ * Perform a {@link #put(Object, Object)} of all key/value pairs in <var>array</var>
+ * @param array The array whose contents are to be retrieved.
+ */
+ public void putAll(ArrayMap<? extends K, ? extends V> array) {
+ final int N = array.mSize;
+ ensureCapacity(mSize + N);
+ if (mSize == 0) {
+ if (N > 0) {
+ System.arraycopy(array.mHashes, 0, mHashes, 0, N);
+ System.arraycopy(array.mArray, 0, mArray, 0, N<<1);
+ mSize = N;
+ }
+ } else {
+ for (int i=0; i<N; i++) {
+ put(array.keyAt(i), array.valueAt(i));
+ }
+ }
+ }
+
+ /**
+ * Remove an existing key from the array map.
+ * @param key The key of the mapping to remove.
+ * @return Returns the value that was stored under the key, or null if there
+ * was no such key.
+ */
+ @Override
+ public V remove(Object key) {
+ final int index = indexOfKey(key);
+ if (index >= 0) {
+ return removeAt(index);
+ }
+
+ return null;
+ }
+
+ /**
+ * Remove the key/value mapping at the given index.
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ *
+ * @param index The desired index, must be between 0 and {@link #size()}-1.
+ * @return Returns the value that was stored at this index.
+ */
+ public V removeAt(int index) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+
+ final Object old = mArray[(index << 1) + 1];
+ final int osize = mSize;
+ final int nsize;
+ if (osize <= 1) {
+ // Now empty.
+ if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to 0");
+ final int[] ohashes = mHashes;
+ final Object[] oarray = mArray;
+ mHashes = EmptyArray.INT;
+ mArray = EmptyArray.OBJECT;
+ freeArrays(ohashes, oarray, osize);
+ nsize = 0;
+ } else {
+ nsize = osize - 1;
+ if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) {
+ // Shrunk enough to reduce size of arrays. We don't allow it to
+ // shrink smaller than (BASE_SIZE*2) to avoid flapping between
+ // that and BASE_SIZE.
+ final int n = osize > (BASE_SIZE*2) ? (osize + (osize>>1)) : (BASE_SIZE*2);
+
+ if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to " + n);
+
+ final int[] ohashes = mHashes;
+ final Object[] oarray = mArray;
+ allocArrays(n);
+
+ if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
+ throw new ConcurrentModificationException();
+ }
+
+ if (index > 0) {
+ if (DEBUG) Log.d(TAG, "remove: copy from 0-" + index + " to 0");
+ System.arraycopy(ohashes, 0, mHashes, 0, index);
+ System.arraycopy(oarray, 0, mArray, 0, index << 1);
+ }
+ if (index < nsize) {
+ if (DEBUG) Log.d(TAG, "remove: copy from " + (index+1) + "-" + nsize
+ + " to " + index);
+ System.arraycopy(ohashes, index + 1, mHashes, index, nsize - index);
+ System.arraycopy(oarray, (index + 1) << 1, mArray, index << 1,
+ (nsize - index) << 1);
+ }
+ } else {
+ if (index < nsize) {
+ if (DEBUG) Log.d(TAG, "remove: move " + (index+1) + "-" + nsize
+ + " to " + index);
+ System.arraycopy(mHashes, index + 1, mHashes, index, nsize - index);
+ System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1,
+ (nsize - index) << 1);
+ }
+ mArray[nsize << 1] = null;
+ mArray[(nsize << 1) + 1] = null;
+ }
+ }
+ if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
+ throw new ConcurrentModificationException();
+ }
+ mSize = nsize;
+ return (V)old;
+ }
+
+ /**
+ * Return the number of items in this array map.
+ */
+ @Override
+ public int size() {
+ return mSize;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>This implementation returns false if the object is not a map, or
+ * if the maps have different sizes. Otherwise, for each key in this map,
+ * values of both maps are compared. If the values for any key are not
+ * equal, the method returns false, otherwise it returns true.
+ */
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object instanceof Map) {
+ Map<?, ?> map = (Map<?, ?>) object;
+ if (size() != map.size()) {
+ return false;
+ }
+
+ try {
+ for (int i=0; i<mSize; i++) {
+ K key = keyAt(i);
+ V mine = valueAt(i);
+ Object theirs = map.get(key);
+ if (mine == null) {
+ if (theirs != null || !map.containsKey(key)) {
+ return false;
+ }
+ } else if (!mine.equals(theirs)) {
+ return false;
+ }
+ }
+ } catch (NullPointerException ignored) {
+ return false;
+ } catch (ClassCastException ignored) {
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ final int[] hashes = mHashes;
+ final Object[] array = mArray;
+ int result = 0;
+ for (int i = 0, v = 1, s = mSize; i < s; i++, v+=2) {
+ Object value = array[v];
+ result += hashes[i] ^ (value == null ? 0 : value.hashCode());
+ }
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>This implementation composes a string by iterating over its mappings. If
+ * this map contains itself as a key or a value, the string "(this Map)"
+ * will appear in its place.
+ */
+ @Override
+ public String toString() {
+ if (isEmpty()) {
+ return "{}";
+ }
+
+ StringBuilder buffer = new StringBuilder(mSize * 28);
+ buffer.append('{');
+ for (int i=0; i<mSize; i++) {
+ if (i > 0) {
+ buffer.append(", ");
+ }
+ Object key = keyAt(i);
+ if (key != this) {
+ buffer.append(key);
+ } else {
+ buffer.append("(this Map)");
+ }
+ buffer.append('=');
+ Object value = valueAt(i);
+ if (value != this) {
+ buffer.append(ArrayUtils.deepToString(value));
+ } else {
+ buffer.append("(this Map)");
+ }
+ }
+ buffer.append('}');
+ return buffer.toString();
+ }
+
+ // ------------------------------------------------------------------------
+ // Interop with traditional Java containers. Not as efficient as using
+ // specialized collection APIs.
+ // ------------------------------------------------------------------------
+
+ private MapCollections<K, V> getCollection() {
+ if (mCollections == null) {
+ mCollections = new MapCollections<K, V>() {
+ @Override
+ protected int colGetSize() {
+ return mSize;
+ }
+
+ @Override
+ protected Object colGetEntry(int index, int offset) {
+ return mArray[(index<<1) + offset];
+ }
+
+ @Override
+ protected int colIndexOfKey(Object key) {
+ return indexOfKey(key);
+ }
+
+ @Override
+ protected int colIndexOfValue(Object value) {
+ return indexOfValue(value);
+ }
+
+ @Override
+ protected Map<K, V> colGetMap() {
+ return ArrayMap.this;
+ }
+
+ @Override
+ protected void colPut(K key, V value) {
+ put(key, value);
+ }
+
+ @Override
+ protected V colSetValue(int index, V value) {
+ return setValueAt(index, value);
+ }
+
+ @Override
+ protected void colRemoveAt(int index) {
+ removeAt(index);
+ }
+
+ @Override
+ protected void colClear() {
+ clear();
+ }
+ };
+ }
+ return mCollections;
+ }
+
+ /**
+ * Determine if the array map contains all of the keys in the given collection.
+ * @param collection The collection whose contents are to be checked against.
+ * @return Returns true if this array map contains a key for every entry
+ * in <var>collection</var>, else returns false.
+ */
+ public boolean containsAll(Collection<?> collection) {
+ return MapCollections.containsAllHelper(this, collection);
+ }
+
+ /**
+ * Perform a {@link #put(Object, Object)} of all key/value pairs in <var>map</var>
+ * @param map The map whose contents are to be retrieved.
+ */
+ @Override
+ public void putAll(Map<? extends K, ? extends V> map) {
+ ensureCapacity(mSize + map.size());
+ for (Map.Entry<? extends K, ? extends V> entry : map.entrySet()) {
+ put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ /**
+ * Remove all keys in the array map that exist in the given collection.
+ * @param collection The collection whose contents are to be used to remove keys.
+ * @return Returns true if any keys were removed from the array map, else false.
+ */
+ public boolean removeAll(Collection<?> collection) {
+ return MapCollections.removeAllHelper(this, collection);
+ }
+
+ /**
+ * Remove all keys in the array map that do <b>not</b> exist in the given collection.
+ * @param collection The collection whose contents are to be used to determine which
+ * keys to keep.
+ * @return Returns true if any keys were removed from the array map, else false.
+ */
+ public boolean retainAll(Collection<?> collection) {
+ return MapCollections.retainAllHelper(this, collection);
+ }
+
+ /**
+ * Return a {@link java.util.Set} for iterating over and interacting with all mappings
+ * in the array map.
+ *
+ * <p><b>Note:</b> this is a very inefficient way to access the array contents, it
+ * requires generating a number of temporary objects and allocates additional state
+ * information associated with the container that will remain for the life of the container.</p>
+ *
+ * <p><b>Note:</b></p> the semantics of this
+ * Set are subtly different than that of a {@link java.util.HashMap}: most important,
+ * the {@link java.util.Map.Entry Map.Entry} object returned by its iterator is a single
+ * object that exists for the entire iterator, so you can <b>not</b> hold on to it
+ * after calling {@link java.util.Iterator#next() Iterator.next}.</p>
+ */
+ @Override
+ public Set<Map.Entry<K, V>> entrySet() {
+ return getCollection().getEntrySet();
+ }
+
+ /**
+ * Return a {@link java.util.Set} for iterating over and interacting with all keys
+ * in the array map.
+ *
+ * <p><b>Note:</b> this is a fairly inefficient way to access the array contents, it
+ * requires generating a number of temporary objects and allocates additional state
+ * information associated with the container that will remain for the life of the container.</p>
+ */
+ @Override
+ public Set<K> keySet() {
+ return getCollection().getKeySet();
+ }
+
+ /**
+ * Return a {@link java.util.Collection} for iterating over and interacting with all values
+ * in the array map.
+ *
+ * <p><b>Note:</b> this is a fairly inefficient way to access the array contents, it
+ * requires generating a number of temporary objects and allocates additional state
+ * information associated with the container that will remain for the life of the container.</p>
+ */
+ @Override
+ public Collection<V> values() {
+ return getCollection().getValues();
+ }
+}
diff --git a/android/util/ArraySet.java b/android/util/ArraySet.java
new file mode 100644
index 0000000..7f652ba
--- /dev/null
+++ b/android/util/ArraySet.java
@@ -0,0 +1,982 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.compat.annotation.UnsupportedAppUsage;
+
+import libcore.util.EmptyArray;
+
+import java.lang.reflect.Array;
+import java.util.Collection;
+import java.util.ConcurrentModificationException;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Predicate;
+
+/**
+ * ArraySet is a generic set data structure that is designed to be more memory efficient than a
+ * traditional {@link java.util.HashSet}. The design is very similar to
+ * {@link ArrayMap}, with all of the caveats described there. This implementation is
+ * separate from ArrayMap, however, so the Object array contains only one item for each
+ * entry in the set (instead of a pair for a mapping).
+ *
+ * <p>Note that this implementation is not intended to be appropriate for data structures
+ * that may contain large numbers of items. It is generally slower than a traditional
+ * HashSet, since lookups require a binary search and adds and removes require inserting
+ * and deleting entries in the array. For containers holding up to hundreds of items,
+ * the performance difference is not significant, less than 50%.</p>
+ *
+ * <p>Because this container is intended to better balance memory use, unlike most other
+ * standard Java containers it will shrink its array as items are removed from it. Currently
+ * you have no control over this shrinking -- if you set a capacity and then remove an
+ * item, it may reduce the capacity to better match the current size. In the future an
+ * explicit call to set the capacity should turn off this aggressive shrinking behavior.</p>
+ *
+ * <p>This structure is <b>NOT</b> thread-safe.</p>
+ */
+public final class ArraySet<E> implements Collection<E>, Set<E> {
+ private static final boolean DEBUG = false;
+ private static final String TAG = "ArraySet";
+
+ /**
+ * The minimum amount by which the capacity of a ArraySet will increase.
+ * This is tuned to be relatively space-efficient.
+ */
+ private static final int BASE_SIZE = 4;
+
+ /**
+ * Maximum number of entries to have in array caches.
+ */
+ private static final int CACHE_SIZE = 10;
+
+ /**
+ * Caches of small array objects to avoid spamming garbage. The cache
+ * Object[] variable is a pointer to a linked list of array objects.
+ * The first entry in the array is a pointer to the next array in the
+ * list; the second entry is a pointer to the int[] hash code array for it.
+ */
+ static Object[] sBaseCache;
+ static int sBaseCacheSize;
+ static Object[] sTwiceBaseCache;
+ static int sTwiceBaseCacheSize;
+ /**
+ * Separate locks for each cache since each can be accessed independently of the other without
+ * risk of a deadlock.
+ */
+ private static final Object sBaseCacheLock = new Object();
+ private static final Object sTwiceBaseCacheLock = new Object();
+
+ private final boolean mIdentityHashCode;
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Hashes are an implementation detail. Use public API.
+ int[] mHashes;
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Storage is an implementation detail. Use public API.
+ Object[] mArray;
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Use size()
+ int mSize;
+ private MapCollections<E, E> mCollections;
+
+ private int binarySearch(int[] hashes, int hash) {
+ try {
+ return ContainerHelpers.binarySearch(hashes, mSize, hash);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new ConcurrentModificationException();
+ }
+ }
+
+
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Hashes are an implementation detail. Use indexOfKey(Object).
+ private int indexOf(Object key, int hash) {
+ final int N = mSize;
+
+ // Important fast case: if nothing is in here, nothing to look for.
+ if (N == 0) {
+ return ~0;
+ }
+
+ int index = binarySearch(mHashes, hash);
+
+ // If the hash code wasn't found, then we have no entry for this key.
+ if (index < 0) {
+ return index;
+ }
+
+ // If the key at the returned index matches, that's what we want.
+ if (key.equals(mArray[index])) {
+ return index;
+ }
+
+ // Search for a matching key after the index.
+ int end;
+ for (end = index + 1; end < N && mHashes[end] == hash; end++) {
+ if (key.equals(mArray[end])) return end;
+ }
+
+ // Search for a matching key before the index.
+ for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
+ if (key.equals(mArray[i])) return i;
+ }
+
+ // Key not found -- return negative value indicating where a
+ // new entry for this key should go. We use the end of the
+ // hash chain to reduce the number of array entries that will
+ // need to be copied when inserting.
+ return ~end;
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Use indexOf(null)
+ private int indexOfNull() {
+ final int N = mSize;
+
+ // Important fast case: if nothing is in here, nothing to look for.
+ if (N == 0) {
+ return ~0;
+ }
+
+ int index = binarySearch(mHashes, 0);
+
+ // If the hash code wasn't found, then we have no entry for this key.
+ if (index < 0) {
+ return index;
+ }
+
+ // If the key at the returned index matches, that's what we want.
+ if (null == mArray[index]) {
+ return index;
+ }
+
+ // Search for a matching key after the index.
+ int end;
+ for (end = index + 1; end < N && mHashes[end] == 0; end++) {
+ if (null == mArray[end]) return end;
+ }
+
+ // Search for a matching key before the index.
+ for (int i = index - 1; i >= 0 && mHashes[i] == 0; i--) {
+ if (null == mArray[i]) return i;
+ }
+
+ // Key not found -- return negative value indicating where a
+ // new entry for this key should go. We use the end of the
+ // hash chain to reduce the number of array entries that will
+ // need to be copied when inserting.
+ return ~end;
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Allocations are an implementation detail.
+ private void allocArrays(final int size) {
+ if (size == (BASE_SIZE * 2)) {
+ synchronized (sTwiceBaseCacheLock) {
+ if (sTwiceBaseCache != null) {
+ final Object[] array = sTwiceBaseCache;
+ try {
+ mArray = array;
+ sTwiceBaseCache = (Object[]) array[0];
+ mHashes = (int[]) array[1];
+ if (mHashes != null) {
+ array[0] = array[1] = null;
+ sTwiceBaseCacheSize--;
+ if (DEBUG) {
+ Log.d(TAG, "Retrieving 2x cache " + mHashes + " now have "
+ + sTwiceBaseCacheSize + " entries");
+ }
+ return;
+ }
+ } catch (ClassCastException e) {
+ }
+ // Whoops! Someone trampled the array (probably due to not protecting
+ // their access with a lock). Our cache is corrupt; report and give up.
+ Slog.wtf(TAG, "Found corrupt ArraySet cache: [0]=" + array[0]
+ + " [1]=" + array[1]);
+ sTwiceBaseCache = null;
+ sTwiceBaseCacheSize = 0;
+ }
+ }
+ } else if (size == BASE_SIZE) {
+ synchronized (sBaseCacheLock) {
+ if (sBaseCache != null) {
+ final Object[] array = sBaseCache;
+ try {
+ mArray = array;
+ sBaseCache = (Object[]) array[0];
+ mHashes = (int[]) array[1];
+ if (mHashes != null) {
+ array[0] = array[1] = null;
+ sBaseCacheSize--;
+ if (DEBUG) {
+ Log.d(TAG, "Retrieving 1x cache " + mHashes + " now have "
+ + sBaseCacheSize + " entries");
+ }
+ return;
+ }
+ } catch (ClassCastException e) {
+ }
+ // Whoops! Someone trampled the array (probably due to not protecting
+ // their access with a lock). Our cache is corrupt; report and give up.
+ Slog.wtf(TAG, "Found corrupt ArraySet cache: [0]=" + array[0]
+ + " [1]=" + array[1]);
+ sBaseCache = null;
+ sBaseCacheSize = 0;
+ }
+ }
+ }
+
+ mHashes = new int[size];
+ mArray = new Object[size];
+ }
+
+ /**
+ * Make sure <b>NOT</b> to call this method with arrays that can still be modified. In other
+ * words, don't pass mHashes or mArray in directly.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Allocations are an implementation detail.
+ private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
+ if (hashes.length == (BASE_SIZE * 2)) {
+ synchronized (sTwiceBaseCacheLock) {
+ if (sTwiceBaseCacheSize < CACHE_SIZE) {
+ array[0] = sTwiceBaseCache;
+ array[1] = hashes;
+ for (int i = size - 1; i >= 2; i--) {
+ array[i] = null;
+ }
+ sTwiceBaseCache = array;
+ sTwiceBaseCacheSize++;
+ if (DEBUG) {
+ Log.d(TAG, "Storing 2x cache " + array + " now have " + sTwiceBaseCacheSize
+ + " entries");
+ }
+ }
+ }
+ } else if (hashes.length == BASE_SIZE) {
+ synchronized (sBaseCacheLock) {
+ if (sBaseCacheSize < CACHE_SIZE) {
+ array[0] = sBaseCache;
+ array[1] = hashes;
+ for (int i = size - 1; i >= 2; i--) {
+ array[i] = null;
+ }
+ sBaseCache = array;
+ sBaseCacheSize++;
+ if (DEBUG) {
+ Log.d(TAG, "Storing 1x cache " + array + " now have "
+ + sBaseCacheSize + " entries");
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Create a new empty ArraySet. The default capacity of an array map is 0, and
+ * will grow once items are added to it.
+ */
+ public ArraySet() {
+ this(0, false);
+ }
+
+ /**
+ * Create a new ArraySet with a given initial capacity.
+ */
+ public ArraySet(int capacity) {
+ this(capacity, false);
+ }
+
+ /** {@hide} */
+ public ArraySet(int capacity, boolean identityHashCode) {
+ mIdentityHashCode = identityHashCode;
+ if (capacity == 0) {
+ mHashes = EmptyArray.INT;
+ mArray = EmptyArray.OBJECT;
+ } else {
+ allocArrays(capacity);
+ }
+ mSize = 0;
+ }
+
+ /**
+ * Create a new ArraySet with the mappings from the given ArraySet.
+ */
+ public ArraySet(ArraySet<E> set) {
+ this();
+ if (set != null) {
+ addAll(set);
+ }
+ }
+
+ /**
+ * Create a new ArraySet with items from the given collection.
+ */
+ public ArraySet(Collection<? extends E> set) {
+ this();
+ if (set != null) {
+ addAll(set);
+ }
+ }
+
+ /**
+ * Create a new ArraySet with items from the given array
+ */
+ public ArraySet(@Nullable E[] array) {
+ this();
+ if (array != null) {
+ for (E value : array) {
+ add(value);
+ }
+ }
+ }
+
+ /**
+ * Make the array map empty. All storage is released.
+ */
+ @Override
+ public void clear() {
+ if (mSize != 0) {
+ final int[] ohashes = mHashes;
+ final Object[] oarray = mArray;
+ final int osize = mSize;
+ mHashes = EmptyArray.INT;
+ mArray = EmptyArray.OBJECT;
+ mSize = 0;
+ freeArrays(ohashes, oarray, osize);
+ }
+ if (mSize != 0) {
+ throw new ConcurrentModificationException();
+ }
+ }
+
+ /**
+ * Ensure the array map can hold at least <var>minimumCapacity</var>
+ * items.
+ */
+ public void ensureCapacity(int minimumCapacity) {
+ final int oSize = mSize;
+ if (mHashes.length < minimumCapacity) {
+ final int[] ohashes = mHashes;
+ final Object[] oarray = mArray;
+ allocArrays(minimumCapacity);
+ if (mSize > 0) {
+ System.arraycopy(ohashes, 0, mHashes, 0, mSize);
+ System.arraycopy(oarray, 0, mArray, 0, mSize);
+ }
+ freeArrays(ohashes, oarray, mSize);
+ }
+ if (mSize != oSize) {
+ throw new ConcurrentModificationException();
+ }
+ }
+
+ /**
+ * Check whether a value exists in the set.
+ *
+ * @param key The value to search for.
+ * @return Returns true if the value exists, else false.
+ */
+ @Override
+ public boolean contains(Object key) {
+ return indexOf(key) >= 0;
+ }
+
+ /**
+ * Returns the index of a value in the set.
+ *
+ * @param key The value to search for.
+ * @return Returns the index of the value if it exists, else a negative integer.
+ */
+ public int indexOf(Object key) {
+ return key == null ? indexOfNull()
+ : indexOf(key, mIdentityHashCode ? System.identityHashCode(key) : key.hashCode());
+ }
+
+ /**
+ * Return the value at the given index in the array.
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ *
+ * @param index The desired index, must be between 0 and {@link #size()}-1.
+ * @return Returns the value stored at the given index.
+ */
+ public E valueAt(int index) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ return valueAtUnchecked(index);
+ }
+
+ /**
+ * Returns the value at the given index in the array without checking that the index is within
+ * bounds. This allows testing values at the end of the internal array, outside of the
+ * [0, mSize) bounds.
+ *
+ * @hide
+ */
+ @TestApi
+ public E valueAtUnchecked(int index) {
+ return (E) mArray[index];
+ }
+
+ /**
+ * Return true if the array map contains no items.
+ */
+ @Override
+ public boolean isEmpty() {
+ return mSize <= 0;
+ }
+
+ /**
+ * Adds the specified object to this set. The set is not modified if it
+ * already contains the object.
+ *
+ * @param value the object to add.
+ * @return {@code true} if this set is modified, {@code false} otherwise.
+ */
+ @Override
+ public boolean add(E value) {
+ final int oSize = mSize;
+ final int hash;
+ int index;
+ if (value == null) {
+ hash = 0;
+ index = indexOfNull();
+ } else {
+ hash = mIdentityHashCode ? System.identityHashCode(value) : value.hashCode();
+ index = indexOf(value, hash);
+ }
+ if (index >= 0) {
+ return false;
+ }
+
+ index = ~index;
+ if (oSize >= mHashes.length) {
+ final int n = oSize >= (BASE_SIZE * 2) ? (oSize + (oSize >> 1))
+ : (oSize >= BASE_SIZE ? (BASE_SIZE * 2) : BASE_SIZE);
+
+ if (DEBUG) Log.d(TAG, "add: grow from " + mHashes.length + " to " + n);
+
+ final int[] ohashes = mHashes;
+ final Object[] oarray = mArray;
+ allocArrays(n);
+
+ if (oSize != mSize) {
+ throw new ConcurrentModificationException();
+ }
+
+ if (mHashes.length > 0) {
+ if (DEBUG) Log.d(TAG, "add: copy 0-" + oSize + " to 0");
+ System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
+ System.arraycopy(oarray, 0, mArray, 0, oarray.length);
+ }
+
+ freeArrays(ohashes, oarray, oSize);
+ }
+
+ if (index < oSize) {
+ if (DEBUG) {
+ Log.d(TAG, "add: move " + index + "-" + (oSize - index) + " to " + (index + 1));
+ }
+ System.arraycopy(mHashes, index, mHashes, index + 1, oSize - index);
+ System.arraycopy(mArray, index, mArray, index + 1, oSize - index);
+ }
+
+ if (oSize != mSize || index >= mHashes.length) {
+ throw new ConcurrentModificationException();
+ }
+
+ mHashes[index] = hash;
+ mArray[index] = value;
+ mSize++;
+ return true;
+ }
+
+ /**
+ * Special fast path for appending items to the end of the array without validation.
+ * The array must already be large enough to contain the item.
+ * @hide
+ */
+ public void append(E value) {
+ final int oSize = mSize;
+ final int index = mSize;
+ final int hash = value == null ? 0
+ : (mIdentityHashCode ? System.identityHashCode(value) : value.hashCode());
+ if (index >= mHashes.length) {
+ throw new IllegalStateException("Array is full");
+ }
+ if (index > 0 && mHashes[index - 1] > hash) {
+ // Cannot optimize since it would break the sorted order - fallback to add()
+ if (DEBUG) {
+ RuntimeException e = new RuntimeException("here");
+ e.fillInStackTrace();
+ Log.w(TAG, "New hash " + hash
+ + " is before end of array hash " + mHashes[index - 1]
+ + " at index " + index, e);
+ }
+ add(value);
+ return;
+ }
+
+ if (oSize != mSize) {
+ throw new ConcurrentModificationException();
+ }
+
+ mSize = index + 1;
+ mHashes[index] = hash;
+ mArray[index] = value;
+ }
+
+ /**
+ * Perform a {@link #add(Object)} of all values in <var>array</var>
+ * @param array The array whose contents are to be retrieved.
+ */
+ public void addAll(ArraySet<? extends E> array) {
+ final int N = array.mSize;
+ ensureCapacity(mSize + N);
+ if (mSize == 0) {
+ if (N > 0) {
+ System.arraycopy(array.mHashes, 0, mHashes, 0, N);
+ System.arraycopy(array.mArray, 0, mArray, 0, N);
+ if (0 != mSize) {
+ throw new ConcurrentModificationException();
+ }
+ mSize = N;
+ }
+ } else {
+ for (int i = 0; i < N; i++) {
+ add(array.valueAt(i));
+ }
+ }
+ }
+
+ /**
+ * Removes the specified object from this set.
+ *
+ * @param object the object to remove.
+ * @return {@code true} if this set was modified, {@code false} otherwise.
+ */
+ @Override
+ public boolean remove(Object object) {
+ final int index = indexOf(object);
+ if (index >= 0) {
+ removeAt(index);
+ return true;
+ }
+ return false;
+ }
+
+ /** Returns true if the array size should be decreased. */
+ private boolean shouldShrink() {
+ return mHashes.length > (BASE_SIZE * 2) && mSize < mHashes.length / 3;
+ }
+
+ /**
+ * Returns the new size the array should have. Is only valid if {@link #shouldShrink} returns
+ * true.
+ */
+ private int getNewShrunkenSize() {
+ // We don't allow it to shrink smaller than (BASE_SIZE*2) to avoid flapping between that
+ // and BASE_SIZE.
+ return mSize > (BASE_SIZE * 2) ? (mSize + (mSize >> 1)) : (BASE_SIZE * 2);
+ }
+
+ /**
+ * Remove the key/value mapping at the given index.
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ *
+ * @param index The desired index, must be between 0 and {@link #size()}-1.
+ * @return Returns the value that was stored at this index.
+ */
+ public E removeAt(int index) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ final int oSize = mSize;
+ final Object old = mArray[index];
+ if (oSize <= 1) {
+ // Now empty.
+ if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to 0");
+ clear();
+ } else {
+ final int nSize = oSize - 1;
+ if (shouldShrink()) {
+ // Shrunk enough to reduce size of arrays.
+ final int n = getNewShrunkenSize();
+
+ if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to " + n);
+
+ final int[] ohashes = mHashes;
+ final Object[] oarray = mArray;
+ allocArrays(n);
+
+ if (index > 0) {
+ if (DEBUG) Log.d(TAG, "remove: copy from 0-" + index + " to 0");
+ System.arraycopy(ohashes, 0, mHashes, 0, index);
+ System.arraycopy(oarray, 0, mArray, 0, index);
+ }
+ if (index < nSize) {
+ if (DEBUG) {
+ Log.d(TAG, "remove: copy from " + (index + 1) + "-" + nSize
+ + " to " + index);
+ }
+ System.arraycopy(ohashes, index + 1, mHashes, index, nSize - index);
+ System.arraycopy(oarray, index + 1, mArray, index, nSize - index);
+ }
+ } else {
+ if (index < nSize) {
+ if (DEBUG) {
+ Log.d(TAG, "remove: move " + (index + 1) + "-" + nSize + " to " + index);
+ }
+ System.arraycopy(mHashes, index + 1, mHashes, index, nSize - index);
+ System.arraycopy(mArray, index + 1, mArray, index, nSize - index);
+ }
+ mArray[nSize] = null;
+ }
+ if (oSize != mSize) {
+ throw new ConcurrentModificationException();
+ }
+ mSize = nSize;
+ }
+ return (E) old;
+ }
+
+ /**
+ * Perform a {@link #remove(Object)} of all values in <var>array</var>
+ * @param array The array whose contents are to be removed.
+ */
+ public boolean removeAll(ArraySet<? extends E> array) {
+ // TODO: If array is sufficiently large, a marking approach might be beneficial. In a first
+ // pass, use the property that the sets are sorted by hash to make this linear passes
+ // (except for hash collisions, which means worst case still n*m), then do one
+ // collection pass into a new array. This avoids binary searches and excessive memcpy.
+ final int N = array.mSize;
+
+ // Note: ArraySet does not make thread-safety guarantees. So instead of OR-ing together all
+ // the single results, compare size before and after.
+ final int originalSize = mSize;
+ for (int i = 0; i < N; i++) {
+ remove(array.valueAt(i));
+ }
+ return originalSize != mSize;
+ }
+
+ /**
+ * Removes all values that satisfy the predicate. This implementation avoids using the
+ * {@link #iterator()}.
+ *
+ * @param filter A predicate which returns true for elements to be removed
+ */
+ @Override
+ public boolean removeIf(Predicate<? super E> filter) {
+ if (mSize == 0) {
+ return false;
+ }
+
+ // Intentionally not using removeAt() to avoid unnecessary intermediate resizing.
+
+ int replaceIndex = 0;
+ int numRemoved = 0;
+ for (int i = 0; i < mSize; ++i) {
+ if (filter.test((E) mArray[i])) {
+ numRemoved++;
+ } else {
+ if (replaceIndex != i) {
+ mArray[replaceIndex] = mArray[i];
+ mHashes[replaceIndex] = mHashes[i];
+ }
+ replaceIndex++;
+ }
+ }
+
+ if (numRemoved == 0) {
+ return false;
+ } else if (numRemoved == mSize) {
+ clear();
+ return true;
+ }
+
+ mSize -= numRemoved;
+ if (shouldShrink()) {
+ // Shrunk enough to reduce size of arrays.
+ final int n = getNewShrunkenSize();
+ final int[] ohashes = mHashes;
+ final Object[] oarray = mArray;
+ allocArrays(n);
+
+ System.arraycopy(ohashes, 0, mHashes, 0, mSize);
+ System.arraycopy(oarray, 0, mArray, 0, mSize);
+ } else {
+ // Null out values at the end of the array. Not doing it in the loop above to avoid
+ // writing twice to the same index or writing unnecessarily if the array would have been
+ // discarded anyway.
+ for (int i = mSize; i < mArray.length; ++i) {
+ mArray[i] = null;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Return the number of items in this array map.
+ */
+ @Override
+ public int size() {
+ return mSize;
+ }
+
+ @Override
+ public Object[] toArray() {
+ Object[] result = new Object[mSize];
+ System.arraycopy(mArray, 0, result, 0, mSize);
+ return result;
+ }
+
+ @Override
+ public <T> T[] toArray(T[] array) {
+ if (array.length < mSize) {
+ @SuppressWarnings("unchecked") T[] newArray =
+ (T[]) Array.newInstance(array.getClass().getComponentType(), mSize);
+ array = newArray;
+ }
+ System.arraycopy(mArray, 0, array, 0, mSize);
+ if (array.length > mSize) {
+ array[mSize] = null;
+ }
+ return array;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>This implementation returns false if the object is not a set, or
+ * if the sets have different sizes. Otherwise, for each value in this
+ * set, it checks to make sure the value also exists in the other set.
+ * If any value doesn't exist, the method returns false; otherwise, it
+ * returns true.
+ */
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object instanceof Set) {
+ Set<?> set = (Set<?>) object;
+ if (size() != set.size()) {
+ return false;
+ }
+
+ try {
+ for (int i = 0; i < mSize; i++) {
+ E mine = valueAt(i);
+ if (!set.contains(mine)) {
+ return false;
+ }
+ }
+ } catch (NullPointerException ignored) {
+ return false;
+ } catch (ClassCastException ignored) {
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ final int[] hashes = mHashes;
+ int result = 0;
+ for (int i = 0, s = mSize; i < s; i++) {
+ result += hashes[i];
+ }
+ return result;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>This implementation composes a string by iterating over its values. If
+ * this set contains itself as a value, the string "(this Set)"
+ * will appear in its place.
+ */
+ @Override
+ public String toString() {
+ if (isEmpty()) {
+ return "{}";
+ }
+
+ StringBuilder buffer = new StringBuilder(mSize * 14);
+ buffer.append('{');
+ for (int i = 0; i < mSize; i++) {
+ if (i > 0) {
+ buffer.append(", ");
+ }
+ Object value = valueAt(i);
+ if (value != this) {
+ buffer.append(value);
+ } else {
+ buffer.append("(this Set)");
+ }
+ }
+ buffer.append('}');
+ return buffer.toString();
+ }
+
+ // ------------------------------------------------------------------------
+ // Interop with traditional Java containers. Not as efficient as using
+ // specialized collection APIs.
+ // ------------------------------------------------------------------------
+
+ private MapCollections<E, E> getCollection() {
+ if (mCollections == null) {
+ mCollections = new MapCollections<E, E>() {
+ @Override
+ protected int colGetSize() {
+ return mSize;
+ }
+
+ @Override
+ protected Object colGetEntry(int index, int offset) {
+ return mArray[index];
+ }
+
+ @Override
+ protected int colIndexOfKey(Object key) {
+ return indexOf(key);
+ }
+
+ @Override
+ protected int colIndexOfValue(Object value) {
+ return indexOf(value);
+ }
+
+ @Override
+ protected Map<E, E> colGetMap() {
+ throw new UnsupportedOperationException("not a map");
+ }
+
+ @Override
+ protected void colPut(E key, E value) {
+ add(key);
+ }
+
+ @Override
+ protected E colSetValue(int index, E value) {
+ throw new UnsupportedOperationException("not a map");
+ }
+
+ @Override
+ protected void colRemoveAt(int index) {
+ removeAt(index);
+ }
+
+ @Override
+ protected void colClear() {
+ clear();
+ }
+ };
+ }
+ return mCollections;
+ }
+
+ /**
+ * Return an {@link java.util.Iterator} over all values in the set.
+ *
+ * <p><b>Note:</b> this is a fairly inefficient way to access the array contents, it
+ * requires generating a number of temporary objects and allocates additional state
+ * information associated with the container that will remain for the life of the container.</p>
+ */
+ @Override
+ public Iterator<E> iterator() {
+ return getCollection().getKeySet().iterator();
+ }
+
+ /**
+ * Determine if the array set contains all of the values in the given collection.
+ * @param collection The collection whose contents are to be checked against.
+ * @return Returns true if this array set contains a value for every entry
+ * in <var>collection</var>, else returns false.
+ */
+ @Override
+ public boolean containsAll(Collection<?> collection) {
+ Iterator<?> it = collection.iterator();
+ while (it.hasNext()) {
+ if (!contains(it.next())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Perform an {@link #add(Object)} of all values in <var>collection</var>
+ * @param collection The collection whose contents are to be retrieved.
+ */
+ @Override
+ public boolean addAll(Collection<? extends E> collection) {
+ ensureCapacity(mSize + collection.size());
+ boolean added = false;
+ for (E value : collection) {
+ added |= add(value);
+ }
+ return added;
+ }
+
+ /**
+ * Remove all values in the array set that exist in the given collection.
+ * @param collection The collection whose contents are to be used to remove values.
+ * @return Returns true if any values were removed from the array set, else false.
+ */
+ @Override
+ public boolean removeAll(Collection<?> collection) {
+ boolean removed = false;
+ for (Object value : collection) {
+ removed |= remove(value);
+ }
+ return removed;
+ }
+
+ /**
+ * Remove all values in the array set that do <b>not</b> exist in the given collection.
+ * @param collection The collection whose contents are to be used to determine which
+ * values to keep.
+ * @return Returns true if any values were removed from the array set, else false.
+ */
+ @Override
+ public boolean retainAll(Collection<?> collection) {
+ boolean removed = false;
+ for (int i = mSize - 1; i >= 0; i--) {
+ if (!collection.contains(mArray[i])) {
+ removeAt(i);
+ removed = true;
+ }
+ }
+ return removed;
+ }
+}
diff --git a/android/util/ArraySetPerfTest.java b/android/util/ArraySetPerfTest.java
new file mode 100644
index 0000000..b24bf42
--- /dev/null
+++ b/android/util/ArraySetPerfTest.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.function.Predicate;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class ArraySetPerfTest {
+ private static final int NUM_ITERATIONS = 100;
+ private static final int SET_SIZE_SMALL = 10;
+ private static final int SET_SIZE_LARGE = 50;
+
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ @Test
+ public void testValueAt_InBounds() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ ArraySet<Integer> set = new ArraySet<>();
+ set.add(0);
+ while (state.keepRunning()) {
+ for (int i = 0; i < NUM_ITERATIONS; ++i) {
+ set.valueAt(0);
+ }
+ }
+ }
+
+ @Test
+ public void testValueAt_OutOfBounds_Negative() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ ArraySet<Integer> set = new ArraySet<>();
+ while (state.keepRunning()) {
+ for (int i = 0; i < NUM_ITERATIONS; ++i) {
+ try {
+ set.valueAt(-1);
+ } catch (ArrayIndexOutOfBoundsException expected) {
+ // expected
+ }
+ }
+ }
+ }
+
+ /**
+ * Tests the case where ArraySet could index into its array even though the index is out of
+ * bounds.
+ */
+ @Test
+ public void testValueAt_OutOfBounds_EdgeCase() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ ArraySet<Integer> set = new ArraySet<>();
+ set.add(0);
+ while (state.keepRunning()) {
+ for (int i = 0; i < NUM_ITERATIONS; ++i) {
+ try {
+ set.valueAt(1);
+ } catch (ArrayIndexOutOfBoundsException expected) {
+ // expected
+ }
+ }
+ }
+ }
+
+ /**
+ * This is the same code as testRemoveIf_Small_* without the removeIf in order to measure
+ * the performance of the rest of the code in the loop.
+ */
+ @Test
+ public void testRemoveIf_Small_Base() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ Predicate<Integer> predicate = (i) -> i % 2 == 0;
+ while (state.keepRunning()) {
+ for (int i = 0; i < NUM_ITERATIONS; ++i) {
+ ArraySet<Integer> set = new ArraySet<>();
+ for (int j = 0; j < SET_SIZE_SMALL; ++j) {
+ set.add(j);
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testRemoveIf_Small_RemoveNothing() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ Predicate<Integer> predicate = (i) -> false;
+ while (state.keepRunning()) {
+ for (int i = 0; i < NUM_ITERATIONS; ++i) {
+ ArraySet<Integer> set = new ArraySet<>();
+ for (int j = 0; j < SET_SIZE_SMALL; ++j) {
+ set.add(j);
+ }
+ set.removeIf(predicate);
+ }
+ }
+ }
+
+ @Test
+ public void testRemoveIf_Small_RemoveAll() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ Predicate<Integer> predicate = (i) -> true;
+ while (state.keepRunning()) {
+ for (int i = 0; i < NUM_ITERATIONS; ++i) {
+ ArraySet<Integer> set = new ArraySet<>();
+ for (int j = 0; j < SET_SIZE_SMALL; j++) {
+ set.add(j);
+ }
+ set.removeIf(predicate);
+ }
+ }
+ }
+
+ @Test
+ public void testRemoveIf_Small_RemoveHalf() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ Predicate<Integer> predicate = (i) -> i % 2 == 0;
+ while (state.keepRunning()) {
+ for (int i = 0; i < NUM_ITERATIONS; ++i) {
+ ArraySet<Integer> set = new ArraySet<>();
+ for (int j = 0; j < SET_SIZE_SMALL; ++j) {
+ set.add(j);
+ }
+ set.removeIf(predicate);
+ }
+ }
+ }
+
+ /**
+ * This is the same code as testRemoveIf_Large_* without the removeIf in order to measure
+ * the performance of the rest of the code in the loop.
+ */
+ @Test
+ public void testRemoveIf_Large_Base() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ Predicate<Integer> predicate = (i) -> i % 2 == 0;
+ while (state.keepRunning()) {
+ for (int i = 0; i < NUM_ITERATIONS; ++i) {
+ ArraySet<Integer> set = new ArraySet<>();
+ for (int j = 0; j < SET_SIZE_LARGE; ++j) {
+ set.add(j);
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testRemoveIf_Large_RemoveNothing() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ Predicate<Integer> predicate = (i) -> false;
+ while (state.keepRunning()) {
+ for (int i = 0; i < NUM_ITERATIONS; ++i) {
+ ArraySet<Integer> set = new ArraySet<>();
+ for (int j = 0; j < SET_SIZE_LARGE; ++j) {
+ set.add(j);
+ }
+ set.removeIf(predicate);
+ }
+ }
+ }
+
+ @Test
+ public void testRemoveIf_Large_RemoveAll() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ Predicate<Integer> predicate = (i) -> true;
+ while (state.keepRunning()) {
+ for (int i = 0; i < NUM_ITERATIONS; ++i) {
+ ArraySet<Integer> set = new ArraySet<>();
+ for (int j = 0; j < SET_SIZE_LARGE; ++j) {
+ set.add(j);
+ }
+ set.removeIf(predicate);
+ }
+ }
+ }
+
+ @Test
+ public void testRemoveIf_Large_RemoveHalf() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ Predicate<Integer> predicate = (i) -> i % 2 == 0;
+ while (state.keepRunning()) {
+ for (int i = 0; i < NUM_ITERATIONS; ++i) {
+ ArraySet<Integer> set = new ArraySet<>();
+ for (int j = 0; j < SET_SIZE_LARGE; ++j) {
+ set.add(j);
+ }
+ set.removeIf(predicate);
+ }
+ }
+ }
+}
diff --git a/android/util/AtomicFile.java b/android/util/AtomicFile.java
new file mode 100644
index 0000000..cf7ed9b
--- /dev/null
+++ b/android/util/AtomicFile.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.os.FileUtils;
+import android.os.SystemClock;
+
+import libcore.io.IoUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.function.Consumer;
+
+/**
+ * Helper class for performing atomic operations on a file by creating a
+ * backup file until a write has successfully completed. If you need this
+ * on older versions of the platform you can use
+ * {@link android.support.v4.util.AtomicFile} in the v4 support library.
+ * <p>
+ * Atomic file guarantees file integrity by ensuring that a file has
+ * been completely written and sync'd to disk before removing its backup.
+ * As long as the backup file exists, the original file is considered
+ * to be invalid (left over from a previous attempt to write the file).
+ * </p><p>
+ * Atomic file does not confer any file locking semantics.
+ * Do not use this class when the file may be accessed or modified concurrently
+ * by multiple threads or processes. The caller is responsible for ensuring
+ * appropriate mutual exclusion invariants whenever it accesses the file.
+ * </p>
+ */
+public class AtomicFile {
+ private final File mBaseName;
+ private final File mBackupName;
+ private final String mCommitTag;
+ private long mStartTime;
+
+ /**
+ * Create a new AtomicFile for a file located at the given File path.
+ * The secondary backup file will be the same file path with ".bak" appended.
+ */
+ public AtomicFile(File baseName) {
+ this(baseName, null);
+ }
+
+ /**
+ * @hide Internal constructor that also allows you to have the class
+ * automatically log commit events.
+ */
+ public AtomicFile(File baseName, String commitTag) {
+ mBaseName = baseName;
+ mBackupName = new File(baseName.getPath() + ".bak");
+ mCommitTag = commitTag;
+ }
+
+ /**
+ * Return the path to the base file. You should not generally use this,
+ * as the data at that path may not be valid.
+ */
+ public File getBaseFile() {
+ return mBaseName;
+ }
+
+ /**
+ * Delete the atomic file. This deletes both the base and backup files.
+ */
+ public void delete() {
+ mBaseName.delete();
+ mBackupName.delete();
+ }
+
+ /**
+ * Start a new write operation on the file. This returns a FileOutputStream
+ * to which you can write the new file data. The existing file is replaced
+ * with the new data. You <em>must not</em> directly close the given
+ * FileOutputStream; instead call either {@link #finishWrite(FileOutputStream)}
+ * or {@link #failWrite(FileOutputStream)}.
+ *
+ * <p>Note that if another thread is currently performing
+ * a write, this will simply replace whatever that thread is writing
+ * with the new file being written by this thread, and when the other
+ * thread finishes the write the new write operation will no longer be
+ * safe (or will be lost). You must do your own threading protection for
+ * access to AtomicFile.
+ */
+ public FileOutputStream startWrite() throws IOException {
+ return startWrite(mCommitTag != null ? SystemClock.uptimeMillis() : 0);
+ }
+
+ /**
+ * @hide Internal version of {@link #startWrite()} that allows you to specify an earlier
+ * start time of the operation to adjust how the commit is logged.
+ * @param startTime The effective start time of the operation, in the time
+ * base of {@link SystemClock#uptimeMillis()}.
+ */
+ public FileOutputStream startWrite(long startTime) throws IOException {
+ mStartTime = startTime;
+
+ // Rename the current file so it may be used as a backup during the next read
+ if (mBaseName.exists()) {
+ if (!mBackupName.exists()) {
+ if (!mBaseName.renameTo(mBackupName)) {
+ Log.w("AtomicFile", "Couldn't rename file " + mBaseName
+ + " to backup file " + mBackupName);
+ }
+ } else {
+ mBaseName.delete();
+ }
+ }
+ FileOutputStream str = null;
+ try {
+ str = new FileOutputStream(mBaseName);
+ } catch (FileNotFoundException e) {
+ File parent = mBaseName.getParentFile();
+ if (!parent.mkdirs()) {
+ throw new IOException("Couldn't create directory " + mBaseName);
+ }
+ FileUtils.setPermissions(
+ parent.getPath(),
+ FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
+ -1, -1);
+ try {
+ str = new FileOutputStream(mBaseName);
+ } catch (FileNotFoundException e2) {
+ throw new IOException("Couldn't create " + mBaseName);
+ }
+ }
+ return str;
+ }
+
+ /**
+ * Call when you have successfully finished writing to the stream
+ * returned by {@link #startWrite()}. This will close, sync, and
+ * commit the new data. The next attempt to read the atomic file
+ * will return the new file stream.
+ */
+ public void finishWrite(FileOutputStream str) {
+ if (str != null) {
+ FileUtils.sync(str);
+ try {
+ str.close();
+ mBackupName.delete();
+ } catch (IOException e) {
+ Log.w("AtomicFile", "finishWrite: Got exception:", e);
+ }
+ if (mCommitTag != null) {
+ com.android.internal.logging.EventLogTags.writeCommitSysConfigFile(
+ mCommitTag, SystemClock.uptimeMillis() - mStartTime);
+ }
+ }
+ }
+
+ /**
+ * Call when you have failed for some reason at writing to the stream
+ * returned by {@link #startWrite()}. This will close the current
+ * write stream, and roll back to the previous state of the file.
+ */
+ public void failWrite(FileOutputStream str) {
+ if (str != null) {
+ FileUtils.sync(str);
+ try {
+ str.close();
+ mBaseName.delete();
+ mBackupName.renameTo(mBaseName);
+ } catch (IOException e) {
+ Log.w("AtomicFile", "failWrite: Got exception:", e);
+ }
+ }
+ }
+
+ /** @hide
+ * @deprecated This is not safe.
+ */
+ @Deprecated public void truncate() throws IOException {
+ try {
+ FileOutputStream fos = new FileOutputStream(mBaseName);
+ FileUtils.sync(fos);
+ fos.close();
+ } catch (FileNotFoundException e) {
+ throw new IOException("Couldn't append " + mBaseName);
+ } catch (IOException e) {
+ }
+ }
+
+ /** @hide
+ * @deprecated This is not safe.
+ */
+ @Deprecated public FileOutputStream openAppend() throws IOException {
+ try {
+ return new FileOutputStream(mBaseName, true);
+ } catch (FileNotFoundException e) {
+ throw new IOException("Couldn't append " + mBaseName);
+ }
+ }
+
+ /**
+ * Open the atomic file for reading. If there previously was an
+ * incomplete write, this will roll back to the last good data before
+ * opening for read. You should call close() on the FileInputStream when
+ * you are done reading from it.
+ *
+ * <p>Note that if another thread is currently performing
+ * a write, this will incorrectly consider it to be in the state of a bad
+ * write and roll back, causing the new data currently being written to
+ * be dropped. You must do your own threading protection for access to
+ * AtomicFile.
+ */
+ public FileInputStream openRead() throws FileNotFoundException {
+ if (mBackupName.exists()) {
+ mBaseName.delete();
+ mBackupName.renameTo(mBaseName);
+ }
+ return new FileInputStream(mBaseName);
+ }
+
+ /**
+ * @hide
+ * Checks if the original or backup file exists.
+ * @return whether the original or backup file exists.
+ */
+ public boolean exists() {
+ return mBaseName.exists() || mBackupName.exists();
+ }
+
+ /**
+ * Gets the last modified time of the atomic file.
+ * {@hide}
+ *
+ * @return last modified time in milliseconds since epoch. Returns zero if
+ * the file does not exist or an I/O error is encountered.
+ */
+ public long getLastModifiedTime() {
+ if (mBackupName.exists()) {
+ return mBackupName.lastModified();
+ }
+ return mBaseName.lastModified();
+ }
+
+ /**
+ * A convenience for {@link #openRead()} that also reads all of the
+ * file contents into a byte array which is returned.
+ */
+ public byte[] readFully() throws IOException {
+ FileInputStream stream = openRead();
+ try {
+ int pos = 0;
+ int avail = stream.available();
+ byte[] data = new byte[avail];
+ while (true) {
+ int amt = stream.read(data, pos, data.length-pos);
+ //Log.i("foo", "Read " + amt + " bytes at " + pos
+ // + " of avail " + data.length);
+ if (amt <= 0) {
+ //Log.i("foo", "**** FINISHED READING: pos=" + pos
+ // + " len=" + data.length);
+ return data;
+ }
+ pos += amt;
+ avail = stream.available();
+ if (avail > data.length-pos) {
+ byte[] newData = new byte[pos+avail];
+ System.arraycopy(data, 0, newData, 0, pos);
+ data = newData;
+ }
+ }
+ } finally {
+ stream.close();
+ }
+ }
+
+ /** @hide */
+ public void write(Consumer<FileOutputStream> writeContent) {
+ FileOutputStream out = null;
+ try {
+ out = startWrite();
+ writeContent.accept(out);
+ finishWrite(out);
+ } catch (Throwable t) {
+ failWrite(out);
+ throw ExceptionUtils.propagate(t);
+ } finally {
+ IoUtils.closeQuietly(out);
+ }
+ }
+}
diff --git a/android/util/AttributeSet.java b/android/util/AttributeSet.java
new file mode 100644
index 0000000..7f327c7
--- /dev/null
+++ b/android/util/AttributeSet.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * A collection of attributes, as found associated with a tag in an XML
+ * document. Often you will not want to use this interface directly, instead
+ * passing it to {@link android.content.res.Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int)
+ * Resources.Theme.obtainStyledAttributes()}
+ * which will take care of parsing the attributes for you. In particular,
+ * the Resources API will convert resource references (attribute values such as
+ * "@string/my_label" in the original XML) to the desired type
+ * for you; if you use AttributeSet directly then you will need to manually
+ * check for resource references
+ * (with {@link #getAttributeResourceValue(int, int)}) and do the resource
+ * lookup yourself if needed. Direct use of AttributeSet also prevents the
+ * application of themes and styles when retrieving attribute values.
+ *
+ * <p>This interface provides an efficient mechanism for retrieving
+ * data from compiled XML files, which can be retrieved for a particular
+ * XmlPullParser through {@link Xml#asAttributeSet
+ * Xml.asAttributeSet()}. Normally this will return an implementation
+ * of the interface that works on top of a generic XmlPullParser, however it
+ * is more useful in conjunction with compiled XML resources:
+ *
+ * <pre>
+ * XmlPullParser parser = resources.getXml(myResource);
+ * AttributeSet attributes = Xml.asAttributeSet(parser);</pre>
+ *
+ * <p>The implementation returned here, unlike using
+ * the implementation on top of a generic XmlPullParser,
+ * is highly optimized by retrieving pre-computed information that was
+ * generated by aapt when compiling your resources. For example,
+ * the {@link #getAttributeFloatValue(int, float)} method returns a floating
+ * point number previous stored in the compiled resource instead of parsing
+ * at runtime the string originally in the XML file.
+ *
+ * <p>This interface also provides additional information contained in the
+ * compiled XML resource that is not available in a normal XML file, such
+ * as {@link #getAttributeNameResource(int)} which returns the resource
+ * identifier associated with a particular XML attribute name.
+ *
+ * @see XmlPullParser
+ */
+public interface AttributeSet {
+ /**
+ * Returns the number of attributes available in the set.
+ *
+ * <p>See also {@link XmlPullParser#getAttributeCount XmlPullParser.getAttributeCount()},
+ * which this method corresponds to when parsing a compiled XML file.</p>
+ *
+ * @return A positive integer, or 0 if the set is empty.
+ */
+ public int getAttributeCount();
+
+ /**
+ * Returns the namespace of the specified attribute.
+ *
+ * <p>See also {@link XmlPullParser#getAttributeNamespace XmlPullParser.getAttributeNamespace()},
+ * which this method corresponds to when parsing a compiled XML file.</p>
+ *
+ * @param index Index of the desired attribute, 0...count-1.
+ *
+ * @return A String containing the namespace of the attribute, or null if th
+ * attribute cannot be found.
+ */
+ default String getAttributeNamespace (int index) {
+ // This is a new method since the first interface definition, so add stub impl.
+ return null;
+ }
+
+ /**
+ * Returns the name of the specified attribute.
+ *
+ * <p>See also {@link XmlPullParser#getAttributeName XmlPullParser.getAttributeName()},
+ * which this method corresponds to when parsing a compiled XML file.</p>
+ *
+ * @param index Index of the desired attribute, 0...count-1.
+ *
+ * @return A String containing the name of the attribute, or null if the
+ * attribute cannot be found.
+ */
+ public String getAttributeName(int index);
+
+ /**
+ * Returns the value of the specified attribute as a string representation.
+ *
+ * @param index Index of the desired attribute, 0...count-1.
+ *
+ * @return A String containing the value of the attribute, or null if the
+ * attribute cannot be found.
+ */
+ public String getAttributeValue(int index);
+
+ /**
+ * Returns the value of the specified attribute as a string representation.
+ * The lookup is performed using the attribute name.
+ *
+ * @param namespace The namespace of the attribute to get the value from.
+ * @param name The name of the attribute to get the value from.
+ *
+ * @return A String containing the value of the attribute, or null if the
+ * attribute cannot be found.
+ */
+ public String getAttributeValue(String namespace, String name);
+
+ /**
+ * Returns a description of the current position of the attribute set.
+ * For instance, if the attribute set is loaded from an XML document,
+ * the position description could indicate the current line number.
+ *
+ * @return A string representation of the current position in the set,
+ * may be null.
+ */
+ public String getPositionDescription();
+
+ /**
+ * Return the resource ID associated with the given attribute name. This
+ * will be the identifier for an attribute resource, which can be used by
+ * styles. Returns 0 if there is no resource associated with this
+ * attribute.
+ *
+ * <p>Note that this is different than {@link #getAttributeResourceValue}
+ * in that it returns a resource identifier for the attribute name; the
+ * other method returns this attribute's value as a resource identifier.
+ *
+ * @param index Index of the desired attribute, 0...count-1.
+ *
+ * @return The resource identifier, 0 if none.
+ */
+ public int getAttributeNameResource(int index);
+
+ /**
+ * Return the index of the value of 'attribute' in the list 'options'.
+ *
+ * @param namespace Namespace of attribute to retrieve.
+ * @param attribute Name of attribute to retrieve.
+ * @param options List of strings whose values we are checking against.
+ * @param defaultValue Value returned if attribute doesn't exist or no
+ * match is found.
+ *
+ * @return Index in to 'options' or defaultValue.
+ */
+ public int getAttributeListValue(String namespace, String attribute,
+ String[] options, int defaultValue);
+
+ /**
+ * Return the boolean value of 'attribute'.
+ *
+ * @param namespace Namespace of attribute to retrieve.
+ * @param attribute The attribute to retrieve.
+ * @param defaultValue What to return if the attribute isn't found.
+ *
+ * @return Resulting value.
+ */
+ public boolean getAttributeBooleanValue(String namespace, String attribute,
+ boolean defaultValue);
+
+ /**
+ * Return the value of 'attribute' as a resource identifier.
+ *
+ * <p>Note that this is different than {@link #getAttributeNameResource}
+ * in that it returns the value contained in this attribute as a
+ * resource identifier (i.e., a value originally of the form
+ * "@package:type/resource"); the other method returns a resource
+ * identifier that identifies the name of the attribute.
+ *
+ * @param namespace Namespace of attribute to retrieve.
+ * @param attribute The attribute to retrieve.
+ * @param defaultValue What to return if the attribute isn't found.
+ *
+ * @return Resulting value.
+ */
+ public int getAttributeResourceValue(String namespace, String attribute,
+ int defaultValue);
+
+ /**
+ * Return the integer value of 'attribute'.
+ *
+ * @param namespace Namespace of attribute to retrieve.
+ * @param attribute The attribute to retrieve.
+ * @param defaultValue What to return if the attribute isn't found.
+ *
+ * @return Resulting value.
+ */
+ public int getAttributeIntValue(String namespace, String attribute,
+ int defaultValue);
+
+ /**
+ * Return the boolean value of 'attribute' that is formatted as an
+ * unsigned value. In particular, the formats 0xn...n and #n...n are
+ * handled.
+ *
+ * @param namespace Namespace of attribute to retrieve.
+ * @param attribute The attribute to retrieve.
+ * @param defaultValue What to return if the attribute isn't found.
+ *
+ * @return Resulting value.
+ */
+ public int getAttributeUnsignedIntValue(String namespace, String attribute,
+ int defaultValue);
+
+ /**
+ * Return the float value of 'attribute'.
+ *
+ * @param namespace Namespace of attribute to retrieve.
+ * @param attribute The attribute to retrieve.
+ * @param defaultValue What to return if the attribute isn't found.
+ *
+ * @return Resulting value.
+ */
+ public float getAttributeFloatValue(String namespace, String attribute,
+ float defaultValue);
+
+ /**
+ * Return the index of the value of attribute at 'index' in the list
+ * 'options'.
+ *
+ * @param index Index of the desired attribute, 0...count-1.
+ * @param options List of strings whose values we are checking against.
+ * @param defaultValue Value returned if attribute doesn't exist or no
+ * match is found.
+ *
+ * @return Index in to 'options' or defaultValue.
+ */
+ public int getAttributeListValue(int index, String[] options, int defaultValue);
+
+ /**
+ * Return the boolean value of attribute at 'index'.
+ *
+ * @param index Index of the desired attribute, 0...count-1.
+ * @param defaultValue What to return if the attribute isn't found.
+ *
+ * @return Resulting value.
+ */
+ public boolean getAttributeBooleanValue(int index, boolean defaultValue);
+
+ /**
+ * Return the value of attribute at 'index' as a resource identifier.
+ *
+ * <p>Note that this is different than {@link #getAttributeNameResource}
+ * in that it returns the value contained in this attribute as a
+ * resource identifier (i.e., a value originally of the form
+ * "@package:type/resource"); the other method returns a resource
+ * identifier that identifies the name of the attribute.
+ *
+ * @param index Index of the desired attribute, 0...count-1.
+ * @param defaultValue What to return if the attribute isn't found.
+ *
+ * @return Resulting value.
+ */
+ public int getAttributeResourceValue(int index, int defaultValue);
+
+ /**
+ * Return the integer value of attribute at 'index'.
+ *
+ * @param index Index of the desired attribute, 0...count-1.
+ * @param defaultValue What to return if the attribute isn't found.
+ *
+ * @return Resulting value.
+ */
+ public int getAttributeIntValue(int index, int defaultValue);
+
+ /**
+ * Return the integer value of attribute at 'index' that is formatted as an
+ * unsigned value. In particular, the formats 0xn...n and #n...n are
+ * handled.
+ *
+ * @param index Index of the desired attribute, 0...count-1.
+ * @param defaultValue What to return if the attribute isn't found.
+ *
+ * @return Resulting value.
+ */
+ public int getAttributeUnsignedIntValue(int index, int defaultValue);
+
+ /**
+ * Return the float value of attribute at 'index'.
+ *
+ * @param index Index of the desired attribute, 0...count-1.
+ * @param defaultValue What to return if the attribute isn't found.
+ *
+ * @return Resulting value.
+ */
+ public float getAttributeFloatValue(int index, float defaultValue);
+
+ /**
+ * Return the value of the "id" attribute or null if there is not one.
+ * Equivalent to getAttributeValue(null, "id").
+ *
+ * @return The id attribute's value or null.
+ */
+ public String getIdAttribute();
+
+ /**
+ * Return the value of the "class" attribute or null if there is not one.
+ * Equivalent to getAttributeValue(null, "class").
+ *
+ * @return The class attribute's value or null.
+ */
+ public String getClassAttribute();
+
+ /**
+ * Return the integer value of the "id" attribute or defaultValue if there
+ * is none.
+ * Equivalent to getAttributeResourceValue(null, "id", defaultValue);
+ *
+ * @param defaultValue What to return if the "id" attribute isn't found.
+ * @return int Resulting value.
+ */
+ public int getIdAttributeResourceValue(int defaultValue);
+
+ /**
+
+ * Return the value of the "style" attribute or 0 if there is not one.
+ * Equivalent to getAttributeResourceValue(null, "style").
+ *
+ * @return The style attribute's resource identifier or 0.
+ */
+ public int getStyleAttribute();
+}
diff --git a/android/util/BackupUtils.java b/android/util/BackupUtils.java
new file mode 100644
index 0000000..474ceda
--- /dev/null
+++ b/android/util/BackupUtils.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+/**
+ * Utility methods for Backup/Restore
+ * @hide
+ */
+public class BackupUtils {
+
+ public static final int NULL = 0;
+ public static final int NOT_NULL = 1;
+
+ /**
+ * Thrown when there is a backup version mismatch
+ * between the data received and what the system can handle
+ */
+ public static class BadVersionException extends Exception {
+ public BadVersionException(String message) {
+ super(message);
+ }
+ }
+
+ public static String readString(DataInputStream in) throws IOException {
+ return (in.readByte() == NOT_NULL) ? in.readUTF() : null;
+ }
+
+ public static void writeString(DataOutputStream out, String val) throws IOException {
+ if (val != null) {
+ out.writeByte(NOT_NULL);
+ out.writeUTF(val);
+ } else {
+ out.writeByte(NULL);
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/util/Base64.java b/android/util/Base64.java
new file mode 100644
index 0000000..92abd7c
--- /dev/null
+++ b/android/util/Base64.java
@@ -0,0 +1,744 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.compat.annotation.UnsupportedAppUsage;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Utilities for encoding and decoding the Base64 representation of
+ * binary data. See RFCs <a
+ * href="http://www.ietf.org/rfc/rfc2045.txt">2045</a> and <a
+ * href="http://www.ietf.org/rfc/rfc3548.txt">3548</a>.
+ */
+public class Base64 {
+ /**
+ * Default values for encoder/decoder flags.
+ */
+ public static final int DEFAULT = 0;
+
+ /**
+ * Encoder flag bit to omit the padding '=' characters at the end
+ * of the output (if any).
+ */
+ public static final int NO_PADDING = 1;
+
+ /**
+ * Encoder flag bit to omit all line terminators (i.e., the output
+ * will be on one long line).
+ */
+ public static final int NO_WRAP = 2;
+
+ /**
+ * Encoder flag bit to indicate lines should be terminated with a
+ * CRLF pair instead of just an LF. Has no effect if {@code
+ * NO_WRAP} is specified as well.
+ */
+ public static final int CRLF = 4;
+
+ /**
+ * Encoder/decoder flag bit to indicate using the "URL and
+ * filename safe" variant of Base64 (see RFC 3548 section 4) where
+ * {@code -} and {@code _} are used in place of {@code +} and
+ * {@code /}.
+ */
+ public static final int URL_SAFE = 8;
+
+ /**
+ * Flag to pass to {@link Base64OutputStream} to indicate that it
+ * should not close the output stream it is wrapping when it
+ * itself is closed.
+ */
+ public static final int NO_CLOSE = 16;
+
+ // --------------------------------------------------------
+ // shared code
+ // --------------------------------------------------------
+
+ /* package */ static abstract class Coder {
+ public byte[] output;
+ public int op;
+
+ /**
+ * Encode/decode another block of input data. this.output is
+ * provided by the caller, and must be big enough to hold all
+ * the coded data. On exit, this.opwill be set to the length
+ * of the coded data.
+ *
+ * @param finish true if this is the final call to process for
+ * this object. Will finalize the coder state and
+ * include any final bytes in the output.
+ *
+ * @return true if the input so far is good; false if some
+ * error has been detected in the input stream..
+ */
+ public abstract boolean process(byte[] input, int offset, int len, boolean finish);
+
+ /**
+ * @return the maximum number of bytes a call to process()
+ * could produce for the given number of input bytes. This may
+ * be an overestimate.
+ */
+ public abstract int maxOutputSize(int len);
+ }
+
+ // --------------------------------------------------------
+ // decoding
+ // --------------------------------------------------------
+
+ /**
+ * Decode the Base64-encoded data in input and return the data in
+ * a new byte array.
+ *
+ * <p>The padding '=' characters at the end are considered optional, but
+ * if any are present, there must be the correct number of them.
+ *
+ * @param str the input String to decode, which is converted to
+ * bytes using the default charset
+ * @param flags controls certain features of the decoded output.
+ * Pass {@code DEFAULT} to decode standard Base64.
+ *
+ * @throws IllegalArgumentException if the input contains
+ * incorrect padding
+ */
+ public static byte[] decode(String str, int flags) {
+ return decode(str.getBytes(), flags);
+ }
+
+ /**
+ * Decode the Base64-encoded data in input and return the data in
+ * a new byte array.
+ *
+ * <p>The padding '=' characters at the end are considered optional, but
+ * if any are present, there must be the correct number of them.
+ *
+ * @param input the input array to decode
+ * @param flags controls certain features of the decoded output.
+ * Pass {@code DEFAULT} to decode standard Base64.
+ *
+ * @throws IllegalArgumentException if the input contains
+ * incorrect padding
+ */
+ public static byte[] decode(byte[] input, int flags) {
+ return decode(input, 0, input.length, flags);
+ }
+
+ /**
+ * Decode the Base64-encoded data in input and return the data in
+ * a new byte array.
+ *
+ * <p>The padding '=' characters at the end are considered optional, but
+ * if any are present, there must be the correct number of them.
+ *
+ * @param input the data to decode
+ * @param offset the position within the input array at which to start
+ * @param len the number of bytes of input to decode
+ * @param flags controls certain features of the decoded output.
+ * Pass {@code DEFAULT} to decode standard Base64.
+ *
+ * @throws IllegalArgumentException if the input contains
+ * incorrect padding
+ */
+ public static byte[] decode(byte[] input, int offset, int len, int flags) {
+ // Allocate space for the most data the input could represent.
+ // (It could contain less if it contains whitespace, etc.)
+ Decoder decoder = new Decoder(flags, new byte[len*3/4]);
+
+ if (!decoder.process(input, offset, len, true)) {
+ throw new IllegalArgumentException("bad base-64");
+ }
+
+ // Maybe we got lucky and allocated exactly enough output space.
+ if (decoder.op == decoder.output.length) {
+ return decoder.output;
+ }
+
+ // Need to shorten the array, so allocate a new one of the
+ // right size and copy.
+ byte[] temp = new byte[decoder.op];
+ System.arraycopy(decoder.output, 0, temp, 0, decoder.op);
+ return temp;
+ }
+
+ /* package */ static class Decoder extends Coder {
+ /**
+ * Lookup table for turning bytes into their position in the
+ * Base64 alphabet.
+ */
+ private static final int DECODE[] = {
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
+ -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ };
+
+ /**
+ * Decode lookup table for the "web safe" variant (RFC 3548
+ * sec. 4) where - and _ replace + and /.
+ */
+ private static final int DECODE_WEBSAFE[] = {
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1,
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63,
+ -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ };
+
+ /** Non-data values in the DECODE arrays. */
+ private static final int SKIP = -1;
+ private static final int EQUALS = -2;
+
+ /**
+ * States 0-3 are reading through the next input tuple.
+ * State 4 is having read one '=' and expecting exactly
+ * one more.
+ * State 5 is expecting no more data or padding characters
+ * in the input.
+ * State 6 is the error state; an error has been detected
+ * in the input and no future input can "fix" it.
+ */
+ private int state; // state number (0 to 6)
+ private int value;
+
+ final private int[] alphabet;
+
+ public Decoder(int flags, byte[] output) {
+ this.output = output;
+
+ alphabet = ((flags & URL_SAFE) == 0) ? DECODE : DECODE_WEBSAFE;
+ state = 0;
+ value = 0;
+ }
+
+ /**
+ * @return an overestimate for the number of bytes {@code
+ * len} bytes could decode to.
+ */
+ public int maxOutputSize(int len) {
+ return len * 3/4 + 10;
+ }
+
+ /**
+ * Decode another block of input data.
+ *
+ * @return true if the state machine is still healthy. false if
+ * bad base-64 data has been detected in the input stream.
+ */
+ public boolean process(byte[] input, int offset, int len, boolean finish) {
+ if (this.state == 6) return false;
+
+ int p = offset;
+ len += offset;
+
+ // Using local variables makes the decoder about 12%
+ // faster than if we manipulate the member variables in
+ // the loop. (Even alphabet makes a measurable
+ // difference, which is somewhat surprising to me since
+ // the member variable is final.)
+ int state = this.state;
+ int value = this.value;
+ int op = 0;
+ final byte[] output = this.output;
+ final int[] alphabet = this.alphabet;
+
+ while (p < len) {
+ // Try the fast path: we're starting a new tuple and the
+ // next four bytes of the input stream are all data
+ // bytes. This corresponds to going through states
+ // 0-1-2-3-0. We expect to use this method for most of
+ // the data.
+ //
+ // If any of the next four bytes of input are non-data
+ // (whitespace, etc.), value will end up negative. (All
+ // the non-data values in decode are small negative
+ // numbers, so shifting any of them up and or'ing them
+ // together will result in a value with its top bit set.)
+ //
+ // You can remove this whole block and the output should
+ // be the same, just slower.
+ if (state == 0) {
+ while (p+4 <= len &&
+ (value = ((alphabet[input[p] & 0xff] << 18) |
+ (alphabet[input[p+1] & 0xff] << 12) |
+ (alphabet[input[p+2] & 0xff] << 6) |
+ (alphabet[input[p+3] & 0xff]))) >= 0) {
+ output[op+2] = (byte) value;
+ output[op+1] = (byte) (value >> 8);
+ output[op] = (byte) (value >> 16);
+ op += 3;
+ p += 4;
+ }
+ if (p >= len) break;
+ }
+
+ // The fast path isn't available -- either we've read a
+ // partial tuple, or the next four input bytes aren't all
+ // data, or whatever. Fall back to the slower state
+ // machine implementation.
+
+ int d = alphabet[input[p++] & 0xff];
+
+ switch (state) {
+ case 0:
+ if (d >= 0) {
+ value = d;
+ ++state;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 1:
+ if (d >= 0) {
+ value = (value << 6) | d;
+ ++state;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 2:
+ if (d >= 0) {
+ value = (value << 6) | d;
+ ++state;
+ } else if (d == EQUALS) {
+ // Emit the last (partial) output tuple;
+ // expect exactly one more padding character.
+ output[op++] = (byte) (value >> 4);
+ state = 4;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 3:
+ if (d >= 0) {
+ // Emit the output triple and return to state 0.
+ value = (value << 6) | d;
+ output[op+2] = (byte) value;
+ output[op+1] = (byte) (value >> 8);
+ output[op] = (byte) (value >> 16);
+ op += 3;
+ state = 0;
+ } else if (d == EQUALS) {
+ // Emit the last (partial) output tuple;
+ // expect no further data or padding characters.
+ output[op+1] = (byte) (value >> 2);
+ output[op] = (byte) (value >> 10);
+ op += 2;
+ state = 5;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 4:
+ if (d == EQUALS) {
+ ++state;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 5:
+ if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+ }
+ }
+
+ if (!finish) {
+ // We're out of input, but a future call could provide
+ // more.
+ this.state = state;
+ this.value = value;
+ this.op = op;
+ return true;
+ }
+
+ // Done reading input. Now figure out where we are left in
+ // the state machine and finish up.
+
+ switch (state) {
+ case 0:
+ // Output length is a multiple of three. Fine.
+ break;
+ case 1:
+ // Read one extra input byte, which isn't enough to
+ // make another output byte. Illegal.
+ this.state = 6;
+ return false;
+ case 2:
+ // Read two extra input bytes, enough to emit 1 more
+ // output byte. Fine.
+ output[op++] = (byte) (value >> 4);
+ break;
+ case 3:
+ // Read three extra input bytes, enough to emit 2 more
+ // output bytes. Fine.
+ output[op++] = (byte) (value >> 10);
+ output[op++] = (byte) (value >> 2);
+ break;
+ case 4:
+ // Read one padding '=' when we expected 2. Illegal.
+ this.state = 6;
+ return false;
+ case 5:
+ // Read all the padding '='s we expected and no more.
+ // Fine.
+ break;
+ }
+
+ this.state = state;
+ this.op = op;
+ return true;
+ }
+ }
+
+ // --------------------------------------------------------
+ // encoding
+ // --------------------------------------------------------
+
+ /**
+ * Base64-encode the given data and return a newly allocated
+ * String with the result.
+ *
+ * @param input the data to encode
+ * @param flags controls certain features of the encoded output.
+ * Passing {@code DEFAULT} results in output that
+ * adheres to RFC 2045.
+ */
+ public static String encodeToString(byte[] input, int flags) {
+ try {
+ return new String(encode(input, flags), "US-ASCII");
+ } catch (UnsupportedEncodingException e) {
+ // US-ASCII is guaranteed to be available.
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Base64-encode the given data and return a newly allocated
+ * String with the result.
+ *
+ * @param input the data to encode
+ * @param offset the position within the input array at which to
+ * start
+ * @param len the number of bytes of input to encode
+ * @param flags controls certain features of the encoded output.
+ * Passing {@code DEFAULT} results in output that
+ * adheres to RFC 2045.
+ */
+ public static String encodeToString(byte[] input, int offset, int len, int flags) {
+ try {
+ return new String(encode(input, offset, len, flags), "US-ASCII");
+ } catch (UnsupportedEncodingException e) {
+ // US-ASCII is guaranteed to be available.
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Base64-encode the given data and return a newly allocated
+ * byte[] with the result.
+ *
+ * @param input the data to encode
+ * @param flags controls certain features of the encoded output.
+ * Passing {@code DEFAULT} results in output that
+ * adheres to RFC 2045.
+ */
+ public static byte[] encode(byte[] input, int flags) {
+ return encode(input, 0, input.length, flags);
+ }
+
+ /**
+ * Base64-encode the given data and return a newly allocated
+ * byte[] with the result.
+ *
+ * @param input the data to encode
+ * @param offset the position within the input array at which to
+ * start
+ * @param len the number of bytes of input to encode
+ * @param flags controls certain features of the encoded output.
+ * Passing {@code DEFAULT} results in output that
+ * adheres to RFC 2045.
+ */
+ public static byte[] encode(byte[] input, int offset, int len, int flags) {
+ Encoder encoder = new Encoder(flags, null);
+
+ // Compute the exact length of the array we will produce.
+ int output_len = len / 3 * 4;
+
+ // Account for the tail of the data and the padding bytes, if any.
+ if (encoder.do_padding) {
+ if (len % 3 > 0) {
+ output_len += 4;
+ }
+ } else {
+ switch (len % 3) {
+ case 0: break;
+ case 1: output_len += 2; break;
+ case 2: output_len += 3; break;
+ }
+ }
+
+ // Account for the newlines, if any.
+ if (encoder.do_newline && len > 0) {
+ output_len += (((len-1) / (3 * Encoder.LINE_GROUPS)) + 1) *
+ (encoder.do_cr ? 2 : 1);
+ }
+
+ encoder.output = new byte[output_len];
+ encoder.process(input, offset, len, true);
+
+ assert encoder.op == output_len;
+
+ return encoder.output;
+ }
+
+ /* package */ static class Encoder extends Coder {
+ /**
+ * Emit a new line every this many output tuples. Corresponds to
+ * a 76-character line length (the maximum allowable according to
+ * <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>).
+ */
+ public static final int LINE_GROUPS = 19;
+
+ /**
+ * Lookup table for turning Base64 alphabet positions (6 bits)
+ * into output bytes.
+ */
+ private static final byte ENCODE[] = {
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
+ 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
+ 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
+ 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/',
+ };
+
+ /**
+ * Lookup table for turning Base64 alphabet positions (6 bits)
+ * into output bytes.
+ */
+ private static final byte ENCODE_WEBSAFE[] = {
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
+ 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
+ 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
+ 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_',
+ };
+
+ final private byte[] tail;
+ /* package */ int tailLen;
+ private int count;
+
+ final public boolean do_padding;
+ final public boolean do_newline;
+ final public boolean do_cr;
+ final private byte[] alphabet;
+
+ public Encoder(int flags, byte[] output) {
+ this.output = output;
+
+ do_padding = (flags & NO_PADDING) == 0;
+ do_newline = (flags & NO_WRAP) == 0;
+ do_cr = (flags & CRLF) != 0;
+ alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE;
+
+ tail = new byte[2];
+ tailLen = 0;
+
+ count = do_newline ? LINE_GROUPS : -1;
+ }
+
+ /**
+ * @return an overestimate for the number of bytes {@code
+ * len} bytes could encode to.
+ */
+ public int maxOutputSize(int len) {
+ return len * 8/5 + 10;
+ }
+
+ public boolean process(byte[] input, int offset, int len, boolean finish) {
+ // Using local variables makes the encoder about 9% faster.
+ final byte[] alphabet = this.alphabet;
+ final byte[] output = this.output;
+ int op = 0;
+ int count = this.count;
+
+ int p = offset;
+ len += offset;
+ int v = -1;
+
+ // First we need to concatenate the tail of the previous call
+ // with any input bytes available now and see if we can empty
+ // the tail.
+
+ switch (tailLen) {
+ case 0:
+ // There was no tail.
+ break;
+
+ case 1:
+ if (p+2 <= len) {
+ // A 1-byte tail with at least 2 bytes of
+ // input available now.
+ v = ((tail[0] & 0xff) << 16) |
+ ((input[p++] & 0xff) << 8) |
+ (input[p++] & 0xff);
+ tailLen = 0;
+ };
+ break;
+
+ case 2:
+ if (p+1 <= len) {
+ // A 2-byte tail with at least 1 byte of input.
+ v = ((tail[0] & 0xff) << 16) |
+ ((tail[1] & 0xff) << 8) |
+ (input[p++] & 0xff);
+ tailLen = 0;
+ }
+ break;
+ }
+
+ if (v != -1) {
+ output[op++] = alphabet[(v >> 18) & 0x3f];
+ output[op++] = alphabet[(v >> 12) & 0x3f];
+ output[op++] = alphabet[(v >> 6) & 0x3f];
+ output[op++] = alphabet[v & 0x3f];
+ if (--count == 0) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ count = LINE_GROUPS;
+ }
+ }
+
+ // At this point either there is no tail, or there are fewer
+ // than 3 bytes of input available.
+
+ // The main loop, turning 3 input bytes into 4 output bytes on
+ // each iteration.
+ while (p+3 <= len) {
+ v = ((input[p] & 0xff) << 16) |
+ ((input[p+1] & 0xff) << 8) |
+ (input[p+2] & 0xff);
+ output[op] = alphabet[(v >> 18) & 0x3f];
+ output[op+1] = alphabet[(v >> 12) & 0x3f];
+ output[op+2] = alphabet[(v >> 6) & 0x3f];
+ output[op+3] = alphabet[v & 0x3f];
+ p += 3;
+ op += 4;
+ if (--count == 0) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ count = LINE_GROUPS;
+ }
+ }
+
+ if (finish) {
+ // Finish up the tail of the input. Note that we need to
+ // consume any bytes in tail before any bytes
+ // remaining in input; there should be at most two bytes
+ // total.
+
+ if (p-tailLen == len-1) {
+ int t = 0;
+ v = ((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 4;
+ tailLen -= t;
+ output[op++] = alphabet[(v >> 6) & 0x3f];
+ output[op++] = alphabet[v & 0x3f];
+ if (do_padding) {
+ output[op++] = '=';
+ output[op++] = '=';
+ }
+ if (do_newline) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ }
+ } else if (p-tailLen == len-2) {
+ int t = 0;
+ v = (((tailLen > 1 ? tail[t++] : input[p++]) & 0xff) << 10) |
+ (((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 2);
+ tailLen -= t;
+ output[op++] = alphabet[(v >> 12) & 0x3f];
+ output[op++] = alphabet[(v >> 6) & 0x3f];
+ output[op++] = alphabet[v & 0x3f];
+ if (do_padding) {
+ output[op++] = '=';
+ }
+ if (do_newline) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ }
+ } else if (do_newline && op > 0 && count != LINE_GROUPS) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ }
+
+ assert tailLen == 0;
+ assert p == len;
+ } else {
+ // Save the leftovers in tail to be consumed on the next
+ // call to encodeInternal.
+
+ if (p == len-1) {
+ tail[tailLen++] = input[p];
+ } else if (p == len-2) {
+ tail[tailLen++] = input[p];
+ tail[tailLen++] = input[p+1];
+ }
+ }
+
+ this.op = op;
+ this.count = count;
+
+ return true;
+ }
+ }
+
+ @UnsupportedAppUsage
+ private Base64() { } // don't instantiate
+}
diff --git a/android/util/Base64DataException.java b/android/util/Base64DataException.java
new file mode 100644
index 0000000..de12ee1
--- /dev/null
+++ b/android/util/Base64DataException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import java.io.IOException;
+
+/**
+ * This exception is thrown by {@link Base64InputStream} or {@link Base64OutputStream}
+ * when an error is detected in the data being decoded. This allows problems with the base64 data
+ * to be disambiguated from errors in the underlying streams (e.g. actual connection errors.)
+ */
+public class Base64DataException extends IOException {
+ public Base64DataException(String detailMessage) {
+ super(detailMessage);
+ }
+}
diff --git a/android/util/Base64InputStream.java b/android/util/Base64InputStream.java
new file mode 100644
index 0000000..9eba5b5
--- /dev/null
+++ b/android/util/Base64InputStream.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * An InputStream that does Base64 decoding on the data read through
+ * it.
+ */
+public class Base64InputStream extends FilterInputStream {
+ private final Base64.Coder coder;
+
+ private static byte[] EMPTY = new byte[0];
+
+ private static final int BUFFER_SIZE = 2048;
+ private boolean eof;
+ private byte[] inputBuffer;
+ private int outputStart;
+ private int outputEnd;
+
+ /**
+ * An InputStream that performs Base64 decoding on the data read
+ * from the wrapped stream.
+ *
+ * @param in the InputStream to read the source data from
+ * @param flags bit flags for controlling the decoder; see the
+ * constants in {@link Base64}
+ */
+ public Base64InputStream(InputStream in, int flags) {
+ this(in, flags, false);
+ }
+
+ /**
+ * Performs Base64 encoding or decoding on the data read from the
+ * wrapped InputStream.
+ *
+ * @param in the InputStream to read the source data from
+ * @param flags bit flags for controlling the decoder; see the
+ * constants in {@link Base64}
+ * @param encode true to encode, false to decode
+ *
+ * @hide
+ */
+ public Base64InputStream(InputStream in, int flags, boolean encode) {
+ super(in);
+ eof = false;
+ inputBuffer = new byte[BUFFER_SIZE];
+ if (encode) {
+ coder = new Base64.Encoder(flags, null);
+ } else {
+ coder = new Base64.Decoder(flags, null);
+ }
+ coder.output = new byte[coder.maxOutputSize(BUFFER_SIZE)];
+ outputStart = 0;
+ outputEnd = 0;
+ }
+
+ public boolean markSupported() {
+ return false;
+ }
+
+ public void mark(int readlimit) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void reset() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void close() throws IOException {
+ in.close();
+ inputBuffer = null;
+ }
+
+ public int available() {
+ return outputEnd - outputStart;
+ }
+
+ public long skip(long n) throws IOException {
+ if (outputStart >= outputEnd) {
+ refill();
+ }
+ if (outputStart >= outputEnd) {
+ return 0;
+ }
+ long bytes = Math.min(n, outputEnd-outputStart);
+ outputStart += bytes;
+ return bytes;
+ }
+
+ public int read() throws IOException {
+ if (outputStart >= outputEnd) {
+ refill();
+ }
+ if (outputStart >= outputEnd) {
+ return -1;
+ } else {
+ return coder.output[outputStart++] & 0xff;
+ }
+ }
+
+ public int read(byte[] b, int off, int len) throws IOException {
+ if (outputStart >= outputEnd) {
+ refill();
+ }
+ if (outputStart >= outputEnd) {
+ return -1;
+ }
+ int bytes = Math.min(len, outputEnd-outputStart);
+ System.arraycopy(coder.output, outputStart, b, off, bytes);
+ outputStart += bytes;
+ return bytes;
+ }
+
+ /**
+ * Read data from the input stream into inputBuffer, then
+ * decode/encode it into the empty coder.output, and reset the
+ * outputStart and outputEnd pointers.
+ */
+ private void refill() throws IOException {
+ if (eof) return;
+ int bytesRead = in.read(inputBuffer);
+ boolean success;
+ if (bytesRead == -1) {
+ eof = true;
+ success = coder.process(EMPTY, 0, 0, true);
+ } else {
+ success = coder.process(inputBuffer, 0, bytesRead, false);
+ }
+ if (!success) {
+ throw new Base64DataException("bad base-64");
+ }
+ outputEnd = coder.op;
+ outputStart = 0;
+ }
+}
diff --git a/android/util/Base64OutputStream.java b/android/util/Base64OutputStream.java
new file mode 100644
index 0000000..48fadeb
--- /dev/null
+++ b/android/util/Base64OutputStream.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.compat.annotation.UnsupportedAppUsage;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * An OutputStream that does Base64 encoding on the data written to
+ * it, writing the resulting data to another OutputStream.
+ */
+public class Base64OutputStream extends FilterOutputStream {
+ private final Base64.Coder coder;
+ private final int flags;
+
+ private byte[] buffer = null;
+ private int bpos = 0;
+
+ private static byte[] EMPTY = new byte[0];
+
+ /**
+ * Performs Base64 encoding on the data written to the stream,
+ * writing the encoded data to another OutputStream.
+ *
+ * @param out the OutputStream to write the encoded data to
+ * @param flags bit flags for controlling the encoder; see the
+ * constants in {@link Base64}
+ */
+ public Base64OutputStream(OutputStream out, int flags) {
+ this(out, flags, true);
+ }
+
+ /**
+ * Performs Base64 encoding or decoding on the data written to the
+ * stream, writing the encoded/decoded data to another
+ * OutputStream.
+ *
+ * @param out the OutputStream to write the encoded data to
+ * @param flags bit flags for controlling the encoder; see the
+ * constants in {@link Base64}
+ * @param encode true to encode, false to decode
+ *
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public Base64OutputStream(OutputStream out, int flags, boolean encode) {
+ super(out);
+ this.flags = flags;
+ if (encode) {
+ coder = new Base64.Encoder(flags, null);
+ } else {
+ coder = new Base64.Decoder(flags, null);
+ }
+ }
+
+ public void write(int b) throws IOException {
+ // To avoid invoking the encoder/decoder routines for single
+ // bytes, we buffer up calls to write(int) in an internal
+ // byte array to transform them into writes of decently-sized
+ // arrays.
+
+ if (buffer == null) {
+ buffer = new byte[1024];
+ }
+ if (bpos >= buffer.length) {
+ // internal buffer full; write it out.
+ internalWrite(buffer, 0, bpos, false);
+ bpos = 0;
+ }
+ buffer[bpos++] = (byte) b;
+ }
+
+ /**
+ * Flush any buffered data from calls to write(int). Needed
+ * before doing a write(byte[], int, int) or a close().
+ */
+ private void flushBuffer() throws IOException {
+ if (bpos > 0) {
+ internalWrite(buffer, 0, bpos, false);
+ bpos = 0;
+ }
+ }
+
+ public void write(byte[] b, int off, int len) throws IOException {
+ if (len <= 0) return;
+ flushBuffer();
+ internalWrite(b, off, len, false);
+ }
+
+ public void close() throws IOException {
+ IOException thrown = null;
+ try {
+ flushBuffer();
+ internalWrite(EMPTY, 0, 0, true);
+ } catch (IOException e) {
+ thrown = e;
+ }
+
+ try {
+ if ((flags & Base64.NO_CLOSE) == 0) {
+ out.close();
+ } else {
+ out.flush();
+ }
+ } catch (IOException e) {
+ if (thrown == null) {
+ thrown = e;
+ } else {
+ thrown.addSuppressed(e);
+ }
+ }
+
+ if (thrown != null) {
+ throw thrown;
+ }
+ }
+
+ /**
+ * Write the given bytes to the encoder/decoder.
+ *
+ * @param finish true if this is the last batch of input, to cause
+ * encoder/decoder state to be finalized.
+ */
+ private void internalWrite(byte[] b, int off, int len, boolean finish) throws IOException {
+ coder.output = embiggen(coder.output, coder.maxOutputSize(len));
+ if (!coder.process(b, off, len, finish)) {
+ throw new Base64DataException("bad base-64");
+ }
+ out.write(coder.output, 0, coder.op);
+ }
+
+ /**
+ * If b.length is at least len, return b. Otherwise return a new
+ * byte array of length len.
+ */
+ private byte[] embiggen(byte[] b, int len) {
+ if (b == null || b.length < len) {
+ return new byte[len];
+ } else {
+ return b;
+ }
+ }
+}
diff --git a/android/util/BridgeXmlPullAttributes.java b/android/util/BridgeXmlPullAttributes.java
new file mode 100644
index 0000000..2f4b0ca
--- /dev/null
+++ b/android/util/BridgeXmlPullAttributes.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.util;
+
+import com.android.ide.common.rendering.api.AttrResourceValue;
+import com.android.ide.common.rendering.api.ResourceNamespace;
+import com.android.ide.common.rendering.api.ResourceReference;
+import com.android.ide.common.rendering.api.ResourceValue;
+import com.android.internal.util.XmlUtils;
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.BridgeConstants;
+import com.android.layoutlib.bridge.android.BridgeContext;
+import com.android.layoutlib.bridge.android.UnresolvedResourceValue;
+import com.android.layoutlib.bridge.android.XmlPullParserResolver;
+import com.android.layoutlib.bridge.impl.ResourceHelper;
+import com.android.resources.ResourceType;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.util.Map;
+import java.util.function.Function;
+
+/**
+ * A correct implementation of the {@link AttributeSet} interface on top of a XmlPullParser
+ */
+public class BridgeXmlPullAttributes extends XmlPullAttributes implements ResolvingAttributeSet {
+
+ interface EnumValueSupplier {
+ @Nullable
+ Map<String, Integer> getEnumValues(
+ @NonNull ResourceNamespace namespace, @NonNull String attrName);
+ }
+
+ private final BridgeContext mContext;
+ private final ResourceNamespace mXmlFileResourceNamespace;
+ private final ResourceNamespace.Resolver mResourceNamespaceResolver;
+ private final Function<String, Map<String, Integer>> mFrameworkEnumValueSupplier;
+ private final EnumValueSupplier mProjectEnumValueSupplier;
+
+ // VisibleForTesting
+ BridgeXmlPullAttributes(
+ @NonNull XmlPullParser parser,
+ @NonNull BridgeContext context,
+ @NonNull ResourceNamespace xmlFileResourceNamespace,
+ @NonNull Function<String, Map<String, Integer>> frameworkEnumValueSupplier,
+ @NonNull EnumValueSupplier projectEnumValueSupplier) {
+ super(parser);
+ mContext = context;
+ mFrameworkEnumValueSupplier = frameworkEnumValueSupplier;
+ mProjectEnumValueSupplier = projectEnumValueSupplier;
+ mXmlFileResourceNamespace = xmlFileResourceNamespace;
+ mResourceNamespaceResolver =
+ new XmlPullParserResolver(
+ mParser, context.getLayoutlibCallback().getImplicitNamespaces());
+ }
+
+ public BridgeXmlPullAttributes(
+ @NonNull XmlPullParser parser,
+ @NonNull BridgeContext context,
+ @NonNull ResourceNamespace xmlFileResourceNamespace) {
+ this(parser, context, xmlFileResourceNamespace, Bridge::getEnumValues, (ns, attrName) -> {
+ ResourceValue attr =
+ context.getRenderResources().getUnresolvedResource(
+ ResourceReference.attr(ns, attrName));
+ return attr instanceof AttrResourceValue
+ ? ((AttrResourceValue) attr).getAttributeValues()
+ : null;
+ });
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see android.util.XmlPullAttributes#getAttributeNameResource(int)
+ *
+ * This methods must return com.android.internal.R.attr.<name> matching
+ * the name of the attribute.
+ * It returns 0 if it doesn't find anything.
+ */
+ @Override
+ public int getAttributeNameResource(int index) {
+ // get the attribute name.
+ String name = getAttributeName(index);
+
+ // get the attribute namespace
+ String ns = mParser.getAttributeNamespace(index);
+
+ if (BridgeConstants.NS_RESOURCES.equals(ns)) {
+ return Bridge.getResourceId(ResourceType.ATTR, name);
+ }
+
+ // this is not an attribute in the android namespace, we query the customviewloader, if
+ // the namespaces match.
+ if (mContext.getLayoutlibCallback().getNamespace().equals(ns)) {
+ // TODO(namespaces): cache the namespace objects.
+ ResourceNamespace namespace = ResourceNamespace.fromNamespaceUri(ns);
+ if (namespace != null) {
+ return mContext.getLayoutlibCallback().getOrGenerateResourceId(
+ ResourceReference.attr(namespace, name));
+ }
+ }
+
+ return 0;
+ }
+
+ @Override
+ public int getAttributeListValue(String namespace, String attribute,
+ String[] options, int defaultValue) {
+ String value = getAttributeValue(namespace, attribute);
+ if (value != null) {
+ ResourceValue r = getResourceValue(value);
+
+ if (r != null) {
+ value = r.getValue();
+ }
+
+ return XmlUtils.convertValueToList(value, options, defaultValue);
+ }
+
+ return defaultValue;
+ }
+
+ @Override
+ public boolean getAttributeBooleanValue(String namespace, String attribute,
+ boolean defaultValue) {
+ String value = getAttributeValue(namespace, attribute);
+ if (value != null) {
+ ResourceValue r = getResourceValue(value);
+
+ if (r != null) {
+ value = r.getValue();
+ }
+
+ return XmlUtils.convertValueToBoolean(value, defaultValue);
+ }
+
+ return defaultValue;
+ }
+
+ @Override
+ public int getAttributeResourceValue(String namespace, String attribute, int defaultValue) {
+ String value = getAttributeValue(namespace, attribute);
+
+ return resolveResourceValue(value, defaultValue);
+ }
+
+ @Override
+ public int getAttributeIntValue(String namespace, String attribute, int defaultValue) {
+ String value = getAttributeValue(namespace, attribute);
+ if (value == null) {
+ return defaultValue;
+ }
+
+ ResourceValue r = getResourceValue(value);
+
+ if (r != null) {
+ value = r.getValue();
+ }
+
+ if (value.charAt(0) == '#') {
+ return ResourceHelper.getColor(value);
+ }
+
+ try {
+ return XmlUtils.convertValueToInt(value, defaultValue);
+ } catch (NumberFormatException e) {
+ // This is probably an enum
+ Map<String, Integer> enumValues = null;
+ if (BridgeConstants.NS_RESOURCES.equals(namespace)) {
+ enumValues = mFrameworkEnumValueSupplier.apply(attribute);
+ } else {
+ ResourceNamespace attrNamespace = ResourceNamespace.fromNamespaceUri(namespace);
+ if (attrNamespace != null) {
+ enumValues = mProjectEnumValueSupplier.getEnumValues(attrNamespace, attribute);
+ }
+ }
+
+ Integer enumValue = enumValues != null ? enumValues.get(value) : null;
+ if (enumValue != null) {
+ return enumValue;
+ }
+
+ // We weren't able to find the enum int value
+ throw e;
+ }
+ }
+
+ @Override
+ public int getAttributeUnsignedIntValue(String namespace, String attribute,
+ int defaultValue) {
+ String value = getAttributeValue(namespace, attribute);
+ if (value != null) {
+ ResourceValue r = getResourceValue(value);
+
+ if (r != null) {
+ value = r.getValue();
+ }
+
+ return XmlUtils.convertValueToUnsignedInt(value, defaultValue);
+ }
+
+ return defaultValue;
+ }
+
+ @Override
+ public float getAttributeFloatValue(String namespace, String attribute,
+ float defaultValue) {
+ String s = getAttributeValue(namespace, attribute);
+ if (s != null) {
+ ResourceValue r = getResourceValue(s);
+
+ if (r != null) {
+ s = r.getValue();
+ }
+
+ return Float.parseFloat(s);
+ }
+
+ return defaultValue;
+ }
+
+ @Override
+ public int getAttributeListValue(int index,
+ String[] options, int defaultValue) {
+ return XmlUtils.convertValueToList(
+ getAttributeValue(index), options, defaultValue);
+ }
+
+ @Override
+ public boolean getAttributeBooleanValue(int index, boolean defaultValue) {
+ String value = getAttributeValue(index);
+ if (value != null) {
+ ResourceValue r = getResourceValue(value);
+
+ if (r != null) {
+ value = r.getValue();
+ }
+
+ return XmlUtils.convertValueToBoolean(value, defaultValue);
+ }
+
+ return defaultValue;
+ }
+
+ @Override
+ public int getAttributeResourceValue(int index, int defaultValue) {
+ String value = getAttributeValue(index);
+
+ return resolveResourceValue(value, defaultValue);
+ }
+
+ @Override
+ public int getAttributeIntValue(int index, int defaultValue) {
+ return getAttributeIntValue(
+ mParser.getAttributeNamespace(index), getAttributeName(index), defaultValue);
+ }
+
+ @Override
+ public int getAttributeUnsignedIntValue(int index, int defaultValue) {
+ String value = getAttributeValue(index);
+ if (value != null) {
+ ResourceValue r = getResourceValue(value);
+
+ if (r != null) {
+ value = r.getValue();
+ }
+
+ return XmlUtils.convertValueToUnsignedInt(value, defaultValue);
+ }
+
+ return defaultValue;
+ }
+
+ @Override
+ public float getAttributeFloatValue(int index, float defaultValue) {
+ String s = getAttributeValue(index);
+ if (s != null) {
+ ResourceValue r = getResourceValue(s);
+
+ if (r != null) {
+ s = r.getValue();
+ }
+
+ return Float.parseFloat(s);
+ }
+
+ return defaultValue;
+ }
+
+ @Override
+ @Nullable
+ public ResourceValue getResolvedAttributeValue(@Nullable String namespace,
+ @NonNull String name) {
+ String s = getAttributeValue(namespace, name);
+ return s == null ? null : getResourceValue(s);
+ }
+
+ // -- private helper methods
+
+ /**
+ * Returns a resolved {@link ResourceValue} from a given value.
+ */
+ private ResourceValue getResourceValue(String value) {
+ // now look for this particular value
+ return mContext.getRenderResources().resolveResValue(
+ new UnresolvedResourceValue(
+ value, mXmlFileResourceNamespace, mResourceNamespaceResolver));
+ }
+
+ /**
+ * Resolves and return a value to its associated integer.
+ */
+ private int resolveResourceValue(String value, int defaultValue) {
+ ResourceValue resource = getResourceValue(value);
+ if (resource != null) {
+ return resource.isFramework() ?
+ Bridge.getResourceId(resource.getResourceType(), resource.getName()) :
+ mContext.getLayoutlibCallback().getOrGenerateResourceId(resource.asReference());
+ }
+
+ return defaultValue;
+ }
+}
diff --git a/android/util/CloseGuard.java b/android/util/CloseGuard.java
new file mode 100644
index 0000000..ba504a3
--- /dev/null
+++ b/android/util/CloseGuard.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.annotation.NonNull;
+
+/**
+ * CloseGuard is a mechanism for flagging implicit finalizer cleanup of
+ * resources that should have been cleaned up by explicit close
+ * methods (aka "explicit termination methods" in Effective Java).
+ * <p>
+ * A simple example: <pre> {@code
+ * class Foo {
+ *
+ * private final CloseGuard guard = new CloseGuard();
+ *
+ * ...
+ *
+ * public Foo() {
+ * ...;
+ * guard.open("cleanup");
+ * }
+ *
+ * public void cleanup() {
+ * guard.close();
+ * ...;
+ * if (Build.VERSION.SDK_INT >= 28) {
+ * Reference.reachabilityFence(this);
+ * }
+ * // For full correctness in the absence of a close() call, other methods may also need
+ * // reachabilityFence() calls.
+ * }
+ *
+ * protected void finalize() throws Throwable {
+ * try {
+ * // Note that guard could be null if the constructor threw.
+ * if (guard != null) {
+ * guard.warnIfOpen();
+ * }
+ * cleanup();
+ * } finally {
+ * super.finalize();
+ * }
+ * }
+ * }
+ * }</pre>
+ *
+ * In usage where the resource to be explicitly cleaned up is
+ * allocated after object construction, CloseGuard protection can
+ * be deferred. For example: <pre> {@code
+ * class Bar {
+ *
+ * private final CloseGuard guard = new CloseGuard();
+ *
+ * ...
+ *
+ * public Bar() {
+ * ...;
+ * }
+ *
+ * public void connect() {
+ * ...;
+ * guard.open("cleanup");
+ * }
+ *
+ * public void cleanup() {
+ * guard.close();
+ * ...;
+ * if (Build.VERSION.SDK_INT >= 28) {
+ * Reference.reachabilityFence(this);
+ * }
+ * // For full correctness in the absence of a close() call, other methods may also need
+ * // reachabilityFence() calls.
+ * }
+ *
+ * protected void finalize() throws Throwable {
+ * try {
+ * // Note that guard could be null if the constructor threw.
+ * if (guard != null) {
+ * guard.warnIfOpen();
+ * }
+ * cleanup();
+ * } finally {
+ * super.finalize();
+ * }
+ * }
+ * }
+ * }</pre>
+ *
+ * When used in a constructor, calls to {@code open} should occur at
+ * the end of the constructor since an exception that would cause
+ * abrupt termination of the constructor will mean that the user will
+ * not have a reference to the object to cleanup explicitly. When used
+ * in a method, the call to {@code open} should occur just after
+ * resource acquisition.
+ */
+public final class CloseGuard {
+ private final dalvik.system.CloseGuard mImpl;
+
+ /**
+ * Constructs a new CloseGuard instance.
+ * {@link #open(String)} can be used to set up the instance to warn on failure to close.
+ */
+ public CloseGuard() {
+ mImpl = dalvik.system.CloseGuard.get();
+ }
+
+ /**
+ * Initializes the instance with a warning that the caller should have explicitly called the
+ * {@code closeMethodName} method instead of relying on finalization.
+ *
+ * @param closeMethodName non-null name of explicit termination method. Printed by warnIfOpen.
+ * @throws NullPointerException if closeMethodName is null.
+ */
+ public void open(@NonNull String closeMethodName) {
+ mImpl.open(closeMethodName);
+ }
+
+ /** Marks this CloseGuard instance as closed to avoid warnings on finalization. */
+ public void close() {
+ mImpl.close();
+ }
+
+ /**
+ * Logs a warning if the caller did not properly cleanup by calling an explicit close method
+ * before finalization.
+ */
+ public void warnIfOpen() {
+ mImpl.warnIfOpen();
+ }
+}
diff --git a/android/util/Config.java b/android/util/Config.java
new file mode 100644
index 0000000..70dc9aa
--- /dev/null
+++ b/android/util/Config.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * @deprecated This class is not useful, it just returns the same value for
+ * all constants, and has always done this. Do not use it.
+ */
+@Deprecated
+public final class Config {
+ /** @hide */ public Config() {}
+
+ /**
+ * @deprecated Always false.
+ */
+ @Deprecated
+ public static final boolean DEBUG = false;
+
+ /**
+ * @deprecated Always true.
+ */
+ @Deprecated
+ public static final boolean RELEASE = true;
+
+ /**
+ * @deprecated Always false.
+ */
+ @Deprecated
+ public static final boolean PROFILE = false;
+
+ /**
+ * @deprecated Always false.
+ */
+ @Deprecated
+ public static final boolean LOGV = false;
+
+ /**
+ * @deprecated Always true.
+ */
+ @Deprecated
+ public static final boolean LOGD = true;
+}
diff --git a/android/util/ContainerHelpers.java b/android/util/ContainerHelpers.java
new file mode 100644
index 0000000..4e5fefb
--- /dev/null
+++ b/android/util/ContainerHelpers.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+class ContainerHelpers {
+
+ // This is Arrays.binarySearch(), but doesn't do any argument validation.
+ static int binarySearch(int[] array, int size, int value) {
+ int lo = 0;
+ int hi = size - 1;
+
+ while (lo <= hi) {
+ final int mid = (lo + hi) >>> 1;
+ final int midVal = array[mid];
+
+ if (midVal < value) {
+ lo = mid + 1;
+ } else if (midVal > value) {
+ hi = mid - 1;
+ } else {
+ return mid; // value found
+ }
+ }
+ return ~lo; // value not present
+ }
+
+ static int binarySearch(long[] array, int size, long value) {
+ int lo = 0;
+ int hi = size - 1;
+
+ while (lo <= hi) {
+ final int mid = (lo + hi) >>> 1;
+ final long midVal = array[mid];
+
+ if (midVal < value) {
+ lo = mid + 1;
+ } else if (midVal > value) {
+ hi = mid - 1;
+ } else {
+ return mid; // value found
+ }
+ }
+ return ~lo; // value not present
+ }
+}
diff --git a/android/util/DataUnit.java b/android/util/DataUnit.java
new file mode 100644
index 0000000..cf045b8
--- /dev/null
+++ b/android/util/DataUnit.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import java.time.temporal.ChronoUnit;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A {@code DataUnit} represents data sizes at a given unit of granularity and
+ * provides utility methods to convert across units.
+ * <p>
+ * Note that both SI units (powers of 10) and IEC units (powers of 2) are
+ * supported, and you'll need to pick the correct one for your use-case. For
+ * example, Wikipedia defines a "kilobyte" as an SI unit of 1000 bytes, and a
+ * "kibibyte" as an IEC unit of 1024 bytes.
+ * <p>
+ * This design is mirrored after {@link TimeUnit} and {@link ChronoUnit}.
+ *
+ * @hide
+ */
+public enum DataUnit {
+ KILOBYTES { @Override public long toBytes(long v) { return v * 1_000; } },
+ MEGABYTES { @Override public long toBytes(long v) { return v * 1_000_000; } },
+ GIGABYTES { @Override public long toBytes(long v) { return v * 1_000_000_000; } },
+ KIBIBYTES { @Override public long toBytes(long v) { return v * 1_024; } },
+ MEBIBYTES { @Override public long toBytes(long v) { return v * 1_048_576; } },
+ GIBIBYTES { @Override public long toBytes(long v) { return v * 1_073_741_824; } };
+
+ public long toBytes(long v) {
+ throw new AbstractMethodError();
+ }
+}
diff --git a/android/util/DayOfMonthCursor.java b/android/util/DayOfMonthCursor.java
new file mode 100644
index 0000000..393b98e
--- /dev/null
+++ b/android/util/DayOfMonthCursor.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * Helps control and display a month view of a calendar that has a current
+ * selected day.
+ * <ul>
+ * <li>Keeps track of current month, day, year</li>
+ * <li>Keeps track of current cursor position (row, column)</li>
+ * <li>Provides methods to help display the calendar</li>
+ * <li>Provides methods to move the cursor up / down / left / right.</li>
+ * </ul>
+ *
+ * This should be used by anyone who presents a month view to users and wishes
+ * to behave consistently with other widgets and apps; if we ever change our
+ * mind about when to flip the month, we can change it here only.
+ *
+ * @hide
+ */
+public class DayOfMonthCursor extends MonthDisplayHelper {
+
+ private int mRow;
+ private int mColumn;
+
+ /**
+ * @param year The initial year.
+ * @param month The initial month.
+ * @param dayOfMonth The initial dayOfMonth.
+ * @param weekStartDay What dayOfMonth of the week the week should start,
+ * in terms of {@link java.util.Calendar} constants such as
+ * {@link java.util.Calendar#SUNDAY}.
+ */
+ public DayOfMonthCursor(int year, int month, int dayOfMonth, int weekStartDay) {
+ super(year, month, weekStartDay);
+ mRow = getRowOf(dayOfMonth);
+ mColumn = getColumnOf(dayOfMonth);
+ }
+
+
+ public int getSelectedRow() {
+ return mRow;
+ }
+
+ public int getSelectedColumn() {
+ return mColumn;
+ }
+
+ public void setSelectedRowColumn(int row, int col) {
+ mRow = row;
+ mColumn = col;
+ }
+
+ public int getSelectedDayOfMonth() {
+ return getDayAt(mRow, mColumn);
+ }
+
+ /**
+ * @return 0 if the selection is in the current month, otherwise -1 or +1
+ * depending on whether the selection is in the first or last row.
+ */
+ public int getSelectedMonthOffset() {
+ if (isWithinCurrentMonth(mRow, mColumn)) {
+ return 0;
+ }
+ if (mRow == 0) {
+ return -1;
+ }
+ return 1;
+ }
+
+ public void setSelectedDayOfMonth(int dayOfMonth) {
+ mRow = getRowOf(dayOfMonth);
+ mColumn = getColumnOf(dayOfMonth);
+ }
+
+ public boolean isSelected(int row, int column) {
+ return (mRow == row) && (mColumn == column);
+ }
+
+ /**
+ * Move up one box, potentially flipping to the previous month.
+ * @return Whether the month was flipped to the previous month
+ * due to the move.
+ */
+ public boolean up() {
+ if (isWithinCurrentMonth(mRow - 1, mColumn)) {
+ // within current month, just move up
+ mRow--;
+ return false;
+ }
+ // flip back to previous month, same column, first position within month
+ previousMonth();
+ mRow = 5;
+ while(!isWithinCurrentMonth(mRow, mColumn)) {
+ mRow--;
+ }
+ return true;
+ }
+
+ /**
+ * Move down one box, potentially flipping to the next month.
+ * @return Whether the month was flipped to the next month
+ * due to the move.
+ */
+ public boolean down() {
+ if (isWithinCurrentMonth(mRow + 1, mColumn)) {
+ // within current month, just move down
+ mRow++;
+ return false;
+ }
+ // flip to next month, same column, first position within month
+ nextMonth();
+ mRow = 0;
+ while (!isWithinCurrentMonth(mRow, mColumn)) {
+ mRow++;
+ }
+ return true;
+ }
+
+ /**
+ * Move left one box, potentially flipping to the previous month.
+ * @return Whether the month was flipped to the previous month
+ * due to the move.
+ */
+ public boolean left() {
+ if (mColumn == 0) {
+ mRow--;
+ mColumn = 6;
+ } else {
+ mColumn--;
+ }
+
+ if (isWithinCurrentMonth(mRow, mColumn)) {
+ return false;
+ }
+
+ // need to flip to last day of previous month
+ previousMonth();
+ int lastDay = getNumberOfDaysInMonth();
+ mRow = getRowOf(lastDay);
+ mColumn = getColumnOf(lastDay);
+ return true;
+ }
+
+ /**
+ * Move right one box, potentially flipping to the next month.
+ * @return Whether the month was flipped to the next month
+ * due to the move.
+ */
+ public boolean right() {
+ if (mColumn == 6) {
+ mRow++;
+ mColumn = 0;
+ } else {
+ mColumn++;
+ }
+
+ if (isWithinCurrentMonth(mRow, mColumn)) {
+ return false;
+ }
+
+ // need to flip to first day of next month
+ nextMonth();
+ mRow = 0;
+ mColumn = 0;
+ while (!isWithinCurrentMonth(mRow, mColumn)) {
+ mColumn++;
+ }
+ return true;
+ }
+
+}
diff --git a/android/util/DebugUtils.java b/android/util/DebugUtils.java
new file mode 100644
index 0000000..6d5e830
--- /dev/null
+++ b/android/util/DebugUtils.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+
+import java.io.PrintWriter;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+/**
+ * <p>Various utilities for debugging and logging.</p>
+ */
+public class DebugUtils {
+ /** @hide */ public DebugUtils() {}
+
+ /**
+ * <p>Filters objects against the <code>ANDROID_OBJECT_FILTER</code>
+ * environment variable. This environment variable can filter objects
+ * based on their class name and attribute values.</p>
+ *
+ * <p>Here is the syntax for <code>ANDROID_OBJECT_FILTER</code>:</p>
+ *
+ * <p><code>ClassName@attribute1=value1@attribute2=value2...</code></p>
+ *
+ * <p>Examples:</p>
+ * <ul>
+ * <li>Select TextView instances: <code>TextView</code></li>
+ * <li>Select TextView instances of text "Loading" and bottom offset of 22:
+ * <code>TextView@text=Loading.*@bottom=22</code></li>
+ * </ul>
+ *
+ * <p>The class name and the values are regular expressions.</p>
+ *
+ * <p>This class is useful for debugging and logging purpose:</p>
+ * <pre>
+ * if (DEBUG) {
+ * if (DebugUtils.isObjectSelected(childView) && LOGV_ENABLED) {
+ * Log.v(TAG, "Object " + childView + " logged!");
+ * }
+ * }
+ * </pre>
+ *
+ * <p><strong>NOTE</strong>: This method is very expensive as it relies
+ * heavily on regular expressions and reflection. Calls to this method
+ * should always be stripped out of the release binaries and avoided
+ * as much as possible in debug mode.</p>
+ *
+ * @param object any object to match against the ANDROID_OBJECT_FILTER
+ * environement variable
+ * @return true if object is selected by the ANDROID_OBJECT_FILTER
+ * environment variable, false otherwise
+ */
+ public static boolean isObjectSelected(Object object) {
+ boolean match = false;
+ String s = System.getenv("ANDROID_OBJECT_FILTER");
+ if (s != null && s.length() > 0) {
+ String[] selectors = s.split("@");
+ // first selector == class name
+ if (object.getClass().getSimpleName().matches(selectors[0])) {
+ // check potential attributes
+ for (int i = 1; i < selectors.length; i++) {
+ String[] pair = selectors[i].split("=");
+ Class<?> klass = object.getClass();
+ try {
+ Method declaredMethod = null;
+ Class<?> parent = klass;
+ do {
+ declaredMethod = parent.getDeclaredMethod("get" +
+ pair[0].substring(0, 1).toUpperCase(Locale.ROOT) +
+ pair[0].substring(1),
+ (Class[]) null);
+ } while ((parent = klass.getSuperclass()) != null &&
+ declaredMethod == null);
+
+ if (declaredMethod != null) {
+ Object value = declaredMethod
+ .invoke(object, (Object[])null);
+ match |= (value != null ?
+ value.toString() : "null").matches(pair[1]);
+ }
+ } catch (NoSuchMethodException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+ return match;
+ }
+
+ /** @hide */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ public static void buildShortClassTag(Object cls, StringBuilder out) {
+ if (cls == null) {
+ out.append("null");
+ } else {
+ String simpleName = cls.getClass().getSimpleName();
+ if (simpleName == null || simpleName.isEmpty()) {
+ simpleName = cls.getClass().getName();
+ int end = simpleName.lastIndexOf('.');
+ if (end > 0) {
+ simpleName = simpleName.substring(end+1);
+ }
+ }
+ out.append(simpleName);
+ out.append('{');
+ out.append(Integer.toHexString(System.identityHashCode(cls)));
+ }
+ }
+
+ /** @hide */
+ public static void printSizeValue(PrintWriter pw, long number) {
+ float result = number;
+ String suffix = "";
+ if (result > 900) {
+ suffix = "KB";
+ result = result / 1024;
+ }
+ if (result > 900) {
+ suffix = "MB";
+ result = result / 1024;
+ }
+ if (result > 900) {
+ suffix = "GB";
+ result = result / 1024;
+ }
+ if (result > 900) {
+ suffix = "TB";
+ result = result / 1024;
+ }
+ if (result > 900) {
+ suffix = "PB";
+ result = result / 1024;
+ }
+ String value;
+ if (result < 1) {
+ value = String.format("%.2f", result);
+ } else if (result < 10) {
+ value = String.format("%.1f", result);
+ } else if (result < 100) {
+ value = String.format("%.0f", result);
+ } else {
+ value = String.format("%.0f", result);
+ }
+ pw.print(value);
+ pw.print(suffix);
+ }
+
+ /** @hide */
+ public static String sizeValueToString(long number, StringBuilder outBuilder) {
+ if (outBuilder == null) {
+ outBuilder = new StringBuilder(32);
+ }
+ float result = number;
+ String suffix = "";
+ if (result > 900) {
+ suffix = "KB";
+ result = result / 1024;
+ }
+ if (result > 900) {
+ suffix = "MB";
+ result = result / 1024;
+ }
+ if (result > 900) {
+ suffix = "GB";
+ result = result / 1024;
+ }
+ if (result > 900) {
+ suffix = "TB";
+ result = result / 1024;
+ }
+ if (result > 900) {
+ suffix = "PB";
+ result = result / 1024;
+ }
+ String value;
+ if (result < 1) {
+ value = String.format("%.2f", result);
+ } else if (result < 10) {
+ value = String.format("%.1f", result);
+ } else if (result < 100) {
+ value = String.format("%.0f", result);
+ } else {
+ value = String.format("%.0f", result);
+ }
+ outBuilder.append(value);
+ outBuilder.append(suffix);
+ return outBuilder.toString();
+ }
+
+ /**
+ * Use prefixed constants (static final values) on given class to turn value
+ * into human-readable string.
+ *
+ * @hide
+ */
+ public static String valueToString(Class<?> clazz, String prefix, int value) {
+ for (Field field : clazz.getDeclaredFields()) {
+ final int modifiers = field.getModifiers();
+ if (Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers)
+ && field.getType().equals(int.class) && field.getName().startsWith(prefix)) {
+ try {
+ if (value == field.getInt(null)) {
+ return constNameWithoutPrefix(prefix, field);
+ }
+ } catch (IllegalAccessException ignored) {
+ }
+ }
+ }
+ return Integer.toString(value);
+ }
+
+ /**
+ * Use prefixed constants (static final values) on given class to turn flags
+ * into human-readable string.
+ *
+ * @hide
+ */
+ public static String flagsToString(Class<?> clazz, String prefix, int flags) {
+ final StringBuilder res = new StringBuilder();
+ boolean flagsWasZero = flags == 0;
+
+ for (Field field : clazz.getDeclaredFields()) {
+ final int modifiers = field.getModifiers();
+ if (Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers)
+ && field.getType().equals(int.class) && field.getName().startsWith(prefix)) {
+ try {
+ final int value = field.getInt(null);
+ if (value == 0 && flagsWasZero) {
+ return constNameWithoutPrefix(prefix, field);
+ }
+ if (value != 0 && (flags & value) == value) {
+ flags &= ~value;
+ res.append(constNameWithoutPrefix(prefix, field)).append('|');
+ }
+ } catch (IllegalAccessException ignored) {
+ }
+ }
+ }
+ if (flags != 0 || res.length() == 0) {
+ res.append(Integer.toHexString(flags));
+ } else {
+ res.deleteCharAt(res.length() - 1);
+ }
+ return res.toString();
+ }
+
+ private static String constNameWithoutPrefix(String prefix, Field field) {
+ return field.getName().substring(prefix.length());
+ }
+
+ /**
+ * Returns method names from current stack trace, where {@link StackTraceElement#getClass}
+ * starts with the given classes name
+ *
+ * @hide
+ */
+ public static List<String> callersWithin(Class<?> cls, int offset) {
+ List<String> result = Arrays.stream(Thread.currentThread().getStackTrace())
+ .skip(offset + 3)
+ .filter(st -> st.getClassName().startsWith(cls.getName()))
+ .map(StackTraceElement::getMethodName)
+ .collect(Collectors.toList());
+ Collections.reverse(result);
+ return result;
+ }
+}
diff --git a/android/util/DisplayMetrics.java b/android/util/DisplayMetrics.java
new file mode 100644
index 0000000..9f6065e
--- /dev/null
+++ b/android/util/DisplayMetrics.java
@@ -0,0 +1,426 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.SystemProperties;
+
+
+/**
+ * A structure describing general information about a display, such as its
+ * size, density, and font scaling.
+ * <p>To access the DisplayMetrics members, retrieve display metrics like this:</p>
+ * <pre>context.getResources().getDisplayMetrics();</pre>
+ */
+public class DisplayMetrics {
+ /**
+ * Standard quantized DPI for low-density screens.
+ */
+ public static final int DENSITY_LOW = 120;
+
+ /**
+ * Intermediate density for screens that sit between {@link #DENSITY_LOW} (120dpi) and
+ * {@link #DENSITY_MEDIUM} (160dpi). This is not a density that applications should target,
+ * instead relying on the system to scale their {@link #DENSITY_MEDIUM} assets for them.
+ */
+ public static final int DENSITY_140 = 140;
+
+ /**
+ * Standard quantized DPI for medium-density screens.
+ */
+ public static final int DENSITY_MEDIUM = 160;
+
+ /**
+ * Intermediate density for screens that sit between {@link #DENSITY_MEDIUM} (160dpi) and
+ * {@link #DENSITY_HIGH} (240dpi). This is not a density that applications should target,
+ * instead relying on the system to scale their {@link #DENSITY_HIGH} assets for them.
+ */
+ public static final int DENSITY_180 = 180;
+
+ /**
+ * Intermediate density for screens that sit between {@link #DENSITY_MEDIUM} (160dpi) and
+ * {@link #DENSITY_HIGH} (240dpi). This is not a density that applications should target,
+ * instead relying on the system to scale their {@link #DENSITY_HIGH} assets for them.
+ */
+ public static final int DENSITY_200 = 200;
+
+ /**
+ * This is a secondary density, added for some common screen configurations.
+ * It is recommended that applications not generally target this as a first
+ * class density -- that is, don't supply specific graphics for this
+ * density, instead allow the platform to scale from other densities
+ * (typically {@link #DENSITY_HIGH}) as
+ * appropriate. In most cases (such as using bitmaps in
+ * {@link android.graphics.drawable.Drawable}) the platform
+ * can perform this scaling at load time, so the only cost is some slight
+ * startup runtime overhead.
+ *
+ * <p>This density was original introduced to correspond with a
+ * 720p TV screen: the density for 1080p televisions is
+ * {@link #DENSITY_XHIGH}, and the value here provides the same UI
+ * size for a TV running at 720p. It has also found use in 7" tablets,
+ * when these devices have 1280x720 displays.
+ */
+ public static final int DENSITY_TV = 213;
+
+ /**
+ * Intermediate density for screens that sit between {@link #DENSITY_MEDIUM} (160dpi) and
+ * {@link #DENSITY_HIGH} (240dpi). This is not a density that applications should target,
+ * instead relying on the system to scale their {@link #DENSITY_HIGH} assets for them.
+ */
+ public static final int DENSITY_220 = 220;
+
+ /**
+ * Standard quantized DPI for high-density screens.
+ */
+ public static final int DENSITY_HIGH = 240;
+
+ /**
+ * Intermediate density for screens that sit between {@link #DENSITY_HIGH} (240dpi) and
+ * {@link #DENSITY_XHIGH} (320dpi). This is not a density that applications should target,
+ * instead relying on the system to scale their {@link #DENSITY_XHIGH} assets for them.
+ */
+ public static final int DENSITY_260 = 260;
+
+ /**
+ * Intermediate density for screens that sit between {@link #DENSITY_HIGH} (240dpi) and
+ * {@link #DENSITY_XHIGH} (320dpi). This is not a density that applications should target,
+ * instead relying on the system to scale their {@link #DENSITY_XHIGH} assets for them.
+ */
+ public static final int DENSITY_280 = 280;
+
+ /**
+ * Intermediate density for screens that sit between {@link #DENSITY_HIGH} (240dpi) and
+ * {@link #DENSITY_XHIGH} (320dpi). This is not a density that applications should target,
+ * instead relying on the system to scale their {@link #DENSITY_XHIGH} assets for them.
+ */
+ public static final int DENSITY_300 = 300;
+
+ /**
+ * Standard quantized DPI for extra-high-density screens.
+ */
+ public static final int DENSITY_XHIGH = 320;
+
+ /**
+ * Intermediate density for screens that sit somewhere between
+ * {@link #DENSITY_XHIGH} (320 dpi) and {@link #DENSITY_XXHIGH} (480 dpi).
+ * This is not a density that applications should target, instead relying
+ * on the system to scale their {@link #DENSITY_XXHIGH} assets for them.
+ */
+ public static final int DENSITY_340 = 340;
+
+ /**
+ * Intermediate density for screens that sit somewhere between
+ * {@link #DENSITY_XHIGH} (320 dpi) and {@link #DENSITY_XXHIGH} (480 dpi).
+ * This is not a density that applications should target, instead relying
+ * on the system to scale their {@link #DENSITY_XXHIGH} assets for them.
+ */
+ public static final int DENSITY_360 = 360;
+
+ /**
+ * Intermediate density for screens that sit somewhere between
+ * {@link #DENSITY_XHIGH} (320 dpi) and {@link #DENSITY_XXHIGH} (480 dpi).
+ * This is not a density that applications should target, instead relying
+ * on the system to scale their {@link #DENSITY_XXHIGH} assets for them.
+ */
+ public static final int DENSITY_400 = 400;
+
+ /**
+ * Intermediate density for screens that sit somewhere between
+ * {@link #DENSITY_XHIGH} (320 dpi) and {@link #DENSITY_XXHIGH} (480 dpi).
+ * This is not a density that applications should target, instead relying
+ * on the system to scale their {@link #DENSITY_XXHIGH} assets for them.
+ */
+ public static final int DENSITY_420 = 420;
+
+ /**
+ * Intermediate density for screens that sit somewhere between
+ * {@link #DENSITY_XHIGH} (320 dpi) and {@link #DENSITY_XXHIGH} (480 dpi).
+ * This is not a density that applications should target, instead relying
+ * on the system to scale their {@link #DENSITY_XXHIGH} assets for them.
+ */
+ public static final int DENSITY_440 = 440;
+
+ /**
+ * Intermediate density for screens that sit somewhere between
+ * {@link #DENSITY_XHIGH} (320 dpi) and {@link #DENSITY_XXHIGH} (480 dpi).
+ * This is not a density that applications should target, instead relying
+ * on the system to scale their {@link #DENSITY_XXHIGH} assets for them.
+ */
+ public static final int DENSITY_450 = 450;
+
+ /**
+ * Standard quantized DPI for extra-extra-high-density screens.
+ */
+ public static final int DENSITY_XXHIGH = 480;
+
+ /**
+ * Intermediate density for screens that sit somewhere between
+ * {@link #DENSITY_XXHIGH} (480 dpi) and {@link #DENSITY_XXXHIGH} (640 dpi).
+ * This is not a density that applications should target, instead relying
+ * on the system to scale their {@link #DENSITY_XXXHIGH} assets for them.
+ */
+ public static final int DENSITY_560 = 560;
+
+ /**
+ * Intermediate density for screens that sit somewhere between
+ * {@link #DENSITY_XXHIGH} (480 dpi) and {@link #DENSITY_XXXHIGH} (640 dpi).
+ * This is not a density that applications should target, instead relying
+ * on the system to scale their {@link #DENSITY_XXXHIGH} assets for them.
+ */
+ public static final int DENSITY_600 = 600;
+
+ /**
+ * Standard quantized DPI for extra-extra-extra-high-density screens. Applications
+ * should not generally worry about this density; relying on XHIGH graphics
+ * being scaled up to it should be sufficient for almost all cases. A typical
+ * use of this density would be 4K television screens -- 3840x2160, which
+ * is 2x a traditional HD 1920x1080 screen which runs at DENSITY_XHIGH.
+ */
+ public static final int DENSITY_XXXHIGH = 640;
+
+ /**
+ * The reference density used throughout the system.
+ */
+ public static final int DENSITY_DEFAULT = DENSITY_MEDIUM;
+
+ /**
+ * Scaling factor to convert a density in DPI units to the density scale.
+ * @hide
+ */
+ public static final float DENSITY_DEFAULT_SCALE = 1.0f / DENSITY_DEFAULT;
+
+ /**
+ * The device's current density.
+ * <p>
+ * This value reflects any changes made to the device density. To obtain
+ * the device's stable density, use {@link #DENSITY_DEVICE_STABLE}.
+ *
+ * @hide This value should not be used.
+ * @deprecated Use {@link #DENSITY_DEVICE_STABLE} to obtain the stable
+ * device density or {@link #densityDpi} to obtain the current
+ * density for a specific display.
+ */
+ @Deprecated
+ @UnsupportedAppUsage
+ public static int DENSITY_DEVICE = getDeviceDensity();
+
+ /**
+ * The device's stable density.
+ * <p>
+ * This value is constant at run time and may not reflect the current
+ * display density. To obtain the current density for a specific display,
+ * use {@link #densityDpi}.
+ */
+ public static final int DENSITY_DEVICE_STABLE = getDeviceDensity();
+
+ /**
+ * The absolute width of the available display size in pixels.
+ */
+ public int widthPixels;
+ /**
+ * The absolute height of the available display size in pixels.
+ */
+ public int heightPixels;
+ /**
+ * The logical density of the display. This is a scaling factor for the
+ * Density Independent Pixel unit, where one DIP is one pixel on an
+ * approximately 160 dpi screen (for example a 240x320, 1.5"x2" screen),
+ * providing the baseline of the system's display. Thus on a 160dpi screen
+ * this density value will be 1; on a 120 dpi screen it would be .75; etc.
+ *
+ * <p>This value does not exactly follow the real screen size (as given by
+ * {@link #xdpi} and {@link #ydpi}), but rather is used to scale the size of
+ * the overall UI in steps based on gross changes in the display dpi. For
+ * example, a 240x320 screen will have a density of 1 even if its width is
+ * 1.8", 1.3", etc. However, if the screen resolution is increased to
+ * 320x480 but the screen size remained 1.5"x2" then the density would be
+ * increased (probably to 1.5).
+ *
+ * @see #DENSITY_DEFAULT
+ */
+ public float density;
+ /**
+ * The screen density expressed as dots-per-inch. May be either
+ * {@link #DENSITY_LOW}, {@link #DENSITY_MEDIUM}, or {@link #DENSITY_HIGH}.
+ */
+ public int densityDpi;
+ /**
+ * A scaling factor for fonts displayed on the display. This is the same
+ * as {@link #density}, except that it may be adjusted in smaller
+ * increments at runtime based on a user preference for the font size.
+ */
+ public float scaledDensity;
+ /**
+ * The exact physical pixels per inch of the screen in the X dimension.
+ */
+ public float xdpi;
+ /**
+ * The exact physical pixels per inch of the screen in the Y dimension.
+ */
+ public float ydpi;
+
+ /**
+ * The reported display width prior to any compatibility mode scaling
+ * being applied.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public int noncompatWidthPixels;
+ /**
+ * The reported display height prior to any compatibility mode scaling
+ * being applied.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public int noncompatHeightPixels;
+ /**
+ * The reported display density prior to any compatibility mode scaling
+ * being applied.
+ * @hide
+ */
+ public float noncompatDensity;
+ /**
+ * The reported display density prior to any compatibility mode scaling
+ * being applied.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public int noncompatDensityDpi;
+ /**
+ * The reported scaled density prior to any compatibility mode scaling
+ * being applied.
+ * @hide
+ */
+ public float noncompatScaledDensity;
+ /**
+ * The reported display xdpi prior to any compatibility mode scaling
+ * being applied.
+ * @hide
+ */
+ public float noncompatXdpi;
+ /**
+ * The reported display ydpi prior to any compatibility mode scaling
+ * being applied.
+ * @hide
+ */
+ public float noncompatYdpi;
+
+ public DisplayMetrics() {
+ }
+
+ public void setTo(DisplayMetrics o) {
+ if (this == o) {
+ return;
+ }
+
+ widthPixels = o.widthPixels;
+ heightPixels = o.heightPixels;
+ density = o.density;
+ densityDpi = o.densityDpi;
+ scaledDensity = o.scaledDensity;
+ xdpi = o.xdpi;
+ ydpi = o.ydpi;
+ noncompatWidthPixels = o.noncompatWidthPixels;
+ noncompatHeightPixels = o.noncompatHeightPixels;
+ noncompatDensity = o.noncompatDensity;
+ noncompatDensityDpi = o.noncompatDensityDpi;
+ noncompatScaledDensity = o.noncompatScaledDensity;
+ noncompatXdpi = o.noncompatXdpi;
+ noncompatYdpi = o.noncompatYdpi;
+ }
+
+ public void setToDefaults() {
+ widthPixels = 0;
+ heightPixels = 0;
+ density = DENSITY_DEVICE / (float) DENSITY_DEFAULT;
+ densityDpi = DENSITY_DEVICE;
+ scaledDensity = density;
+ xdpi = DENSITY_DEVICE;
+ ydpi = DENSITY_DEVICE;
+ noncompatWidthPixels = widthPixels;
+ noncompatHeightPixels = heightPixels;
+ noncompatDensity = density;
+ noncompatDensityDpi = densityDpi;
+ noncompatScaledDensity = scaledDensity;
+ noncompatXdpi = xdpi;
+ noncompatYdpi = ydpi;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return o instanceof DisplayMetrics && equals((DisplayMetrics)o);
+ }
+
+ /**
+ * Returns true if these display metrics equal the other display metrics.
+ *
+ * @param other The display metrics with which to compare.
+ * @return True if the display metrics are equal.
+ */
+ public boolean equals(DisplayMetrics other) {
+ return equalsPhysical(other)
+ && scaledDensity == other.scaledDensity
+ && noncompatScaledDensity == other.noncompatScaledDensity;
+ }
+
+ /**
+ * Returns true if the physical aspects of the two display metrics
+ * are equal. This ignores the scaled density, which is a logical
+ * attribute based on the current desired font size.
+ *
+ * @param other The display metrics with which to compare.
+ * @return True if the display metrics are equal.
+ * @hide
+ */
+ public boolean equalsPhysical(DisplayMetrics other) {
+ return other != null
+ && widthPixels == other.widthPixels
+ && heightPixels == other.heightPixels
+ && density == other.density
+ && densityDpi == other.densityDpi
+ && xdpi == other.xdpi
+ && ydpi == other.ydpi
+ && noncompatWidthPixels == other.noncompatWidthPixels
+ && noncompatHeightPixels == other.noncompatHeightPixels
+ && noncompatDensity == other.noncompatDensity
+ && noncompatDensityDpi == other.noncompatDensityDpi
+ && noncompatXdpi == other.noncompatXdpi
+ && noncompatYdpi == other.noncompatYdpi;
+ }
+
+ @Override
+ public int hashCode() {
+ return widthPixels * heightPixels * densityDpi;
+ }
+
+ @Override
+ public String toString() {
+ return "DisplayMetrics{density=" + density + ", width=" + widthPixels +
+ ", height=" + heightPixels + ", scaledDensity=" + scaledDensity +
+ ", xdpi=" + xdpi + ", ydpi=" + ydpi + "}";
+ }
+
+ private static int getDeviceDensity() {
+ // qemu.sf.lcd_density can be used to override ro.sf.lcd_density
+ // when running in the emulator, allowing for dynamic configurations.
+ // The reason for this is that ro.sf.lcd_density is write-once and is
+ // set by the init process when it parses build.prop before anything else.
+ return SystemProperties.getInt("qemu.sf.lcd_density",
+ SystemProperties.getInt("ro.sf.lcd_density", DENSITY_DEFAULT));
+ }
+}
diff --git a/android/util/EventLog.java b/android/util/EventLog.java
new file mode 100644
index 0000000..ead4e46
--- /dev/null
+++ b/android/util/EventLog.java
@@ -0,0 +1,456 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.compat.annotation.UnsupportedAppUsage;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Access to the system diagnostic event record. System diagnostic events are
+ * used to record certain system-level events (such as garbage collection,
+ * activity manager state, system watchdogs, and other low level activity),
+ * which may be automatically collected and analyzed during system development.
+ *
+ * <p>This is <b>not</b> the main "logcat" debugging log ({@link android.util.Log})!
+ * These diagnostic events are for system integrators, not application authors.
+ *
+ * <p>Events use integer tag codes corresponding to /system/etc/event-log-tags.
+ * They carry a payload of one or more int, long, or String values. The
+ * event-log-tags file defines the payload contents for each type code.
+ */
+public class EventLog {
+ /** @hide */ public EventLog() {}
+
+ private static final String TAG = "EventLog";
+
+ private static final String TAGS_FILE = "/system/etc/event-log-tags";
+ private static final String COMMENT_PATTERN = "^\\s*(#.*)?$";
+ private static final String TAG_PATTERN = "^\\s*(\\d+)\\s+(\\w+)\\s*(\\(.*\\))?\\s*$";
+ private static HashMap<String, Integer> sTagCodes = null;
+ private static HashMap<Integer, String> sTagNames = null;
+
+ /** A previously logged event read from the logs. Instances are thread safe. */
+ public static final class Event {
+ private final ByteBuffer mBuffer;
+ private Exception mLastWtf;
+
+ // Layout of event log entry received from Android logger.
+ // see system/core/liblog/include/log/log_read.h
+ private static final int LENGTH_OFFSET = 0;
+ private static final int HEADER_SIZE_OFFSET = 2;
+ private static final int PROCESS_OFFSET = 4;
+ private static final int THREAD_OFFSET = 8;
+ private static final int SECONDS_OFFSET = 12;
+ private static final int NANOSECONDS_OFFSET = 16;
+ private static final int UID_OFFSET = 24;
+
+ // Layout for event log v1 format, v2 and v3 use HEADER_SIZE_OFFSET
+ private static final int V1_PAYLOAD_START = 20;
+ private static final int TAG_LENGTH = 4;
+
+ // Value types
+ private static final byte INT_TYPE = 0;
+ private static final byte LONG_TYPE = 1;
+ private static final byte STRING_TYPE = 2;
+ private static final byte LIST_TYPE = 3;
+ private static final byte FLOAT_TYPE = 4;
+
+ /** @param data containing event, read from the system */
+ @UnsupportedAppUsage
+ /*package*/ Event(byte[] data) {
+ mBuffer = ByteBuffer.wrap(data);
+ mBuffer.order(ByteOrder.nativeOrder());
+ }
+
+ /** @return the process ID which wrote the log entry */
+ public int getProcessId() {
+ return mBuffer.getInt(PROCESS_OFFSET);
+ }
+
+ /**
+ * @return the UID which wrote the log entry
+ * @hide
+ */
+ @SystemApi
+ public int getUid() {
+ try {
+ return mBuffer.getInt(UID_OFFSET);
+ } catch (IndexOutOfBoundsException e) {
+ // buffer won't contain the UID if the caller doesn't have permission.
+ return -1;
+ }
+ }
+
+ /** @return the thread ID which wrote the log entry */
+ public int getThreadId() {
+ return mBuffer.getInt(THREAD_OFFSET);
+ }
+
+ /** @return the wall clock time when the entry was written */
+ public long getTimeNanos() {
+ return mBuffer.getInt(SECONDS_OFFSET) * 1000000000l
+ + mBuffer.getInt(NANOSECONDS_OFFSET);
+ }
+
+ /** @return the type tag code of the entry */
+ public int getTag() {
+ return mBuffer.getInt(getHeaderSize());
+ }
+
+ private int getHeaderSize() {
+ int length = mBuffer.getShort(HEADER_SIZE_OFFSET);
+ if (length != 0) {
+ return length;
+ }
+ return V1_PAYLOAD_START;
+ }
+ /** @return one of Integer, Long, Float, String, null, or Object[] of same. */
+ public synchronized Object getData() {
+ try {
+ int offset = getHeaderSize();
+ mBuffer.limit(offset + mBuffer.getShort(LENGTH_OFFSET));
+ if ((offset + TAG_LENGTH) >= mBuffer.limit()) {
+ // no payload
+ return null;
+ }
+ mBuffer.position(offset + TAG_LENGTH); // Just after the tag.
+ return decodeObject();
+ } catch (IllegalArgumentException e) {
+ Log.wtf(TAG, "Illegal entry payload: tag=" + getTag(), e);
+ mLastWtf = e;
+ return null;
+ } catch (BufferUnderflowException e) {
+ Log.wtf(TAG, "Truncated entry payload: tag=" + getTag(), e);
+ mLastWtf = e;
+ return null;
+ }
+ }
+
+ /**
+ * Construct a new EventLog object from the current object, copying all log metadata
+ * but replacing the actual payload with the content provided.
+ * @hide
+ */
+ public Event withNewData(@Nullable Object object) {
+ byte[] payload = encodeObject(object);
+ if (payload.length > 65535 - TAG_LENGTH) {
+ throw new IllegalArgumentException("Payload too long");
+ }
+ int headerLength = getHeaderSize();
+ byte[] newBytes = new byte[headerLength + TAG_LENGTH + payload.length];
+ // Copy header (including the 4 bytes of tag integer at the beginning of payload)
+ System.arraycopy(mBuffer.array(), 0, newBytes, 0, headerLength + TAG_LENGTH);
+ // Fill in encoded objects
+ System.arraycopy(payload, 0, newBytes, headerLength + TAG_LENGTH, payload.length);
+ Event result = new Event(newBytes);
+ // Patch payload length in header
+ result.mBuffer.putShort(LENGTH_OFFSET, (short) (payload.length + TAG_LENGTH));
+ return result;
+ }
+
+ /** @return the loggable item at the current position in mBuffer. */
+ private Object decodeObject() {
+ byte type = mBuffer.get();
+ switch (type) {
+ case INT_TYPE:
+ return mBuffer.getInt();
+
+ case LONG_TYPE:
+ return mBuffer.getLong();
+
+ case FLOAT_TYPE:
+ return mBuffer.getFloat();
+
+ case STRING_TYPE:
+ try {
+ int length = mBuffer.getInt();
+ int start = mBuffer.position();
+ mBuffer.position(start + length);
+ return new String(mBuffer.array(), start, length, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ Log.wtf(TAG, "UTF-8 is not supported", e);
+ mLastWtf = e;
+ return null;
+ }
+
+ case LIST_TYPE:
+ int length = mBuffer.get();
+ if (length < 0) length += 256; // treat as signed byte
+ Object[] array = new Object[length];
+ for (int i = 0; i < length; ++i) array[i] = decodeObject();
+ return array;
+
+ default:
+ throw new IllegalArgumentException("Unknown entry type: " + type);
+ }
+ }
+
+ private static @NonNull byte[] encodeObject(@Nullable Object object) {
+ if (object == null) {
+ return new byte[0];
+ }
+ if (object instanceof Integer) {
+ return ByteBuffer.allocate(1 + 4)
+ .order(ByteOrder.nativeOrder())
+ .put(INT_TYPE)
+ .putInt((Integer) object)
+ .array();
+ } else if (object instanceof Long) {
+ return ByteBuffer.allocate(1 + 8)
+ .order(ByteOrder.nativeOrder())
+ .put(LONG_TYPE)
+ .putLong((Long) object)
+ .array();
+ } else if (object instanceof Float) {
+ return ByteBuffer.allocate(1 + 4)
+ .order(ByteOrder.nativeOrder())
+ .put(FLOAT_TYPE)
+ .putFloat((Float) object)
+ .array();
+ } else if (object instanceof String) {
+ String string = (String) object;
+ byte[] bytes;
+ try {
+ bytes = string.getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ bytes = new byte[0];
+ }
+ return ByteBuffer.allocate(1 + 4 + bytes.length)
+ .order(ByteOrder.nativeOrder())
+ .put(STRING_TYPE)
+ .putInt(bytes.length)
+ .put(bytes)
+ .array();
+ } else if (object instanceof Object[]) {
+ Object[] objects = (Object[]) object;
+ if (objects.length > 255) {
+ throw new IllegalArgumentException("Object array too long");
+ }
+ byte[][] bytes = new byte[objects.length][];
+ int totalLength = 0;
+ for (int i = 0; i < objects.length; i++) {
+ bytes[i] = encodeObject(objects[i]);
+ totalLength += bytes[i].length;
+ }
+ ByteBuffer buffer = ByteBuffer.allocate(1 + 1 + totalLength)
+ .order(ByteOrder.nativeOrder())
+ .put(LIST_TYPE)
+ .put((byte) objects.length);
+ for (int i = 0; i < objects.length; i++) {
+ buffer.put(bytes[i]);
+ }
+ return buffer.array();
+ } else {
+ throw new IllegalArgumentException("Unknown object type " + object);
+ }
+ }
+
+ /** @hide */
+ public static Event fromBytes(byte[] data) {
+ return new Event(data);
+ }
+
+ /** @hide */
+ public byte[] getBytes() {
+ byte[] bytes = mBuffer.array();
+ return Arrays.copyOf(bytes, bytes.length);
+ }
+
+ /**
+ * Retreive the last WTF error generated by this object.
+ * @hide
+ */
+ //VisibleForTesting
+ public Exception getLastError() {
+ return mLastWtf;
+ }
+
+ /**
+ * Clear the error state for this object.
+ * @hide
+ */
+ //VisibleForTesting
+ public void clearError() {
+ mLastWtf = null;
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public boolean equals(Object o) {
+ // Not using ByteBuffer.equals since it takes buffer position into account and we
+ // always use absolute positions here.
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Event other = (Event) o;
+ return Arrays.equals(mBuffer.array(), other.mBuffer.array());
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public int hashCode() {
+ // Not using ByteBuffer.hashCode since it takes buffer position into account and we
+ // always use absolute positions here.
+ return Arrays.hashCode(mBuffer.array());
+ }
+ }
+
+ // We assume that the native methods deal with any concurrency issues.
+
+ /**
+ * Record an event log message.
+ * @param tag The event type tag code
+ * @param value A value to log
+ * @return The number of bytes written
+ */
+ public static native int writeEvent(int tag, int value);
+
+ /**
+ * Record an event log message.
+ * @param tag The event type tag code
+ * @param value A value to log
+ * @return The number of bytes written
+ */
+ public static native int writeEvent(int tag, long value);
+
+ /**
+ * Record an event log message.
+ * @param tag The event type tag code
+ * @param value A value to log
+ * @return The number of bytes written
+ */
+ public static native int writeEvent(int tag, float value);
+
+ /**
+ * Record an event log message.
+ * @param tag The event type tag code
+ * @param str A value to log
+ * @return The number of bytes written
+ */
+ public static native int writeEvent(int tag, String str);
+
+ /**
+ * Record an event log message.
+ * @param tag The event type tag code
+ * @param list A list of values to log
+ * @return The number of bytes written
+ */
+ public static native int writeEvent(int tag, Object... list);
+
+ /**
+ * Read events from the log, filtered by type.
+ * @param tags to search for
+ * @param output container to add events into
+ * @throws IOException if something goes wrong reading events
+ */
+ public static native void readEvents(int[] tags, Collection<Event> output)
+ throws IOException;
+
+ /**
+ * Read events from the log, filtered by type, blocking until logs are about to be overwritten.
+ * @param tags to search for
+ * @param timestamp timestamp allow logs before this time to be overwritten.
+ * @param output container to add events into
+ * @throws IOException if something goes wrong reading events
+ * @hide
+ */
+ @SystemApi
+ public static native void readEventsOnWrapping(int[] tags, long timestamp,
+ Collection<Event> output)
+ throws IOException;
+
+ /**
+ * Get the name associated with an event type tag code.
+ * @param tag code to look up
+ * @return the name of the tag, or null if no tag has that number
+ */
+ public static String getTagName(int tag) {
+ readTagsFile();
+ return sTagNames.get(tag);
+ }
+
+ /**
+ * Get the event type tag code associated with an event name.
+ * @param name of event to look up
+ * @return the tag code, or -1 if no tag has that name
+ */
+ public static int getTagCode(String name) {
+ readTagsFile();
+ Integer code = sTagCodes.get(name);
+ return code != null ? code : -1;
+ }
+
+ /**
+ * Read TAGS_FILE, populating sTagCodes and sTagNames, if not already done.
+ */
+ private static synchronized void readTagsFile() {
+ if (sTagCodes != null && sTagNames != null) return;
+
+ sTagCodes = new HashMap<String, Integer>();
+ sTagNames = new HashMap<Integer, String>();
+
+ Pattern comment = Pattern.compile(COMMENT_PATTERN);
+ Pattern tag = Pattern.compile(TAG_PATTERN);
+ BufferedReader reader = null;
+ String line;
+
+ try {
+ reader = new BufferedReader(new FileReader(TAGS_FILE), 256);
+ while ((line = reader.readLine()) != null) {
+ if (comment.matcher(line).matches()) continue;
+
+ Matcher m = tag.matcher(line);
+ if (!m.matches()) {
+ Log.wtf(TAG, "Bad entry in " + TAGS_FILE + ": " + line);
+ continue;
+ }
+
+ try {
+ int num = Integer.parseInt(m.group(1));
+ String name = m.group(2);
+ sTagCodes.put(name, num);
+ sTagNames.put(num, name);
+ } catch (NumberFormatException e) {
+ Log.wtf(TAG, "Error in " + TAGS_FILE + ": " + line, e);
+ }
+ }
+ } catch (IOException e) {
+ Log.wtf(TAG, "Error reading " + TAGS_FILE, e);
+ // Leave the maps existing but unpopulated
+ } finally {
+ try { if (reader != null) reader.close(); } catch (IOException e) {}
+ }
+ }
+}
diff --git a/android/util/EventLogTags.java b/android/util/EventLogTags.java
new file mode 100644
index 0000000..f4ce4fd
--- /dev/null
+++ b/android/util/EventLogTags.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+
+/**
+ * @deprecated This class is no longer functional.
+ * Use {@link android.util.EventLog} instead.
+ */
+@Deprecated
+public class EventLogTags {
+ public static class Description {
+ public final int mTag;
+ public final String mName;
+
+ Description(int tag, String name) {
+ mTag = tag;
+ mName = name;
+ }
+ }
+
+ public EventLogTags() throws IOException {}
+
+ public EventLogTags(BufferedReader input) throws IOException {}
+
+ public Description get(String name) { return null; }
+
+ public Description get(int tag) { return null; }
+}
diff --git a/android/util/ExceptionUtils.java b/android/util/ExceptionUtils.java
new file mode 100644
index 0000000..1a397b3
--- /dev/null
+++ b/android/util/ExceptionUtils.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.ParcelableException;
+
+import com.android.internal.util.Preconditions;
+
+import java.io.IOException;
+
+/**
+ * Utility methods for proxying richer exceptions across Binder calls.
+ *
+ * @hide
+ */
+public class ExceptionUtils {
+ public static RuntimeException wrap(IOException e) {
+ throw new ParcelableException(e);
+ }
+
+ public static void maybeUnwrapIOException(RuntimeException e) throws IOException {
+ if (e instanceof ParcelableException) {
+ ((ParcelableException) e).maybeRethrow(IOException.class);
+ }
+ }
+
+ public static String getCompleteMessage(String msg, Throwable t) {
+ final StringBuilder builder = new StringBuilder();
+ if (msg != null) {
+ builder.append(msg).append(": ");
+ }
+ builder.append(t.getMessage());
+ while ((t = t.getCause()) != null) {
+ builder.append(": ").append(t.getMessage());
+ }
+ return builder.toString();
+ }
+
+ public static String getCompleteMessage(Throwable t) {
+ return getCompleteMessage(null, t);
+ }
+
+ public static <E extends Throwable> void propagateIfInstanceOf(
+ @Nullable Throwable t, Class<E> c) throws E {
+ if (t != null && c.isInstance(t)) {
+ throw c.cast(t);
+ }
+ }
+
+ /**
+ * @param <E> a checked exception that is ok to throw without wrapping
+ */
+ public static <E extends Exception> RuntimeException propagate(@NonNull Throwable t, Class<E> c)
+ throws E {
+ propagateIfInstanceOf(t, c);
+ return propagate(t);
+ }
+
+ public static RuntimeException propagate(@NonNull Throwable t) {
+ Preconditions.checkNotNull(t);
+ propagateIfInstanceOf(t, Error.class);
+ propagateIfInstanceOf(t, RuntimeException.class);
+ throw new RuntimeException(t);
+ }
+
+ /**
+ * Gets the root {@link Throwable#getCause() cause} of {@code t}
+ */
+ public static @NonNull Throwable getRootCause(@NonNull Throwable t) {
+ while (t.getCause() != null) t = t.getCause();
+ return t;
+ }
+
+ /**
+ * Appends {@code cause} at the end of the causal chain of {@code t}
+ *
+ * @return {@code t} for convenience
+ */
+ public static @NonNull Throwable appendCause(@NonNull Throwable t, @Nullable Throwable cause) {
+ if (cause != null) {
+ getRootCause(t).initCause(cause);
+ }
+ return t;
+ }
+}
\ No newline at end of file
diff --git a/android/util/FastImmutableArraySet.java b/android/util/FastImmutableArraySet.java
new file mode 100644
index 0000000..4175c60
--- /dev/null
+++ b/android/util/FastImmutableArraySet.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import java.util.AbstractSet;
+import java.util.Iterator;
+
+/**
+ * A fast immutable set wrapper for an array that is optimized for non-concurrent iteration.
+ * The same iterator instance is reused each time to avoid creating lots of garbage.
+ * Iterating over an array in this fashion is 2.5x faster than iterating over a {@link HashSet}
+ * so it is worth copying the contents of the set to an array when iterating over it
+ * hundreds of times.
+ * @hide
+ */
+public final class FastImmutableArraySet<T> extends AbstractSet<T> {
+ FastIterator<T> mIterator;
+ T[] mContents;
+
+ public FastImmutableArraySet(T[] contents) {
+ mContents = contents;
+ }
+
+ @Override
+ public Iterator<T> iterator() {
+ FastIterator<T> it = mIterator;
+ if (it == null) {
+ it = new FastIterator<T>(mContents);
+ mIterator = it;
+ } else {
+ it.mIndex = 0;
+ }
+ return it;
+ }
+
+ @Override
+ public int size() {
+ return mContents.length;
+ }
+
+ private static final class FastIterator<T> implements Iterator<T> {
+ private final T[] mContents;
+ int mIndex;
+
+ public FastIterator(T[] contents) {
+ mContents = contents;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return mIndex != mContents.length;
+ }
+
+ @Override
+ public T next() {
+ return mContents[mIndex++];
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+}
diff --git a/android/util/FeatureFlagUtils.java b/android/util/FeatureFlagUtils.java
new file mode 100644
index 0000000..cd20b35
--- /dev/null
+++ b/android/util/FeatureFlagUtils.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.annotation.TestApi;
+import android.content.Context;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import android.text.TextUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Util class to get feature flag information.
+ *
+ * @hide
+ */
+@TestApi
+public class FeatureFlagUtils {
+
+ public static final String FFLAG_PREFIX = "sys.fflag.";
+ public static final String FFLAG_OVERRIDE_PREFIX = FFLAG_PREFIX + "override.";
+ public static final String PERSIST_PREFIX = "persist." + FFLAG_OVERRIDE_PREFIX;
+ public static final String SEAMLESS_TRANSFER = "settings_seamless_transfer";
+ public static final String HEARING_AID_SETTINGS = "settings_bluetooth_hearing_aid";
+ public static final String SCREENRECORD_LONG_PRESS = "settings_screenrecord_long_press";
+ public static final String DYNAMIC_SYSTEM = "settings_dynamic_system";
+ public static final String SETTINGS_WIFITRACKER2 = "settings_wifitracker2";
+ public static final String SETTINGS_FUSE_FLAG = "settings_fuse";
+ /** @hide */
+ public static final String SETTINGS_DO_NOT_RESTORE_PRESERVED =
+ "settings_do_not_restore_preserved";
+ /** @hide */
+ public static final String SETTINGS_SCHEDULES_FLAG = "settings_schedules";
+
+ private static final Map<String, String> DEFAULT_FLAGS;
+
+ static {
+ DEFAULT_FLAGS = new HashMap<>();
+ DEFAULT_FLAGS.put("settings_audio_switcher", "true");
+ DEFAULT_FLAGS.put("settings_systemui_theme", "true");
+ DEFAULT_FLAGS.put(SETTINGS_FUSE_FLAG, "true");
+ DEFAULT_FLAGS.put(DYNAMIC_SYSTEM, "false");
+ DEFAULT_FLAGS.put(SEAMLESS_TRANSFER, "false");
+ DEFAULT_FLAGS.put(HEARING_AID_SETTINGS, "false");
+ DEFAULT_FLAGS.put(SCREENRECORD_LONG_PRESS, "false");
+ DEFAULT_FLAGS.put("settings_wifi_details_datausage_header", "false");
+ DEFAULT_FLAGS.put("settings_skip_direction_mutable", "true");
+ DEFAULT_FLAGS.put(SETTINGS_WIFITRACKER2, "true");
+ DEFAULT_FLAGS.put("settings_controller_loading_enhancement", "false");
+ DEFAULT_FLAGS.put("settings_conditionals", "false");
+ // This flags guards a feature introduced in R and will be removed in the next release
+ // (b/148367230).
+ DEFAULT_FLAGS.put(SETTINGS_DO_NOT_RESTORE_PRESERVED, "true");
+
+ DEFAULT_FLAGS.put("settings_tether_all_in_one", "false");
+ DEFAULT_FLAGS.put(SETTINGS_SCHEDULES_FLAG, "false");
+ DEFAULT_FLAGS.put("settings_contextual_home2", "true");
+ }
+
+ /**
+ * Whether or not a flag is enabled.
+ *
+ * @param feature the flag name
+ * @return true if the flag is enabled (either by default in system, or override by user)
+ */
+ public static boolean isEnabled(Context context, String feature) {
+ // Override precedence:
+ // Settings.Global -> sys.fflag.override.* -> static list
+
+ // Step 1: check if feature flag is set in Settings.Global.
+ String value;
+ if (context != null) {
+ value = Settings.Global.getString(context.getContentResolver(), feature);
+ if (!TextUtils.isEmpty(value)) {
+ return Boolean.parseBoolean(value);
+ }
+ }
+
+ // Step 2: check if feature flag has any override. Flag name: sys.fflag.override.<feature>
+ value = SystemProperties.get(FFLAG_OVERRIDE_PREFIX + feature);
+ if (!TextUtils.isEmpty(value)) {
+ return Boolean.parseBoolean(value);
+ }
+ // Step 3: check if feature flag has any default value.
+ value = getAllFeatureFlags().get(feature);
+ return Boolean.parseBoolean(value);
+ }
+
+ /**
+ * Override feature flag to new state.
+ */
+ public static void setEnabled(Context context, String feature, boolean enabled) {
+ SystemProperties.set(FFLAG_OVERRIDE_PREFIX + feature, enabled ? "true" : "false");
+ }
+
+ /**
+ * Returns all feature flags in their raw form.
+ */
+ public static Map<String, String> getAllFeatureFlags() {
+ return DEFAULT_FLAGS;
+ }
+}
diff --git a/android/util/FloatMath.java b/android/util/FloatMath.java
new file mode 100644
index 0000000..bb7d15f
--- /dev/null
+++ b/android/util/FloatMath.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * Math routines similar to those found in {@link java.lang.Math}.
+ *
+ * <p>Historically these methods were faster than the equivalent double-based
+ * {@link java.lang.Math} methods. On versions of Android with a JIT they
+ * became slower and have since been re-implemented to wrap calls to
+ * {@link java.lang.Math}. {@link java.lang.Math} should be used in
+ * preference.
+ *
+ * <p>All methods were removed from the public API in version 23.
+ *
+ * @deprecated Use {@link java.lang.Math} instead.
+ */
+@Deprecated
+public class FloatMath {
+
+ /** Prevents instantiation. */
+ private FloatMath() {}
+
+ /**
+ * Returns the float conversion of the most positive (i.e. closest to
+ * positive infinity) integer value which is less than the argument.
+ *
+ * @param value to be converted
+ * @return the floor of value
+ * @removed
+ */
+ public static float floor(float value) {
+ return (float) Math.floor(value);
+ }
+
+ /**
+ * Returns the float conversion of the most negative (i.e. closest to
+ * negative infinity) integer value which is greater than the argument.
+ *
+ * @param value to be converted
+ * @return the ceiling of value
+ * @removed
+ */
+ public static float ceil(float value) {
+ return (float) Math.ceil(value);
+ }
+
+ /**
+ * Returns the closest float approximation of the sine of the argument.
+ *
+ * @param angle to compute the cosine of, in radians
+ * @return the sine of angle
+ * @removed
+ */
+ public static float sin(float angle) {
+ return (float) Math.sin(angle);
+ }
+
+ /**
+ * Returns the closest float approximation of the cosine of the argument.
+ *
+ * @param angle to compute the cosine of, in radians
+ * @return the cosine of angle
+ * @removed
+ */
+ public static float cos(float angle) {
+ return (float) Math.cos(angle);
+ }
+
+ /**
+ * Returns the closest float approximation of the square root of the
+ * argument.
+ *
+ * @param value to compute sqrt of
+ * @return the square root of value
+ * @removed
+ */
+ public static float sqrt(float value) {
+ return (float) Math.sqrt(value);
+ }
+
+ /**
+ * Returns the closest float approximation of the raising "e" to the power
+ * of the argument.
+ *
+ * @param value to compute the exponential of
+ * @return the exponential of value
+ * @removed
+ */
+ public static float exp(float value) {
+ return (float) Math.exp(value);
+ }
+
+ /**
+ * Returns the closest float approximation of the result of raising {@code
+ * x} to the power of {@code y}.
+ *
+ * @param x the base of the operation.
+ * @param y the exponent of the operation.
+ * @return {@code x} to the power of {@code y}.
+ * @removed
+ */
+ public static float pow(float x, float y) {
+ return (float) Math.pow(x, y);
+ }
+
+ /**
+ * Returns {@code sqrt(}<i>{@code x}</i><sup>{@code 2}</sup>{@code +} <i>
+ * {@code y}</i><sup>{@code 2}</sup>{@code )}.
+ *
+ * @param x a float number
+ * @param y a float number
+ * @return the hypotenuse
+ * @removed
+ */
+ public static float hypot(float x, float y) {
+ return (float) Math.hypot(x, y);
+ }
+}
diff --git a/android/util/FloatProperty.java b/android/util/FloatProperty.java
new file mode 100644
index 0000000..4aac196
--- /dev/null
+++ b/android/util/FloatProperty.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.util;
+
+/**
+ * An implementation of {@link android.util.Property} to be used specifically with fields of type
+ * <code>float</code>. This type-specific subclass enables performance benefit by allowing
+ * calls to a {@link #setValue(Object, float) setValue()} function that takes the primitive
+ * <code>float</code> type and avoids autoboxing and other overhead associated with the
+ * <code>Float</code> class.
+ *
+ * @param <T> The class on which the Property is declared.
+ */
+public abstract class FloatProperty<T> extends Property<T, Float> {
+
+ public FloatProperty(String name) {
+ super(Float.class, name);
+ }
+
+ /**
+ * A type-specific variant of {@link #set(Object, Float)} that is faster when dealing
+ * with fields of type <code>float</code>.
+ */
+ public abstract void setValue(T object, float value);
+
+ @Override
+ final public void set(T object, Float value) {
+ setValue(object, value);
+ }
+
+}
\ No newline at end of file
diff --git a/android/util/Half.java b/android/util/Half.java
new file mode 100644
index 0000000..fe536a6
--- /dev/null
+++ b/android/util/Half.java
@@ -0,0 +1,904 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.annotation.HalfFloat;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import libcore.util.FP16;
+
+/**
+ * <p>The {@code Half} class is a wrapper and a utility class to manipulate half-precision 16-bit
+ * <a href="https://en.wikipedia.org/wiki/Half-precision_floating-point_format">IEEE 754</a>
+ * floating point data types (also called fp16 or binary16). A half-precision float can be
+ * created from or converted to single-precision floats, and is stored in a short data type.
+ * To distinguish short values holding half-precision floats from regular short values,
+ * it is recommended to use the <code>@HalfFloat</code> annotation.</p>
+ *
+ * <p>The IEEE 754 standard specifies an fp16 as having the following format:</p>
+ * <ul>
+ * <li>Sign bit: 1 bit</li>
+ * <li>Exponent width: 5 bits</li>
+ * <li>Significand: 10 bits</li>
+ * </ul>
+ *
+ * <p>The format is laid out as follows:</p>
+ * <pre>
+ * 1 11111 1111111111
+ * ^ --^-- -----^----
+ * sign | |_______ significand
+ * |
+ * -- exponent
+ * </pre>
+ *
+ * <p>Half-precision floating points can be useful to save memory and/or
+ * bandwidth at the expense of range and precision when compared to single-precision
+ * floating points (fp32).</p>
+ * <p>To help you decide whether fp16 is the right storage type for you need, please
+ * refer to the table below that shows the available precision throughout the range of
+ * possible values. The <em>precision</em> column indicates the step size between two
+ * consecutive numbers in a specific part of the range.</p>
+ *
+ * <table summary="Precision of fp16 across the range">
+ * <tr><th>Range start</th><th>Precision</th></tr>
+ * <tr><td>0</td><td>1 ⁄ 16,777,216</td></tr>
+ * <tr><td>1 ⁄ 16,384</td><td>1 ⁄ 16,777,216</td></tr>
+ * <tr><td>1 ⁄ 8,192</td><td>1 ⁄ 8,388,608</td></tr>
+ * <tr><td>1 ⁄ 4,096</td><td>1 ⁄ 4,194,304</td></tr>
+ * <tr><td>1 ⁄ 2,048</td><td>1 ⁄ 2,097,152</td></tr>
+ * <tr><td>1 ⁄ 1,024</td><td>1 ⁄ 1,048,576</td></tr>
+ * <tr><td>1 ⁄ 512</td><td>1 ⁄ 524,288</td></tr>
+ * <tr><td>1 ⁄ 256</td><td>1 ⁄ 262,144</td></tr>
+ * <tr><td>1 ⁄ 128</td><td>1 ⁄ 131,072</td></tr>
+ * <tr><td>1 ⁄ 64</td><td>1 ⁄ 65,536</td></tr>
+ * <tr><td>1 ⁄ 32</td><td>1 ⁄ 32,768</td></tr>
+ * <tr><td>1 ⁄ 16</td><td>1 ⁄ 16,384</td></tr>
+ * <tr><td>1 ⁄ 8</td><td>1 ⁄ 8,192</td></tr>
+ * <tr><td>1 ⁄ 4</td><td>1 ⁄ 4,096</td></tr>
+ * <tr><td>1 ⁄ 2</td><td>1 ⁄ 2,048</td></tr>
+ * <tr><td>1</td><td>1 ⁄ 1,024</td></tr>
+ * <tr><td>2</td><td>1 ⁄ 512</td></tr>
+ * <tr><td>4</td><td>1 ⁄ 256</td></tr>
+ * <tr><td>8</td><td>1 ⁄ 128</td></tr>
+ * <tr><td>16</td><td>1 ⁄ 64</td></tr>
+ * <tr><td>32</td><td>1 ⁄ 32</td></tr>
+ * <tr><td>64</td><td>1 ⁄ 16</td></tr>
+ * <tr><td>128</td><td>1 ⁄ 8</td></tr>
+ * <tr><td>256</td><td>1 ⁄ 4</td></tr>
+ * <tr><td>512</td><td>1 ⁄ 2</td></tr>
+ * <tr><td>1,024</td><td>1</td></tr>
+ * <tr><td>2,048</td><td>2</td></tr>
+ * <tr><td>4,096</td><td>4</td></tr>
+ * <tr><td>8,192</td><td>8</td></tr>
+ * <tr><td>16,384</td><td>16</td></tr>
+ * <tr><td>32,768</td><td>32</td></tr>
+ * </table>
+ *
+ * <p>This table shows that numbers higher than 1024 lose all fractional precision.</p>
+ */
+@SuppressWarnings("SimplifiableIfStatement")
+public final class Half extends Number implements Comparable<Half> {
+ /**
+ * The number of bits used to represent a half-precision float value.
+ */
+ public static final int SIZE = 16;
+
+ /**
+ * Epsilon is the difference between 1.0 and the next value representable
+ * by a half-precision floating-point.
+ */
+ public static final @HalfFloat short EPSILON = (short) 0x1400;
+
+ /**
+ * Maximum exponent a finite half-precision float may have.
+ */
+ public static final int MAX_EXPONENT = 15;
+ /**
+ * Minimum exponent a normalized half-precision float may have.
+ */
+ public static final int MIN_EXPONENT = -14;
+
+ /**
+ * Smallest negative value a half-precision float may have.
+ */
+ public static final @HalfFloat short LOWEST_VALUE = (short) 0xfbff;
+ /**
+ * Maximum positive finite value a half-precision float may have.
+ */
+ public static final @HalfFloat short MAX_VALUE = (short) 0x7bff;
+ /**
+ * Smallest positive normal value a half-precision float may have.
+ */
+ public static final @HalfFloat short MIN_NORMAL = (short) 0x0400;
+ /**
+ * Smallest positive non-zero value a half-precision float may have.
+ */
+ public static final @HalfFloat short MIN_VALUE = (short) 0x0001;
+ /**
+ * A Not-a-Number representation of a half-precision float.
+ */
+ public static final @HalfFloat short NaN = (short) 0x7e00;
+ /**
+ * Negative infinity of type half-precision float.
+ */
+ public static final @HalfFloat short NEGATIVE_INFINITY = (short) 0xfc00;
+ /**
+ * Negative 0 of type half-precision float.
+ */
+ public static final @HalfFloat short NEGATIVE_ZERO = (short) 0x8000;
+ /**
+ * Positive infinity of type half-precision float.
+ */
+ public static final @HalfFloat short POSITIVE_INFINITY = (short) 0x7c00;
+ /**
+ * Positive 0 of type half-precision float.
+ */
+ public static final @HalfFloat short POSITIVE_ZERO = (short) 0x0000;
+
+ private final @HalfFloat short mValue;
+
+ /**
+ * Constructs a newly allocated {@code Half} object that represents the
+ * half-precision float type argument.
+ *
+ * @param value The value to be represented by the {@code Half}
+ */
+ public Half(@HalfFloat short value) {
+ mValue = value;
+ }
+
+ /**
+ * Constructs a newly allocated {@code Half} object that represents the
+ * argument converted to a half-precision float.
+ *
+ * @param value The value to be represented by the {@code Half}
+ *
+ * @see #toHalf(float)
+ */
+ public Half(float value) {
+ mValue = toHalf(value);
+ }
+
+ /**
+ * Constructs a newly allocated {@code Half} object that
+ * represents the argument converted to a half-precision float.
+ *
+ * @param value The value to be represented by the {@code Half}
+ *
+ * @see #toHalf(float)
+ */
+ public Half(double value) {
+ mValue = toHalf((float) value);
+ }
+
+ /**
+ * <p>Constructs a newly allocated {@code Half} object that represents the
+ * half-precision float value represented by the string.
+ * The string is converted to a half-precision float value as if by the
+ * {@link #valueOf(String)} method.</p>
+ *
+ * <p>Calling this constructor is equivalent to calling:</p>
+ * <pre>
+ * new Half(Float.parseFloat(value))
+ * </pre>
+ *
+ * @param value A string to be converted to a {@code Half}
+ * @throws NumberFormatException if the string does not contain a parsable number
+ *
+ * @see Float#valueOf(java.lang.String)
+ * @see #toHalf(float)
+ */
+ public Half(@NonNull String value) throws NumberFormatException {
+ mValue = toHalf(Float.parseFloat(value));
+ }
+
+ /**
+ * Returns the half-precision value of this {@code Half} as a {@code short}
+ * containing the bit representation described in {@link Half}.
+ *
+ * @return The half-precision float value represented by this object
+ */
+ public @HalfFloat short halfValue() {
+ return mValue;
+ }
+
+ /**
+ * Returns the value of this {@code Half} as a {@code byte} after
+ * a narrowing primitive conversion.
+ *
+ * @return The half-precision float value represented by this object
+ * converted to type {@code byte}
+ */
+ @Override
+ public byte byteValue() {
+ return (byte) toFloat(mValue);
+ }
+
+ /**
+ * Returns the value of this {@code Half} as a {@code short} after
+ * a narrowing primitive conversion.
+ *
+ * @return The half-precision float value represented by this object
+ * converted to type {@code short}
+ */
+ @Override
+ public short shortValue() {
+ return (short) toFloat(mValue);
+ }
+
+ /**
+ * Returns the value of this {@code Half} as a {@code int} after
+ * a narrowing primitive conversion.
+ *
+ * @return The half-precision float value represented by this object
+ * converted to type {@code int}
+ */
+ @Override
+ public int intValue() {
+ return (int) toFloat(mValue);
+ }
+
+ /**
+ * Returns the value of this {@code Half} as a {@code long} after
+ * a narrowing primitive conversion.
+ *
+ * @return The half-precision float value represented by this object
+ * converted to type {@code long}
+ */
+ @Override
+ public long longValue() {
+ return (long) toFloat(mValue);
+ }
+
+ /**
+ * Returns the value of this {@code Half} as a {@code float} after
+ * a widening primitive conversion.
+ *
+ * @return The half-precision float value represented by this object
+ * converted to type {@code float}
+ */
+ @Override
+ public float floatValue() {
+ return toFloat(mValue);
+ }
+
+ /**
+ * Returns the value of this {@code Half} as a {@code double} after
+ * a widening primitive conversion.
+ *
+ * @return The half-precision float value represented by this object
+ * converted to type {@code double}
+ */
+ @Override
+ public double doubleValue() {
+ return toFloat(mValue);
+ }
+
+ /**
+ * Returns true if this {@code Half} value represents a Not-a-Number,
+ * false otherwise.
+ *
+ * @return True if the value is a NaN, false otherwise
+ */
+ public boolean isNaN() {
+ return isNaN(mValue);
+ }
+
+ /**
+ * Compares this object against the specified object. The result is {@code true}
+ * if and only if the argument is not {@code null} and is a {@code Half} object
+ * that represents the same half-precision value as the this object. Two
+ * half-precision values are considered to be the same if and only if the method
+ * {@link #halfToIntBits(short)} returns an identical {@code int} value for both.
+ *
+ * @param o The object to compare
+ * @return True if the objects are the same, false otherwise
+ *
+ * @see #halfToIntBits(short)
+ */
+ @Override
+ public boolean equals(@Nullable Object o) {
+ return (o instanceof Half) &&
+ (halfToIntBits(((Half) o).mValue) == halfToIntBits(mValue));
+ }
+
+ /**
+ * Returns a hash code for this {@code Half} object. The result is the
+ * integer bit representation, exactly as produced by the method
+ * {@link #halfToIntBits(short)}, of the primitive half-precision float
+ * value represented by this {@code Half} object.
+ *
+ * @return A hash code value for this object
+ */
+ @Override
+ public int hashCode() {
+ return hashCode(mValue);
+ }
+
+ /**
+ * Returns a string representation of the specified half-precision
+ * float value. See {@link #toString(short)} for more information.
+ *
+ * @return A string representation of this {@code Half} object
+ */
+ @NonNull
+ @Override
+ public String toString() {
+ return toString(mValue);
+ }
+
+ /**
+ * <p>Compares the two specified half-precision float values. The following
+ * conditions apply during the comparison:</p>
+ *
+ * <ul>
+ * <li>{@link #NaN} is considered by this method to be equal to itself and greater
+ * than all other half-precision float values (including {@code #POSITIVE_INFINITY})</li>
+ * <li>{@link #POSITIVE_ZERO} is considered by this method to be greater than
+ * {@link #NEGATIVE_ZERO}.</li>
+ * </ul>
+ *
+ * @param h The half-precision float value to compare to the half-precision value
+ * represented by this {@code Half} object
+ *
+ * @return The value {@code 0} if {@code x} is numerically equal to {@code y}; a
+ * value less than {@code 0} if {@code x} is numerically less than {@code y};
+ * and a value greater than {@code 0} if {@code x} is numerically greater
+ * than {@code y}
+ */
+ @Override
+ public int compareTo(@NonNull Half h) {
+ return compare(mValue, h.mValue);
+ }
+
+ /**
+ * Returns a hash code for a half-precision float value.
+ *
+ * @param h The value to hash
+ *
+ * @return A hash code value for a half-precision float value
+ */
+ public static int hashCode(@HalfFloat short h) {
+ return halfToIntBits(h);
+ }
+
+ /**
+ * <p>Compares the two specified half-precision float values. The following
+ * conditions apply during the comparison:</p>
+ *
+ * <ul>
+ * <li>{@link #NaN} is considered by this method to be equal to itself and greater
+ * than all other half-precision float values (including {@code #POSITIVE_INFINITY})</li>
+ * <li>{@link #POSITIVE_ZERO} is considered by this method to be greater than
+ * {@link #NEGATIVE_ZERO}.</li>
+ * </ul>
+ *
+ * @param x The first half-precision float value to compare.
+ * @param y The second half-precision float value to compare
+ *
+ * @return The value {@code 0} if {@code x} is numerically equal to {@code y}, a
+ * value less than {@code 0} if {@code x} is numerically less than {@code y},
+ * and a value greater than {@code 0} if {@code x} is numerically greater
+ * than {@code y}
+ */
+ public static int compare(@HalfFloat short x, @HalfFloat short y) {
+ return FP16.compare(x, y);
+ }
+
+ /**
+ * <p>Returns a representation of the specified half-precision float value
+ * according to the bit layout described in {@link Half}.</p>
+ *
+ * <p>Similar to {@link #halfToIntBits(short)}, this method collapses all
+ * possible Not-a-Number values to a single canonical Not-a-Number value
+ * defined by {@link #NaN}.</p>
+ *
+ * @param h A half-precision float value
+ * @return The bits that represent the half-precision float value
+ *
+ * @see #halfToIntBits(short)
+ */
+ public static @HalfFloat short halfToShortBits(@HalfFloat short h) {
+ return (h & FP16.EXPONENT_SIGNIFICAND_MASK) > FP16.POSITIVE_INFINITY ? NaN : h;
+ }
+
+ /**
+ * <p>Returns a representation of the specified half-precision float value
+ * according to the bit layout described in {@link Half}.</p>
+ *
+ * <p>Unlike {@link #halfToRawIntBits(short)}, this method collapses all
+ * possible Not-a-Number values to a single canonical Not-a-Number value
+ * defined by {@link #NaN}.</p>
+ *
+ * @param h A half-precision float value
+ * @return The bits that represent the half-precision float value
+ *
+ * @see #halfToRawIntBits(short)
+ * @see #halfToShortBits(short)
+ * @see #intBitsToHalf(int)
+ */
+ public static int halfToIntBits(@HalfFloat short h) {
+ return (h & FP16.EXPONENT_SIGNIFICAND_MASK) > FP16.POSITIVE_INFINITY ? NaN : h & 0xffff;
+ }
+
+ /**
+ * <p>Returns a representation of the specified half-precision float value
+ * according to the bit layout described in {@link Half}.</p>
+ *
+ * <p>The argument is considered to be a representation of a half-precision
+ * float value according to the bit layout described in {@link Half}. The 16
+ * most significant bits of the returned value are set to 0.</p>
+ *
+ * @param h A half-precision float value
+ * @return The bits that represent the half-precision float value
+ *
+ * @see #halfToIntBits(short)
+ * @see #intBitsToHalf(int)
+ */
+ public static int halfToRawIntBits(@HalfFloat short h) {
+ return h & 0xffff;
+ }
+
+ /**
+ * <p>Returns the half-precision float value corresponding to a given
+ * bit representation.</p>
+ *
+ * <p>The argument is considered to be a representation of a half-precision
+ * float value according to the bit layout described in {@link Half}. The 16
+ * most significant bits of the argument are ignored.</p>
+ *
+ * @param bits An integer
+ * @return The half-precision float value with the same bit pattern
+ */
+ public static @HalfFloat short intBitsToHalf(int bits) {
+ return (short) (bits & 0xffff);
+ }
+
+ /**
+ * Returns the first parameter with the sign of the second parameter.
+ * This method treats NaNs as having a sign.
+ *
+ * @param magnitude A half-precision float value providing the magnitude of the result
+ * @param sign A half-precision float value providing the sign of the result
+ * @return A value with the magnitude of the first parameter and the sign
+ * of the second parameter
+ */
+ public static @HalfFloat short copySign(@HalfFloat short magnitude, @HalfFloat short sign) {
+ return (short) ((sign & FP16.SIGN_MASK) | (magnitude & FP16.EXPONENT_SIGNIFICAND_MASK));
+ }
+
+ /**
+ * Returns the absolute value of the specified half-precision float.
+ * Special values are handled in the following ways:
+ * <ul>
+ * <li>If the specified half-precision float is NaN, the result is NaN</li>
+ * <li>If the specified half-precision float is zero (negative or positive),
+ * the result is positive zero (see {@link #POSITIVE_ZERO})</li>
+ * <li>If the specified half-precision float is infinity (negative or positive),
+ * the result is positive infinity (see {@link #POSITIVE_INFINITY})</li>
+ * </ul>
+ *
+ * @param h A half-precision float value
+ * @return The absolute value of the specified half-precision float
+ */
+ public static @HalfFloat short abs(@HalfFloat short h) {
+ return (short) (h & FP16.EXPONENT_SIGNIFICAND_MASK);
+ }
+
+ /**
+ * Returns the closest integral half-precision float value to the specified
+ * half-precision float value. Special values are handled in the
+ * following ways:
+ * <ul>
+ * <li>If the specified half-precision float is NaN, the result is NaN</li>
+ * <li>If the specified half-precision float is infinity (negative or positive),
+ * the result is infinity (with the same sign)</li>
+ * <li>If the specified half-precision float is zero (negative or positive),
+ * the result is zero (with the same sign)</li>
+ * </ul>
+ *
+ * <p class=note>
+ * <strong>Note:</strong> Unlike the identically named
+ * <code class=prettyprint>int java.lang.Math.round(float)</code> method,
+ * this returns a Half value stored in a short, <strong>not</strong> an
+ * actual short integer result.
+ *
+ * @param h A half-precision float value
+ * @return The value of the specified half-precision float rounded to the nearest
+ * half-precision float value
+ */
+ public static @HalfFloat short round(@HalfFloat short h) {
+ return FP16.rint(h);
+ }
+
+ /**
+ * Returns the smallest half-precision float value toward negative infinity
+ * greater than or equal to the specified half-precision float value.
+ * Special values are handled in the following ways:
+ * <ul>
+ * <li>If the specified half-precision float is NaN, the result is NaN</li>
+ * <li>If the specified half-precision float is infinity (negative or positive),
+ * the result is infinity (with the same sign)</li>
+ * <li>If the specified half-precision float is zero (negative or positive),
+ * the result is zero (with the same sign)</li>
+ * </ul>
+ *
+ * @param h A half-precision float value
+ * @return The smallest half-precision float value toward negative infinity
+ * greater than or equal to the specified half-precision float value
+ */
+ public static @HalfFloat short ceil(@HalfFloat short h) {
+ return FP16.ceil(h);
+ }
+
+ /**
+ * Returns the largest half-precision float value toward positive infinity
+ * less than or equal to the specified half-precision float value.
+ * Special values are handled in the following ways:
+ * <ul>
+ * <li>If the specified half-precision float is NaN, the result is NaN</li>
+ * <li>If the specified half-precision float is infinity (negative or positive),
+ * the result is infinity (with the same sign)</li>
+ * <li>If the specified half-precision float is zero (negative or positive),
+ * the result is zero (with the same sign)</li>
+ * </ul>
+ *
+ * @param h A half-precision float value
+ * @return The largest half-precision float value toward positive infinity
+ * less than or equal to the specified half-precision float value
+ */
+ public static @HalfFloat short floor(@HalfFloat short h) {
+ return FP16.floor(h);
+ }
+
+ /**
+ * Returns the truncated half-precision float value of the specified
+ * half-precision float value. Special values are handled in the following ways:
+ * <ul>
+ * <li>If the specified half-precision float is NaN, the result is NaN</li>
+ * <li>If the specified half-precision float is infinity (negative or positive),
+ * the result is infinity (with the same sign)</li>
+ * <li>If the specified half-precision float is zero (negative or positive),
+ * the result is zero (with the same sign)</li>
+ * </ul>
+ *
+ * @param h A half-precision float value
+ * @return The truncated half-precision float value of the specified
+ * half-precision float value
+ */
+ public static @HalfFloat short trunc(@HalfFloat short h) {
+ return FP16.trunc(h);
+ }
+
+ /**
+ * Returns the smaller of two half-precision float values (the value closest
+ * to negative infinity). Special values are handled in the following ways:
+ * <ul>
+ * <li>If either value is NaN, the result is NaN</li>
+ * <li>{@link #NEGATIVE_ZERO} is smaller than {@link #POSITIVE_ZERO}</li>
+ * </ul>
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ * @return The smaller of the two specified half-precision values
+ */
+ public static @HalfFloat short min(@HalfFloat short x, @HalfFloat short y) {
+ return FP16.min(x, y);
+ }
+
+ /**
+ * Returns the larger of two half-precision float values (the value closest
+ * to positive infinity). Special values are handled in the following ways:
+ * <ul>
+ * <li>If either value is NaN, the result is NaN</li>
+ * <li>{@link #POSITIVE_ZERO} is greater than {@link #NEGATIVE_ZERO}</li>
+ * </ul>
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ *
+ * @return The larger of the two specified half-precision values
+ */
+ public static @HalfFloat short max(@HalfFloat short x, @HalfFloat short y) {
+ return FP16.max(x, y);
+ }
+
+ /**
+ * Returns true if the first half-precision float value is less (smaller
+ * toward negative infinity) than the second half-precision float value.
+ * If either of the values is NaN, the result is false.
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ *
+ * @return True if x is less than y, false otherwise
+ */
+ public static boolean less(@HalfFloat short x, @HalfFloat short y) {
+ return FP16.less(x, y);
+ }
+
+ /**
+ * Returns true if the first half-precision float value is less (smaller
+ * toward negative infinity) than or equal to the second half-precision
+ * float value. If either of the values is NaN, the result is false.
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ *
+ * @return True if x is less than or equal to y, false otherwise
+ */
+ public static boolean lessEquals(@HalfFloat short x, @HalfFloat short y) {
+ return FP16.lessEquals(x, y);
+ }
+
+ /**
+ * Returns true if the first half-precision float value is greater (larger
+ * toward positive infinity) than the second half-precision float value.
+ * If either of the values is NaN, the result is false.
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ *
+ * @return True if x is greater than y, false otherwise
+ */
+ public static boolean greater(@HalfFloat short x, @HalfFloat short y) {
+ return FP16.greater(x, y);
+ }
+
+ /**
+ * Returns true if the first half-precision float value is greater (larger
+ * toward positive infinity) than or equal to the second half-precision float
+ * value. If either of the values is NaN, the result is false.
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ *
+ * @return True if x is greater than y, false otherwise
+ */
+ public static boolean greaterEquals(@HalfFloat short x, @HalfFloat short y) {
+ return FP16.greaterEquals(x, y);
+ }
+
+ /**
+ * Returns true if the two half-precision float values are equal.
+ * If either of the values is NaN, the result is false. {@link #POSITIVE_ZERO}
+ * and {@link #NEGATIVE_ZERO} are considered equal.
+ *
+ * @param x The first half-precision value
+ * @param y The second half-precision value
+ *
+ * @return True if x is equal to y, false otherwise
+ */
+ public static boolean equals(@HalfFloat short x, @HalfFloat short y) {
+ return FP16.equals(x, y);
+ }
+
+ /**
+ * Returns the sign of the specified half-precision float.
+ *
+ * @param h A half-precision float value
+ * @return 1 if the value is positive, -1 if the value is negative
+ */
+ public static int getSign(@HalfFloat short h) {
+ return (h & FP16.SIGN_MASK) == 0 ? 1 : -1;
+ }
+
+ /**
+ * Returns the unbiased exponent used in the representation of
+ * the specified half-precision float value. if the value is NaN
+ * or infinite, this* method returns {@link #MAX_EXPONENT} + 1.
+ * If the argument is 0 or a subnormal representation, this method
+ * returns {@link #MIN_EXPONENT} - 1.
+ *
+ * @param h A half-precision float value
+ * @return The unbiased exponent of the specified value
+ */
+ public static int getExponent(@HalfFloat short h) {
+ return ((h >>> FP16.EXPONENT_SHIFT) & FP16.SHIFTED_EXPONENT_MASK) - FP16.EXPONENT_BIAS;
+ }
+
+ /**
+ * Returns the significand, or mantissa, used in the representation
+ * of the specified half-precision float value.
+ *
+ * @param h A half-precision float value
+ * @return The significand, or significand, of the specified vlaue
+ */
+ public static int getSignificand(@HalfFloat short h) {
+ return h & FP16.SIGNIFICAND_MASK;
+ }
+
+ /**
+ * Returns true if the specified half-precision float value represents
+ * infinity, false otherwise.
+ *
+ * @param h A half-precision float value
+ * @return True if the value is positive infinity or negative infinity,
+ * false otherwise
+ */
+ public static boolean isInfinite(@HalfFloat short h) {
+ return FP16.isInfinite(h);
+ }
+
+ /**
+ * Returns true if the specified half-precision float value represents
+ * a Not-a-Number, false otherwise.
+ *
+ * @param h A half-precision float value
+ * @return True if the value is a NaN, false otherwise
+ */
+ public static boolean isNaN(@HalfFloat short h) {
+ return FP16.isNaN(h);
+ }
+
+ /**
+ * Returns true if the specified half-precision float value is normalized
+ * (does not have a subnormal representation). If the specified value is
+ * {@link #POSITIVE_INFINITY}, {@link #NEGATIVE_INFINITY},
+ * {@link #POSITIVE_ZERO}, {@link #NEGATIVE_ZERO}, NaN or any subnormal
+ * number, this method returns false.
+ *
+ * @param h A half-precision float value
+ * @return True if the value is normalized, false otherwise
+ */
+ public static boolean isNormalized(@HalfFloat short h) {
+ return FP16.isNormalized(h);
+ }
+
+ /**
+ * <p>Converts the specified half-precision float value into a
+ * single-precision float value. The following special cases are handled:</p>
+ * <ul>
+ * <li>If the input is {@link #NaN}, the returned value is {@link Float#NaN}</li>
+ * <li>If the input is {@link #POSITIVE_INFINITY} or
+ * {@link #NEGATIVE_INFINITY}, the returned value is respectively
+ * {@link Float#POSITIVE_INFINITY} or {@link Float#NEGATIVE_INFINITY}</li>
+ * <li>If the input is 0 (positive or negative), the returned value is +/-0.0f</li>
+ * <li>Otherwise, the returned value is a normalized single-precision float value</li>
+ * </ul>
+ *
+ * @param h The half-precision float value to convert to single-precision
+ * @return A normalized single-precision float value
+ */
+ public static float toFloat(@HalfFloat short h) {
+ return FP16.toFloat(h);
+ }
+
+ /**
+ * <p>Converts the specified single-precision float value into a
+ * half-precision float value. The following special cases are handled:</p>
+ * <ul>
+ * <li>If the input is NaN (see {@link Float#isNaN(float)}), the returned
+ * value is {@link #NaN}</li>
+ * <li>If the input is {@link Float#POSITIVE_INFINITY} or
+ * {@link Float#NEGATIVE_INFINITY}, the returned value is respectively
+ * {@link #POSITIVE_INFINITY} or {@link #NEGATIVE_INFINITY}</li>
+ * <li>If the input is 0 (positive or negative), the returned value is
+ * {@link #POSITIVE_ZERO} or {@link #NEGATIVE_ZERO}</li>
+ * <li>If the input is a less than {@link #MIN_VALUE}, the returned value
+ * is flushed to {@link #POSITIVE_ZERO} or {@link #NEGATIVE_ZERO}</li>
+ * <li>If the input is a less than {@link #MIN_NORMAL}, the returned value
+ * is a denorm half-precision float</li>
+ * <li>Otherwise, the returned value is rounded to the nearest
+ * representable half-precision float value</li>
+ * </ul>
+ *
+ * @param f The single-precision float value to convert to half-precision
+ * @return A half-precision float value
+ */
+ @SuppressWarnings("StatementWithEmptyBody")
+ public static @HalfFloat short toHalf(float f) {
+ return FP16.toHalf(f);
+ }
+
+ /**
+ * Returns a {@code Half} instance representing the specified
+ * half-precision float value.
+ *
+ * @param h A half-precision float value
+ * @return a {@code Half} instance representing {@code h}
+ */
+ public static @NonNull Half valueOf(@HalfFloat short h) {
+ return new Half(h);
+ }
+
+ /**
+ * Returns a {@code Half} instance representing the specified float value.
+ *
+ * @param f A float value
+ * @return a {@code Half} instance representing {@code f}
+ */
+ public static @NonNull Half valueOf(float f) {
+ return new Half(f);
+ }
+
+ /**
+ * Returns a {@code Half} instance representing the specified string value.
+ * Calling this method is equivalent to calling
+ * <code>toHalf(Float.parseString(h))</code>. See {@link Float#valueOf(String)}
+ * for more information on the format of the string representation.
+ *
+ * @param s The string to be parsed
+ * @return a {@code Half} instance representing {@code h}
+ * @throws NumberFormatException if the string does not contain a parsable
+ * half-precision float value
+ */
+ public static @NonNull Half valueOf(@NonNull String s) {
+ return new Half(s);
+ }
+
+ /**
+ * Returns the half-precision float value represented by the specified string.
+ * Calling this method is equivalent to calling
+ * <code>toHalf(Float.parseString(h))</code>. See {@link Float#valueOf(String)}
+ * for more information on the format of the string representation.
+ *
+ * @param s The string to be parsed
+ * @return A half-precision float value represented by the string
+ * @throws NumberFormatException if the string does not contain a parsable
+ * half-precision float value
+ */
+ public static @HalfFloat short parseHalf(@NonNull String s) throws NumberFormatException {
+ return toHalf(Float.parseFloat(s));
+ }
+
+ /**
+ * Returns a string representation of the specified half-precision
+ * float value. Calling this method is equivalent to calling
+ * <code>Float.toString(toFloat(h))</code>. See {@link Float#toString(float)}
+ * for more information on the format of the string representation.
+ *
+ * @param h A half-precision float value
+ * @return A string representation of the specified value
+ */
+ @NonNull
+ public static String toString(@HalfFloat short h) {
+ return Float.toString(toFloat(h));
+ }
+
+ /**
+ * <p>Returns a hexadecimal string representation of the specified half-precision
+ * float value. If the value is a NaN, the result is <code>"NaN"</code>,
+ * otherwise the result follows this format:</p>
+ * <ul>
+ * <li>If the sign is positive, no sign character appears in the result</li>
+ * <li>If the sign is negative, the first character is <code>'-'</code></li>
+ * <li>If the value is inifinity, the string is <code>"Infinity"</code></li>
+ * <li>If the value is 0, the string is <code>"0x0.0p0"</code></li>
+ * <li>If the value has a normalized representation, the exponent and
+ * significand are represented in the string in two fields. The significand
+ * starts with <code>"0x1."</code> followed by its lowercase hexadecimal
+ * representation. Trailing zeroes are removed unless all digits are 0, then
+ * a single zero is used. The significand representation is followed by the
+ * exponent, represented by <code>"p"</code>, itself followed by a decimal
+ * string of the unbiased exponent</li>
+ * <li>If the value has a subnormal representation, the significand starts
+ * with <code>"0x0."</code> followed by its lowercase hexadecimal
+ * representation. Trailing zeroes are removed unless all digits are 0, then
+ * a single zero is used. The significand representation is followed by the
+ * exponent, represented by <code>"p-14"</code></li>
+ * </ul>
+ *
+ * @param h A half-precision float value
+ * @return A hexadecimal string representation of the specified value
+ */
+ @NonNull
+ public static String toHexString(@HalfFloat short h) {
+ return FP16.toHexString(h);
+ }
+}
diff --git a/android/util/HashedStringCache.java b/android/util/HashedStringCache.java
new file mode 100644
index 0000000..1f2b956
--- /dev/null
+++ b/android/util/HashedStringCache.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Environment;
+import android.os.storage.StorageManager;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.File;
+import java.nio.charset.Charset;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+/**
+ * HashedStringCache provides hashing functionality with an underlying LRUCache and expiring salt.
+ * Salt and expiration time are being stored under the tag passed in by the calling package --
+ * intended usage is the calling package name.
+ * @hide
+ */
+public class HashedStringCache {
+ private static HashedStringCache sHashedStringCache = null;
+ private static final Charset UTF_8 = Charset.forName("UTF-8");
+ private static final int HASH_CACHE_SIZE = 100;
+ private static final int HASH_LENGTH = 8;
+ @VisibleForTesting
+ static final String HASH_SALT = "_hash_salt";
+ @VisibleForTesting
+ static final String HASH_SALT_DATE = "_hash_salt_date";
+ @VisibleForTesting
+ static final String HASH_SALT_GEN = "_hash_salt_gen";
+ // For privacy we need to rotate the salt regularly
+ private static final long DAYS_TO_MILLIS = 1000 * 60 * 60 * 24;
+ private static final int MAX_SALT_DAYS = 100;
+ private final LruCache<String, String> mHashes;
+ private final SecureRandom mSecureRandom;
+ private final Object mPreferenceLock = new Object();
+ private final MessageDigest mDigester;
+ private byte[] mSalt;
+ private int mSaltGen;
+ private SharedPreferences mSharedPreferences;
+
+ private static final String TAG = "HashedStringCache";
+ private static final boolean DEBUG = false;
+
+ private HashedStringCache() {
+ mHashes = new LruCache<>(HASH_CACHE_SIZE);
+ mSecureRandom = new SecureRandom();
+ try {
+ mDigester = MessageDigest.getInstance("MD5");
+ } catch (NoSuchAlgorithmException impossible) {
+ // this can't happen - MD5 is always present
+ throw new RuntimeException(impossible);
+ }
+ }
+
+ /**
+ * @return - instance of the HashedStringCache
+ * @hide
+ */
+ public static HashedStringCache getInstance() {
+ if (sHashedStringCache == null) {
+ sHashedStringCache = new HashedStringCache();
+ }
+ return sHashedStringCache;
+ }
+
+ /**
+ * Take the string and context and create a hash of the string. Trigger refresh on salt if salt
+ * is more than 7 days old
+ * @param context - callers context to retrieve SharedPreferences
+ * @param clearText - string that needs to be hashed
+ * @param tag - class name to use for storing values in shared preferences
+ * @param saltExpirationDays - number of days we may keep the same salt
+ * special value -1 will short-circuit and always return null.
+ * @return - HashResult containing the hashed string and the generation of the hash salt, null
+ * if clearText string is empty
+ *
+ * @hide
+ */
+ public HashResult hashString(Context context, String tag, String clearText,
+ int saltExpirationDays) {
+ if (saltExpirationDays == -1 || context == null
+ || TextUtils.isEmpty(clearText) || TextUtils.isEmpty(tag)) {
+ return null;
+ }
+
+ populateSaltValues(context, tag, saltExpirationDays);
+ String hashText = mHashes.get(clearText);
+ if (hashText != null) {
+ return new HashResult(hashText, mSaltGen);
+ }
+
+ mDigester.reset();
+ mDigester.update(mSalt);
+ mDigester.update(clearText.getBytes(UTF_8));
+ byte[] bytes = mDigester.digest();
+ int len = Math.min(HASH_LENGTH, bytes.length);
+ hashText = Base64.encodeToString(bytes, 0, len, Base64.NO_PADDING | Base64.NO_WRAP);
+ mHashes.put(clearText, hashText);
+
+ return new HashResult(hashText, mSaltGen);
+ }
+
+ /**
+ * Populates the mSharedPreferences and checks if there is a salt present and if it's older than
+ * 7 days
+ * @param tag - class name to use for storing values in shared preferences
+ * @param saltExpirationDays - number of days we may keep the same salt
+ * @param saltDate - the date retrieved from configuration
+ * @return - true if no salt or salt is older than 7 days
+ */
+ private boolean checkNeedsNewSalt(String tag, int saltExpirationDays, long saltDate) {
+ if (saltDate == 0 || saltExpirationDays < -1) {
+ return true;
+ }
+ if (saltExpirationDays > MAX_SALT_DAYS) {
+ saltExpirationDays = MAX_SALT_DAYS;
+ }
+ long now = System.currentTimeMillis();
+ long delta = now - saltDate;
+ // Check for delta < 0 to make sure we catch if someone puts their phone far in the
+ // future and then goes back to normal time.
+ return delta >= saltExpirationDays * DAYS_TO_MILLIS || delta < 0;
+ }
+
+ /**
+ * Populate the salt and saltGen member variables if they aren't already set / need refreshing.
+ * @param context - to get sharedPreferences
+ * @param tag - class name to use for storing values in shared preferences
+ * @param saltExpirationDays - number of days we may keep the same salt
+ */
+ private void populateSaltValues(Context context, String tag, int saltExpirationDays) {
+ synchronized (mPreferenceLock) {
+ // check if we need to refresh the salt
+ mSharedPreferences = getHashSharedPreferences(context);
+ long saltDate = mSharedPreferences.getLong(tag + HASH_SALT_DATE, 0);
+ boolean needsNewSalt = checkNeedsNewSalt(tag, saltExpirationDays, saltDate);
+ if (needsNewSalt) {
+ mHashes.evictAll();
+ }
+ if (mSalt == null || needsNewSalt) {
+ String saltString = mSharedPreferences.getString(tag + HASH_SALT, null);
+ mSaltGen = mSharedPreferences.getInt(tag + HASH_SALT_GEN, 0);
+ if (saltString == null || needsNewSalt) {
+ mSaltGen++;
+ byte[] saltBytes = new byte[16];
+ mSecureRandom.nextBytes(saltBytes);
+ saltString = Base64.encodeToString(saltBytes,
+ Base64.NO_PADDING | Base64.NO_WRAP);
+ mSharedPreferences.edit()
+ .putString(tag + HASH_SALT, saltString)
+ .putInt(tag + HASH_SALT_GEN, mSaltGen)
+ .putLong(tag + HASH_SALT_DATE, System.currentTimeMillis()).apply();
+ if (DEBUG) {
+ Log.d(TAG, "created a new salt: " + saltString);
+ }
+ }
+ mSalt = saltString.getBytes(UTF_8);
+ }
+ }
+ }
+
+ /**
+ * Android:ui doesn't have persistent preferences, so need to fall back on this hack originally
+ * from ChooserActivity.java
+ * @param context
+ * @return
+ */
+ private SharedPreferences getHashSharedPreferences(Context context) {
+ final File prefsFile = new File(new File(
+ Environment.getDataUserCePackageDirectory(
+ StorageManager.UUID_PRIVATE_INTERNAL,
+ context.getUserId(), context.getPackageName()),
+ "shared_prefs"),
+ "hashed_cache.xml");
+ return context.getSharedPreferences(prefsFile, Context.MODE_PRIVATE);
+ }
+
+ /**
+ * Helper class to hold hashed string and salt generation.
+ */
+ public class HashResult {
+ public String hashedString;
+ public int saltGeneration;
+
+ public HashResult(String hString, int saltGen) {
+ hashedString = hString;
+ saltGeneration = saltGen;
+ }
+ }
+}
diff --git a/android/util/IconDrawableFactory.java b/android/util/IconDrawableFactory.java
new file mode 100644
index 0000000..721e6b3
--- /dev/null
+++ b/android/util/IconDrawableFactory.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.util;
+
+import android.annotation.UserIdInt;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageItemInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+/**
+ * Utility class to load app drawables with appropriate badging.
+ *
+ * @hide
+ */
+public class IconDrawableFactory {
+
+ protected final Context mContext;
+ protected final PackageManager mPm;
+ protected final UserManager mUm;
+ protected final LauncherIcons mLauncherIcons;
+ protected final boolean mEmbedShadow;
+
+ private IconDrawableFactory(Context context, boolean embedShadow) {
+ mContext = context;
+ mPm = context.getPackageManager();
+ mUm = context.getSystemService(UserManager.class);
+ mLauncherIcons = new LauncherIcons(context);
+ mEmbedShadow = embedShadow;
+ }
+
+ protected boolean needsBadging(ApplicationInfo appInfo, @UserIdInt int userId) {
+ return appInfo.isInstantApp() || mUm.isManagedProfile(userId);
+ }
+
+ @UnsupportedAppUsage
+ public Drawable getBadgedIcon(ApplicationInfo appInfo) {
+ return getBadgedIcon(appInfo, UserHandle.getUserId(appInfo.uid));
+ }
+
+ public Drawable getBadgedIcon(ApplicationInfo appInfo, @UserIdInt int userId) {
+ return getBadgedIcon(appInfo, appInfo, userId);
+ }
+
+ @UnsupportedAppUsage
+ public Drawable getBadgedIcon(PackageItemInfo itemInfo, ApplicationInfo appInfo,
+ @UserIdInt int userId) {
+ Drawable icon = mPm.loadUnbadgedItemIcon(itemInfo, appInfo);
+ if (!mEmbedShadow && !needsBadging(appInfo, userId)) {
+ return icon;
+ }
+
+ icon = getShadowedIcon(icon);
+ if (appInfo.isInstantApp()) {
+ int badgeColor = Resources.getSystem().getColor(
+ com.android.internal.R.color.instant_app_badge, null);
+ icon = mLauncherIcons.getBadgedDrawable(icon,
+ com.android.internal.R.drawable.ic_instant_icon_badge_bolt,
+ badgeColor);
+ }
+ if (mUm.hasBadge(userId)) {
+ icon = mLauncherIcons.getBadgedDrawable(icon,
+ mUm.getUserIconBadgeResId(userId),
+ mUm.getUserBadgeColor(userId));
+ }
+ return icon;
+ }
+
+ /**
+ * Add shadow to the icon if {@link AdaptiveIconDrawable}
+ */
+ public Drawable getShadowedIcon(Drawable icon) {
+ return mLauncherIcons.wrapIconDrawableWithShadow(icon);
+ }
+
+ @UnsupportedAppUsage
+ public static IconDrawableFactory newInstance(Context context) {
+ return new IconDrawableFactory(context, true);
+ }
+
+ public static IconDrawableFactory newInstance(Context context, boolean embedShadow) {
+ return new IconDrawableFactory(context, embedShadow);
+ }
+}
diff --git a/android/util/IntArray.java b/android/util/IntArray.java
new file mode 100644
index 0000000..5a74ec0
--- /dev/null
+++ b/android/util/IntArray.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.Preconditions;
+
+import libcore.util.EmptyArray;
+
+import java.util.Arrays;
+
+/**
+ * Implements a growing array of int primitives.
+ *
+ * @hide
+ */
+public class IntArray implements Cloneable {
+ private static final int MIN_CAPACITY_INCREMENT = 12;
+
+ private int[] mValues;
+ private int mSize;
+
+ private IntArray(int[] array, int size) {
+ mValues = array;
+ mSize = Preconditions.checkArgumentInRange(size, 0, array.length, "size");
+ }
+
+ /**
+ * Creates an empty IntArray with the default initial capacity.
+ */
+ public IntArray() {
+ this(10);
+ }
+
+ /**
+ * Creates an empty IntArray with the specified initial capacity.
+ */
+ public IntArray(int initialCapacity) {
+ if (initialCapacity == 0) {
+ mValues = EmptyArray.INT;
+ } else {
+ mValues = ArrayUtils.newUnpaddedIntArray(initialCapacity);
+ }
+ mSize = 0;
+ }
+
+ /**
+ * Creates an IntArray wrapping the given primitive int array.
+ */
+ public static IntArray wrap(int[] array) {
+ return new IntArray(array, array.length);
+ }
+
+ /**
+ * Creates an IntArray from the given primitive int array, copying it.
+ */
+ public static IntArray fromArray(int[] array, int size) {
+ return wrap(Arrays.copyOf(array, size));
+ }
+
+ /**
+ * Changes the size of this IntArray. If this IntArray is shrinked, the backing array capacity
+ * is unchanged. If the new size is larger than backing array capacity, a new backing array is
+ * created from the current content of this IntArray padded with 0s.
+ */
+ public void resize(int newSize) {
+ Preconditions.checkArgumentNonnegative(newSize);
+ if (newSize <= mValues.length) {
+ Arrays.fill(mValues, newSize, mValues.length, 0);
+ } else {
+ ensureCapacity(newSize - mSize);
+ }
+ mSize = newSize;
+ }
+
+ /**
+ * Appends the specified value to the end of this array.
+ */
+ public void add(int value) {
+ add(mSize, value);
+ }
+
+ /**
+ * Inserts a value at the specified position in this array. If the specified index is equal to
+ * the length of the array, the value is added at the end.
+ *
+ * @throws IndexOutOfBoundsException when index < 0 || index > size()
+ */
+ public void add(int index, int value) {
+ ensureCapacity(1);
+ int rightSegment = mSize - index;
+ mSize++;
+ ArrayUtils.checkBounds(mSize, index);
+
+ if (rightSegment != 0) {
+ // Move by 1 all values from the right of 'index'
+ System.arraycopy(mValues, index, mValues, index + 1, rightSegment);
+ }
+
+ mValues[index] = value;
+ }
+
+ /**
+ * Searches the array for the specified value using the binary search algorithm. The array must
+ * be sorted (as by the {@link Arrays#sort(int[], int, int)} method) prior to making this call.
+ * If it is not sorted, the results are undefined. If the range contains multiple elements with
+ * the specified value, there is no guarantee which one will be found.
+ *
+ * @param value The value to search for.
+ * @return index of the search key, if it is contained in the array; otherwise, <i>(-(insertion
+ * point) - 1)</i>. The insertion point is defined as the point at which the key would
+ * be inserted into the array: the index of the first element greater than the key, or
+ * {@link #size()} if all elements in the array are less than the specified key.
+ * Note that this guarantees that the return value will be >= 0 if and only if the key
+ * is found.
+ */
+ public int binarySearch(int value) {
+ return ContainerHelpers.binarySearch(mValues, mSize, value);
+ }
+
+ /**
+ * Adds the values in the specified array to this array.
+ */
+ public void addAll(IntArray values) {
+ final int count = values.mSize;
+ ensureCapacity(count);
+
+ System.arraycopy(values.mValues, 0, mValues, mSize, count);
+ mSize += count;
+ }
+
+ /**
+ * Ensures capacity to append at least <code>count</code> values.
+ */
+ private void ensureCapacity(int count) {
+ final int currentSize = mSize;
+ final int minCapacity = currentSize + count;
+ if (minCapacity >= mValues.length) {
+ final int targetCap = currentSize + (currentSize < (MIN_CAPACITY_INCREMENT / 2) ?
+ MIN_CAPACITY_INCREMENT : currentSize >> 1);
+ final int newCapacity = targetCap > minCapacity ? targetCap : minCapacity;
+ final int[] newValues = ArrayUtils.newUnpaddedIntArray(newCapacity);
+ System.arraycopy(mValues, 0, newValues, 0, currentSize);
+ mValues = newValues;
+ }
+ }
+
+ /**
+ * Removes all values from this array.
+ */
+ public void clear() {
+ mSize = 0;
+ }
+
+ @Override
+ public IntArray clone() throws CloneNotSupportedException {
+ final IntArray clone = (IntArray) super.clone();
+ clone.mValues = mValues.clone();
+ return clone;
+ }
+
+ /**
+ * Returns the value at the specified position in this array.
+ */
+ public int get(int index) {
+ ArrayUtils.checkBounds(mSize, index);
+ return mValues[index];
+ }
+
+ /**
+ * Sets the value at the specified position in this array.
+ */
+ public void set(int index, int value) {
+ ArrayUtils.checkBounds(mSize, index);
+ mValues[index] = value;
+ }
+
+ /**
+ * Returns the index of the first occurrence of the specified value in this
+ * array, or -1 if this array does not contain the value.
+ */
+ public int indexOf(int value) {
+ final int n = mSize;
+ for (int i = 0; i < n; i++) {
+ if (mValues[i] == value) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Removes the value at the specified index from this array.
+ */
+ public void remove(int index) {
+ ArrayUtils.checkBounds(mSize, index);
+ System.arraycopy(mValues, index + 1, mValues, index, mSize - index - 1);
+ mSize--;
+ }
+
+ /**
+ * Returns the number of values in this array.
+ */
+ public int size() {
+ return mSize;
+ }
+
+ /**
+ * Returns a new array with the contents of this IntArray.
+ */
+ public int[] toArray() {
+ return Arrays.copyOf(mValues, mSize);
+ }
+}
diff --git a/android/util/IntProperty.java b/android/util/IntProperty.java
new file mode 100644
index 0000000..9e21ced
--- /dev/null
+++ b/android/util/IntProperty.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.util;
+
+/**
+ * An implementation of {@link android.util.Property} to be used specifically with fields of type
+ * <code>int</code>. This type-specific subclass enables performance benefit by allowing
+ * calls to a {@link #setValue(Object, int) setValue()} function that takes the primitive
+ * <code>int</code> type and avoids autoboxing and other overhead associated with the
+ * <code>Integer</code> class.
+ *
+ * @param <T> The class on which the Property is declared.
+ */
+public abstract class IntProperty<T> extends Property<T, Integer> {
+
+ public IntProperty(String name) {
+ super(Integer.class, name);
+ }
+
+ /**
+ * A type-specific variant of {@link #set(Object, Integer)} that is faster when dealing
+ * with fields of type <code>int</code>.
+ */
+ public abstract void setValue(T object, int value);
+
+ @Override
+ final public void set(T object, Integer value) {
+ setValue(object, value.intValue());
+ }
+
+}
\ No newline at end of file
diff --git a/android/util/JsonReader.java b/android/util/JsonReader.java
new file mode 100644
index 0000000..50f63f8
--- /dev/null
+++ b/android/util/JsonReader.java
@@ -0,0 +1,1173 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import libcore.internal.StringPool;
+
+import java.io.Closeable;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Reads a JSON (<a href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>)
+ * encoded value as a stream of tokens. This stream includes both literal
+ * values (strings, numbers, booleans, and nulls) as well as the begin and
+ * end delimiters of objects and arrays. The tokens are traversed in
+ * depth-first order, the same order that they appear in the JSON document.
+ * Within JSON objects, name/value pairs are represented by a single token.
+ *
+ * <h3>Parsing JSON</h3>
+ * To create a recursive descent parser for your own JSON streams, first create
+ * an entry point method that creates a {@code JsonReader}.
+ *
+ * <p>Next, create handler methods for each structure in your JSON text. You'll
+ * need a method for each object type and for each array type.
+ * <ul>
+ * <li>Within <strong>array handling</strong> methods, first call {@link
+ * #beginArray} to consume the array's opening bracket. Then create a
+ * while loop that accumulates values, terminating when {@link #hasNext}
+ * is false. Finally, read the array's closing bracket by calling {@link
+ * #endArray}.
+ * <li>Within <strong>object handling</strong> methods, first call {@link
+ * #beginObject} to consume the object's opening brace. Then create a
+ * while loop that assigns values to local variables based on their name.
+ * This loop should terminate when {@link #hasNext} is false. Finally,
+ * read the object's closing brace by calling {@link #endObject}.
+ * </ul>
+ * <p>When a nested object or array is encountered, delegate to the
+ * corresponding handler method.
+ *
+ * <p>When an unknown name is encountered, strict parsers should fail with an
+ * exception. Lenient parsers should call {@link #skipValue()} to recursively
+ * skip the value's nested tokens, which may otherwise conflict.
+ *
+ * <p>If a value may be null, you should first check using {@link #peek()}.
+ * Null literals can be consumed using either {@link #nextNull()} or {@link
+ * #skipValue()}.
+ *
+ * <h3>Example</h3>
+ * Suppose we'd like to parse a stream of messages such as the following: <pre> {@code
+ * [
+ * {
+ * "id": 912345678901,
+ * "text": "How do I read JSON on Android?",
+ * "geo": null,
+ * "user": {
+ * "name": "android_newb",
+ * "followers_count": 41
+ * }
+ * },
+ * {
+ * "id": 912345678902,
+ * "text": "@android_newb just use android.util.JsonReader!",
+ * "geo": [50.454722, -104.606667],
+ * "user": {
+ * "name": "jesse",
+ * "followers_count": 2
+ * }
+ * }
+ * ]}</pre>
+ * This code implements the parser for the above structure: <pre> {@code
+ *
+ * public List<Message> readJsonStream(InputStream in) throws IOException {
+ * JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8"));
+ * try {
+ * return readMessagesArray(reader);
+ * } finally {
+ * reader.close();
+ * }
+ * }
+ *
+ * public List<Message> readMessagesArray(JsonReader reader) throws IOException {
+ * List<Message> messages = new ArrayList<Message>();
+ *
+ * reader.beginArray();
+ * while (reader.hasNext()) {
+ * messages.add(readMessage(reader));
+ * }
+ * reader.endArray();
+ * return messages;
+ * }
+ *
+ * public Message readMessage(JsonReader reader) throws IOException {
+ * long id = -1;
+ * String text = null;
+ * User user = null;
+ * List<Double> geo = null;
+ *
+ * reader.beginObject();
+ * while (reader.hasNext()) {
+ * String name = reader.nextName();
+ * if (name.equals("id")) {
+ * id = reader.nextLong();
+ * } else if (name.equals("text")) {
+ * text = reader.nextString();
+ * } else if (name.equals("geo") && reader.peek() != JsonToken.NULL) {
+ * geo = readDoublesArray(reader);
+ * } else if (name.equals("user")) {
+ * user = readUser(reader);
+ * } else {
+ * reader.skipValue();
+ * }
+ * }
+ * reader.endObject();
+ * return new Message(id, text, user, geo);
+ * }
+ *
+ * public List<Double> readDoublesArray(JsonReader reader) throws IOException {
+ * List<Double> doubles = new ArrayList<Double>();
+ *
+ * reader.beginArray();
+ * while (reader.hasNext()) {
+ * doubles.add(reader.nextDouble());
+ * }
+ * reader.endArray();
+ * return doubles;
+ * }
+ *
+ * public User readUser(JsonReader reader) throws IOException {
+ * String username = null;
+ * int followersCount = -1;
+ *
+ * reader.beginObject();
+ * while (reader.hasNext()) {
+ * String name = reader.nextName();
+ * if (name.equals("name")) {
+ * username = reader.nextString();
+ * } else if (name.equals("followers_count")) {
+ * followersCount = reader.nextInt();
+ * } else {
+ * reader.skipValue();
+ * }
+ * }
+ * reader.endObject();
+ * return new User(username, followersCount);
+ * }}</pre>
+ *
+ * <h3>Number Handling</h3>
+ * This reader permits numeric values to be read as strings and string values to
+ * be read as numbers. For example, both elements of the JSON array {@code
+ * [1, "1"]} may be read using either {@link #nextInt} or {@link #nextString}.
+ * This behavior is intended to prevent lossy numeric conversions: double is
+ * JavaScript's only numeric type and very large values like {@code
+ * 9007199254740993} cannot be represented exactly on that platform. To minimize
+ * precision loss, extremely large values should be written and read as strings
+ * in JSON.
+ *
+ * <p>Each {@code JsonReader} may be used to read a single JSON stream. Instances
+ * of this class are not thread safe.
+ */
+public final class JsonReader implements Closeable {
+
+ private static final String TRUE = "true";
+ private static final String FALSE = "false";
+
+ private final StringPool stringPool = new StringPool();
+
+ /** The input JSON. */
+ private final Reader in;
+
+ /** True to accept non-spec compliant JSON */
+ private boolean lenient = false;
+
+ /**
+ * Use a manual buffer to easily read and unread upcoming characters, and
+ * also so we can create strings without an intermediate StringBuilder.
+ * We decode literals directly out of this buffer, so it must be at least as
+ * long as the longest token that can be reported as a number.
+ */
+ private final char[] buffer = new char[1024];
+ private int pos = 0;
+ private int limit = 0;
+
+ /*
+ * The offset of the first character in the buffer.
+ */
+ private int bufferStartLine = 1;
+ private int bufferStartColumn = 1;
+
+ private final List<JsonScope> stack = new ArrayList<JsonScope>();
+ {
+ push(JsonScope.EMPTY_DOCUMENT);
+ }
+
+ /**
+ * The type of the next token to be returned by {@link #peek} and {@link
+ * #advance}. If null, peek() will assign a value.
+ */
+ private JsonToken token;
+
+ /** The text of the next name. */
+ private String name;
+
+ /*
+ * For the next literal value, we may have the text value, or the position
+ * and length in the buffer.
+ */
+ private String value;
+ private int valuePos;
+ private int valueLength;
+
+ /** True if we're currently handling a skipValue() call. */
+ private boolean skipping = false;
+
+ /**
+ * Creates a new instance that reads a JSON-encoded stream from {@code in}.
+ */
+ public JsonReader(Reader in) {
+ if (in == null) {
+ throw new NullPointerException("in == null");
+ }
+ this.in = in;
+ }
+
+ /**
+ * Configure this parser to be be liberal in what it accepts. By default,
+ * this parser is strict and only accepts JSON as specified by <a
+ * href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>. Setting the
+ * parser to lenient causes it to ignore the following syntax errors:
+ *
+ * <ul>
+ * <li>End of line comments starting with {@code //} or {@code #} and
+ * ending with a newline character.
+ * <li>C-style comments starting with {@code /*} and ending with
+ * {@code *}{@code /}. Such comments may not be nested.
+ * <li>Names that are unquoted or {@code 'single quoted'}.
+ * <li>Strings that are unquoted or {@code 'single quoted'}.
+ * <li>Array elements separated by {@code ;} instead of {@code ,}.
+ * <li>Unnecessary array separators. These are interpreted as if null
+ * was the omitted value.
+ * <li>Names and values separated by {@code =} or {@code =>} instead of
+ * {@code :}.
+ * <li>Name/value pairs separated by {@code ;} instead of {@code ,}.
+ * </ul>
+ */
+ public void setLenient(boolean lenient) {
+ this.lenient = lenient;
+ }
+
+ /**
+ * Returns true if this parser is liberal in what it accepts.
+ */
+ public boolean isLenient() {
+ return lenient;
+ }
+
+ /**
+ * Consumes the next token from the JSON stream and asserts that it is the
+ * beginning of a new array.
+ */
+ public void beginArray() throws IOException {
+ expect(JsonToken.BEGIN_ARRAY);
+ }
+
+ /**
+ * Consumes the next token from the JSON stream and asserts that it is the
+ * end of the current array.
+ */
+ public void endArray() throws IOException {
+ expect(JsonToken.END_ARRAY);
+ }
+
+ /**
+ * Consumes the next token from the JSON stream and asserts that it is the
+ * beginning of a new object.
+ */
+ public void beginObject() throws IOException {
+ expect(JsonToken.BEGIN_OBJECT);
+ }
+
+ /**
+ * Consumes the next token from the JSON stream and asserts that it is the
+ * end of the current object.
+ */
+ public void endObject() throws IOException {
+ expect(JsonToken.END_OBJECT);
+ }
+
+ /**
+ * Consumes {@code expected}.
+ */
+ private void expect(JsonToken expected) throws IOException {
+ peek();
+ if (token != expected) {
+ throw new IllegalStateException("Expected " + expected + " but was " + peek());
+ }
+ advance();
+ }
+
+ /**
+ * Returns true if the current array or object has another element.
+ */
+ public boolean hasNext() throws IOException {
+ peek();
+ return token != JsonToken.END_OBJECT && token != JsonToken.END_ARRAY;
+ }
+
+ /**
+ * Returns the type of the next token without consuming it.
+ */
+ public JsonToken peek() throws IOException {
+ if (token != null) {
+ return token;
+ }
+
+ switch (peekStack()) {
+ case EMPTY_DOCUMENT:
+ replaceTop(JsonScope.NONEMPTY_DOCUMENT);
+ JsonToken firstToken = nextValue();
+ if (!lenient && token != JsonToken.BEGIN_ARRAY && token != JsonToken.BEGIN_OBJECT) {
+ throw new IOException(
+ "Expected JSON document to start with '[' or '{' but was " + token);
+ }
+ return firstToken;
+ case EMPTY_ARRAY:
+ return nextInArray(true);
+ case NONEMPTY_ARRAY:
+ return nextInArray(false);
+ case EMPTY_OBJECT:
+ return nextInObject(true);
+ case DANGLING_NAME:
+ return objectValue();
+ case NONEMPTY_OBJECT:
+ return nextInObject(false);
+ case NONEMPTY_DOCUMENT:
+ try {
+ JsonToken token = nextValue();
+ if (lenient) {
+ return token;
+ }
+ throw syntaxError("Expected EOF");
+ } catch (EOFException e) {
+ return token = JsonToken.END_DOCUMENT; // TODO: avoid throwing here?
+ }
+ case CLOSED:
+ throw new IllegalStateException("JsonReader is closed");
+ default:
+ throw new AssertionError();
+ }
+ }
+
+ /**
+ * Advances the cursor in the JSON stream to the next token.
+ */
+ private JsonToken advance() throws IOException {
+ peek();
+
+ JsonToken result = token;
+ token = null;
+ value = null;
+ name = null;
+ return result;
+ }
+
+ /**
+ * Returns the next token, a {@link JsonToken#NAME property name}, and
+ * consumes it.
+ *
+ * @throws IOException if the next token in the stream is not a property
+ * name.
+ */
+ public String nextName() throws IOException {
+ peek();
+ if (token != JsonToken.NAME) {
+ throw new IllegalStateException("Expected a name but was " + peek());
+ }
+ String result = name;
+ advance();
+ return result;
+ }
+
+ /**
+ * Returns the {@link JsonToken#STRING string} value of the next token,
+ * consuming it. If the next token is a number, this method will return its
+ * string form.
+ *
+ * @throws IllegalStateException if the next token is not a string or if
+ * this reader is closed.
+ */
+ public String nextString() throws IOException {
+ peek();
+ if (token != JsonToken.STRING && token != JsonToken.NUMBER) {
+ throw new IllegalStateException("Expected a string but was " + peek());
+ }
+
+ String result = value;
+ advance();
+ return result;
+ }
+
+ /**
+ * Returns the {@link JsonToken#BOOLEAN boolean} value of the next token,
+ * consuming it.
+ *
+ * @throws IllegalStateException if the next token is not a boolean or if
+ * this reader is closed.
+ */
+ public boolean nextBoolean() throws IOException {
+ peek();
+ if (token != JsonToken.BOOLEAN) {
+ throw new IllegalStateException("Expected a boolean but was " + token);
+ }
+
+ boolean result = (value == TRUE);
+ advance();
+ return result;
+ }
+
+ /**
+ * Consumes the next token from the JSON stream and asserts that it is a
+ * literal null.
+ *
+ * @throws IllegalStateException if the next token is not null or if this
+ * reader is closed.
+ */
+ public void nextNull() throws IOException {
+ peek();
+ if (token != JsonToken.NULL) {
+ throw new IllegalStateException("Expected null but was " + token);
+ }
+
+ advance();
+ }
+
+ /**
+ * Returns the {@link JsonToken#NUMBER double} value of the next token,
+ * consuming it. If the next token is a string, this method will attempt to
+ * parse it as a double using {@link Double#parseDouble(String)}.
+ *
+ * @throws IllegalStateException if the next token is not a literal value.
+ */
+ public double nextDouble() throws IOException {
+ peek();
+ if (token != JsonToken.STRING && token != JsonToken.NUMBER) {
+ throw new IllegalStateException("Expected a double but was " + token);
+ }
+
+ double result = Double.parseDouble(value);
+ advance();
+ return result;
+ }
+
+ /**
+ * Returns the {@link JsonToken#NUMBER long} value of the next token,
+ * consuming it. If the next token is a string, this method will attempt to
+ * parse it as a long. If the next token's numeric value cannot be exactly
+ * represented by a Java {@code long}, this method throws.
+ *
+ * @throws IllegalStateException if the next token is not a literal value.
+ * @throws NumberFormatException if the next literal value cannot be parsed
+ * as a number, or exactly represented as a long.
+ */
+ public long nextLong() throws IOException {
+ peek();
+ if (token != JsonToken.STRING && token != JsonToken.NUMBER) {
+ throw new IllegalStateException("Expected a long but was " + token);
+ }
+
+ long result;
+ try {
+ result = Long.parseLong(value);
+ } catch (NumberFormatException ignored) {
+ double asDouble = Double.parseDouble(value); // don't catch this NumberFormatException
+ result = (long) asDouble;
+ if ((double) result != asDouble) {
+ throw new NumberFormatException(value);
+ }
+ }
+
+ advance();
+ return result;
+ }
+
+ /**
+ * Returns the {@link JsonToken#NUMBER int} value of the next token,
+ * consuming it. If the next token is a string, this method will attempt to
+ * parse it as an int. If the next token's numeric value cannot be exactly
+ * represented by a Java {@code int}, this method throws.
+ *
+ * @throws IllegalStateException if the next token is not a literal value.
+ * @throws NumberFormatException if the next literal value cannot be parsed
+ * as a number, or exactly represented as an int.
+ */
+ public int nextInt() throws IOException {
+ peek();
+ if (token != JsonToken.STRING && token != JsonToken.NUMBER) {
+ throw new IllegalStateException("Expected an int but was " + token);
+ }
+
+ int result;
+ try {
+ result = Integer.parseInt(value);
+ } catch (NumberFormatException ignored) {
+ double asDouble = Double.parseDouble(value); // don't catch this NumberFormatException
+ result = (int) asDouble;
+ if ((double) result != asDouble) {
+ throw new NumberFormatException(value);
+ }
+ }
+
+ advance();
+ return result;
+ }
+
+ /**
+ * Closes this JSON reader and the underlying {@link Reader}.
+ */
+ public void close() throws IOException {
+ value = null;
+ token = null;
+ stack.clear();
+ stack.add(JsonScope.CLOSED);
+ in.close();
+ }
+
+ /**
+ * Skips the next value recursively. If it is an object or array, all nested
+ * elements are skipped. This method is intended for use when the JSON token
+ * stream contains unrecognized or unhandled values.
+ */
+ public void skipValue() throws IOException {
+ skipping = true;
+ try {
+ if (!hasNext() || peek() == JsonToken.END_DOCUMENT) {
+ throw new IllegalStateException("No element left to skip");
+ }
+ int count = 0;
+ do {
+ JsonToken token = advance();
+ if (token == JsonToken.BEGIN_ARRAY || token == JsonToken.BEGIN_OBJECT) {
+ count++;
+ } else if (token == JsonToken.END_ARRAY || token == JsonToken.END_OBJECT) {
+ count--;
+ }
+ } while (count != 0);
+ } finally {
+ skipping = false;
+ }
+ }
+
+ private JsonScope peekStack() {
+ return stack.get(stack.size() - 1);
+ }
+
+ private JsonScope pop() {
+ return stack.remove(stack.size() - 1);
+ }
+
+ private void push(JsonScope newTop) {
+ stack.add(newTop);
+ }
+
+ /**
+ * Replace the value on the top of the stack with the given value.
+ */
+ private void replaceTop(JsonScope newTop) {
+ stack.set(stack.size() - 1, newTop);
+ }
+
+ private JsonToken nextInArray(boolean firstElement) throws IOException {
+ if (firstElement) {
+ replaceTop(JsonScope.NONEMPTY_ARRAY);
+ } else {
+ /* Look for a comma before each element after the first element. */
+ switch (nextNonWhitespace()) {
+ case ']':
+ pop();
+ return token = JsonToken.END_ARRAY;
+ case ';':
+ checkLenient(); // fall-through
+ case ',':
+ break;
+ default:
+ throw syntaxError("Unterminated array");
+ }
+ }
+
+ switch (nextNonWhitespace()) {
+ case ']':
+ if (firstElement) {
+ pop();
+ return token = JsonToken.END_ARRAY;
+ }
+ // fall-through to handle ",]"
+ case ';':
+ case ',':
+ /* In lenient mode, a 0-length literal means 'null' */
+ checkLenient();
+ pos--;
+ value = "null";
+ return token = JsonToken.NULL;
+ default:
+ pos--;
+ return nextValue();
+ }
+ }
+
+ private JsonToken nextInObject(boolean firstElement) throws IOException {
+ /*
+ * Read delimiters. Either a comma/semicolon separating this and the
+ * previous name-value pair, or a close brace to denote the end of the
+ * object.
+ */
+ if (firstElement) {
+ /* Peek to see if this is the empty object. */
+ switch (nextNonWhitespace()) {
+ case '}':
+ pop();
+ return token = JsonToken.END_OBJECT;
+ default:
+ pos--;
+ }
+ } else {
+ switch (nextNonWhitespace()) {
+ case '}':
+ pop();
+ return token = JsonToken.END_OBJECT;
+ case ';':
+ case ',':
+ break;
+ default:
+ throw syntaxError("Unterminated object");
+ }
+ }
+
+ /* Read the name. */
+ int quote = nextNonWhitespace();
+ switch (quote) {
+ case '\'':
+ checkLenient(); // fall-through
+ case '"':
+ name = nextString((char) quote);
+ break;
+ default:
+ checkLenient();
+ pos--;
+ name = nextLiteral(false);
+ if (name.isEmpty()) {
+ throw syntaxError("Expected name");
+ }
+ }
+
+ replaceTop(JsonScope.DANGLING_NAME);
+ return token = JsonToken.NAME;
+ }
+
+ private JsonToken objectValue() throws IOException {
+ /*
+ * Read the name/value separator. Usually a colon ':'. In lenient mode
+ * we also accept an equals sign '=', or an arrow "=>".
+ */
+ switch (nextNonWhitespace()) {
+ case ':':
+ break;
+ case '=':
+ checkLenient();
+ if ((pos < limit || fillBuffer(1)) && buffer[pos] == '>') {
+ pos++;
+ }
+ break;
+ default:
+ throw syntaxError("Expected ':'");
+ }
+
+ replaceTop(JsonScope.NONEMPTY_OBJECT);
+ return nextValue();
+ }
+
+ private JsonToken nextValue() throws IOException {
+ int c = nextNonWhitespace();
+ switch (c) {
+ case '{':
+ push(JsonScope.EMPTY_OBJECT);
+ return token = JsonToken.BEGIN_OBJECT;
+
+ case '[':
+ push(JsonScope.EMPTY_ARRAY);
+ return token = JsonToken.BEGIN_ARRAY;
+
+ case '\'':
+ checkLenient(); // fall-through
+ case '"':
+ value = nextString((char) c);
+ return token = JsonToken.STRING;
+
+ default:
+ pos--;
+ return readLiteral();
+ }
+ }
+
+ /**
+ * Returns true once {@code limit - pos >= minimum}. If the data is
+ * exhausted before that many characters are available, this returns
+ * false.
+ */
+ private boolean fillBuffer(int minimum) throws IOException {
+ // Before clobbering the old characters, update where buffer starts
+ for (int i = 0; i < pos; i++) {
+ if (buffer[i] == '\n') {
+ bufferStartLine++;
+ bufferStartColumn = 1;
+ } else {
+ bufferStartColumn++;
+ }
+ }
+
+ if (limit != pos) {
+ limit -= pos;
+ System.arraycopy(buffer, pos, buffer, 0, limit);
+ } else {
+ limit = 0;
+ }
+
+ pos = 0;
+ int total;
+ while ((total = in.read(buffer, limit, buffer.length - limit)) != -1) {
+ limit += total;
+
+ // if this is the first read, consume an optional byte order mark (BOM) if it exists
+ if (bufferStartLine == 1 && bufferStartColumn == 1
+ && limit > 0 && buffer[0] == '\ufeff') {
+ pos++;
+ bufferStartColumn--;
+ }
+
+ if (limit >= minimum) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private int getLineNumber() {
+ int result = bufferStartLine;
+ for (int i = 0; i < pos; i++) {
+ if (buffer[i] == '\n') {
+ result++;
+ }
+ }
+ return result;
+ }
+
+ private int getColumnNumber() {
+ int result = bufferStartColumn;
+ for (int i = 0; i < pos; i++) {
+ if (buffer[i] == '\n') {
+ result = 1;
+ } else {
+ result++;
+ }
+ }
+ return result;
+ }
+
+ private int nextNonWhitespace() throws IOException {
+ while (pos < limit || fillBuffer(1)) {
+ int c = buffer[pos++];
+ switch (c) {
+ case '\t':
+ case ' ':
+ case '\n':
+ case '\r':
+ continue;
+
+ case '/':
+ if (pos == limit && !fillBuffer(1)) {
+ return c;
+ }
+
+ checkLenient();
+ char peek = buffer[pos];
+ switch (peek) {
+ case '*':
+ // skip a /* c-style comment */
+ pos++;
+ if (!skipTo("*/")) {
+ throw syntaxError("Unterminated comment");
+ }
+ pos += 2;
+ continue;
+
+ case '/':
+ // skip a // end-of-line comment
+ pos++;
+ skipToEndOfLine();
+ continue;
+
+ default:
+ return c;
+ }
+
+ case '#':
+ /*
+ * Skip a # hash end-of-line comment. The JSON RFC doesn't
+ * specify this behaviour, but it's required to parse
+ * existing documents. See http://b/2571423.
+ */
+ checkLenient();
+ skipToEndOfLine();
+ continue;
+
+ default:
+ return c;
+ }
+ }
+
+ throw new EOFException("End of input");
+ }
+
+ private void checkLenient() throws IOException {
+ if (!lenient) {
+ throw syntaxError("Use JsonReader.setLenient(true) to accept malformed JSON");
+ }
+ }
+
+ /**
+ * Advances the position until after the next newline character. If the line
+ * is terminated by "\r\n", the '\n' must be consumed as whitespace by the
+ * caller.
+ */
+ private void skipToEndOfLine() throws IOException {
+ while (pos < limit || fillBuffer(1)) {
+ char c = buffer[pos++];
+ if (c == '\r' || c == '\n') {
+ break;
+ }
+ }
+ }
+
+ private boolean skipTo(String toFind) throws IOException {
+ outer:
+ for (; pos + toFind.length() <= limit || fillBuffer(toFind.length()); pos++) {
+ for (int c = 0; c < toFind.length(); c++) {
+ if (buffer[pos + c] != toFind.charAt(c)) {
+ continue outer;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns the string up to but not including {@code quote}, unescaping any
+ * character escape sequences encountered along the way. The opening quote
+ * should have already been read. This consumes the closing quote, but does
+ * not include it in the returned string.
+ *
+ * @param quote either ' or ".
+ * @throws NumberFormatException if any unicode escape sequences are
+ * malformed.
+ */
+ private String nextString(char quote) throws IOException {
+ StringBuilder builder = null;
+ do {
+ /* the index of the first character not yet appended to the builder. */
+ int start = pos;
+ while (pos < limit) {
+ int c = buffer[pos++];
+
+ if (c == quote) {
+ if (skipping) {
+ return "skipped!";
+ } else if (builder == null) {
+ return stringPool.get(buffer, start, pos - start - 1);
+ } else {
+ builder.append(buffer, start, pos - start - 1);
+ return builder.toString();
+ }
+
+ } else if (c == '\\') {
+ if (builder == null) {
+ builder = new StringBuilder();
+ }
+ builder.append(buffer, start, pos - start - 1);
+ builder.append(readEscapeCharacter());
+ start = pos;
+ }
+ }
+
+ if (builder == null) {
+ builder = new StringBuilder();
+ }
+ builder.append(buffer, start, pos - start);
+ } while (fillBuffer(1));
+
+ throw syntaxError("Unterminated string");
+ }
+
+ /**
+ * Reads the value up to but not including any delimiter characters. This
+ * does not consume the delimiter character.
+ *
+ * @param assignOffsetsOnly true for this method to only set the valuePos
+ * and valueLength fields and return a null result. This only works if
+ * the literal is short; a string is returned otherwise.
+ */
+ private String nextLiteral(boolean assignOffsetsOnly) throws IOException {
+ StringBuilder builder = null;
+ valuePos = -1;
+ valueLength = 0;
+ int i = 0;
+
+ findNonLiteralCharacter:
+ while (true) {
+ for (; pos + i < limit; i++) {
+ switch (buffer[pos + i]) {
+ case '/':
+ case '\\':
+ case ';':
+ case '#':
+ case '=':
+ checkLenient(); // fall-through
+ case '{':
+ case '}':
+ case '[':
+ case ']':
+ case ':':
+ case ',':
+ case ' ':
+ case '\t':
+ case '\f':
+ case '\r':
+ case '\n':
+ break findNonLiteralCharacter;
+ }
+ }
+
+ /*
+ * Attempt to load the entire literal into the buffer at once. If
+ * we run out of input, add a non-literal character at the end so
+ * that decoding doesn't need to do bounds checks.
+ */
+ if (i < buffer.length) {
+ if (fillBuffer(i + 1)) {
+ continue;
+ } else {
+ buffer[limit] = '\0';
+ break;
+ }
+ }
+
+ // use a StringBuilder when the value is too long. It must be an unquoted string.
+ if (builder == null) {
+ builder = new StringBuilder();
+ }
+ builder.append(buffer, pos, i);
+ valueLength += i;
+ pos += i;
+ i = 0;
+ if (!fillBuffer(1)) {
+ break;
+ }
+ }
+
+ String result;
+ if (assignOffsetsOnly && builder == null) {
+ valuePos = pos;
+ result = null;
+ } else if (skipping) {
+ result = "skipped!";
+ } else if (builder == null) {
+ result = stringPool.get(buffer, pos, i);
+ } else {
+ builder.append(buffer, pos, i);
+ result = builder.toString();
+ }
+ valueLength += i;
+ pos += i;
+ return result;
+ }
+
+ @Override public String toString() {
+ return getClass().getSimpleName() + " near " + getSnippet();
+ }
+
+ /**
+ * Unescapes the character identified by the character or characters that
+ * immediately follow a backslash. The backslash '\' should have already
+ * been read. This supports both unicode escapes "u000A" and two-character
+ * escapes "\n".
+ *
+ * @throws NumberFormatException if any unicode escape sequences are
+ * malformed.
+ */
+ private char readEscapeCharacter() throws IOException {
+ if (pos == limit && !fillBuffer(1)) {
+ throw syntaxError("Unterminated escape sequence");
+ }
+
+ char escaped = buffer[pos++];
+ switch (escaped) {
+ case 'u':
+ if (pos + 4 > limit && !fillBuffer(4)) {
+ throw syntaxError("Unterminated escape sequence");
+ }
+ String hex = stringPool.get(buffer, pos, 4);
+ pos += 4;
+ return (char) Integer.parseInt(hex, 16);
+
+ case 't':
+ return '\t';
+
+ case 'b':
+ return '\b';
+
+ case 'n':
+ return '\n';
+
+ case 'r':
+ return '\r';
+
+ case 'f':
+ return '\f';
+
+ case '\'':
+ case '"':
+ case '\\':
+ default:
+ return escaped;
+ }
+ }
+
+ /**
+ * Reads a null, boolean, numeric or unquoted string literal value.
+ */
+ private JsonToken readLiteral() throws IOException {
+ value = nextLiteral(true);
+ if (valueLength == 0) {
+ throw syntaxError("Expected literal value");
+ }
+ token = decodeLiteral();
+ if (token == JsonToken.STRING) {
+ checkLenient();
+ }
+ return token;
+ }
+
+ /**
+ * Assigns {@code nextToken} based on the value of {@code nextValue}.
+ */
+ private JsonToken decodeLiteral() throws IOException {
+ if (valuePos == -1) {
+ // it was too long to fit in the buffer so it can only be a string
+ return JsonToken.STRING;
+ } else if (valueLength == 4
+ && ('n' == buffer[valuePos ] || 'N' == buffer[valuePos ])
+ && ('u' == buffer[valuePos + 1] || 'U' == buffer[valuePos + 1])
+ && ('l' == buffer[valuePos + 2] || 'L' == buffer[valuePos + 2])
+ && ('l' == buffer[valuePos + 3] || 'L' == buffer[valuePos + 3])) {
+ value = "null";
+ return JsonToken.NULL;
+ } else if (valueLength == 4
+ && ('t' == buffer[valuePos ] || 'T' == buffer[valuePos ])
+ && ('r' == buffer[valuePos + 1] || 'R' == buffer[valuePos + 1])
+ && ('u' == buffer[valuePos + 2] || 'U' == buffer[valuePos + 2])
+ && ('e' == buffer[valuePos + 3] || 'E' == buffer[valuePos + 3])) {
+ value = TRUE;
+ return JsonToken.BOOLEAN;
+ } else if (valueLength == 5
+ && ('f' == buffer[valuePos ] || 'F' == buffer[valuePos ])
+ && ('a' == buffer[valuePos + 1] || 'A' == buffer[valuePos + 1])
+ && ('l' == buffer[valuePos + 2] || 'L' == buffer[valuePos + 2])
+ && ('s' == buffer[valuePos + 3] || 'S' == buffer[valuePos + 3])
+ && ('e' == buffer[valuePos + 4] || 'E' == buffer[valuePos + 4])) {
+ value = FALSE;
+ return JsonToken.BOOLEAN;
+ } else {
+ value = stringPool.get(buffer, valuePos, valueLength);
+ return decodeNumber(buffer, valuePos, valueLength);
+ }
+ }
+
+ /**
+ * Determine whether the characters is a JSON number. Numbers are of the
+ * form -12.34e+56. Fractional and exponential parts are optional. Leading
+ * zeroes are not allowed in the value or exponential part, but are allowed
+ * in the fraction.
+ */
+ private JsonToken decodeNumber(char[] chars, int offset, int length) {
+ int i = offset;
+ int c = chars[i];
+
+ if (c == '-') {
+ c = chars[++i];
+ }
+
+ if (c == '0') {
+ c = chars[++i];
+ } else if (c >= '1' && c <= '9') {
+ c = chars[++i];
+ while (c >= '0' && c <= '9') {
+ c = chars[++i];
+ }
+ } else {
+ return JsonToken.STRING;
+ }
+
+ if (c == '.') {
+ c = chars[++i];
+ while (c >= '0' && c <= '9') {
+ c = chars[++i];
+ }
+ }
+
+ if (c == 'e' || c == 'E') {
+ c = chars[++i];
+ if (c == '+' || c == '-') {
+ c = chars[++i];
+ }
+ if (c >= '0' && c <= '9') {
+ c = chars[++i];
+ while (c >= '0' && c <= '9') {
+ c = chars[++i];
+ }
+ } else {
+ return JsonToken.STRING;
+ }
+ }
+
+ if (i == offset + length) {
+ return JsonToken.NUMBER;
+ } else {
+ return JsonToken.STRING;
+ }
+ }
+
+ /**
+ * Throws a new IO exception with the given message and a context snippet
+ * with this reader's content.
+ */
+ private IOException syntaxError(String message) throws IOException {
+ throw new MalformedJsonException(message
+ + " at line " + getLineNumber() + " column " + getColumnNumber());
+ }
+
+ private CharSequence getSnippet() {
+ StringBuilder snippet = new StringBuilder();
+ int beforePos = Math.min(pos, 20);
+ snippet.append(buffer, pos - beforePos, beforePos);
+ int afterPos = Math.min(limit - pos, 20);
+ snippet.append(buffer, pos, afterPos);
+ return snippet;
+ }
+}
diff --git a/android/util/JsonScope.java b/android/util/JsonScope.java
new file mode 100644
index 0000000..ca534e9
--- /dev/null
+++ b/android/util/JsonScope.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * Lexical scoping elements within a JSON reader or writer.
+ */
+enum JsonScope {
+
+ /**
+ * An array with no elements requires no separators or newlines before
+ * it is closed.
+ */
+ EMPTY_ARRAY,
+
+ /**
+ * A array with at least one value requires a comma and newline before
+ * the next element.
+ */
+ NONEMPTY_ARRAY,
+
+ /**
+ * An object with no name/value pairs requires no separators or newlines
+ * before it is closed.
+ */
+ EMPTY_OBJECT,
+
+ /**
+ * An object whose most recent element is a key. The next element must
+ * be a value.
+ */
+ DANGLING_NAME,
+
+ /**
+ * An object with at least one name/value pair requires a comma and
+ * newline before the next element.
+ */
+ NONEMPTY_OBJECT,
+
+ /**
+ * No object or array has been started.
+ */
+ EMPTY_DOCUMENT,
+
+ /**
+ * A document with at an array or object.
+ */
+ NONEMPTY_DOCUMENT,
+
+ /**
+ * A document that's been closed and cannot be accessed.
+ */
+ CLOSED,
+}
diff --git a/android/util/JsonToken.java b/android/util/JsonToken.java
new file mode 100644
index 0000000..45bc6ca
--- /dev/null
+++ b/android/util/JsonToken.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * A structure, name or value type in a JSON-encoded string.
+ */
+public enum JsonToken {
+
+ /**
+ * The opening of a JSON array. Written using {@link JsonWriter#beginObject}
+ * and read using {@link JsonReader#beginObject}.
+ */
+ BEGIN_ARRAY,
+
+ /**
+ * The closing of a JSON array. Written using {@link JsonWriter#endArray}
+ * and read using {@link JsonReader#endArray}.
+ */
+ END_ARRAY,
+
+ /**
+ * The opening of a JSON object. Written using {@link JsonWriter#beginObject}
+ * and read using {@link JsonReader#beginObject}.
+ */
+ BEGIN_OBJECT,
+
+ /**
+ * The closing of a JSON object. Written using {@link JsonWriter#endObject}
+ * and read using {@link JsonReader#endObject}.
+ */
+ END_OBJECT,
+
+ /**
+ * A JSON property name. Within objects, tokens alternate between names and
+ * their values. Written using {@link JsonWriter#name} and read using {@link
+ * JsonReader#nextName}
+ */
+ NAME,
+
+ /**
+ * A JSON string.
+ */
+ STRING,
+
+ /**
+ * A JSON number represented in this API by a Java {@code double}, {@code
+ * long}, or {@code int}.
+ */
+ NUMBER,
+
+ /**
+ * A JSON {@code true} or {@code false}.
+ */
+ BOOLEAN,
+
+ /**
+ * A JSON {@code null}.
+ */
+ NULL,
+
+ /**
+ * The end of the JSON stream. This sentinel value is returned by {@link
+ * JsonReader#peek()} to signal that the JSON-encoded value has no more
+ * tokens.
+ */
+ END_DOCUMENT
+}
diff --git a/android/util/JsonWriter.java b/android/util/JsonWriter.java
new file mode 100644
index 0000000..c1e6e40
--- /dev/null
+++ b/android/util/JsonWriter.java
@@ -0,0 +1,528 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Writes a JSON (<a href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>)
+ * encoded value to a stream, one token at a time. The stream includes both
+ * literal values (strings, numbers, booleans and nulls) as well as the begin
+ * and end delimiters of objects and arrays.
+ *
+ * <h3>Encoding JSON</h3>
+ * To encode your data as JSON, create a new {@code JsonWriter}. Each JSON
+ * document must contain one top-level array or object. Call methods on the
+ * writer as you walk the structure's contents, nesting arrays and objects as
+ * necessary:
+ * <ul>
+ * <li>To write <strong>arrays</strong>, first call {@link #beginArray()}.
+ * Write each of the array's elements with the appropriate {@link #value}
+ * methods or by nesting other arrays and objects. Finally close the array
+ * using {@link #endArray()}.
+ * <li>To write <strong>objects</strong>, first call {@link #beginObject()}.
+ * Write each of the object's properties by alternating calls to
+ * {@link #name} with the property's value. Write property values with the
+ * appropriate {@link #value} method or by nesting other objects or arrays.
+ * Finally close the object using {@link #endObject()}.
+ * </ul>
+ *
+ * <h3>Example</h3>
+ * Suppose we'd like to encode a stream of messages such as the following: <pre> {@code
+ * [
+ * {
+ * "id": 912345678901,
+ * "text": "How do I write JSON on Android?",
+ * "geo": null,
+ * "user": {
+ * "name": "android_newb",
+ * "followers_count": 41
+ * }
+ * },
+ * {
+ * "id": 912345678902,
+ * "text": "@android_newb just use android.util.JsonWriter!",
+ * "geo": [50.454722, -104.606667],
+ * "user": {
+ * "name": "jesse",
+ * "followers_count": 2
+ * }
+ * }
+ * ]}</pre>
+ * This code encodes the above structure: <pre> {@code
+ * public void writeJsonStream(OutputStream out, List<Message> messages) throws IOException {
+ * JsonWriter writer = new JsonWriter(new OutputStreamWriter(out, "UTF-8"));
+ * writer.setIndent(" ");
+ * writeMessagesArray(writer, messages);
+ * writer.close();
+ * }
+ *
+ * public void writeMessagesArray(JsonWriter writer, List<Message> messages) throws IOException {
+ * writer.beginArray();
+ * for (Message message : messages) {
+ * writeMessage(writer, message);
+ * }
+ * writer.endArray();
+ * }
+ *
+ * public void writeMessage(JsonWriter writer, Message message) throws IOException {
+ * writer.beginObject();
+ * writer.name("id").value(message.getId());
+ * writer.name("text").value(message.getText());
+ * if (message.getGeo() != null) {
+ * writer.name("geo");
+ * writeDoublesArray(writer, message.getGeo());
+ * } else {
+ * writer.name("geo").nullValue();
+ * }
+ * writer.name("user");
+ * writeUser(writer, message.getUser());
+ * writer.endObject();
+ * }
+ *
+ * public void writeUser(JsonWriter writer, User user) throws IOException {
+ * writer.beginObject();
+ * writer.name("name").value(user.getName());
+ * writer.name("followers_count").value(user.getFollowersCount());
+ * writer.endObject();
+ * }
+ *
+ * public void writeDoublesArray(JsonWriter writer, List<Double> doubles) throws IOException {
+ * writer.beginArray();
+ * for (Double value : doubles) {
+ * writer.value(value);
+ * }
+ * writer.endArray();
+ * }}</pre>
+ *
+ * <p>Each {@code JsonWriter} may be used to write a single JSON stream.
+ * Instances of this class are not thread safe. Calls that would result in a
+ * malformed JSON string will fail with an {@link IllegalStateException}.
+ */
+public final class JsonWriter implements Closeable {
+
+ /** The output data, containing at most one top-level array or object. */
+ private final Writer out;
+
+ private final List<JsonScope> stack = new ArrayList<JsonScope>();
+ {
+ stack.add(JsonScope.EMPTY_DOCUMENT);
+ }
+
+ /**
+ * A string containing a full set of spaces for a single level of
+ * indentation, or null for no pretty printing.
+ */
+ private String indent;
+
+ /**
+ * The name/value separator; either ":" or ": ".
+ */
+ private String separator = ":";
+
+ private boolean lenient;
+
+ /**
+ * Creates a new instance that writes a JSON-encoded stream to {@code out}.
+ * For best performance, ensure {@link Writer} is buffered; wrapping in
+ * {@link java.io.BufferedWriter BufferedWriter} if necessary.
+ */
+ public JsonWriter(Writer out) {
+ if (out == null) {
+ throw new NullPointerException("out == null");
+ }
+ this.out = out;
+ }
+
+ /**
+ * Sets the indentation string to be repeated for each level of indentation
+ * in the encoded document. If {@code indent.isEmpty()} the encoded document
+ * will be compact. Otherwise the encoded document will be more
+ * human-readable.
+ *
+ * @param indent a string containing only whitespace.
+ */
+ public void setIndent(String indent) {
+ if (indent.isEmpty()) {
+ this.indent = null;
+ this.separator = ":";
+ } else {
+ this.indent = indent;
+ this.separator = ": ";
+ }
+ }
+
+ /**
+ * Configure this writer to relax its syntax rules. By default, this writer
+ * only emits well-formed JSON as specified by <a
+ * href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>. Setting the writer
+ * to lenient permits the following:
+ * <ul>
+ * <li>Top-level values of any type. With strict writing, the top-level
+ * value must be an object or an array.
+ * <li>Numbers may be {@link Double#isNaN() NaNs} or {@link
+ * Double#isInfinite() infinities}.
+ * </ul>
+ */
+ public void setLenient(boolean lenient) {
+ this.lenient = lenient;
+ }
+
+ /**
+ * Returns true if this writer has relaxed syntax rules.
+ */
+ public boolean isLenient() {
+ return lenient;
+ }
+
+ /**
+ * Begins encoding a new array. Each call to this method must be paired with
+ * a call to {@link #endArray}.
+ *
+ * @return this writer.
+ */
+ public JsonWriter beginArray() throws IOException {
+ return open(JsonScope.EMPTY_ARRAY, "[");
+ }
+
+ /**
+ * Ends encoding the current array.
+ *
+ * @return this writer.
+ */
+ public JsonWriter endArray() throws IOException {
+ return close(JsonScope.EMPTY_ARRAY, JsonScope.NONEMPTY_ARRAY, "]");
+ }
+
+ /**
+ * Begins encoding a new object. Each call to this method must be paired
+ * with a call to {@link #endObject}.
+ *
+ * @return this writer.
+ */
+ public JsonWriter beginObject() throws IOException {
+ return open(JsonScope.EMPTY_OBJECT, "{");
+ }
+
+ /**
+ * Ends encoding the current object.
+ *
+ * @return this writer.
+ */
+ public JsonWriter endObject() throws IOException {
+ return close(JsonScope.EMPTY_OBJECT, JsonScope.NONEMPTY_OBJECT, "}");
+ }
+
+ /**
+ * Enters a new scope by appending any necessary whitespace and the given
+ * bracket.
+ */
+ private JsonWriter open(JsonScope empty, String openBracket) throws IOException {
+ beforeValue(true);
+ stack.add(empty);
+ out.write(openBracket);
+ return this;
+ }
+
+ /**
+ * Closes the current scope by appending any necessary whitespace and the
+ * given bracket.
+ */
+ private JsonWriter close(JsonScope empty, JsonScope nonempty, String closeBracket)
+ throws IOException {
+ JsonScope context = peek();
+ if (context != nonempty && context != empty) {
+ throw new IllegalStateException("Nesting problem: " + stack);
+ }
+
+ stack.remove(stack.size() - 1);
+ if (context == nonempty) {
+ newline();
+ }
+ out.write(closeBracket);
+ return this;
+ }
+
+ /**
+ * Returns the value on the top of the stack.
+ */
+ private JsonScope peek() {
+ return stack.get(stack.size() - 1);
+ }
+
+ /**
+ * Replace the value on the top of the stack with the given value.
+ */
+ private void replaceTop(JsonScope topOfStack) {
+ stack.set(stack.size() - 1, topOfStack);
+ }
+
+ /**
+ * Encodes the property name.
+ *
+ * @param name the name of the forthcoming value. May not be null.
+ * @return this writer.
+ */
+ public JsonWriter name(String name) throws IOException {
+ if (name == null) {
+ throw new NullPointerException("name == null");
+ }
+ beforeName();
+ string(name);
+ return this;
+ }
+
+ /**
+ * Encodes {@code value}.
+ *
+ * @param value the literal string value, or null to encode a null literal.
+ * @return this writer.
+ */
+ public JsonWriter value(String value) throws IOException {
+ if (value == null) {
+ return nullValue();
+ }
+ beforeValue(false);
+ string(value);
+ return this;
+ }
+
+ /**
+ * Encodes {@code null}.
+ *
+ * @return this writer.
+ */
+ public JsonWriter nullValue() throws IOException {
+ beforeValue(false);
+ out.write("null");
+ return this;
+ }
+
+ /**
+ * Encodes {@code value}.
+ *
+ * @return this writer.
+ */
+ public JsonWriter value(boolean value) throws IOException {
+ beforeValue(false);
+ out.write(value ? "true" : "false");
+ return this;
+ }
+
+ /**
+ * Encodes {@code value}.
+ *
+ * @param value a finite value. May not be {@link Double#isNaN() NaNs} or
+ * {@link Double#isInfinite() infinities} unless this writer is lenient.
+ * @return this writer.
+ */
+ public JsonWriter value(double value) throws IOException {
+ if (!lenient && (Double.isNaN(value) || Double.isInfinite(value))) {
+ throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
+ }
+ beforeValue(false);
+ out.append(Double.toString(value));
+ return this;
+ }
+
+ /**
+ * Encodes {@code value}.
+ *
+ * @return this writer.
+ */
+ public JsonWriter value(long value) throws IOException {
+ beforeValue(false);
+ out.write(Long.toString(value));
+ return this;
+ }
+
+ /**
+ * Encodes {@code value}.
+ *
+ * @param value a finite value. May not be {@link Double#isNaN() NaNs} or
+ * {@link Double#isInfinite() infinities} unless this writer is lenient.
+ * @return this writer.
+ */
+ public JsonWriter value(Number value) throws IOException {
+ if (value == null) {
+ return nullValue();
+ }
+
+ String string = value.toString();
+ if (!lenient &&
+ (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN"))) {
+ throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
+ }
+ beforeValue(false);
+ out.append(string);
+ return this;
+ }
+
+ /**
+ * Ensures all buffered data is written to the underlying {@link Writer}
+ * and flushes that writer.
+ */
+ public void flush() throws IOException {
+ out.flush();
+ }
+
+ /**
+ * Flushes and closes this writer and the underlying {@link Writer}.
+ *
+ * @throws IOException if the JSON document is incomplete.
+ */
+ public void close() throws IOException {
+ out.close();
+
+ if (peek() != JsonScope.NONEMPTY_DOCUMENT) {
+ throw new IOException("Incomplete document");
+ }
+ }
+
+ private void string(String value) throws IOException {
+ out.write("\"");
+ for (int i = 0, length = value.length(); i < length; i++) {
+ char c = value.charAt(i);
+
+ /*
+ * From RFC 4627, "All Unicode characters may be placed within the
+ * quotation marks except for the characters that must be escaped:
+ * quotation mark, reverse solidus, and the control characters
+ * (U+0000 through U+001F)."
+ *
+ * We also escape '\u2028' and '\u2029', which JavaScript interprets
+ * as newline characters. This prevents eval() from failing with a
+ * syntax error.
+ * http://code.google.com/p/google-gson/issues/detail?id=341
+ */
+ switch (c) {
+ case '"':
+ case '\\':
+ out.write('\\');
+ out.write(c);
+ break;
+
+ case '\t':
+ out.write("\\t");
+ break;
+
+ case '\b':
+ out.write("\\b");
+ break;
+
+ case '\n':
+ out.write("\\n");
+ break;
+
+ case '\r':
+ out.write("\\r");
+ break;
+
+ case '\f':
+ out.write("\\f");
+ break;
+
+ case '\u2028':
+ case '\u2029':
+ out.write(String.format("\\u%04x", (int) c));
+ break;
+
+ default:
+ if (c <= 0x1F) {
+ out.write(String.format("\\u%04x", (int) c));
+ } else {
+ out.write(c);
+ }
+ break;
+ }
+
+ }
+ out.write("\"");
+ }
+
+ private void newline() throws IOException {
+ if (indent == null) {
+ return;
+ }
+
+ out.write("\n");
+ for (int i = 1; i < stack.size(); i++) {
+ out.write(indent);
+ }
+ }
+
+ /**
+ * Inserts any necessary separators and whitespace before a name. Also
+ * adjusts the stack to expect the name's value.
+ */
+ private void beforeName() throws IOException {
+ JsonScope context = peek();
+ if (context == JsonScope.NONEMPTY_OBJECT) { // first in object
+ out.write(',');
+ } else if (context != JsonScope.EMPTY_OBJECT) { // not in an object!
+ throw new IllegalStateException("Nesting problem: " + stack);
+ }
+ newline();
+ replaceTop(JsonScope.DANGLING_NAME);
+ }
+
+ /**
+ * Inserts any necessary separators and whitespace before a literal value,
+ * inline array, or inline object. Also adjusts the stack to expect either a
+ * closing bracket or another element.
+ *
+ * @param root true if the value is a new array or object, the two values
+ * permitted as top-level elements.
+ */
+ private void beforeValue(boolean root) throws IOException {
+ switch (peek()) {
+ case EMPTY_DOCUMENT: // first in document
+ if (!lenient && !root) {
+ throw new IllegalStateException(
+ "JSON must start with an array or an object.");
+ }
+ replaceTop(JsonScope.NONEMPTY_DOCUMENT);
+ break;
+
+ case EMPTY_ARRAY: // first in array
+ replaceTop(JsonScope.NONEMPTY_ARRAY);
+ newline();
+ break;
+
+ case NONEMPTY_ARRAY: // another in array
+ out.append(',');
+ newline();
+ break;
+
+ case DANGLING_NAME: // value for name
+ out.append(separator);
+ replaceTop(JsonScope.NONEMPTY_OBJECT);
+ break;
+
+ case NONEMPTY_DOCUMENT:
+ throw new IllegalStateException(
+ "JSON must have only one top-level value.");
+
+ default:
+ throw new IllegalStateException("Nesting problem: " + stack);
+ }
+ }
+}
diff --git a/android/util/KeyValueListParser.java b/android/util/KeyValueListParser.java
new file mode 100644
index 0000000..fbc66e6
--- /dev/null
+++ b/android/util/KeyValueListParser.java
@@ -0,0 +1,429 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.util;
+
+import android.text.TextUtils;
+import android.util.proto.ProtoOutputStream;
+
+import java.io.PrintWriter;
+import java.time.Duration;
+import java.time.format.DateTimeParseException;
+
+/**
+ * Parses a list of key=value pairs, separated by some delimiter, and puts the results in
+ * an internal Map. Values can be then queried by key, or if not found, a default value
+ * can be used.
+ * @hide
+ */
+public class KeyValueListParser {
+ private final ArrayMap<String, String> mValues = new ArrayMap<>();
+ private final TextUtils.StringSplitter mSplitter;
+
+ /**
+ * Constructs a new KeyValueListParser. This can be reused for different strings
+ * by calling {@link #setString(String)}.
+ * @param delim The delimiter that separates key=value pairs.
+ */
+ public KeyValueListParser(char delim) {
+ mSplitter = new TextUtils.SimpleStringSplitter(delim);
+ }
+
+ /**
+ * Resets the parser with a new string to parse. The string is expected to be in the following
+ * format:
+ * <pre>key1=value,key2=value,key3=value</pre>
+ *
+ * where the delimiter is a comma.
+ *
+ * @param str the string to parse.
+ * @throws IllegalArgumentException if the string is malformed.
+ */
+ public void setString(String str) throws IllegalArgumentException {
+ mValues.clear();
+ if (str != null) {
+ mSplitter.setString(str);
+ for (String pair : mSplitter) {
+ int sep = pair.indexOf('=');
+ if (sep < 0) {
+ mValues.clear();
+ throw new IllegalArgumentException(
+ "'" + pair + "' in '" + str + "' is not a valid key-value pair");
+ }
+ mValues.put(pair.substring(0, sep).trim(), pair.substring(sep + 1).trim());
+ }
+ }
+ }
+
+ /**
+ * Get the value for key as an int.
+ * @param key The key to lookup.
+ * @param def The value to return if the key was not found, or the value was not a long.
+ * @return the int value associated with the key.
+ */
+ public int getInt(String key, int def) {
+ String value = mValues.get(key);
+ if (value != null) {
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ // fallthrough
+ }
+ }
+ return def;
+ }
+
+ /**
+ * Get the value for key as a long.
+ * @param key The key to lookup.
+ * @param def The value to return if the key was not found, or the value was not a long.
+ * @return the long value associated with the key.
+ */
+ public long getLong(String key, long def) {
+ String value = mValues.get(key);
+ if (value != null) {
+ try {
+ return Long.parseLong(value);
+ } catch (NumberFormatException e) {
+ // fallthrough
+ }
+ }
+ return def;
+ }
+
+ /**
+ * Get the value for key as a float.
+ * @param key The key to lookup.
+ * @param def The value to return if the key was not found, or the value was not a float.
+ * @return the float value associated with the key.
+ */
+ public float getFloat(String key, float def) {
+ String value = mValues.get(key);
+ if (value != null) {
+ try {
+ return Float.parseFloat(value);
+ } catch (NumberFormatException e) {
+ // fallthrough
+ }
+ }
+ return def;
+ }
+
+ /**
+ * Get the value for key as a string.
+ * @param key The key to lookup.
+ * @param def The value to return if the key was not found.
+ * @return the string value associated with the key.
+ */
+ public String getString(String key, String def) {
+ String value = mValues.get(key);
+ if (value != null) {
+ return value;
+ }
+ return def;
+ }
+
+ /**
+ * Get the value for key as a boolean.
+ * @param key The key to lookup.
+ * @param def The value to return if the key was not found.
+ * @return the string value associated with the key.
+ */
+ public boolean getBoolean(String key, boolean def) {
+ String value = mValues.get(key);
+ if (value != null) {
+ try {
+ return Boolean.parseBoolean(value);
+ } catch (NumberFormatException e) {
+ // fallthrough
+ }
+ }
+ return def;
+ }
+
+ /**
+ * Get the value for key as an integer array.
+ *
+ * The value should be encoded as "0:1:2:3:4"
+ *
+ * @param key The key to lookup.
+ * @param def The value to return if the key was not found.
+ * @return the int[] value associated with the key.
+ */
+ public int[] getIntArray(String key, int[] def) {
+ String value = mValues.get(key);
+ if (value != null) {
+ try {
+ String[] parts = value.split(":");
+ if (parts.length > 0) {
+ int[] ret = new int[parts.length];
+ for (int i = 0; i < parts.length; i++) {
+ ret[i] = Integer.parseInt(parts[i]);
+ }
+ return ret;
+ }
+ } catch (NumberFormatException e) {
+ // fallthrough
+ }
+ }
+ return def;
+ }
+
+ /**
+ * @return the number of keys.
+ */
+ public int size() {
+ return mValues.size();
+ }
+
+ /**
+ * @return the key at {@code index}. Use with {@link #size()} to enumerate all key-value pairs.
+ */
+ public String keyAt(int index) {
+ return mValues.keyAt(index);
+ }
+
+ /**
+ * {@hide}
+ * Parse a duration in millis based on java.time.Duration or just a number (millis)
+ */
+ public long getDurationMillis(String key, long def) {
+ String value = mValues.get(key);
+ if (value != null) {
+ try {
+ if (value.startsWith("P") || value.startsWith("p")) {
+ return Duration.parse(value).toMillis();
+ } else {
+ return Long.parseLong(value);
+ }
+ } catch (NumberFormatException | DateTimeParseException e) {
+ // fallthrough
+ }
+ }
+ return def;
+ }
+
+ /** Represents an integer config value. */
+ public static class IntValue {
+ private final String mKey;
+ private final int mDefaultValue;
+ private int mValue;
+
+ /** Constructor, initialize with a config key and a default value. */
+ public IntValue(String key, int defaultValue) {
+ mKey = key;
+ mDefaultValue = defaultValue;
+ mValue = mDefaultValue;
+ }
+
+ /** Read a value from {@link KeyValueListParser} */
+ public void parse(KeyValueListParser parser) {
+ mValue = parser.getInt(mKey, mDefaultValue);
+ }
+
+ /** Return the config key. */
+ public String getKey() {
+ return mKey;
+ }
+
+ /** Return the default value. */
+ public int getDefaultValue() {
+ return mDefaultValue;
+ }
+
+ /** Return the actual config value. */
+ public int getValue() {
+ return mValue;
+ }
+
+ /** Overwrites with a value. */
+ public void setValue(int value) {
+ mValue = value;
+ }
+
+ /** Used for dumpsys */
+ public void dump(PrintWriter pw, String prefix) {
+ pw.print(prefix);
+ pw.print(mKey);
+ pw.print("=");
+ pw.print(mValue);
+ pw.println();
+ }
+
+ /** Used for proto dumpsys */
+ public void dumpProto(ProtoOutputStream proto, long tag) {
+ proto.write(tag, mValue);
+ }
+ }
+
+ /** Represents an long config value. */
+ public static class LongValue {
+ private final String mKey;
+ private final long mDefaultValue;
+ private long mValue;
+
+ /** Constructor, initialize with a config key and a default value. */
+ public LongValue(String key, long defaultValue) {
+ mKey = key;
+ mDefaultValue = defaultValue;
+ mValue = mDefaultValue;
+ }
+
+ /** Read a value from {@link KeyValueListParser} */
+ public void parse(KeyValueListParser parser) {
+ mValue = parser.getLong(mKey, mDefaultValue);
+ }
+
+ /** Return the config key. */
+ public String getKey() {
+ return mKey;
+ }
+
+ /** Return the default value. */
+ public long getDefaultValue() {
+ return mDefaultValue;
+ }
+
+ /** Return the actual config value. */
+ public long getValue() {
+ return mValue;
+ }
+
+ /** Overwrites with a value. */
+ public void setValue(long value) {
+ mValue = value;
+ }
+
+ /** Used for dumpsys */
+ public void dump(PrintWriter pw, String prefix) {
+ pw.print(prefix);
+ pw.print(mKey);
+ pw.print("=");
+ pw.print(mValue);
+ pw.println();
+ }
+
+ /** Used for proto dumpsys */
+ public void dumpProto(ProtoOutputStream proto, long tag) {
+ proto.write(tag, mValue);
+ }
+ }
+
+ /** Represents an string config value. */
+ public static class StringValue {
+ private final String mKey;
+ private final String mDefaultValue;
+ private String mValue;
+
+ /** Constructor, initialize with a config key and a default value. */
+ public StringValue(String key, String defaultValue) {
+ mKey = key;
+ mDefaultValue = defaultValue;
+ mValue = mDefaultValue;
+ }
+
+ /** Read a value from {@link KeyValueListParser} */
+ public void parse(KeyValueListParser parser) {
+ mValue = parser.getString(mKey, mDefaultValue);
+ }
+
+ /** Return the config key. */
+ public String getKey() {
+ return mKey;
+ }
+
+ /** Return the default value. */
+ public String getDefaultValue() {
+ return mDefaultValue;
+ }
+
+ /** Return the actual config value. */
+ public String getValue() {
+ return mValue;
+ }
+
+ /** Overwrites with a value. */
+ public void setValue(String value) {
+ mValue = value;
+ }
+
+ /** Used for dumpsys */
+ public void dump(PrintWriter pw, String prefix) {
+ pw.print(prefix);
+ pw.print(mKey);
+ pw.print("=");
+ pw.print(mValue);
+ pw.println();
+ }
+
+ /** Used for proto dumpsys */
+ public void dumpProto(ProtoOutputStream proto, long tag) {
+ proto.write(tag, mValue);
+ }
+ }
+
+ /** Represents an float config value. */
+ public static class FloatValue {
+ private final String mKey;
+ private final float mDefaultValue;
+ private float mValue;
+
+ /** Constructor, initialize with a config key and a default value. */
+ public FloatValue(String key, float defaultValue) {
+ mKey = key;
+ mDefaultValue = defaultValue;
+ mValue = mDefaultValue;
+ }
+
+ /** Read a value from {@link KeyValueListParser} */
+ public void parse(KeyValueListParser parser) {
+ mValue = parser.getFloat(mKey, mDefaultValue);
+ }
+
+ /** Return the config key. */
+ public String getKey() {
+ return mKey;
+ }
+
+ /** Return the default value. */
+ public float getDefaultValue() {
+ return mDefaultValue;
+ }
+
+ /** Return the actual config value. */
+ public float getValue() {
+ return mValue;
+ }
+
+ /** Overwrites with a value. */
+ public void setValue(float value) {
+ mValue = value;
+ }
+
+ /** Used for dumpsys */
+ public void dump(PrintWriter pw, String prefix) {
+ pw.print(prefix);
+ pw.print(mKey);
+ pw.print("=");
+ pw.print(mValue);
+ pw.println();
+ }
+
+ /** Used for proto dumpsys */
+ public void dumpProto(ProtoOutputStream proto, long tag) {
+ proto.write(tag, mValue);
+ }
+ }
+}
diff --git a/android/util/KeyValueSettingObserver.java b/android/util/KeyValueSettingObserver.java
new file mode 100644
index 0000000..9fca8b2
--- /dev/null
+++ b/android/util/KeyValueSettingObserver.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+
+/**
+ * Abstract class for observing changes to a specified setting stored as a comma-separated key value
+ * list of parameters. Registers and unregisters a {@link ContentObserver} and handles updates when
+ * the setting changes.
+ *
+ * <p>Subclasses should pass in the relevant setting's {@link Uri} in the constructor and implement
+ * {@link #update(KeyValueListParser)} to receive updates when the value changes.
+ * Calls to {@link #update(KeyValueListParser)} only trigger after calling {@link
+ * #start()}.
+ *
+ * <p>To get the most up-to-date parameter values, first call {@link #start()} before accessing the
+ * values to start observing changes, and then call {@link #stop()} once finished.
+ *
+ * @hide
+ */
+public abstract class KeyValueSettingObserver {
+ private static final String TAG = "KeyValueSettingObserver";
+
+ private final KeyValueListParser mParser = new KeyValueListParser(',');
+
+ private final ContentObserver mObserver;
+ private final ContentResolver mResolver;
+ private final Uri mSettingUri;
+
+ public KeyValueSettingObserver(Handler handler, ContentResolver resolver,
+ Uri uri) {
+ mObserver = new SettingObserver(handler);
+ mResolver = resolver;
+ mSettingUri = uri;
+ }
+
+ /** Starts observing changes for the setting. Pair with {@link #stop()}. */
+ public void start() {
+ mResolver.registerContentObserver(mSettingUri, false, mObserver);
+ setParserValue();
+ update(mParser);
+ }
+
+ /** Stops observing changes for the setting. */
+ public void stop() {
+ mResolver.unregisterContentObserver(mObserver);
+ }
+
+ /**
+ * Returns the {@link String} representation of the setting. Subclasses should implement this
+ * for their setting.
+ */
+ public abstract String getSettingValue(ContentResolver resolver);
+
+ /** Updates the parser with the current setting value. */
+ private void setParserValue() {
+ String setting = getSettingValue(mResolver);
+ try {
+ mParser.setString(setting);
+ } catch (IllegalArgumentException e) {
+ Slog.e(TAG, "Malformed setting: " + setting);
+ }
+ }
+
+ /** Subclasses should implement this to update references to their parameters. */
+ public abstract void update(KeyValueListParser parser);
+
+ private class SettingObserver extends ContentObserver {
+ private SettingObserver(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ setParserValue();
+ update(mParser);
+ }
+ }
+}
diff --git a/android/util/LauncherIcons.java b/android/util/LauncherIcons.java
new file mode 100644
index 0000000..e652e17
--- /dev/null
+++ b/android/util/LauncherIcons.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.util;
+
+import android.app.ActivityThread;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.DrawableWrapper;
+import android.graphics.drawable.LayerDrawable;
+
+/**
+ * Utility class to handle icon treatments (e.g., shadow generation) for the Launcher icons.
+ * @hide
+ */
+public final class LauncherIcons {
+
+ // Percent of actual icon size
+ private static final float ICON_SIZE_BLUR_FACTOR = 0.5f/48;
+ // Percent of actual icon size
+ private static final float ICON_SIZE_KEY_SHADOW_DELTA_FACTOR = 1f/48;
+
+ private static final int KEY_SHADOW_ALPHA = 61;
+ private static final int AMBIENT_SHADOW_ALPHA = 30;
+
+ private final SparseArray<Bitmap> mShadowCache = new SparseArray<>();
+ private final int mIconSize;
+ private final Resources mRes;
+
+ public LauncherIcons(Context context) {
+ mRes = context.getResources();
+ mIconSize = mRes.getDimensionPixelSize(android.R.dimen.app_icon_size);
+ }
+
+ public Drawable wrapIconDrawableWithShadow(Drawable drawable) {
+ if (!(drawable instanceof AdaptiveIconDrawable)) {
+ return drawable;
+ }
+ Bitmap shadow = getShadowBitmap((AdaptiveIconDrawable) drawable);
+ return new ShadowDrawable(shadow, drawable);
+ }
+
+ private Bitmap getShadowBitmap(AdaptiveIconDrawable d) {
+ int shadowSize = Math.max(mIconSize, d.getIntrinsicHeight());
+ synchronized (mShadowCache) {
+ Bitmap shadow = mShadowCache.get(shadowSize);
+ if (shadow != null) {
+ return shadow;
+ }
+ }
+
+ d.setBounds(0, 0, shadowSize, shadowSize);
+
+ float blur = ICON_SIZE_BLUR_FACTOR * shadowSize;
+ float keyShadowDistance = ICON_SIZE_KEY_SHADOW_DELTA_FACTOR * shadowSize;
+
+ int bitmapSize = (int) (shadowSize + 2 * blur + keyShadowDistance);
+ Bitmap shadow = Bitmap.createBitmap(bitmapSize, bitmapSize, Bitmap.Config.ARGB_8888);
+
+ Canvas canvas = new Canvas(shadow);
+ canvas.translate(blur + keyShadowDistance / 2, blur);
+
+ Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ paint.setColor(Color.TRANSPARENT);
+
+ // Draw ambient shadow
+ paint.setShadowLayer(blur, 0, 0, AMBIENT_SHADOW_ALPHA << 24);
+ canvas.drawPath(d.getIconMask(), paint);
+
+ // Draw key shadow
+ canvas.translate(0, keyShadowDistance);
+ paint.setShadowLayer(blur, 0, 0, KEY_SHADOW_ALPHA << 24);
+ canvas.drawPath(d.getIconMask(), paint);
+
+ canvas.setBitmap(null);
+ synchronized (mShadowCache) {
+ mShadowCache.put(shadowSize, shadow);
+ }
+ return shadow;
+ }
+
+ public Drawable getBadgeDrawable(int foregroundRes, int backgroundColor) {
+ return getBadgedDrawable(null, foregroundRes, backgroundColor);
+ }
+
+ public Drawable getBadgedDrawable(Drawable base, int foregroundRes, int backgroundColor) {
+ Resources overlayableRes =
+ ActivityThread.currentActivityThread().getApplication().getResources();
+
+ // ic_corp_icon_badge_shadow is not work-profile-specific.
+ Drawable badgeShadow = overlayableRes.getDrawable(
+ com.android.internal.R.drawable.ic_corp_icon_badge_shadow);
+
+ // ic_corp_icon_badge_color is not work-profile-specific.
+ Drawable badgeColor = overlayableRes.getDrawable(
+ com.android.internal.R.drawable.ic_corp_icon_badge_color)
+ .getConstantState().newDrawable().mutate();
+
+ Drawable badgeForeground = overlayableRes.getDrawable(foregroundRes);
+ badgeForeground.setTint(backgroundColor);
+
+ Drawable[] drawables = base == null
+ ? new Drawable[] {badgeShadow, badgeColor, badgeForeground }
+ : new Drawable[] {base, badgeShadow, badgeColor, badgeForeground };
+ return new LayerDrawable(drawables);
+ }
+
+ /**
+ * A drawable which draws a shadow bitmap behind a drawable
+ */
+ private static class ShadowDrawable extends DrawableWrapper {
+
+ final MyConstantState mState;
+
+ public ShadowDrawable(Bitmap shadow, Drawable dr) {
+ super(dr);
+ mState = new MyConstantState(shadow, dr.getConstantState());
+ }
+
+ ShadowDrawable(MyConstantState state) {
+ super(state.mChildState.newDrawable());
+ mState = state;
+ }
+
+ @Override
+ public ConstantState getConstantState() {
+ return mState;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ Rect bounds = getBounds();
+ canvas.drawBitmap(mState.mShadow, null, bounds, mState.mPaint);
+ canvas.save();
+ // Ratio of child drawable size to shadow bitmap size
+ float factor = 1 / (1 + 2 * ICON_SIZE_BLUR_FACTOR + ICON_SIZE_KEY_SHADOW_DELTA_FACTOR);
+
+ canvas.translate(
+ bounds.width() * factor *
+ (ICON_SIZE_BLUR_FACTOR + ICON_SIZE_KEY_SHADOW_DELTA_FACTOR / 2),
+ bounds.height() * factor * ICON_SIZE_BLUR_FACTOR);
+ canvas.scale(factor, factor);
+ super.draw(canvas);
+ canvas.restore();
+ }
+
+ private static class MyConstantState extends ConstantState {
+
+ final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
+ final Bitmap mShadow;
+ final ConstantState mChildState;
+
+ MyConstantState(Bitmap shadow, ConstantState childState) {
+ mShadow = shadow;
+ mChildState = childState;
+ }
+
+ @Override
+ public Drawable newDrawable() {
+ return new ShadowDrawable(this);
+ }
+
+ @Override
+ public int getChangingConfigurations() {
+ return mChildState.getChangingConfigurations();
+ }
+ }
+ }
+}
diff --git a/android/util/LayoutDirection.java b/android/util/LayoutDirection.java
new file mode 100644
index 0000000..03077e4
--- /dev/null
+++ b/android/util/LayoutDirection.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * A class for defining layout directions. A layout direction can be left-to-right (LTR)
+ * or right-to-left (RTL). It can also be inherited (from a parent) or deduced from the default
+ * language script of a locale.
+ */
+public final class LayoutDirection {
+
+ // No instantiation
+ private LayoutDirection() {}
+
+ /**
+ * An undefined layout direction.
+ * @hide
+ */
+ public static final int UNDEFINED = -1;
+
+ /**
+ * Horizontal layout direction is from Left to Right.
+ */
+ public static final int LTR = 0;
+
+ /**
+ * Horizontal layout direction is from Right to Left.
+ */
+ public static final int RTL = 1;
+
+ /**
+ * Horizontal layout direction is inherited.
+ */
+ public static final int INHERIT = 2;
+
+ /**
+ * Horizontal layout direction is deduced from the default language script for the locale.
+ */
+ public static final int LOCALE = 3;
+}
diff --git a/android/util/LocalLog.java b/android/util/LocalLog.java
new file mode 100644
index 0000000..fda5e0d
--- /dev/null
+++ b/android/util/LocalLog.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.SystemClock;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Iterator;
+
+/**
+ * @hide
+ */
+public final class LocalLog {
+
+ private final Deque<String> mLog;
+ private final int mMaxLines;
+
+ /**
+ * {@code true} to use log timestamps expressed in local date/time, {@code false} to use log
+ * timestamped expressed with the elapsed realtime clock and UTC system clock. {@code false} is
+ * useful when logging behavior that modifies device time zone or system clock.
+ */
+ private final boolean mUseLocalTimestamps;
+
+ @UnsupportedAppUsage
+ public LocalLog(int maxLines) {
+ this(maxLines, true /* useLocalTimestamps */);
+ }
+
+ public LocalLog(int maxLines, boolean useLocalTimestamps) {
+ mMaxLines = Math.max(0, maxLines);
+ mLog = new ArrayDeque<>(mMaxLines);
+ mUseLocalTimestamps = useLocalTimestamps;
+ }
+
+ @UnsupportedAppUsage
+ public void log(String msg) {
+ if (mMaxLines <= 0) {
+ return;
+ }
+ final String logLine;
+ if (mUseLocalTimestamps) {
+ logLine = String.format("%s - %s", LocalDateTime.now(), msg);
+ } else {
+ logLine = String.format(
+ "%s / %s - %s", SystemClock.elapsedRealtime(), Instant.now(), msg);
+ }
+ append(logLine);
+ }
+
+ private synchronized void append(String logLine) {
+ while (mLog.size() >= mMaxLines) {
+ mLog.remove();
+ }
+ mLog.add(logLine);
+ }
+
+ @UnsupportedAppUsage
+ public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ dump(pw);
+ }
+
+ public synchronized void dump(PrintWriter pw) {
+ dump("", pw);
+ }
+
+ /**
+ * Dumps the content of local log to print writer with each log entry predeced with indent
+ *
+ * @param indent indent that precedes each log entry
+ * @param pw printer writer to write into
+ */
+ public synchronized void dump(String indent, PrintWriter pw) {
+ Iterator<String> itr = mLog.iterator();
+ while (itr.hasNext()) {
+ pw.printf("%s%s\n", indent, itr.next());
+ }
+ }
+
+ public synchronized void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ reverseDump(pw);
+ }
+
+ public synchronized void reverseDump(PrintWriter pw) {
+ Iterator<String> itr = mLog.descendingIterator();
+ while (itr.hasNext()) {
+ pw.println(itr.next());
+ }
+ }
+
+ public static class ReadOnlyLocalLog {
+ private final LocalLog mLog;
+ ReadOnlyLocalLog(LocalLog log) {
+ mLog = log;
+ }
+ @UnsupportedAppUsage
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ mLog.dump(pw);
+ }
+ public void dump(PrintWriter pw) {
+ mLog.dump(pw);
+ }
+ public void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ mLog.reverseDump(pw);
+ }
+ public void reverseDump(PrintWriter pw) {
+ mLog.reverseDump(pw);
+ }
+ }
+
+ @UnsupportedAppUsage
+ public ReadOnlyLocalLog readOnlyLocalLog() {
+ return new ReadOnlyLocalLog(this);
+ }
+}
diff --git a/android/util/Log.java b/android/util/Log.java
new file mode 100644
index 0000000..b94e48b
--- /dev/null
+++ b/android/util/Log.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.net.UnknownHostException;
+
+/**
+ * Mock Log implementation for testing on non android host.
+ */
+public final class Log {
+
+ /**
+ * Priority constant for the println method; use Log.v.
+ */
+ public static final int VERBOSE = 2;
+
+ /**
+ * Priority constant for the println method; use Log.d.
+ */
+ public static final int DEBUG = 3;
+
+ /**
+ * Priority constant for the println method; use Log.i.
+ */
+ public static final int INFO = 4;
+
+ /**
+ * Priority constant for the println method; use Log.w.
+ */
+ public static final int WARN = 5;
+
+ /**
+ * Priority constant for the println method; use Log.e.
+ */
+ public static final int ERROR = 6;
+
+ /**
+ * Priority constant for the println method.
+ */
+ public static final int ASSERT = 7;
+
+ private Log() {
+ }
+
+ /**
+ * Send a {@link #VERBOSE} log message.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ */
+ public static int v(String tag, String msg) {
+ return println(LOG_ID_MAIN, VERBOSE, tag, msg);
+ }
+
+ /**
+ * Send a {@link #VERBOSE} log message and log the exception.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ * @param tr An exception to log
+ */
+ public static int v(String tag, String msg, Throwable tr) {
+ return println(LOG_ID_MAIN, VERBOSE, tag, msg + '\n' + getStackTraceString(tr));
+ }
+
+ /**
+ * Send a {@link #DEBUG} log message.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ */
+ public static int d(String tag, String msg) {
+ return println(LOG_ID_MAIN, DEBUG, tag, msg);
+ }
+
+ /**
+ * Send a {@link #DEBUG} log message and log the exception.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ * @param tr An exception to log
+ */
+ public static int d(String tag, String msg, Throwable tr) {
+ return println(LOG_ID_MAIN, DEBUG, tag, msg + '\n' + getStackTraceString(tr));
+ }
+
+ /**
+ * Send an {@link #INFO} log message.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ */
+ public static int i(String tag, String msg) {
+ return println(LOG_ID_MAIN, INFO, tag, msg);
+ }
+
+ /**
+ * Send a {@link #INFO} log message and log the exception.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ * @param tr An exception to log
+ */
+ public static int i(String tag, String msg, Throwable tr) {
+ return println(LOG_ID_MAIN, INFO, tag, msg + '\n' + getStackTraceString(tr));
+ }
+
+ /**
+ * Send a {@link #WARN} log message.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ */
+ public static int w(String tag, String msg) {
+ return println(LOG_ID_MAIN, WARN, tag, msg);
+ }
+
+ /**
+ * Send a {@link #WARN} log message and log the exception.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ * @param tr An exception to log
+ */
+ public static int w(String tag, String msg, Throwable tr) {
+ return println(LOG_ID_MAIN, WARN, tag, msg + '\n' + getStackTraceString(tr));
+ }
+
+ /*
+ * Send a {@link #WARN} log message and log the exception.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param tr An exception to log
+ */
+ public static int w(String tag, Throwable tr) {
+ return println(LOG_ID_MAIN, WARN, tag, getStackTraceString(tr));
+ }
+
+ /**
+ * Send an {@link #ERROR} log message.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ */
+ public static int e(String tag, String msg) {
+ return println(LOG_ID_MAIN, ERROR, tag, msg);
+ }
+
+ /**
+ * Send a {@link #ERROR} log message and log the exception.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ * @param tr An exception to log
+ */
+ public static int e(String tag, String msg, Throwable tr) {
+ return println(LOG_ID_MAIN, ERROR, tag, msg + '\n' + getStackTraceString(tr));
+ }
+
+ /**
+ * Handy function to get a loggable stack trace from a Throwable
+ * @param tr An exception to log
+ */
+ public static String getStackTraceString(Throwable tr) {
+ if (tr == null) {
+ return "";
+ }
+
+ // This is to reduce the amount of log spew that apps do in the non-error
+ // condition of the network being unavailable.
+ Throwable t = tr;
+ while (t != null) {
+ if (t instanceof UnknownHostException) {
+ return "";
+ }
+ t = t.getCause();
+ }
+
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ tr.printStackTrace(pw);
+ pw.flush();
+ return sw.toString();
+ }
+
+ /**
+ * Low-level logging call.
+ * @param priority The priority/type of this log message
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param msg The message you would like logged.
+ * @return The number of bytes written.
+ */
+ public static int println(int priority, String tag, String msg) {
+ return println(LOG_ID_MAIN, priority, tag, msg);
+ }
+
+ /** @hide */ public static final int LOG_ID_MAIN = 0;
+ /** @hide */ public static final int LOG_ID_RADIO = 1;
+ /** @hide */ public static final int LOG_ID_EVENTS = 2;
+ /** @hide */ public static final int LOG_ID_SYSTEM = 3;
+ /** @hide */ public static final int LOG_ID_CRASH = 4;
+
+ /** @hide */ @SuppressWarnings("unused")
+ public static int println(int bufID,
+ int priority, String tag, String msg) {
+ return 0;
+ }
+}
diff --git a/android/util/LogPrinter.java b/android/util/LogPrinter.java
new file mode 100644
index 0000000..68f64d0
--- /dev/null
+++ b/android/util/LogPrinter.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * Implementation of a {@link android.util.Printer} that sends its output
+ * to the system log.
+ */
+public class LogPrinter implements Printer {
+ private final int mPriority;
+ private final String mTag;
+ private final int mBuffer;
+
+ /**
+ * Create a new Printer that sends to the log with the given priority
+ * and tag.
+ *
+ * @param priority The desired log priority:
+ * {@link android.util.Log#VERBOSE Log.VERBOSE},
+ * {@link android.util.Log#DEBUG Log.DEBUG},
+ * {@link android.util.Log#INFO Log.INFO},
+ * {@link android.util.Log#WARN Log.WARN}, or
+ * {@link android.util.Log#ERROR Log.ERROR}.
+ * @param tag A string tag to associate with each printed log statement.
+ */
+ public LogPrinter(int priority, String tag) {
+ mPriority = priority;
+ mTag = tag;
+ mBuffer = Log.LOG_ID_MAIN;
+ }
+
+ /**
+ * @hide
+ * Same as above, but buffer is one of the LOG_ID_ constants from android.util.Log.
+ */
+ public LogPrinter(int priority, String tag, int buffer) {
+ mPriority = priority;
+ mTag = tag;
+ mBuffer = buffer;
+ }
+
+ public void println(String x) {
+ Log.println_native(mBuffer, mPriority, mTag, x);
+ }
+}
diff --git a/android/util/LogWriter.java b/android/util/LogWriter.java
new file mode 100644
index 0000000..a674ae1
--- /dev/null
+++ b/android/util/LogWriter.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.compat.annotation.UnsupportedAppUsage;
+
+import java.io.Writer;
+
+/** @hide */
+public class LogWriter extends Writer {
+ private final int mPriority;
+ private final String mTag;
+ private final int mBuffer;
+ private StringBuilder mBuilder = new StringBuilder(128);
+
+ /**
+ * Create a new Writer that sends to the log with the given priority
+ * and tag.
+ *
+ * @param priority The desired log priority:
+ * {@link android.util.Log#VERBOSE Log.VERBOSE},
+ * {@link android.util.Log#DEBUG Log.DEBUG},
+ * {@link android.util.Log#INFO Log.INFO},
+ * {@link android.util.Log#WARN Log.WARN}, or
+ * {@link android.util.Log#ERROR Log.ERROR}.
+ * @param tag A string tag to associate with each printed log statement.
+ */
+ @UnsupportedAppUsage
+ public LogWriter(int priority, String tag) {
+ mPriority = priority;
+ mTag = tag;
+ mBuffer = Log.LOG_ID_MAIN;
+ }
+
+ /**
+ * @hide
+ * Same as above, but buffer is one of the LOG_ID_ constants from android.util.Log.
+ */
+ public LogWriter(int priority, String tag, int buffer) {
+ mPriority = priority;
+ mTag = tag;
+ mBuffer = buffer;
+ }
+
+ @Override public void close() {
+ flushBuilder();
+ }
+
+ @Override public void flush() {
+ flushBuilder();
+ }
+
+ @Override public void write(char[] buf, int offset, int count) {
+ for(int i = 0; i < count; i++) {
+ char c = buf[offset + i];
+ if ( c == '\n') {
+ flushBuilder();
+ }
+ else {
+ mBuilder.append(c);
+ }
+ }
+ }
+
+ private void flushBuilder() {
+ if (mBuilder.length() > 0) {
+ Log.println_native(mBuffer, mPriority, mTag, mBuilder.toString());
+ mBuilder.delete(0, mBuilder.length());
+ }
+ }
+}
diff --git a/android/util/Log_Delegate.java b/android/util/Log_Delegate.java
new file mode 100644
index 0000000..7f432ab
--- /dev/null
+++ b/android/util/Log_Delegate.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+class Log_Delegate {
+ // to replicate prefix visible when using 'adb logcat'
+ private static char priorityChar(int priority) {
+ switch (priority) {
+ case Log.VERBOSE:
+ return 'V';
+ case Log.DEBUG:
+ return 'D';
+ case Log.INFO:
+ return 'I';
+ case Log.WARN:
+ return 'W';
+ case Log.ERROR:
+ return 'E';
+ case Log.ASSERT:
+ return 'A';
+ default:
+ return '?';
+ }
+ }
+
+ @LayoutlibDelegate
+ static int println_native(int bufID, int priority, String tag, String msgs) {
+ String prefix = priorityChar(priority) + "/" + tag + ": ";
+ for (String msg: msgs.split("\n")) {
+ System.out.println(prefix + msg);
+ }
+ return 0;
+ }
+
+}
diff --git a/android/util/LongArray.java b/android/util/LongArray.java
new file mode 100644
index 0000000..93bcd6b
--- /dev/null
+++ b/android/util/LongArray.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.Preconditions;
+
+import libcore.util.EmptyArray;
+
+import java.util.Arrays;
+
+/**
+ * Implements a growing array of long primitives.
+ *
+ * @hide
+ */
+public class LongArray implements Cloneable {
+ private static final int MIN_CAPACITY_INCREMENT = 12;
+
+ private long[] mValues;
+ private int mSize;
+
+ private LongArray(long[] array, int size) {
+ mValues = array;
+ mSize = Preconditions.checkArgumentInRange(size, 0, array.length, "size");
+ }
+
+ /**
+ * Creates an empty LongArray with the default initial capacity.
+ */
+ @UnsupportedAppUsage
+ public LongArray() {
+ this(10);
+ }
+
+ /**
+ * Creates an empty LongArray with the specified initial capacity.
+ */
+ public LongArray(int initialCapacity) {
+ if (initialCapacity == 0) {
+ mValues = EmptyArray.LONG;
+ } else {
+ mValues = ArrayUtils.newUnpaddedLongArray(initialCapacity);
+ }
+ mSize = 0;
+ }
+
+ /**
+ * Creates an LongArray wrapping the given primitive long array.
+ */
+ public static LongArray wrap(long[] array) {
+ return new LongArray(array, array.length);
+ }
+
+ /**
+ * Creates an LongArray from the given primitive long array, copying it.
+ */
+ public static LongArray fromArray(long[] array, int size) {
+ return wrap(Arrays.copyOf(array, size));
+ }
+
+ /**
+ * Changes the size of this LongArray. If this LongArray is shrinked, the backing array capacity
+ * is unchanged. If the new size is larger than backing array capacity, a new backing array is
+ * created from the current content of this LongArray padded with 0s.
+ */
+ public void resize(int newSize) {
+ Preconditions.checkArgumentNonnegative(newSize);
+ if (newSize <= mValues.length) {
+ Arrays.fill(mValues, newSize, mValues.length, 0);
+ } else {
+ ensureCapacity(newSize - mSize);
+ }
+ mSize = newSize;
+ }
+
+ /**
+ * Appends the specified value to the end of this array.
+ */
+ public void add(long value) {
+ add(mSize, value);
+ }
+
+ /**
+ * Inserts a value at the specified position in this array. If the specified index is equal to
+ * the length of the array, the value is added at the end.
+ *
+ * @throws IndexOutOfBoundsException when index < 0 || index > size()
+ */
+ @UnsupportedAppUsage
+ public void add(int index, long value) {
+ ensureCapacity(1);
+ int rightSegment = mSize - index;
+ mSize++;
+ ArrayUtils.checkBounds(mSize, index);
+
+ if (rightSegment != 0) {
+ // Move by 1 all values from the right of 'index'
+ System.arraycopy(mValues, index, mValues, index + 1, rightSegment);
+ }
+
+ mValues[index] = value;
+ }
+
+ /**
+ * Adds the values in the specified array to this array.
+ */
+ public void addAll(LongArray values) {
+ final int count = values.mSize;
+ ensureCapacity(count);
+
+ System.arraycopy(values.mValues, 0, mValues, mSize, count);
+ mSize += count;
+ }
+
+ /**
+ * Ensures capacity to append at least <code>count</code> values.
+ */
+ private void ensureCapacity(int count) {
+ final int currentSize = mSize;
+ final int minCapacity = currentSize + count;
+ if (minCapacity >= mValues.length) {
+ final int targetCap = currentSize + (currentSize < (MIN_CAPACITY_INCREMENT / 2) ?
+ MIN_CAPACITY_INCREMENT : currentSize >> 1);
+ final int newCapacity = targetCap > minCapacity ? targetCap : minCapacity;
+ final long[] newValues = ArrayUtils.newUnpaddedLongArray(newCapacity);
+ System.arraycopy(mValues, 0, newValues, 0, currentSize);
+ mValues = newValues;
+ }
+ }
+
+ /**
+ * Removes all values from this array.
+ */
+ public void clear() {
+ mSize = 0;
+ }
+
+ @Override
+ public LongArray clone() {
+ LongArray clone = null;
+ try {
+ clone = (LongArray) super.clone();
+ clone.mValues = mValues.clone();
+ } catch (CloneNotSupportedException cnse) {
+ /* ignore */
+ }
+ return clone;
+ }
+
+ /**
+ * Returns the value at the specified position in this array.
+ */
+ @UnsupportedAppUsage
+ public long get(int index) {
+ ArrayUtils.checkBounds(mSize, index);
+ return mValues[index];
+ }
+
+ /**
+ * Sets the value at the specified position in this array.
+ */
+ public void set(int index, long value) {
+ ArrayUtils.checkBounds(mSize, index);
+ mValues[index] = value;
+ }
+
+ /**
+ * Returns the index of the first occurrence of the specified value in this
+ * array, or -1 if this array does not contain the value.
+ */
+ public int indexOf(long value) {
+ final int n = mSize;
+ for (int i = 0; i < n; i++) {
+ if (mValues[i] == value) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Removes the value at the specified index from this array.
+ */
+ public void remove(int index) {
+ ArrayUtils.checkBounds(mSize, index);
+ System.arraycopy(mValues, index + 1, mValues, index, mSize - index - 1);
+ mSize--;
+ }
+
+ /**
+ * Returns the number of values in this array.
+ */
+ @UnsupportedAppUsage
+ public int size() {
+ return mSize;
+ }
+
+ /**
+ * Returns a new array with the contents of this LongArray.
+ */
+ public long[] toArray() {
+ return Arrays.copyOf(mValues, mSize);
+ }
+
+ /**
+ * Test if each element of {@code a} equals corresponding element from {@code b}
+ */
+ public static boolean elementsEqual(@Nullable LongArray a, @Nullable LongArray b) {
+ if (a == null || b == null) return a == b;
+ if (a.mSize != b.mSize) return false;
+ for (int i = 0; i < a.mSize; i++) {
+ if (a.get(i) != b.get(i)) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/android/util/LongArrayQueue.java b/android/util/LongArrayQueue.java
new file mode 100644
index 0000000..5c701db
--- /dev/null
+++ b/android/util/LongArrayQueue.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.GrowingArrayUtils;
+
+import libcore.util.EmptyArray;
+
+import java.util.NoSuchElementException;
+
+/**
+ * A lightweight implementation for a queue with long values.
+ * Additionally supports getting an element with a specified position from the head of the queue.
+ * The queue grows in size if needed to accommodate new elements.
+ *
+ * @hide
+ */
+public class LongArrayQueue {
+
+ private long[] mValues;
+ private int mSize;
+ private int mHead;
+ private int mTail;
+
+ /**
+ * Initializes a queue with the given starting capacity.
+ *
+ * @param initialCapacity the capacity.
+ */
+ public LongArrayQueue(int initialCapacity) {
+ if (initialCapacity == 0) {
+ mValues = EmptyArray.LONG;
+ } else {
+ mValues = ArrayUtils.newUnpaddedLongArray(initialCapacity);
+ }
+ mSize = 0;
+ mHead = mTail = 0;
+ }
+
+ /**
+ * Initializes a queue with default starting capacity.
+ */
+ public LongArrayQueue() {
+ this(16);
+ }
+
+ private void grow() {
+ if (mSize < mValues.length) {
+ throw new IllegalStateException("Queue not full yet!");
+ }
+ final int newSize = GrowingArrayUtils.growSize(mSize);
+ final long[] newArray = ArrayUtils.newUnpaddedLongArray(newSize);
+ final int r = mValues.length - mHead; // Number of elements on and to the right of head.
+ System.arraycopy(mValues, mHead, newArray, 0, r);
+ System.arraycopy(mValues, 0, newArray, r, mHead);
+ mValues = newArray;
+ mHead = 0;
+ mTail = mSize;
+ }
+
+ /**
+ * Returns the number of elements in the queue.
+ */
+ public int size() {
+ return mSize;
+ }
+
+ /**
+ * Removes all elements from this queue.
+ */
+ public void clear() {
+ mSize = 0;
+ mHead = mTail = 0;
+ }
+
+ /**
+ * Adds a value to the tail of the queue.
+ *
+ * @param value the value to be added.
+ */
+ public void addLast(long value) {
+ if (mSize == mValues.length) {
+ grow();
+ }
+ mValues[mTail] = value;
+ mTail = (mTail + 1) % mValues.length;
+ mSize++;
+ }
+
+ /**
+ * Removes an element from the head of the queue.
+ *
+ * @return the element at the head of the queue.
+ * @throws NoSuchElementException if the queue is empty.
+ */
+ public long removeFirst() {
+ if (mSize == 0) {
+ throw new NoSuchElementException("Queue is empty!");
+ }
+ final long ret = mValues[mHead];
+ mHead = (mHead + 1) % mValues.length;
+ mSize--;
+ return ret;
+ }
+
+ /**
+ * Returns the element at the given position from the head of the queue, where 0 represents the
+ * head of the queue.
+ *
+ * @param position the position from the head of the queue.
+ * @return the element found at the given position.
+ * @throws IndexOutOfBoundsException if {@code position} < {@code 0} or
+ * {@code position} >= {@link #size()}
+ */
+ public long get(int position) {
+ if (position < 0 || position >= mSize) {
+ throw new IndexOutOfBoundsException("Index " + position
+ + " not valid for a queue of size " + mSize);
+ }
+ final int index = (mHead + position) % mValues.length;
+ return mValues[index];
+ }
+
+ /**
+ * Returns the element at the head of the queue, without removing it.
+ *
+ * @return the element at the head of the queue.
+ * @throws NoSuchElementException if the queue is empty
+ */
+ public long peekFirst() {
+ if (mSize == 0) {
+ throw new NoSuchElementException("Queue is empty!");
+ }
+ return mValues[mHead];
+ }
+
+ /**
+ * Returns the element at the tail of the queue.
+ *
+ * @return the element at the tail of the queue.
+ * @throws NoSuchElementException if the queue is empty.
+ */
+ public long peekLast() {
+ if (mSize == 0) {
+ throw new NoSuchElementException("Queue is empty!");
+ }
+ final int index = (mTail == 0) ? mValues.length - 1 : mTail - 1;
+ return mValues[index];
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String toString() {
+ if (mSize <= 0) {
+ return "{}";
+ }
+
+ final StringBuilder buffer = new StringBuilder(mSize * 64);
+ buffer.append('{');
+ buffer.append(get(0));
+ for (int i = 1; i < mSize; i++) {
+ buffer.append(", ");
+ buffer.append(get(i));
+ }
+ buffer.append('}');
+ return buffer.toString();
+ }
+}
diff --git a/android/util/LongSparseArray.java b/android/util/LongSparseArray.java
new file mode 100644
index 0000000..55542d8
--- /dev/null
+++ b/android/util/LongSparseArray.java
@@ -0,0 +1,512 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.GrowingArrayUtils;
+import com.android.internal.util.Preconditions;
+import com.android.internal.util.function.LongObjPredicate;
+
+import libcore.util.EmptyArray;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * SparseArray mapping longs to Objects. Unlike a normal array of Objects,
+ * there can be gaps in the indices. It is intended to be more memory efficient
+ * than using a HashMap to map Longs to Objects, both because it avoids
+ * auto-boxing keys and its data structure doesn't rely on an extra entry object
+ * for each mapping.
+ *
+ * <p>Note that this container keeps its mappings in an array data structure,
+ * using a binary search to find keys. The implementation is not intended to be appropriate for
+ * data structures
+ * that may contain large numbers of items. It is generally slower than a traditional
+ * HashMap, since lookups require a binary search and adds and removes require inserting
+ * and deleting entries in the array. For containers holding up to hundreds of items,
+ * the performance difference is not significant, less than 50%.</p>
+ *
+ * <p>To help with performance, the container includes an optimization when removing
+ * keys: instead of compacting its array immediately, it leaves the removed entry marked
+ * as deleted. The entry can then be re-used for the same key, or compacted later in
+ * a single garbage collection step of all removed entries. This garbage collection will
+ * need to be performed at any time the array needs to be grown or the the map size or
+ * entry values are retrieved.</p>
+ *
+ * <p>It is possible to iterate over the items in this container using
+ * {@link #keyAt(int)} and {@link #valueAt(int)}. Iterating over the keys using
+ * <code>keyAt(int)</code> with ascending values of the index will return the
+ * keys in ascending order, or the values corresponding to the keys in ascending
+ * order in the case of <code>valueAt(int)</code>.</p>
+ */
+public class LongSparseArray<E> implements Cloneable {
+ private static final Object DELETED = new Object();
+ private boolean mGarbage = false;
+
+ private long[] mKeys;
+ private Object[] mValues;
+ private int mSize;
+
+ /**
+ * Creates a new LongSparseArray containing no mappings.
+ */
+ public LongSparseArray() {
+ this(10);
+ }
+
+ /**
+ * Creates a new LongSparseArray containing no mappings that will not
+ * require any additional memory allocation to store the specified
+ * number of mappings. If you supply an initial capacity of 0, the
+ * sparse array will be initialized with a light-weight representation
+ * not requiring any additional array allocations.
+ */
+ public LongSparseArray(int initialCapacity) {
+ if (initialCapacity == 0) {
+ mKeys = EmptyArray.LONG;
+ mValues = EmptyArray.OBJECT;
+ } else {
+ mKeys = ArrayUtils.newUnpaddedLongArray(initialCapacity);
+ mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
+ }
+ mSize = 0;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public LongSparseArray<E> clone() {
+ LongSparseArray<E> clone = null;
+ try {
+ clone = (LongSparseArray<E>) super.clone();
+ clone.mKeys = mKeys.clone();
+ clone.mValues = mValues.clone();
+ } catch (CloneNotSupportedException cnse) {
+ /* ignore */
+ }
+ return clone;
+ }
+
+ /**
+ * Gets the Object mapped from the specified key, or <code>null</code>
+ * if no such mapping has been made.
+ */
+ public E get(long key) {
+ return get(key, null);
+ }
+
+ /**
+ * Gets the Object mapped from the specified key, or the specified Object
+ * if no such mapping has been made.
+ */
+ @SuppressWarnings("unchecked")
+ public E get(long key, E valueIfKeyNotFound) {
+ int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+
+ if (i < 0 || mValues[i] == DELETED) {
+ return valueIfKeyNotFound;
+ } else {
+ return (E) mValues[i];
+ }
+ }
+
+ /**
+ * Removes the mapping from the specified key, if there was any.
+ */
+ public void delete(long key) {
+ int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+
+ if (i >= 0) {
+ if (mValues[i] != DELETED) {
+ mValues[i] = DELETED;
+ mGarbage = true;
+ }
+ }
+ }
+
+ /**
+ * Alias for {@link #delete(long)}.
+ */
+ public void remove(long key) {
+ delete(key);
+ }
+
+ /** @hide */
+ @SuppressWarnings("unchecked")
+ public void removeIf(@NonNull LongObjPredicate<? super E> filter) {
+ Objects.requireNonNull(filter);
+ for (int i = 0; i < mSize; ++i) {
+ if (mValues[i] != DELETED && filter.test(mKeys[i], (E) mValues[i])) {
+ mValues[i] = DELETED;
+ mGarbage = true;
+ }
+ }
+ }
+
+ /**
+ * Removes the mapping at the specified index.
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ */
+ public void removeAt(int index) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ if (mValues[index] != DELETED) {
+ mValues[index] = DELETED;
+ mGarbage = true;
+ }
+ }
+
+ private void gc() {
+ // Log.e("SparseArray", "gc start with " + mSize);
+
+ int n = mSize;
+ int o = 0;
+ long[] keys = mKeys;
+ Object[] values = mValues;
+
+ for (int i = 0; i < n; i++) {
+ Object val = values[i];
+
+ if (val != DELETED) {
+ if (i != o) {
+ keys[o] = keys[i];
+ values[o] = val;
+ values[i] = null;
+ }
+
+ o++;
+ }
+ }
+
+ mGarbage = false;
+ mSize = o;
+
+ // Log.e("SparseArray", "gc end with " + mSize);
+ }
+
+ /**
+ * Adds a mapping from the specified key to the specified value,
+ * replacing the previous mapping from the specified key if there
+ * was one.
+ */
+ public void put(long key, E value) {
+ int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+
+ if (i >= 0) {
+ mValues[i] = value;
+ } else {
+ i = ~i;
+
+ if (i < mSize && mValues[i] == DELETED) {
+ mKeys[i] = key;
+ mValues[i] = value;
+ return;
+ }
+
+ if (mGarbage && mSize >= mKeys.length) {
+ gc();
+
+ // Search again because indices may have changed.
+ i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
+ }
+
+ mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
+ mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
+ mSize++;
+ }
+ }
+
+ /**
+ * Returns the number of key-value mappings that this LongSparseArray
+ * currently stores.
+ */
+ public int size() {
+ if (mGarbage) {
+ gc();
+ }
+
+ return mSize;
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, returns
+ * the key from the <code>index</code>th key-value mapping that this
+ * LongSparseArray stores.
+ *
+ * <p>The keys corresponding to indices in ascending order are guaranteed to
+ * be in ascending order, e.g., <code>keyAt(0)</code> will return the
+ * smallest key and <code>keyAt(size()-1)</code> will return the largest
+ * key.</p>
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ */
+ public long keyAt(int index) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ if (mGarbage) {
+ gc();
+ }
+
+ return mKeys[index];
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, returns
+ * the value from the <code>index</code>th key-value mapping that this
+ * LongSparseArray stores.
+ *
+ * <p>The values corresponding to indices in ascending order are guaranteed
+ * to be associated with keys in ascending order, e.g.,
+ * <code>valueAt(0)</code> will return the value associated with the
+ * smallest key and <code>valueAt(size()-1)</code> will return the value
+ * associated with the largest key.</p>
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ */
+ @SuppressWarnings("unchecked")
+ public E valueAt(int index) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ if (mGarbage) {
+ gc();
+ }
+
+ return (E) mValues[index];
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, sets a new
+ * value for the <code>index</code>th key-value mapping that this
+ * LongSparseArray stores.
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ */
+ public void setValueAt(int index, E value) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ if (mGarbage) {
+ gc();
+ }
+
+ mValues[index] = value;
+ }
+
+ /**
+ * Returns the index for which {@link #keyAt} would return the
+ * specified key, or a negative number if the specified
+ * key is not mapped.
+ */
+ public int indexOfKey(long key) {
+ if (mGarbage) {
+ gc();
+ }
+
+ return ContainerHelpers.binarySearch(mKeys, mSize, key);
+ }
+
+ /**
+ * Returns an index for which {@link #valueAt} would return the
+ * specified key, or a negative number if no keys map to the
+ * specified value.
+ * Beware that this is a linear search, unlike lookups by key,
+ * and that multiple keys can map to the same value and this will
+ * find only one of them.
+ */
+ public int indexOfValue(E value) {
+ if (mGarbage) {
+ gc();
+ }
+
+ for (int i = 0; i < mSize; i++) {
+ if (mValues[i] == value) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Returns an index for which {@link #valueAt} would return the
+ * specified key, or a negative number if no keys map to the
+ * specified value.
+ * <p>Beware that this is a linear search, unlike lookups by key,
+ * and that multiple keys can map to the same value and this will
+ * find only one of them.
+ * <p>Note also that this method uses {@code equals} unlike {@code indexOfValue}.
+ * @hide
+ */
+ public int indexOfValueByValue(E value) {
+ if (mGarbage) {
+ gc();
+ }
+
+ for (int i = 0; i < mSize; i++) {
+ if (value == null) {
+ if (mValues[i] == null) {
+ return i;
+ }
+ } else {
+ if (value.equals(mValues[i])) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Removes all key-value mappings from this LongSparseArray.
+ */
+ public void clear() {
+ int n = mSize;
+ Object[] values = mValues;
+
+ for (int i = 0; i < n; i++) {
+ values[i] = null;
+ }
+
+ mSize = 0;
+ mGarbage = false;
+ }
+
+ /**
+ * Puts a key/value pair into the array, optimizing for the case where
+ * the key is greater than all existing keys in the array.
+ */
+ public void append(long key, E value) {
+ if (mSize != 0 && key <= mKeys[mSize - 1]) {
+ put(key, value);
+ return;
+ }
+
+ if (mGarbage && mSize >= mKeys.length) {
+ gc();
+ }
+
+ mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
+ mValues = GrowingArrayUtils.append(mValues, mSize, value);
+ mSize++;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>This implementation composes a string by iterating over its mappings. If
+ * this map contains itself as a value, the string "(this Map)"
+ * will appear in its place.
+ */
+ @Override
+ public String toString() {
+ if (size() <= 0) {
+ return "{}";
+ }
+
+ StringBuilder buffer = new StringBuilder(mSize * 28);
+ buffer.append('{');
+ for (int i=0; i<mSize; i++) {
+ if (i > 0) {
+ buffer.append(", ");
+ }
+ long key = keyAt(i);
+ buffer.append(key);
+ buffer.append('=');
+ Object value = valueAt(i);
+ if (value != this) {
+ buffer.append(value);
+ } else {
+ buffer.append("(this Map)");
+ }
+ }
+ buffer.append('}');
+ return buffer.toString();
+ }
+
+ /**
+ * @hide
+ */
+ public static class StringParcelling implements
+ com.android.internal.util.Parcelling<LongSparseArray<String>> {
+ @Override
+ public void parcel(LongSparseArray<String> array, Parcel dest, int parcelFlags) {
+ if (array == null) {
+ dest.writeInt(-1);
+ return;
+ }
+
+ int size = array.mSize;
+
+ dest.writeInt(size);
+ dest.writeLongArray(array.mKeys);
+
+ dest.writeStringArray(Arrays.copyOfRange(array.mValues, 0, size, String[].class));
+ }
+
+ @Override
+ public LongSparseArray<String> unparcel(Parcel source) {
+ int size = source.readInt();
+ if (size == -1) {
+ return null;
+ }
+
+ LongSparseArray<String> array = new LongSparseArray<>(0);
+ array.mSize = size;
+ array.mKeys = source.createLongArray();
+ array.mValues = source.createStringArray();
+
+ // Make sure array is sane
+ Preconditions.checkArgument(array.mKeys.length >= size);
+ Preconditions.checkArgument(array.mValues.length >= size);
+
+ if (size > 0) {
+ long last = array.mKeys[0];
+ for (int i = 1; i < size; i++) {
+ Preconditions.checkArgument(last < array.mKeys[i]);
+ }
+ }
+
+ return array;
+ }
+ }
+}
diff --git a/android/util/LongSparseLongArray.java b/android/util/LongSparseLongArray.java
new file mode 100644
index 0000000..c05dd9d
--- /dev/null
+++ b/android/util/LongSparseLongArray.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.GrowingArrayUtils;
+import com.android.internal.util.Preconditions;
+
+import libcore.util.EmptyArray;
+
+/**
+ * Map of {@code long} to {@code long}. Unlike a normal array of longs, there
+ * can be gaps in the indices. It is intended to be more memory efficient than using a
+ * {@code HashMap}, both because it avoids
+ * auto-boxing keys and values and its data structure doesn't rely on an extra entry object
+ * for each mapping.
+ *
+ * <p>Note that this container keeps its mappings in an array data structure,
+ * using a binary search to find keys. The implementation is not intended to be appropriate for
+ * data structures
+ * that may contain large numbers of items. It is generally slower than a traditional
+ * HashMap, since lookups require a binary search and adds and removes require inserting
+ * and deleting entries in the array. For containers holding up to hundreds of items,
+ * the performance difference is not significant, less than 50%.</p>
+ *
+ * <p>It is possible to iterate over the items in this container using
+ * {@link #keyAt(int)} and {@link #valueAt(int)}. Iterating over the keys using
+ * <code>keyAt(int)</code> with ascending values of the index will return the
+ * keys in ascending order, or the values corresponding to the keys in ascending
+ * order in the case of <code>valueAt(int)</code>.</p>
+ *
+ * @hide
+ */
+public class LongSparseLongArray implements Cloneable {
+ @UnsupportedAppUsage(maxTargetSdk = 28) // The type isn't even public.
+ private long[] mKeys;
+ @UnsupportedAppUsage(maxTargetSdk = 28) // The type isn't even public.
+ private long[] mValues;
+ @UnsupportedAppUsage(maxTargetSdk = 28) // The type isn't even public.
+ private int mSize;
+
+ /**
+ * Creates a new SparseLongArray containing no mappings.
+ */
+ public LongSparseLongArray() {
+ this(10);
+ }
+
+ /**
+ * Creates a new SparseLongArray containing no mappings that will not
+ * require any additional memory allocation to store the specified
+ * number of mappings. If you supply an initial capacity of 0, the
+ * sparse array will be initialized with a light-weight representation
+ * not requiring any additional array allocations.
+ */
+ public LongSparseLongArray(int initialCapacity) {
+ if (initialCapacity == 0) {
+ mKeys = EmptyArray.LONG;
+ mValues = EmptyArray.LONG;
+ } else {
+ mKeys = ArrayUtils.newUnpaddedLongArray(initialCapacity);
+ mValues = new long[mKeys.length];
+ }
+ mSize = 0;
+ }
+
+ @Override
+ public LongSparseLongArray clone() {
+ LongSparseLongArray clone = null;
+ try {
+ clone = (LongSparseLongArray) super.clone();
+ clone.mKeys = mKeys.clone();
+ clone.mValues = mValues.clone();
+ } catch (CloneNotSupportedException cnse) {
+ /* ignore */
+ }
+ return clone;
+ }
+
+ /**
+ * Gets the long mapped from the specified key, or <code>0</code>
+ * if no such mapping has been made.
+ */
+ public long get(long key) {
+ return get(key, 0);
+ }
+
+ /**
+ * Gets the long mapped from the specified key, or the specified value
+ * if no such mapping has been made.
+ */
+ public long get(long key, long valueIfKeyNotFound) {
+ int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+
+ if (i < 0) {
+ return valueIfKeyNotFound;
+ } else {
+ return mValues[i];
+ }
+ }
+
+ /**
+ * Removes the mapping from the specified key, if there was any.
+ */
+ public void delete(long key) {
+ int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+
+ if (i >= 0) {
+ removeAt(i);
+ }
+ }
+
+ /**
+ * Removes the mapping at the given index.
+ */
+ public void removeAt(int index) {
+ System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1));
+ System.arraycopy(mValues, index + 1, mValues, index, mSize - (index + 1));
+ mSize--;
+ }
+
+ /**
+ * Adds a mapping from the specified key to the specified value,
+ * replacing the previous mapping from the specified key if there
+ * was one.
+ */
+ public void put(long key, long value) {
+ int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+
+ if (i >= 0) {
+ mValues[i] = value;
+ } else {
+ i = ~i;
+
+ mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
+ mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
+ mSize++;
+ }
+ }
+
+ /**
+ * Returns the number of key-value mappings that this SparseIntArray
+ * currently stores.
+ */
+ public int size() {
+ return mSize;
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, returns
+ * the key from the <code>index</code>th key-value mapping that this
+ * SparseLongArray stores.
+ *
+ * <p>The keys corresponding to indices in ascending order are guaranteed to
+ * be in ascending order, e.g., <code>keyAt(0)</code> will return the
+ * smallest key and <code>keyAt(size()-1)</code> will return the largest
+ * key.</p>
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ */
+ public long keyAt(int index) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ return mKeys[index];
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, returns
+ * the value from the <code>index</code>th key-value mapping that this
+ * SparseLongArray stores.
+ *
+ * <p>The values corresponding to indices in ascending order are guaranteed
+ * to be associated with keys in ascending order, e.g.,
+ * <code>valueAt(0)</code> will return the value associated with the
+ * smallest key and <code>valueAt(size()-1)</code> will return the value
+ * associated with the largest key.</p>
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ */
+ public long valueAt(int index) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ return mValues[index];
+ }
+
+ /**
+ * Returns the index for which {@link #keyAt} would return the
+ * specified key, or a negative number if the specified
+ * key is not mapped.
+ */
+ public int indexOfKey(long key) {
+ return ContainerHelpers.binarySearch(mKeys, mSize, key);
+ }
+
+ /**
+ * Returns an index for which {@link #valueAt} would return the
+ * specified key, or a negative number if no keys map to the
+ * specified value.
+ * Beware that this is a linear search, unlike lookups by key,
+ * and that multiple keys can map to the same value and this will
+ * find only one of them.
+ */
+ public int indexOfValue(long value) {
+ for (int i = 0; i < mSize; i++)
+ if (mValues[i] == value)
+ return i;
+
+ return -1;
+ }
+
+ /**
+ * Removes all key-value mappings from this SparseIntArray.
+ */
+ public void clear() {
+ mSize = 0;
+ }
+
+ /**
+ * Puts a key/value pair into the array, optimizing for the case where
+ * the key is greater than all existing keys in the array.
+ */
+ public void append(long key, long value) {
+ if (mSize != 0 && key <= mKeys[mSize - 1]) {
+ put(key, value);
+ return;
+ }
+
+ mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
+ mValues = GrowingArrayUtils.append(mValues, mSize, value);
+ mSize++;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>This implementation composes a string by iterating over its mappings.
+ */
+ @Override
+ public String toString() {
+ if (size() <= 0) {
+ return "{}";
+ }
+
+ StringBuilder buffer = new StringBuilder(mSize * 28);
+ buffer.append('{');
+ for (int i=0; i<mSize; i++) {
+ if (i > 0) {
+ buffer.append(", ");
+ }
+ long key = keyAt(i);
+ buffer.append(key);
+ buffer.append('=');
+ long value = valueAt(i);
+ buffer.append(value);
+ }
+ buffer.append('}');
+ return buffer.toString();
+ }
+
+ /**
+ * @hide
+ */
+ public static class Parcelling implements
+ com.android.internal.util.Parcelling<LongSparseLongArray> {
+ @Override
+ public void parcel(LongSparseLongArray array, Parcel dest, int parcelFlags) {
+ if (array == null) {
+ dest.writeInt(-1);
+ return;
+ }
+
+ dest.writeInt(array.mSize);
+ dest.writeLongArray(array.mKeys);
+ dest.writeLongArray(array.mValues);
+ }
+
+ @Override
+ public LongSparseLongArray unparcel(Parcel source) {
+ int size = source.readInt();
+ if (size == -1) {
+ return null;
+ }
+
+ LongSparseLongArray array = new LongSparseLongArray(0);
+
+ array.mSize = size;
+ array.mKeys = source.createLongArray();
+ array.mValues = source.createLongArray();
+
+ // Make sure array is sane
+ Preconditions.checkArgument(array.mKeys.length >= size);
+ Preconditions.checkArgument(array.mValues.length >= size);
+
+ if (size > 0) {
+ long last = array.mKeys[0];
+ for (int i = 1; i < size; i++) {
+ Preconditions.checkArgument(last < array.mKeys[i]);
+ }
+ }
+
+ return array;
+ }
+ }
+}
diff --git a/android/util/LruCache.java b/android/util/LruCache.java
new file mode 100644
index 0000000..3f7fdd8
--- /dev/null
+++ b/android/util/LruCache.java
@@ -0,0 +1,383 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.compat.annotation.UnsupportedAppUsage;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * A cache that holds strong references to a limited number of values. Each time
+ * a value is accessed, it is moved to the head of a queue. When a value is
+ * added to a full cache, the value at the end of that queue is evicted and may
+ * become eligible for garbage collection.
+ *
+ * <p>If your cached values hold resources that need to be explicitly released,
+ * override {@link #entryRemoved}.
+ *
+ * <p>If a cache miss should be computed on demand for the corresponding keys,
+ * override {@link #create}. This simplifies the calling code, allowing it to
+ * assume a value will always be returned, even when there's a cache miss.
+ *
+ * <p>By default, the cache size is measured in the number of entries. Override
+ * {@link #sizeOf} to size the cache in different units. For example, this cache
+ * is limited to 4MiB of bitmaps:
+ * <pre> {@code
+ * int cacheSize = 4 * 1024 * 1024; // 4MiB
+ * LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) {
+ * protected int sizeOf(String key, Bitmap value) {
+ * return value.getByteCount();
+ * }
+ * }}</pre>
+ *
+ * <p>This class is thread-safe. Perform multiple cache operations atomically by
+ * synchronizing on the cache: <pre> {@code
+ * synchronized (cache) {
+ * if (cache.get(key) == null) {
+ * cache.put(key, value);
+ * }
+ * }}</pre>
+ *
+ * <p>This class does not allow null to be used as a key or value. A return
+ * value of null from {@link #get}, {@link #put} or {@link #remove} is
+ * unambiguous: the key was not in the cache.
+ *
+ * <p>This class appeared in Android 3.1 (Honeycomb MR1); it's available as part
+ * of <a href="http://developer.android.com/sdk/compatibility-library.html">Android's
+ * Support Package</a> for earlier releases.
+ */
+public class LruCache<K, V> {
+ @UnsupportedAppUsage
+ private final LinkedHashMap<K, V> map;
+
+ /** Size of this cache in units. Not necessarily the number of elements. */
+ private int size;
+ private int maxSize;
+
+ private int putCount;
+ private int createCount;
+ private int evictionCount;
+ private int hitCount;
+ private int missCount;
+
+ /**
+ * @param maxSize for caches that do not override {@link #sizeOf}, this is
+ * the maximum number of entries in the cache. For all other caches,
+ * this is the maximum sum of the sizes of the entries in this cache.
+ */
+ public LruCache(int maxSize) {
+ if (maxSize <= 0) {
+ throw new IllegalArgumentException("maxSize <= 0");
+ }
+ this.maxSize = maxSize;
+ this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
+ }
+
+ /**
+ * Sets the size of the cache.
+ *
+ * @param maxSize The new maximum size.
+ */
+ public void resize(int maxSize) {
+ if (maxSize <= 0) {
+ throw new IllegalArgumentException("maxSize <= 0");
+ }
+
+ synchronized (this) {
+ this.maxSize = maxSize;
+ }
+ trimToSize(maxSize);
+ }
+
+ /**
+ * Returns the value for {@code key} if it exists in the cache or can be
+ * created by {@code #create}. If a value was returned, it is moved to the
+ * head of the queue. This returns null if a value is not cached and cannot
+ * be created.
+ */
+ public final V get(K key) {
+ if (key == null) {
+ throw new NullPointerException("key == null");
+ }
+
+ V mapValue;
+ synchronized (this) {
+ mapValue = map.get(key);
+ if (mapValue != null) {
+ hitCount++;
+ return mapValue;
+ }
+ missCount++;
+ }
+
+ /*
+ * Attempt to create a value. This may take a long time, and the map
+ * may be different when create() returns. If a conflicting value was
+ * added to the map while create() was working, we leave that value in
+ * the map and release the created value.
+ */
+
+ V createdValue = create(key);
+ if (createdValue == null) {
+ return null;
+ }
+
+ synchronized (this) {
+ createCount++;
+ mapValue = map.put(key, createdValue);
+
+ if (mapValue != null) {
+ // There was a conflict so undo that last put
+ map.put(key, mapValue);
+ } else {
+ size += safeSizeOf(key, createdValue);
+ }
+ }
+
+ if (mapValue != null) {
+ entryRemoved(false, key, createdValue, mapValue);
+ return mapValue;
+ } else {
+ trimToSize(maxSize);
+ return createdValue;
+ }
+ }
+
+ /**
+ * Caches {@code value} for {@code key}. The value is moved to the head of
+ * the queue.
+ *
+ * @return the previous value mapped by {@code key}.
+ */
+ public final V put(K key, V value) {
+ if (key == null || value == null) {
+ throw new NullPointerException("key == null || value == null");
+ }
+
+ V previous;
+ synchronized (this) {
+ putCount++;
+ size += safeSizeOf(key, value);
+ previous = map.put(key, value);
+ if (previous != null) {
+ size -= safeSizeOf(key, previous);
+ }
+ }
+
+ if (previous != null) {
+ entryRemoved(false, key, previous, value);
+ }
+
+ trimToSize(maxSize);
+ return previous;
+ }
+
+ /**
+ * Remove the eldest entries until the total of remaining entries is at or
+ * below the requested size.
+ *
+ * @param maxSize the maximum size of the cache before returning. May be -1
+ * to evict even 0-sized elements.
+ */
+ public void trimToSize(int maxSize) {
+ while (true) {
+ K key;
+ V value;
+ synchronized (this) {
+ if (size < 0 || (map.isEmpty() && size != 0)) {
+ throw new IllegalStateException(getClass().getName()
+ + ".sizeOf() is reporting inconsistent results!");
+ }
+
+ if (size <= maxSize) {
+ break;
+ }
+
+ Map.Entry<K, V> toEvict = map.eldest();
+ if (toEvict == null) {
+ break;
+ }
+
+ key = toEvict.getKey();
+ value = toEvict.getValue();
+ map.remove(key);
+ size -= safeSizeOf(key, value);
+ evictionCount++;
+ }
+
+ entryRemoved(true, key, value, null);
+ }
+ }
+
+ /**
+ * Removes the entry for {@code key} if it exists.
+ *
+ * @return the previous value mapped by {@code key}.
+ */
+ public final V remove(K key) {
+ if (key == null) {
+ throw new NullPointerException("key == null");
+ }
+
+ V previous;
+ synchronized (this) {
+ previous = map.remove(key);
+ if (previous != null) {
+ size -= safeSizeOf(key, previous);
+ }
+ }
+
+ if (previous != null) {
+ entryRemoved(false, key, previous, null);
+ }
+
+ return previous;
+ }
+
+ /**
+ * Called for entries that have been evicted or removed. This method is
+ * invoked when a value is evicted to make space, removed by a call to
+ * {@link #remove}, or replaced by a call to {@link #put}. The default
+ * implementation does nothing.
+ *
+ * <p>The method is called without synchronization: other threads may
+ * access the cache while this method is executing.
+ *
+ * @param evicted true if the entry is being removed to make space, false
+ * if the removal was caused by a {@link #put} or {@link #remove}.
+ * @param newValue the new value for {@code key}, if it exists. If non-null,
+ * this removal was caused by a {@link #put} or a {@link #get}. Otherwise it was caused by
+ * an eviction or a {@link #remove}.
+ */
+ protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
+
+ /**
+ * Called after a cache miss to compute a value for the corresponding key.
+ * Returns the computed value or null if no value can be computed. The
+ * default implementation returns null.
+ *
+ * <p>The method is called without synchronization: other threads may
+ * access the cache while this method is executing.
+ *
+ * <p>If a value for {@code key} exists in the cache when this method
+ * returns, the created value will be released with {@link #entryRemoved}
+ * and discarded. This can occur when multiple threads request the same key
+ * at the same time (causing multiple values to be created), or when one
+ * thread calls {@link #put} while another is creating a value for the same
+ * key.
+ */
+ protected V create(K key) {
+ return null;
+ }
+
+ private int safeSizeOf(K key, V value) {
+ int result = sizeOf(key, value);
+ if (result < 0) {
+ throw new IllegalStateException("Negative size: " + key + "=" + value);
+ }
+ return result;
+ }
+
+ /**
+ * Returns the size of the entry for {@code key} and {@code value} in
+ * user-defined units. The default implementation returns 1 so that size
+ * is the number of entries and max size is the maximum number of entries.
+ *
+ * <p>An entry's size must not change while it is in the cache.
+ */
+ protected int sizeOf(K key, V value) {
+ return 1;
+ }
+
+ /**
+ * Clear the cache, calling {@link #entryRemoved} on each removed entry.
+ */
+ public final void evictAll() {
+ trimToSize(-1); // -1 will evict 0-sized elements
+ }
+
+ /**
+ * For caches that do not override {@link #sizeOf}, this returns the number
+ * of entries in the cache. For all other caches, this returns the sum of
+ * the sizes of the entries in this cache.
+ */
+ public synchronized final int size() {
+ return size;
+ }
+
+ /**
+ * For caches that do not override {@link #sizeOf}, this returns the maximum
+ * number of entries in the cache. For all other caches, this returns the
+ * maximum sum of the sizes of the entries in this cache.
+ */
+ public synchronized final int maxSize() {
+ return maxSize;
+ }
+
+ /**
+ * Returns the number of times {@link #get} returned a value that was
+ * already present in the cache.
+ */
+ public synchronized final int hitCount() {
+ return hitCount;
+ }
+
+ /**
+ * Returns the number of times {@link #get} returned null or required a new
+ * value to be created.
+ */
+ public synchronized final int missCount() {
+ return missCount;
+ }
+
+ /**
+ * Returns the number of times {@link #create(Object)} returned a value.
+ */
+ public synchronized final int createCount() {
+ return createCount;
+ }
+
+ /**
+ * Returns the number of times {@link #put} was called.
+ */
+ public synchronized final int putCount() {
+ return putCount;
+ }
+
+ /**
+ * Returns the number of values that have been evicted.
+ */
+ public synchronized final int evictionCount() {
+ return evictionCount;
+ }
+
+ /**
+ * Returns a copy of the current contents of the cache, ordered from least
+ * recently accessed to most recently accessed.
+ */
+ public synchronized final Map<K, V> snapshot() {
+ return new LinkedHashMap<K, V>(map);
+ }
+
+ @Override public synchronized final String toString() {
+ int accesses = hitCount + missCount;
+ int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0;
+ return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]",
+ maxSize, hitCount, missCount, hitPercent);
+ }
+}
diff --git a/android/util/MalformedJsonException.java b/android/util/MalformedJsonException.java
new file mode 100644
index 0000000..63c19ff
--- /dev/null
+++ b/android/util/MalformedJsonException.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import java.io.IOException;
+
+/**
+ * Thrown when a reader encounters malformed JSON. Some syntax errors can be
+ * ignored by calling {@link JsonReader#setLenient(boolean)}.
+ */
+public final class MalformedJsonException extends IOException {
+ private static final long serialVersionUID = 1L;
+
+ public MalformedJsonException(String message) {
+ super(message);
+ }
+}
diff --git a/android/util/MapCollections.java b/android/util/MapCollections.java
new file mode 100644
index 0000000..a521268
--- /dev/null
+++ b/android/util/MapCollections.java
@@ -0,0 +1,561 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import java.lang.reflect.Array;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Helper for writing standard Java collection interfaces to a data
+ * structure like {@link ArrayMap}.
+ * @hide
+ */
+abstract class MapCollections<K, V> {
+ EntrySet mEntrySet;
+ KeySet mKeySet;
+ ValuesCollection mValues;
+
+ final class ArrayIterator<T> implements Iterator<T> {
+ final int mOffset;
+ int mSize;
+ int mIndex;
+ boolean mCanRemove = false;
+
+ ArrayIterator(int offset) {
+ mOffset = offset;
+ mSize = colGetSize();
+ }
+
+ @Override
+ public boolean hasNext() {
+ return mIndex < mSize;
+ }
+
+ @Override
+ public T next() {
+ if (!hasNext()) throw new NoSuchElementException();
+ Object res = colGetEntry(mIndex, mOffset);
+ mIndex++;
+ mCanRemove = true;
+ return (T)res;
+ }
+
+ @Override
+ public void remove() {
+ if (!mCanRemove) {
+ throw new IllegalStateException();
+ }
+ mIndex--;
+ mSize--;
+ mCanRemove = false;
+ colRemoveAt(mIndex);
+ }
+ }
+
+ final class MapIterator implements Iterator<Map.Entry<K, V>>, Map.Entry<K, V> {
+ int mEnd;
+ int mIndex;
+ boolean mEntryValid = false;
+
+ MapIterator() {
+ mEnd = colGetSize() - 1;
+ mIndex = -1;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return mIndex < mEnd;
+ }
+
+ @Override
+ public Map.Entry<K, V> next() {
+ if (!hasNext()) throw new NoSuchElementException();
+ mIndex++;
+ mEntryValid = true;
+ return this;
+ }
+
+ @Override
+ public void remove() {
+ if (!mEntryValid) {
+ throw new IllegalStateException();
+ }
+ colRemoveAt(mIndex);
+ mIndex--;
+ mEnd--;
+ mEntryValid = false;
+ }
+
+ @Override
+ public K getKey() {
+ if (!mEntryValid) {
+ throw new IllegalStateException(
+ "This container does not support retaining Map.Entry objects");
+ }
+ return (K)colGetEntry(mIndex, 0);
+ }
+
+ @Override
+ public V getValue() {
+ if (!mEntryValid) {
+ throw new IllegalStateException(
+ "This container does not support retaining Map.Entry objects");
+ }
+ return (V)colGetEntry(mIndex, 1);
+ }
+
+ @Override
+ public V setValue(V object) {
+ if (!mEntryValid) {
+ throw new IllegalStateException(
+ "This container does not support retaining Map.Entry objects");
+ }
+ return colSetValue(mIndex, object);
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (!mEntryValid) {
+ throw new IllegalStateException(
+ "This container does not support retaining Map.Entry objects");
+ }
+ if (!(o instanceof Map.Entry)) {
+ return false;
+ }
+ Map.Entry<?, ?> e = (Map.Entry<?, ?>) o;
+ return Objects.equals(e.getKey(), colGetEntry(mIndex, 0))
+ && Objects.equals(e.getValue(), colGetEntry(mIndex, 1));
+ }
+
+ @Override
+ public final int hashCode() {
+ if (!mEntryValid) {
+ throw new IllegalStateException(
+ "This container does not support retaining Map.Entry objects");
+ }
+ final Object key = colGetEntry(mIndex, 0);
+ final Object value = colGetEntry(mIndex, 1);
+ return (key == null ? 0 : key.hashCode()) ^
+ (value == null ? 0 : value.hashCode());
+ }
+
+ @Override
+ public final String toString() {
+ return getKey() + "=" + getValue();
+ }
+ }
+
+ final class EntrySet implements Set<Map.Entry<K, V>> {
+ @Override
+ public boolean add(Map.Entry<K, V> object) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean addAll(Collection<? extends Map.Entry<K, V>> collection) {
+ int oldSize = colGetSize();
+ for (Map.Entry<K, V> entry : collection) {
+ colPut(entry.getKey(), entry.getValue());
+ }
+ return oldSize != colGetSize();
+ }
+
+ @Override
+ public void clear() {
+ colClear();
+ }
+
+ @Override
+ public boolean contains(Object o) {
+ if (!(o instanceof Map.Entry))
+ return false;
+ Map.Entry<?, ?> e = (Map.Entry<?, ?>) o;
+ int index = colIndexOfKey(e.getKey());
+ if (index < 0) {
+ return false;
+ }
+ Object foundVal = colGetEntry(index, 1);
+ return Objects.equals(foundVal, e.getValue());
+ }
+
+ @Override
+ public boolean containsAll(Collection<?> collection) {
+ Iterator<?> it = collection.iterator();
+ while (it.hasNext()) {
+ if (!contains(it.next())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return colGetSize() == 0;
+ }
+
+ @Override
+ public Iterator<Map.Entry<K, V>> iterator() {
+ return new MapIterator();
+ }
+
+ @Override
+ public boolean remove(Object object) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean removeAll(Collection<?> collection) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean retainAll(Collection<?> collection) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int size() {
+ return colGetSize();
+ }
+
+ @Override
+ public Object[] toArray() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T> T[] toArray(T[] array) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ return equalsSetHelper(this, object);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 0;
+ for (int i=colGetSize()-1; i>=0; i--) {
+ final Object key = colGetEntry(i, 0);
+ final Object value = colGetEntry(i, 1);
+ result += ( (key == null ? 0 : key.hashCode()) ^
+ (value == null ? 0 : value.hashCode()) );
+ }
+ return result;
+ }
+ };
+
+ final class KeySet implements Set<K> {
+
+ @Override
+ public boolean add(K object) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean addAll(Collection<? extends K> collection) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void clear() {
+ colClear();
+ }
+
+ @Override
+ public boolean contains(Object object) {
+ return colIndexOfKey(object) >= 0;
+ }
+
+ @Override
+ public boolean containsAll(Collection<?> collection) {
+ return containsAllHelper(colGetMap(), collection);
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return colGetSize() == 0;
+ }
+
+ @Override
+ public Iterator<K> iterator() {
+ return new ArrayIterator<K>(0);
+ }
+
+ @Override
+ public boolean remove(Object object) {
+ int index = colIndexOfKey(object);
+ if (index >= 0) {
+ colRemoveAt(index);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean removeAll(Collection<?> collection) {
+ return removeAllHelper(colGetMap(), collection);
+ }
+
+ @Override
+ public boolean retainAll(Collection<?> collection) {
+ return retainAllHelper(colGetMap(), collection);
+ }
+
+ @Override
+ public int size() {
+ return colGetSize();
+ }
+
+ @Override
+ public Object[] toArray() {
+ return toArrayHelper(0);
+ }
+
+ @Override
+ public <T> T[] toArray(T[] array) {
+ return toArrayHelper(array, 0);
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ return equalsSetHelper(this, object);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 0;
+ for (int i=colGetSize()-1; i>=0; i--) {
+ Object obj = colGetEntry(i, 0);
+ result += obj == null ? 0 : obj.hashCode();
+ }
+ return result;
+ }
+ };
+
+ final class ValuesCollection implements Collection<V> {
+
+ @Override
+ public boolean add(V object) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean addAll(Collection<? extends V> collection) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void clear() {
+ colClear();
+ }
+
+ @Override
+ public boolean contains(Object object) {
+ return colIndexOfValue(object) >= 0;
+ }
+
+ @Override
+ public boolean containsAll(Collection<?> collection) {
+ Iterator<?> it = collection.iterator();
+ while (it.hasNext()) {
+ if (!contains(it.next())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return colGetSize() == 0;
+ }
+
+ @Override
+ public Iterator<V> iterator() {
+ return new ArrayIterator<V>(1);
+ }
+
+ @Override
+ public boolean remove(Object object) {
+ int index = colIndexOfValue(object);
+ if (index >= 0) {
+ colRemoveAt(index);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean removeAll(Collection<?> collection) {
+ int N = colGetSize();
+ boolean changed = false;
+ for (int i=0; i<N; i++) {
+ Object cur = colGetEntry(i, 1);
+ if (collection.contains(cur)) {
+ colRemoveAt(i);
+ i--;
+ N--;
+ changed = true;
+ }
+ }
+ return changed;
+ }
+
+ @Override
+ public boolean retainAll(Collection<?> collection) {
+ int N = colGetSize();
+ boolean changed = false;
+ for (int i=0; i<N; i++) {
+ Object cur = colGetEntry(i, 1);
+ if (!collection.contains(cur)) {
+ colRemoveAt(i);
+ i--;
+ N--;
+ changed = true;
+ }
+ }
+ return changed;
+ }
+
+ @Override
+ public int size() {
+ return colGetSize();
+ }
+
+ @Override
+ public Object[] toArray() {
+ return toArrayHelper(1);
+ }
+
+ @Override
+ public <T> T[] toArray(T[] array) {
+ return toArrayHelper(array, 1);
+ }
+ };
+
+ public static <K, V> boolean containsAllHelper(Map<K, V> map, Collection<?> collection) {
+ Iterator<?> it = collection.iterator();
+ while (it.hasNext()) {
+ if (!map.containsKey(it.next())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public static <K, V> boolean removeAllHelper(Map<K, V> map, Collection<?> collection) {
+ int oldSize = map.size();
+ Iterator<?> it = collection.iterator();
+ while (it.hasNext()) {
+ map.remove(it.next());
+ }
+ return oldSize != map.size();
+ }
+
+ public static <K, V> boolean retainAllHelper(Map<K, V> map, Collection<?> collection) {
+ int oldSize = map.size();
+ Iterator<K> it = map.keySet().iterator();
+ while (it.hasNext()) {
+ if (!collection.contains(it.next())) {
+ it.remove();
+ }
+ }
+ return oldSize != map.size();
+ }
+
+ public Object[] toArrayHelper(int offset) {
+ final int N = colGetSize();
+ Object[] result = new Object[N];
+ for (int i=0; i<N; i++) {
+ result[i] = colGetEntry(i, offset);
+ }
+ return result;
+ }
+
+ public <T> T[] toArrayHelper(T[] array, int offset) {
+ final int N = colGetSize();
+ if (array.length < N) {
+ @SuppressWarnings("unchecked") T[] newArray
+ = (T[]) Array.newInstance(array.getClass().getComponentType(), N);
+ array = newArray;
+ }
+ for (int i=0; i<N; i++) {
+ array[i] = (T)colGetEntry(i, offset);
+ }
+ if (array.length > N) {
+ array[N] = null;
+ }
+ return array;
+ }
+
+ public static <T> boolean equalsSetHelper(Set<T> set, Object object) {
+ if (set == object) {
+ return true;
+ }
+ if (object instanceof Set) {
+ Set<?> s = (Set<?>) object;
+
+ try {
+ return set.size() == s.size() && set.containsAll(s);
+ } catch (NullPointerException ignored) {
+ return false;
+ } catch (ClassCastException ignored) {
+ return false;
+ }
+ }
+ return false;
+ }
+
+ public Set<Map.Entry<K, V>> getEntrySet() {
+ if (mEntrySet == null) {
+ mEntrySet = new EntrySet();
+ }
+ return mEntrySet;
+ }
+
+ public Set<K> getKeySet() {
+ if (mKeySet == null) {
+ mKeySet = new KeySet();
+ }
+ return mKeySet;
+ }
+
+ public Collection<V> getValues() {
+ if (mValues == null) {
+ mValues = new ValuesCollection();
+ }
+ return mValues;
+ }
+
+ protected abstract int colGetSize();
+ protected abstract Object colGetEntry(int index, int offset);
+ protected abstract int colIndexOfKey(Object key);
+ protected abstract int colIndexOfValue(Object key);
+ protected abstract Map<K, V> colGetMap();
+ protected abstract void colPut(K key, V value);
+ protected abstract V colSetValue(int index, V value);
+ protected abstract void colRemoveAt(int index);
+ protected abstract void colClear();
+}
diff --git a/android/util/MathUtils.java b/android/util/MathUtils.java
new file mode 100644
index 0000000..971e161
--- /dev/null
+++ b/android/util/MathUtils.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.graphics.Rect;
+
+/**
+ * A class that contains utility methods related to numbers.
+ *
+ * @hide Pending API council approval
+ */
+public final class MathUtils {
+ private static final float DEG_TO_RAD = 3.1415926f / 180.0f;
+ private static final float RAD_TO_DEG = 180.0f / 3.1415926f;
+
+ private MathUtils() {
+ }
+
+ @UnsupportedAppUsage
+ public static float abs(float v) {
+ return v > 0 ? v : -v;
+ }
+
+ @UnsupportedAppUsage
+ public static int constrain(int amount, int low, int high) {
+ return amount < low ? low : (amount > high ? high : amount);
+ }
+
+ public static long constrain(long amount, long low, long high) {
+ return amount < low ? low : (amount > high ? high : amount);
+ }
+
+ @UnsupportedAppUsage
+ public static float constrain(float amount, float low, float high) {
+ return amount < low ? low : (amount > high ? high : amount);
+ }
+
+ public static float log(float a) {
+ return (float) Math.log(a);
+ }
+
+ public static float exp(float a) {
+ return (float) Math.exp(a);
+ }
+
+ public static float pow(float a, float b) {
+ return (float) Math.pow(a, b);
+ }
+
+ public static float sqrt(float a) {
+ return (float) Math.sqrt(a);
+ }
+
+ public static float max(float a, float b) {
+ return a > b ? a : b;
+ }
+
+ @UnsupportedAppUsage
+ public static float max(int a, int b) {
+ return a > b ? a : b;
+ }
+
+ public static float max(float a, float b, float c) {
+ return a > b ? (a > c ? a : c) : (b > c ? b : c);
+ }
+
+ public static float max(int a, int b, int c) {
+ return a > b ? (a > c ? a : c) : (b > c ? b : c);
+ }
+
+ public static float min(float a, float b) {
+ return a < b ? a : b;
+ }
+
+ public static float min(int a, int b) {
+ return a < b ? a : b;
+ }
+
+ public static float min(float a, float b, float c) {
+ return a < b ? (a < c ? a : c) : (b < c ? b : c);
+ }
+
+ public static float min(int a, int b, int c) {
+ return a < b ? (a < c ? a : c) : (b < c ? b : c);
+ }
+
+ public static float dist(float x1, float y1, float x2, float y2) {
+ final float x = (x2 - x1);
+ final float y = (y2 - y1);
+ return (float) Math.hypot(x, y);
+ }
+
+ public static float dist(float x1, float y1, float z1, float x2, float y2, float z2) {
+ final float x = (x2 - x1);
+ final float y = (y2 - y1);
+ final float z = (z2 - z1);
+ return (float) Math.sqrt(x * x + y * y + z * z);
+ }
+
+ public static float mag(float a, float b) {
+ return (float) Math.hypot(a, b);
+ }
+
+ public static float mag(float a, float b, float c) {
+ return (float) Math.sqrt(a * a + b * b + c * c);
+ }
+
+ public static float sq(float v) {
+ return v * v;
+ }
+
+ public static float dot(float v1x, float v1y, float v2x, float v2y) {
+ return v1x * v2x + v1y * v2y;
+ }
+
+ public static float cross(float v1x, float v1y, float v2x, float v2y) {
+ return v1x * v2y - v1y * v2x;
+ }
+
+ public static float radians(float degrees) {
+ return degrees * DEG_TO_RAD;
+ }
+
+ public static float degrees(float radians) {
+ return radians * RAD_TO_DEG;
+ }
+
+ public static float acos(float value) {
+ return (float) Math.acos(value);
+ }
+
+ public static float asin(float value) {
+ return (float) Math.asin(value);
+ }
+
+ public static float atan(float value) {
+ return (float) Math.atan(value);
+ }
+
+ public static float atan2(float a, float b) {
+ return (float) Math.atan2(a, b);
+ }
+
+ public static float tan(float angle) {
+ return (float) Math.tan(angle);
+ }
+
+ @UnsupportedAppUsage
+ public static float lerp(float start, float stop, float amount) {
+ return start + (stop - start) * amount;
+ }
+
+ /**
+ * Returns the interpolation scalar (s) that satisfies the equation: {@code value = }{@link
+ * #lerp}{@code (a, b, s)}
+ *
+ * <p>If {@code a == b}, then this function will return 0.
+ */
+ public static float lerpInv(float a, float b, float value) {
+ return a != b ? ((value - a) / (b - a)) : 0.0f;
+ }
+
+ /** Returns the single argument constrained between [0.0, 1.0]. */
+ public static float saturate(float value) {
+ return constrain(value, 0.0f, 1.0f);
+ }
+
+ /** Returns the saturated (constrained between [0, 1]) result of {@link #lerpInv}. */
+ public static float lerpInvSat(float a, float b, float value) {
+ return saturate(lerpInv(a, b, value));
+ }
+
+ /**
+ * Returns an interpolated angle in degrees between a set of start and end
+ * angles.
+ * <p>
+ * Unlike {@link #lerp(float, float, float)}, the direction and distance of
+ * travel is determined by the shortest angle between the start and end
+ * angles. For example, if the starting angle is 0 and the ending angle is
+ * 350, then the interpolated angle will be in the range [0,-10] rather
+ * than [0,350].
+ *
+ * @param start the starting angle in degrees
+ * @param end the ending angle in degrees
+ * @param amount the position between start and end in the range [0,1]
+ * where 0 is the starting angle and 1 is the ending angle
+ * @return the interpolated angle in degrees
+ */
+ public static float lerpDeg(float start, float end, float amount) {
+ final float minAngle = (((end - start) + 180) % 360) - 180;
+ return minAngle * amount + start;
+ }
+
+ public static float norm(float start, float stop, float value) {
+ return (value - start) / (stop - start);
+ }
+
+ public static float map(float minStart, float minStop, float maxStart, float maxStop, float value) {
+ return maxStart + (maxStop - maxStart) * ((value - minStart) / (minStop - minStart));
+ }
+
+ /**
+ * Calculates a value in [rangeMin, rangeMax] that maps value in [valueMin, valueMax] to
+ * returnVal in [rangeMin, rangeMax].
+ * <p>
+ * Always returns a constrained value in the range [rangeMin, rangeMax], even if value is
+ * outside [valueMin, valueMax].
+ * <p>
+ * Eg:
+ * constrainedMap(0f, 100f, 0f, 1f, 0.5f) = 50f
+ * constrainedMap(20f, 200f, 10f, 20f, 20f) = 200f
+ * constrainedMap(20f, 200f, 10f, 20f, 50f) = 200f
+ * constrainedMap(10f, 50f, 10f, 20f, 5f) = 10f
+ *
+ * @param rangeMin minimum of the range that should be returned.
+ * @param rangeMax maximum of the range that should be returned.
+ * @param valueMin minimum of range to map {@code value} to.
+ * @param valueMax maximum of range to map {@code value} to.
+ * @param value to map to the range [{@code valueMin}, {@code valueMax}]. Note, can be outside
+ * this range, resulting in a clamped value.
+ * @return the mapped value, constrained to [{@code rangeMin}, {@code rangeMax}.
+ */
+ public static float constrainedMap(
+ float rangeMin, float rangeMax, float valueMin, float valueMax, float value) {
+ return lerp(rangeMin, rangeMax, lerpInvSat(valueMin, valueMax, value));
+ }
+
+ /**
+ * Perform Hermite interpolation between two values.
+ * Eg:
+ * smoothStep(0, 0.5f, 0.5f) = 1f
+ * smoothStep(0, 0.5f, 0.25f) = 0.5f
+ *
+ * @param start Left edge.
+ * @param end Right edge.
+ * @param x A value between {@code start} and {@code end}.
+ * @return A number between 0 and 1 representing where {@code x} is in the interpolation.
+ */
+ public static float smoothStep(float start, float end, float x) {
+ return constrain((x - start) / (end - start), 0f, 1f);
+ }
+
+ /**
+ * Returns the sum of the two parameters, or throws an exception if the resulting sum would
+ * cause an overflow or underflow.
+ * @throws IllegalArgumentException when overflow or underflow would occur.
+ */
+ public static int addOrThrow(int a, int b) throws IllegalArgumentException {
+ if (b == 0) {
+ return a;
+ }
+
+ if (b > 0 && a <= (Integer.MAX_VALUE - b)) {
+ return a + b;
+ }
+
+ if (b < 0 && a >= (Integer.MIN_VALUE - b)) {
+ return a + b;
+ }
+ throw new IllegalArgumentException("Addition overflow: " + a + " + " + b);
+ }
+
+ /**
+ * Resize a {@link Rect} so one size would be {@param largestSide}.
+ *
+ * @param outToResize Rectangle that will be resized.
+ * @param largestSide Size of the largest side.
+ */
+ public static void fitRect(Rect outToResize, int largestSide) {
+ if (outToResize.isEmpty()) {
+ return;
+ }
+ float maxSize = Math.max(outToResize.width(), outToResize.height());
+ outToResize.scale(largestSide / maxSize);
+ }
+}
diff --git a/android/util/MemoryIntArray.java b/android/util/MemoryIntArray.java
new file mode 100644
index 0000000..7d287e3
--- /dev/null
+++ b/android/util/MemoryIntArray.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
+
+import libcore.io.IoUtils;
+import dalvik.system.CloseGuard;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.UUID;
+
+/**
+ * This class is an array of integers that is backed by shared memory.
+ * It is useful for efficiently sharing state between processes. The
+ * write and read operations are guaranteed to not result in read/
+ * write memory tear, i.e. they are atomic. However, multiple read/
+ * write operations are <strong>not</strong> synchronized between
+ * each other.
+ * <p>
+ * The data structure is designed to have one owner process that can
+ * read/write. There may be multiple client processes that can only read.
+ * The owner process is the process that created the array. The shared
+ * memory is pinned (not reclaimed by the system) until the owning process
+ * dies or the data structure is closed. This class is <strong>not</strong>
+ * thread safe. You should not interact with an instance of this class
+ * once it is closed. If you pass back to the owner process an instance
+ * it will be read only even in the owning process.
+ * </p>
+ *
+ * @hide
+ */
+public final class MemoryIntArray implements Parcelable, Closeable {
+ private static final String TAG = "MemoryIntArray";
+
+ private static final int MAX_SIZE = 1024;
+
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+
+ private final boolean mIsOwner;
+ private final long mMemoryAddr;
+ private int mFd = -1;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param size The size of the array in terms of integer slots. Cannot be
+ * more than {@link #getMaxSize()}.
+ * @throws IOException If an error occurs while accessing the shared memory.
+ */
+ public MemoryIntArray(int size) throws IOException {
+ if (size > MAX_SIZE) {
+ throw new IllegalArgumentException("Max size is " + MAX_SIZE);
+ }
+ mIsOwner = true;
+ final String name = UUID.randomUUID().toString();
+ mFd = nativeCreate(name, size);
+ mMemoryAddr = nativeOpen(mFd, mIsOwner);
+ mCloseGuard.open("close");
+ }
+
+ private MemoryIntArray(Parcel parcel) throws IOException {
+ mIsOwner = false;
+ ParcelFileDescriptor pfd = parcel.readParcelable(null);
+ if (pfd == null) {
+ throw new IOException("No backing file descriptor");
+ }
+ mFd = pfd.detachFd();
+ mMemoryAddr = nativeOpen(mFd, mIsOwner);
+ mCloseGuard.open("close");
+ }
+
+ /**
+ * @return Gets whether this array is mutable.
+ */
+ public boolean isWritable() {
+ enforceNotClosed();
+ return mIsOwner;
+ }
+
+ /**
+ * Gets the value at a given index.
+ *
+ * @param index The index.
+ * @return The value at this index.
+ * @throws IOException If an error occurs while accessing the shared memory.
+ */
+ public int get(int index) throws IOException {
+ enforceNotClosed();
+ enforceValidIndex(index);
+ return nativeGet(mFd, mMemoryAddr, index);
+ }
+
+ /**
+ * Sets the value at a given index. This method can be called only if
+ * {@link #isWritable()} returns true which means your process is the
+ * owner.
+ *
+ * @param index The index.
+ * @param value The value to set.
+ * @throws IOException If an error occurs while accessing the shared memory.
+ */
+ public void set(int index, int value) throws IOException {
+ enforceNotClosed();
+ enforceWritable();
+ enforceValidIndex(index);
+ nativeSet(mFd, mMemoryAddr, index, value);
+ }
+
+ /**
+ * Gets the array size.
+ *
+ * @throws IOException If an error occurs while accessing the shared memory.
+ */
+ public int size() throws IOException {
+ enforceNotClosed();
+ return nativeSize(mFd);
+ }
+
+ /**
+ * Closes the array releasing resources.
+ *
+ * @throws IOException If an error occurs while accessing the shared memory.
+ */
+ @Override
+ public void close() throws IOException {
+ if (!isClosed()) {
+ nativeClose(mFd, mMemoryAddr, mIsOwner);
+ mFd = -1;
+ mCloseGuard.close();
+ }
+ }
+
+ /**
+ * @return Whether this array is closed and shouldn't be used.
+ */
+ public boolean isClosed() {
+ return mFd == -1;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+
+ IoUtils.closeQuietly(this);
+ } finally {
+ super.finalize();
+ }
+ }
+
+ @Override
+ public int describeContents() {
+ return CONTENTS_FILE_DESCRIPTOR;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ try (ParcelFileDescriptor pfd = ParcelFileDescriptor.fromFd(mFd)) {
+ parcel.writeParcelable(pfd, flags);
+ } catch (IOException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (this == obj) {
+ return true;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ MemoryIntArray other = (MemoryIntArray) obj;
+ return mFd == other.mFd;
+ }
+
+ @Override
+ public int hashCode() {
+ return mFd;
+ }
+
+ private void enforceNotClosed() {
+ if (isClosed()) {
+ throw new IllegalStateException("cannot interact with a closed instance");
+ }
+ }
+
+ private void enforceValidIndex(int index) throws IOException {
+ final int size = size();
+ if (index < 0 || index > size - 1) {
+ throw new IndexOutOfBoundsException(
+ index + " not between 0 and " + (size - 1));
+ }
+ }
+
+ private void enforceWritable() {
+ if (!isWritable()) {
+ throw new UnsupportedOperationException("array is not writable");
+ }
+ }
+
+ private native int nativeCreate(String name, int size);
+ private native long nativeOpen(int fd, boolean owner);
+ private native void nativeClose(int fd, long memoryAddr, boolean owner);
+ private native int nativeGet(int fd, long memoryAddr, int index);
+ private native void nativeSet(int fd, long memoryAddr, int index, int value);
+ private native int nativeSize(int fd);
+
+ /**
+ * @return The max array size.
+ */
+ public static int getMaxSize() {
+ return MAX_SIZE;
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<MemoryIntArray> CREATOR =
+ new Parcelable.Creator<MemoryIntArray>() {
+ @Override
+ public MemoryIntArray createFromParcel(Parcel parcel) {
+ try {
+ return new MemoryIntArray(parcel);
+ } catch (IOException ioe) {
+ throw new IllegalArgumentException("Error unparceling MemoryIntArray");
+ }
+ }
+
+ @Override
+ public MemoryIntArray[] newArray(int size) {
+ return new MemoryIntArray[size];
+ }
+ };
+}
diff --git a/android/util/MergedConfiguration.java b/android/util/MergedConfiguration.java
new file mode 100644
index 0000000..2399ada
--- /dev/null
+++ b/android/util/MergedConfiguration.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.util;
+
+import android.annotation.NonNull;
+import android.content.res.Configuration;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.io.PrintWriter;
+
+/**
+ * Container that holds global and override config and their merge product.
+ * Merged configuration updates automatically whenever global or override configs are updated via
+ * setters.
+ *
+ * {@hide}
+ */
+public class MergedConfiguration implements Parcelable {
+
+ private Configuration mGlobalConfig = new Configuration();
+ private Configuration mOverrideConfig = new Configuration();
+ private Configuration mMergedConfig = new Configuration();
+
+ public MergedConfiguration() {
+ }
+
+ public MergedConfiguration(Configuration globalConfig, Configuration overrideConfig) {
+ setConfiguration(globalConfig, overrideConfig);
+ }
+
+ public MergedConfiguration(Configuration globalConfig) {
+ setGlobalConfiguration(globalConfig);
+ }
+
+ public MergedConfiguration(MergedConfiguration mergedConfiguration) {
+ setConfiguration(mergedConfiguration.getGlobalConfiguration(),
+ mergedConfiguration.getOverrideConfiguration());
+ }
+
+ private MergedConfiguration(Parcel in) {
+ readFromParcel(in);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(mGlobalConfig, flags);
+ dest.writeParcelable(mOverrideConfig, flags);
+ dest.writeParcelable(mMergedConfig, flags);
+ }
+
+ public void readFromParcel(Parcel source) {
+ mGlobalConfig = source.readParcelable(Configuration.class.getClassLoader());
+ mOverrideConfig = source.readParcelable(Configuration.class.getClassLoader());
+ mMergedConfig = source.readParcelable(Configuration.class.getClassLoader());
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @android.annotation.NonNull Creator<MergedConfiguration> CREATOR = new Creator<MergedConfiguration>() {
+ @Override
+ public MergedConfiguration createFromParcel(Parcel in) {
+ return new MergedConfiguration(in);
+ }
+
+ @Override
+ public MergedConfiguration[] newArray(int size) {
+ return new MergedConfiguration[size];
+ }
+ };
+
+ /**
+ * Update global and override configurations.
+ * Merged configuration will automatically be updated.
+ * @param globalConfig New global configuration.
+ * @param overrideConfig New override configuration.
+ */
+ public void setConfiguration(Configuration globalConfig, Configuration overrideConfig) {
+ mGlobalConfig.setTo(globalConfig);
+ mOverrideConfig.setTo(overrideConfig);
+ updateMergedConfig();
+ }
+
+ /**
+ * Update global configurations.
+ * Merged configuration will automatically be updated.
+ * @param globalConfig New global configuration.
+ */
+ public void setGlobalConfiguration(Configuration globalConfig) {
+ mGlobalConfig.setTo(globalConfig);
+ updateMergedConfig();
+ }
+
+ /**
+ * Update override configurations.
+ * Merged configuration will automatically be updated.
+ * @param overrideConfig New override configuration.
+ */
+ public void setOverrideConfiguration(Configuration overrideConfig) {
+ mOverrideConfig.setTo(overrideConfig);
+ updateMergedConfig();
+ }
+
+ public void setTo(MergedConfiguration config) {
+ setConfiguration(config.mGlobalConfig, config.mOverrideConfig);
+ }
+
+ public void unset() {
+ mGlobalConfig.unset();
+ mOverrideConfig.unset();
+ updateMergedConfig();
+ }
+
+ /**
+ * @return Stored global configuration value.
+ */
+ @NonNull
+ public Configuration getGlobalConfiguration() {
+ return mGlobalConfig;
+ }
+
+ /**
+ * @return Stored override configuration value.
+ */
+ public Configuration getOverrideConfiguration() {
+ return mOverrideConfig;
+ }
+
+ /**
+ * @return Stored merged configuration value.
+ */
+ public Configuration getMergedConfiguration() {
+ return mMergedConfig;
+ }
+
+ /** Update merged config when global or override config changes. */
+ private void updateMergedConfig() {
+ mMergedConfig.setTo(mGlobalConfig);
+ mMergedConfig.updateFrom(mOverrideConfig);
+ }
+
+ @Override
+ public String toString() {
+ return "{mGlobalConfig=" + mGlobalConfig + " mOverrideConfig=" + mOverrideConfig + "}";
+ }
+
+ @Override
+ public int hashCode() {
+ return mMergedConfig.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object that) {
+ if (!(that instanceof MergedConfiguration)) {
+ return false;
+ }
+
+ if (that == this) return true;
+ return mMergedConfig.equals(((MergedConfiguration) that).mMergedConfig);
+ }
+
+ public void dump(PrintWriter pw, String prefix) {
+ pw.println(prefix + "mGlobalConfig=" + mGlobalConfig);
+ pw.println(prefix + "mOverrideConfig=" + mOverrideConfig);
+ }
+}
diff --git a/android/util/MonthDisplayHelper.java b/android/util/MonthDisplayHelper.java
new file mode 100644
index 0000000..c3f13fc
--- /dev/null
+++ b/android/util/MonthDisplayHelper.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import java.util.Calendar;
+
+/**
+ * Helps answer common questions that come up when displaying a month in a
+ * 6 row calendar grid format.
+ *
+ * Not thread safe.
+ */
+public class MonthDisplayHelper {
+
+ // display pref
+ private final int mWeekStartDay;
+
+ // holds current month, year, helps compute display
+ private Calendar mCalendar;
+
+ // cached computed stuff that helps with display
+ private int mNumDaysInMonth;
+ private int mNumDaysInPrevMonth;
+ private int mOffset;
+
+
+ /**
+ * @param year The year.
+ * @param month The month.
+ * @param weekStartDay What day of the week the week should start.
+ */
+ public MonthDisplayHelper(int year, int month, int weekStartDay) {
+
+ if (weekStartDay < Calendar.SUNDAY || weekStartDay > Calendar.SATURDAY) {
+ throw new IllegalArgumentException();
+ }
+ mWeekStartDay = weekStartDay;
+
+ mCalendar = Calendar.getInstance();
+ mCalendar.set(Calendar.YEAR, year);
+ mCalendar.set(Calendar.MONTH, month);
+ mCalendar.set(Calendar.DAY_OF_MONTH, 1);
+ mCalendar.set(Calendar.HOUR_OF_DAY, 0);
+ mCalendar.set(Calendar.MINUTE, 0);
+ mCalendar.set(Calendar.SECOND, 0);
+ mCalendar.getTimeInMillis();
+
+ recalculate();
+ }
+
+
+ public MonthDisplayHelper(int year, int month) {
+ this(year, month, Calendar.SUNDAY);
+ }
+
+
+ public int getYear() {
+ return mCalendar.get(Calendar.YEAR);
+ }
+
+ public int getMonth() {
+ return mCalendar.get(Calendar.MONTH);
+ }
+
+
+ public int getWeekStartDay() {
+ return mWeekStartDay;
+ }
+
+ /**
+ * @return The first day of the month using a constants such as
+ * {@link java.util.Calendar#SUNDAY}.
+ */
+ public int getFirstDayOfMonth() {
+ return mCalendar.get(Calendar.DAY_OF_WEEK);
+ }
+
+ /**
+ * @return The number of days in the month.
+ */
+ public int getNumberOfDaysInMonth() {
+ return mNumDaysInMonth;
+ }
+
+
+ /**
+ * @return The offset from displaying everything starting on the very first
+ * box. For example, if the calendar is set to display the first day of
+ * the week as Sunday, and the month starts on a Wednesday, the offset is 3.
+ */
+ public int getOffset() {
+ return mOffset;
+ }
+
+
+ /**
+ * @param row Which row (0-5).
+ * @return the digits of the month to display in one
+ * of the 6 rows of a calendar month display.
+ */
+ public int[] getDigitsForRow(int row) {
+ if (row < 0 || row > 5) {
+ throw new IllegalArgumentException("row " + row
+ + " out of range (0-5)");
+ }
+
+ int [] result = new int[7];
+ for (int column = 0; column < 7; column++) {
+ result[column] = getDayAt(row, column);
+ }
+
+ return result;
+ }
+
+ /**
+ * @param row The row, 0-5, starting from the top.
+ * @param column The column, 0-6, starting from the left.
+ * @return The day at a particular row, column
+ */
+ public int getDayAt(int row, int column) {
+
+ if (row == 0 && column < mOffset) {
+ return mNumDaysInPrevMonth + column - mOffset + 1;
+ }
+
+ int day = 7 * row + column - mOffset + 1;
+
+ return (day > mNumDaysInMonth) ?
+ day - mNumDaysInMonth : day;
+ }
+
+ /**
+ * @return Which row day is in.
+ */
+ public int getRowOf(int day) {
+ return (day + mOffset - 1) / 7;
+ }
+
+ /**
+ * @return Which column day is in.
+ */
+ public int getColumnOf(int day) {
+ return (day + mOffset - 1) % 7;
+ }
+
+ /**
+ * Decrement the month.
+ */
+ public void previousMonth() {
+ mCalendar.add(Calendar.MONTH, -1);
+ recalculate();
+ }
+
+ /**
+ * Increment the month.
+ */
+ public void nextMonth() {
+ mCalendar.add(Calendar.MONTH, 1);
+ recalculate();
+ }
+
+ /**
+ * @return Whether the row and column fall within the month.
+ */
+ public boolean isWithinCurrentMonth(int row, int column) {
+
+ if (row < 0 || column < 0 || row > 5 || column > 6) {
+ return false;
+ }
+
+ if (row == 0 && column < mOffset) {
+ return false;
+ }
+
+ int day = 7 * row + column - mOffset + 1;
+ if (day > mNumDaysInMonth) {
+ return false;
+ }
+ return true;
+ }
+
+
+ // helper method that recalculates cached values based on current month / year
+ private void recalculate() {
+
+ mNumDaysInMonth = mCalendar.getActualMaximum(Calendar.DAY_OF_MONTH);
+
+ mCalendar.add(Calendar.MONTH, -1);
+ mNumDaysInPrevMonth = mCalendar.getActualMaximum(Calendar.DAY_OF_MONTH);
+ mCalendar.add(Calendar.MONTH, 1);
+
+ int firstDayOfMonth = getFirstDayOfMonth();
+ int offset = firstDayOfMonth - mWeekStartDay;
+ if (offset < 0) {
+ offset += 7;
+ }
+ mOffset = offset;
+ }
+}
diff --git a/android/util/MutableBoolean.java b/android/util/MutableBoolean.java
new file mode 100644
index 0000000..44e73cc
--- /dev/null
+++ b/android/util/MutableBoolean.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * @deprecated This class will be removed from a future version of the Android API.
+ */
+@Deprecated
+public final class MutableBoolean {
+ public boolean value;
+
+ public MutableBoolean(boolean value) {
+ this.value = value;
+ }
+}
diff --git a/android/util/MutableByte.java b/android/util/MutableByte.java
new file mode 100644
index 0000000..b9ec25d
--- /dev/null
+++ b/android/util/MutableByte.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * @deprecated This class will be removed from a future version of the Android API.
+ */
+@Deprecated
+public final class MutableByte {
+ public byte value;
+
+ public MutableByte(byte value) {
+ this.value = value;
+ }
+}
diff --git a/android/util/MutableChar.java b/android/util/MutableChar.java
new file mode 100644
index 0000000..9f7a9ae
--- /dev/null
+++ b/android/util/MutableChar.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * @deprecated This class will be removed from a future version of the Android API.
+ */
+@Deprecated
+public final class MutableChar {
+ public char value;
+
+ public MutableChar(char value) {
+ this.value = value;
+ }
+}
diff --git a/android/util/MutableDouble.java b/android/util/MutableDouble.java
new file mode 100644
index 0000000..56e539b
--- /dev/null
+++ b/android/util/MutableDouble.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * @deprecated This class will be removed from a future version of the Android API.
+ */
+@Deprecated
+public final class MutableDouble {
+ public double value;
+
+ public MutableDouble(double value) {
+ this.value = value;
+ }
+}
diff --git a/android/util/MutableFloat.java b/android/util/MutableFloat.java
new file mode 100644
index 0000000..6d7ad59
--- /dev/null
+++ b/android/util/MutableFloat.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * @deprecated This class will be removed from a future version of the Android API.
+ */
+@Deprecated
+public final class MutableFloat {
+ public float value;
+
+ public MutableFloat(float value) {
+ this.value = value;
+ }
+}
diff --git a/android/util/MutableInt.java b/android/util/MutableInt.java
new file mode 100644
index 0000000..bb24566
--- /dev/null
+++ b/android/util/MutableInt.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * @deprecated This class will be removed from a future version of the Android API.
+ */
+@Deprecated
+public final class MutableInt {
+ public int value;
+
+ public MutableInt(int value) {
+ this.value = value;
+ }
+}
diff --git a/android/util/MutableLong.java b/android/util/MutableLong.java
new file mode 100644
index 0000000..86e70e1
--- /dev/null
+++ b/android/util/MutableLong.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * @deprecated This class will be removed from a future version of the Android API.
+ */
+@Deprecated
+public final class MutableLong {
+ public long value;
+
+ public MutableLong(long value) {
+ this.value = value;
+ }
+}
diff --git a/android/util/MutableShort.java b/android/util/MutableShort.java
new file mode 100644
index 0000000..b94ab07
--- /dev/null
+++ b/android/util/MutableShort.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * @deprecated This class will be removed from a future version of the Android API.
+ */
+@Deprecated
+public final class MutableShort {
+ public short value;
+
+ public MutableShort(short value) {
+ this.value = value;
+ }
+}
diff --git a/android/util/NoSuchPropertyException.java b/android/util/NoSuchPropertyException.java
new file mode 100644
index 0000000..b93f983
--- /dev/null
+++ b/android/util/NoSuchPropertyException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.util;
+
+/**
+ * Thrown when code requests a {@link Property} on a class that does
+ * not expose the appropriate method or field.
+ *
+ * @see Property#of(java.lang.Class, java.lang.Class, java.lang.String)
+ */
+public class NoSuchPropertyException extends RuntimeException {
+
+ public NoSuchPropertyException(String s) {
+ super(s);
+ }
+
+}
diff --git a/android/util/NtpTrustedTime.java b/android/util/NtpTrustedTime.java
new file mode 100644
index 0000000..0892c94
--- /dev/null
+++ b/android/util/NtpTrustedTime.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkInfo;
+import android.net.SntpClient;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.Objects;
+import java.util.function.Supplier;
+
+/**
+ * A singleton that connects with a remote NTP server as its trusted time source. This class
+ * is thread-safe. The {@link #forceRefresh()} method is synchronous, i.e. it may occupy the
+ * current thread while performing an NTP request. All other threads calling {@link #forceRefresh()}
+ * will block during that request.
+ *
+ * @hide
+ */
+public class NtpTrustedTime implements TrustedTime {
+
+ /**
+ * The result of a successful NTP query.
+ *
+ * @hide
+ */
+ public static class TimeResult {
+ private final long mTimeMillis;
+ private final long mElapsedRealtimeMillis;
+ private final long mCertaintyMillis;
+
+ public TimeResult(long timeMillis, long elapsedRealtimeMillis, long certaintyMillis) {
+ mTimeMillis = timeMillis;
+ mElapsedRealtimeMillis = elapsedRealtimeMillis;
+ mCertaintyMillis = certaintyMillis;
+ }
+
+ public long getTimeMillis() {
+ return mTimeMillis;
+ }
+
+ public long getElapsedRealtimeMillis() {
+ return mElapsedRealtimeMillis;
+ }
+
+ public long getCertaintyMillis() {
+ return mCertaintyMillis;
+ }
+
+ /** Calculates and returns the current time accounting for the age of this result. */
+ public long currentTimeMillis() {
+ return mTimeMillis + getAgeMillis();
+ }
+
+ /** Calculates and returns the age of this result. */
+ public long getAgeMillis() {
+ return SystemClock.elapsedRealtime() - mElapsedRealtimeMillis;
+ }
+
+ @Override
+ public String toString() {
+ return "TimeResult{"
+ + "mTimeMillis=" + mTimeMillis
+ + ", mElapsedRealtimeMillis=" + mElapsedRealtimeMillis
+ + ", mCertaintyMillis=" + mCertaintyMillis
+ + '}';
+ }
+ }
+
+ private static final String TAG = "NtpTrustedTime";
+ private static final boolean LOGD = false;
+
+ private static NtpTrustedTime sSingleton;
+
+ @NonNull
+ private final Context mContext;
+
+ /**
+ * A supplier that returns the ConnectivityManager. The Supplier can return null if
+ * ConnectivityService isn't running yet.
+ */
+ private final Supplier<ConnectivityManager> mConnectivityManagerSupplier =
+ new Supplier<ConnectivityManager>() {
+ private ConnectivityManager mConnectivityManager;
+
+ @Nullable
+ @Override
+ public synchronized ConnectivityManager get() {
+ // We can't do this at initialization time: ConnectivityService might not be running
+ // yet.
+ if (mConnectivityManager == null) {
+ mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
+ }
+ return mConnectivityManager;
+ }
+ };
+
+ // Declared volatile and accessed outside of synchronized blocks to avoid blocking reads during
+ // forceRefresh().
+ private volatile TimeResult mTimeResult;
+
+ private NtpTrustedTime(Context context) {
+ mContext = Objects.requireNonNull(context);
+ }
+
+ @UnsupportedAppUsage
+ public static synchronized NtpTrustedTime getInstance(Context context) {
+ if (sSingleton == null) {
+ Context appContext = context.getApplicationContext();
+ sSingleton = new NtpTrustedTime(appContext);
+ }
+ return sSingleton;
+ }
+
+ @UnsupportedAppUsage
+ public boolean forceRefresh() {
+ synchronized (this) {
+ NtpConnectionInfo connectionInfo = getNtpConnectionInfo();
+ if (connectionInfo == null) {
+ // missing server config, so no trusted time available
+ if (LOGD) Log.d(TAG, "forceRefresh: invalid server config");
+ return false;
+ }
+
+ ConnectivityManager connectivityManager = mConnectivityManagerSupplier.get();
+ if (connectivityManager == null) {
+ if (LOGD) Log.d(TAG, "forceRefresh: no ConnectivityManager");
+ return false;
+ }
+ final Network network = connectivityManager.getActiveNetwork();
+ final NetworkInfo ni = connectivityManager.getNetworkInfo(network);
+ if (ni == null || !ni.isConnected()) {
+ if (LOGD) Log.d(TAG, "forceRefresh: no connectivity");
+ return false;
+ }
+
+ if (LOGD) Log.d(TAG, "forceRefresh() from cache miss");
+ final SntpClient client = new SntpClient();
+ final String serverName = connectionInfo.getServer();
+ final int timeoutMillis = connectionInfo.getTimeoutMillis();
+ if (client.requestTime(serverName, timeoutMillis, network)) {
+ long ntpCertainty = client.getRoundTripTime() / 2;
+ mTimeResult = new TimeResult(
+ client.getNtpTime(), client.getNtpTimeReference(), ntpCertainty);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Only kept for UnsupportedAppUsage.
+ *
+ * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
+ */
+ @Deprecated
+ @UnsupportedAppUsage
+ public boolean hasCache() {
+ return mTimeResult != null;
+ }
+
+ /**
+ * Only kept for UnsupportedAppUsage.
+ *
+ * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
+ */
+ @Deprecated
+ @Override
+ public long getCacheAge() {
+ TimeResult timeResult = mTimeResult;
+ if (timeResult != null) {
+ return SystemClock.elapsedRealtime() - timeResult.getElapsedRealtimeMillis();
+ } else {
+ return Long.MAX_VALUE;
+ }
+ }
+
+ /**
+ * Only kept for UnsupportedAppUsage.
+ *
+ * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
+ */
+ @Deprecated
+ @UnsupportedAppUsage
+ public long currentTimeMillis() {
+ TimeResult timeResult = mTimeResult;
+ if (timeResult == null) {
+ throw new IllegalStateException("Missing authoritative time source");
+ }
+ if (LOGD) Log.d(TAG, "currentTimeMillis() cache hit");
+
+ // current time is age after the last ntp cache; callers who
+ // want fresh values will hit forceRefresh() first.
+ return timeResult.currentTimeMillis();
+ }
+
+ /**
+ * Only kept for UnsupportedAppUsage.
+ *
+ * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
+ */
+ @Deprecated
+ @UnsupportedAppUsage
+ public long getCachedNtpTime() {
+ if (LOGD) Log.d(TAG, "getCachedNtpTime() cache hit");
+ TimeResult timeResult = mTimeResult;
+ return timeResult == null ? 0 : timeResult.getTimeMillis();
+ }
+
+ /**
+ * Only kept for UnsupportedAppUsage.
+ *
+ * @deprecated Use {@link #getCachedTimeResult()} to obtain a {@link TimeResult} atomically.
+ */
+ @Deprecated
+ @UnsupportedAppUsage
+ public long getCachedNtpTimeReference() {
+ TimeResult timeResult = mTimeResult;
+ return timeResult == null ? 0 : timeResult.getElapsedRealtimeMillis();
+ }
+
+ /**
+ * Returns an object containing the latest NTP information available. Can return {@code null} if
+ * no information is available.
+ */
+ @Nullable
+ public TimeResult getCachedTimeResult() {
+ return mTimeResult;
+ }
+
+ private static class NtpConnectionInfo {
+
+ @NonNull private final String mServer;
+ private final int mTimeoutMillis;
+
+ NtpConnectionInfo(@NonNull String server, int timeoutMillis) {
+ mServer = Objects.requireNonNull(server);
+ mTimeoutMillis = timeoutMillis;
+ }
+
+ @NonNull
+ public String getServer() {
+ return mServer;
+ }
+
+ int getTimeoutMillis() {
+ return mTimeoutMillis;
+ }
+ }
+
+ @GuardedBy("this")
+ private NtpConnectionInfo getNtpConnectionInfo() {
+ final ContentResolver resolver = mContext.getContentResolver();
+
+ final Resources res = mContext.getResources();
+ final String defaultServer = res.getString(
+ com.android.internal.R.string.config_ntpServer);
+ final int defaultTimeoutMillis = res.getInteger(
+ com.android.internal.R.integer.config_ntpTimeout);
+
+ final String secureServer = Settings.Global.getString(
+ resolver, Settings.Global.NTP_SERVER);
+ final int timeoutMillis = Settings.Global.getInt(
+ resolver, Settings.Global.NTP_TIMEOUT, defaultTimeoutMillis);
+
+ final String server = secureServer != null ? secureServer : defaultServer;
+ return TextUtils.isEmpty(server) ? null : new NtpConnectionInfo(server, timeoutMillis);
+ }
+}
diff --git a/android/util/PackageUtils.java b/android/util/PackageUtils.java
new file mode 100644
index 0000000..8061bf3
--- /dev/null
+++ b/android/util/PackageUtils.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.pm.Signature;
+
+import libcore.util.HexEncoding;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+/**
+ * Helper functions applicable to packages.
+ * @hide
+ */
+public final class PackageUtils {
+
+ private PackageUtils() {
+ /* hide constructor */
+ }
+
+ /**
+ * Computes the SHA256 digests of a list of signatures. Items in the
+ * resulting array of hashes correspond to the signatures in the
+ * input array.
+ * @param signatures The signatures.
+ * @return The digest array.
+ */
+ public static @NonNull String[] computeSignaturesSha256Digests(
+ @NonNull Signature[] signatures) {
+ final int signatureCount = signatures.length;
+ final String[] digests = new String[signatureCount];
+ for (int i = 0; i < signatureCount; i++) {
+ digests[i] = computeSha256Digest(signatures[i].toByteArray());
+ }
+ return digests;
+ }
+ /**
+ * Computes a SHA256 digest of the signatures' SHA256 digests. First,
+ * individual hashes for each signature is derived in a hexademical
+ * form, then these strings are sorted based the natural ordering, and
+ * finally a hash is derived from these strings' bytes.
+ * @param signatures The signatures.
+ * @return The digest.
+ */
+ public static @NonNull String computeSignaturesSha256Digest(
+ @NonNull Signature[] signatures) {
+ // Shortcut for optimization - most apps singed by a single cert
+ if (signatures.length == 1) {
+ return computeSha256Digest(signatures[0].toByteArray());
+ }
+
+ // Make sure these are sorted to handle reversed certificates
+ final String[] sha256Digests = computeSignaturesSha256Digests(signatures);
+ return computeSignaturesSha256Digest(sha256Digests);
+ }
+
+ /**
+ * Computes a SHA256 digest in of the signatures SHA256 digests. First,
+ * the strings are sorted based the natural ordering, and then a hash is
+ * derived from these strings' bytes.
+ * @param sha256Digests Signature SHA256 hashes in hexademical form.
+ * @return The digest.
+ */
+ public static @NonNull String computeSignaturesSha256Digest(
+ @NonNull String[] sha256Digests) {
+ // Shortcut for optimization - most apps singed by a single cert
+ if (sha256Digests.length == 1) {
+ return sha256Digests[0];
+ }
+
+ // Make sure these are sorted to handle reversed certificates
+ Arrays.sort(sha256Digests);
+
+ final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ for (String sha256Digest : sha256Digests) {
+ try {
+ bytes.write(sha256Digest.getBytes());
+ } catch (IOException e) {
+ /* ignore - can't happen */
+ }
+ }
+ return computeSha256Digest(bytes.toByteArray());
+ }
+
+ /**
+ * Computes the SHA256 digest of some data.
+ * @param data The data.
+ * @return The digest or null if an error occurs.
+ */
+ public static @Nullable byte[] computeSha256DigestBytes(@NonNull byte[] data) {
+ MessageDigest messageDigest;
+ try {
+ messageDigest = MessageDigest.getInstance("SHA256");
+ } catch (NoSuchAlgorithmException e) {
+ /* can't happen */
+ return null;
+ }
+
+ messageDigest.update(data);
+
+ return messageDigest.digest();
+ }
+
+ /**
+ * Computes the SHA256 digest of some data.
+ * @param data The data.
+ * @return The digest or null if an error occurs.
+ */
+ public static @Nullable String computeSha256Digest(@NonNull byte[] data) {
+ byte[] sha256DigestBytes = computeSha256DigestBytes(data);
+ if (sha256DigestBytes == null) {
+ return null;
+ }
+ return HexEncoding.encodeToString(sha256DigestBytes, true /* uppercase */);
+ }
+}
diff --git a/android/util/Pair.java b/android/util/Pair.java
new file mode 100644
index 0000000..f96da72
--- /dev/null
+++ b/android/util/Pair.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import java.util.Objects;
+
+/**
+ * Container to ease passing around a tuple of two objects. This object provides a sensible
+ * implementation of equals(), returning true if equals() is true on each of the contained
+ * objects.
+ */
+public class Pair<F, S> {
+ public final F first;
+ public final S second;
+
+ /**
+ * Constructor for a Pair.
+ *
+ * @param first the first object in the Pair
+ * @param second the second object in the pair
+ */
+ public Pair(F first, S second) {
+ this.first = first;
+ this.second = second;
+ }
+
+ /**
+ * Checks the two objects for equality by delegating to their respective
+ * {@link Object#equals(Object)} methods.
+ *
+ * @param o the {@link Pair} to which this one is to be checked for equality
+ * @return true if the underlying objects of the Pair are both considered
+ * equal
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof Pair)) {
+ return false;
+ }
+ Pair<?, ?> p = (Pair<?, ?>) o;
+ return Objects.equals(p.first, first) && Objects.equals(p.second, second);
+ }
+
+ /**
+ * Compute a hash code using the hash codes of the underlying objects
+ *
+ * @return a hashcode of the Pair
+ */
+ @Override
+ public int hashCode() {
+ return (first == null ? 0 : first.hashCode()) ^ (second == null ? 0 : second.hashCode());
+ }
+
+ @Override
+ public String toString() {
+ return "Pair{" + String.valueOf(first) + " " + String.valueOf(second) + "}";
+ }
+
+ /**
+ * Convenience method for creating an appropriately typed pair.
+ * @param a the first object in the Pair
+ * @param b the second object in the pair
+ * @return a Pair that is templatized with the types of a and b
+ */
+ public static <A, B> Pair <A, B> create(A a, B b) {
+ return new Pair<A, B>(a, b);
+ }
+}
diff --git a/android/util/PathParser.java b/android/util/PathParser.java
new file mode 100644
index 0000000..1e5ec0b
--- /dev/null
+++ b/android/util/PathParser.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package android.util;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.graphics.Path;
+
+import dalvik.annotation.optimization.FastNative;
+
+/**
+ * @hide
+ */
+public class PathParser {
+ static final String LOGTAG = PathParser.class.getSimpleName();
+
+ /**
+ * @param pathString The string representing a path, the same as "d" string in svg file.
+ * @return the generated Path object.
+ */
+ @UnsupportedAppUsage
+ public static Path createPathFromPathData(String pathString) {
+ if (pathString == null) {
+ throw new IllegalArgumentException("Path string can not be null.");
+ }
+ Path path = new Path();
+ nParseStringForPath(path.mNativePath, pathString, pathString.length());
+ return path;
+ }
+
+ /**
+ * Interpret PathData as path commands and insert the commands to the given path.
+ *
+ * @param data The source PathData to be converted.
+ * @param outPath The Path object where path commands will be inserted.
+ */
+ public static void createPathFromPathData(Path outPath, PathData data) {
+ nCreatePathFromPathData(outPath.mNativePath, data.mNativePathData);
+ }
+
+ /**
+ * @param pathDataFrom The source path represented in PathData
+ * @param pathDataTo The target path represented in PathData
+ * @return whether the <code>nodesFrom</code> can morph into <code>nodesTo</code>
+ */
+ public static boolean canMorph(PathData pathDataFrom, PathData pathDataTo) {
+ return nCanMorph(pathDataFrom.mNativePathData, pathDataTo.mNativePathData);
+ }
+
+ /**
+ * PathData class is a wrapper around the native PathData object, which contains
+ * the result of parsing a path string. Specifically, there are verbs and points
+ * associated with each verb stored in PathData. This data can then be used to
+ * generate commands to manipulate a Path.
+ */
+ public static class PathData {
+ long mNativePathData = 0;
+ public PathData() {
+ mNativePathData = nCreateEmptyPathData();
+ }
+
+ public PathData(PathData data) {
+ mNativePathData = nCreatePathData(data.mNativePathData);
+ }
+
+ public PathData(String pathString) {
+ mNativePathData = nCreatePathDataFromString(pathString, pathString.length());
+ if (mNativePathData == 0) {
+ throw new IllegalArgumentException("Invalid pathData: " + pathString);
+ }
+ }
+
+ public long getNativePtr() {
+ return mNativePathData;
+ }
+
+ /**
+ * Update the path data to match the source.
+ * Before calling this, make sure canMorph(target, source) is true.
+ *
+ * @param source The source path represented in PathData
+ */
+ public void setPathData(PathData source) {
+ nSetPathData(mNativePathData, source.mNativePathData);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ if (mNativePathData != 0) {
+ nFinalize(mNativePathData);
+ mNativePathData = 0;
+ }
+ super.finalize();
+ }
+ }
+
+ /**
+ * Interpolate between the <code>fromData</code> and <code>toData</code> according to the
+ * <code>fraction</code>, and put the resulting path data into <code>outData</code>.
+ *
+ * @param outData The resulting PathData of the interpolation
+ * @param fromData The start value as a PathData.
+ * @param toData The end value as a PathData
+ * @param fraction The fraction to interpolate.
+ */
+ public static boolean interpolatePathData(PathData outData, PathData fromData, PathData toData,
+ float fraction) {
+ return nInterpolatePathData(outData.mNativePathData, fromData.mNativePathData,
+ toData.mNativePathData, fraction);
+ }
+
+ // Native functions are defined below.
+ private static native void nParseStringForPath(long pathPtr, String pathString,
+ int stringLength);
+ private static native long nCreatePathDataFromString(String pathString, int stringLength);
+
+ // ----------------- @FastNative -----------------------
+
+ @FastNative
+ private static native void nCreatePathFromPathData(long outPathPtr, long pathData);
+ @FastNative
+ private static native long nCreateEmptyPathData();
+ @FastNative
+ private static native long nCreatePathData(long nativePtr);
+ @FastNative
+ private static native boolean nInterpolatePathData(long outDataPtr, long fromDataPtr,
+ long toDataPtr, float fraction);
+ @FastNative
+ private static native void nFinalize(long nativePtr);
+ @FastNative
+ private static native boolean nCanMorph(long fromDataPtr, long toDataPtr);
+ @FastNative
+ private static native void nSetPathData(long outDataPtr, long fromDataPtr);
+}
+
+
diff --git a/android/util/PathParser_Delegate.java b/android/util/PathParser_Delegate.java
new file mode 100644
index 0000000..96c093c
--- /dev/null
+++ b/android/util/PathParser_Delegate.java
@@ -0,0 +1,846 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.layoutlib.bridge.Bridge;
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.annotation.NonNull;
+import android.graphics.Path_Delegate;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Delegate that provides implementation for native methods in {@link android.util.PathParser}
+ * <p/>
+ * Through the layoutlib_create tool, selected methods of PathParser have been replaced by calls to
+ * methods of the same name in this delegate class.
+ *
+ * Most of the code has been taken from the implementation in
+ * {@code tools/base/sdk-common/src/main/java/com/android/ide/common/vectordrawable/PathParser.java}
+ * revision be6fe89a3b686db5a75e7e692a148699973957f3
+ */
+public class PathParser_Delegate {
+
+ private static final Logger LOGGER = Logger.getLogger("PathParser");
+
+ // ---- Builder delegate manager ----
+ private static final DelegateManager<PathParser_Delegate> sManager =
+ new DelegateManager<PathParser_Delegate>(PathParser_Delegate.class);
+
+ // ---- delegate data ----
+ @NonNull
+ private PathDataNode[] mPathDataNodes;
+
+ public static PathParser_Delegate getDelegate(long nativePtr) {
+ return sManager.getDelegate(nativePtr);
+ }
+
+ private PathParser_Delegate(@NonNull PathDataNode[] nodes) {
+ mPathDataNodes = nodes;
+ }
+
+ public PathDataNode[] getPathDataNodes() {
+ return mPathDataNodes;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nParseStringForPath(long pathPtr, @NonNull String pathString, int
+ stringLength) {
+ Path_Delegate path_delegate = Path_Delegate.getDelegate(pathPtr);
+ if (path_delegate == null) {
+ return;
+ }
+ assert pathString.length() == stringLength;
+ PathDataNode.nodesToPath(createNodesFromPathData(pathString), path_delegate);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nCreatePathFromPathData(long outPathPtr, long pathData) {
+ Path_Delegate path_delegate = Path_Delegate.getDelegate(outPathPtr);
+ PathParser_Delegate source = sManager.getDelegate(outPathPtr);
+ if (source == null || path_delegate == null) {
+ return;
+ }
+ PathDataNode.nodesToPath(source.mPathDataNodes, path_delegate);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nCreateEmptyPathData() {
+ PathParser_Delegate newDelegate = new PathParser_Delegate(new PathDataNode[0]);
+ return sManager.addNewDelegate(newDelegate);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nCreatePathData(long nativePtr) {
+ PathParser_Delegate source = sManager.getDelegate(nativePtr);
+ if (source == null) {
+ return 0;
+ }
+ PathParser_Delegate dest = new PathParser_Delegate(deepCopyNodes(source.mPathDataNodes));
+ return sManager.addNewDelegate(dest);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static long nCreatePathDataFromString(@NonNull String pathString,
+ int stringLength) {
+ assert pathString.length() == stringLength : "Inconsistent path string length.";
+ PathDataNode[] nodes = createNodesFromPathData(pathString);
+ PathParser_Delegate delegate = new PathParser_Delegate(nodes);
+ return sManager.addNewDelegate(delegate);
+
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nInterpolatePathData(long outDataPtr, long fromDataPtr,
+ long toDataPtr, float fraction) {
+ PathParser_Delegate out = sManager.getDelegate(outDataPtr);
+ PathParser_Delegate from = sManager.getDelegate(fromDataPtr);
+ PathParser_Delegate to = sManager.getDelegate(toDataPtr);
+ if (out == null || from == null || to == null) {
+ return false;
+ }
+ int length = from.mPathDataNodes.length;
+ if (length != to.mPathDataNodes.length) {
+ Bridge.getLog().error(LayoutLog.TAG_BROKEN,
+ "Cannot interpolate path data with different lengths (from " + length + " to " +
+ to.mPathDataNodes.length + ").", null, null);
+ return false;
+ }
+ if (out.mPathDataNodes.length != length) {
+ out.mPathDataNodes = new PathDataNode[length];
+ }
+ for (int i = 0; i < length; i++) {
+ if (out.mPathDataNodes[i] == null) {
+ out.mPathDataNodes[i] = new PathDataNode(from.mPathDataNodes[i]);
+ }
+ out.mPathDataNodes[i].interpolatePathDataNode(from.mPathDataNodes[i],
+ to.mPathDataNodes[i], fraction);
+ }
+ return true;
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nFinalize(long nativePtr) {
+ sManager.removeJavaReferenceFor(nativePtr);
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static boolean nCanMorph(long fromDataPtr, long toDataPtr) {
+ PathParser_Delegate fromPath = PathParser_Delegate.getDelegate(fromDataPtr);
+ PathParser_Delegate toPath = PathParser_Delegate.getDelegate(toDataPtr);
+ if (fromPath == null || toPath == null || fromPath.getPathDataNodes() == null || toPath
+ .getPathDataNodes() == null) {
+ return true;
+ }
+ return PathParser_Delegate.canMorph(fromPath.getPathDataNodes(), toPath.getPathDataNodes());
+ }
+
+ @LayoutlibDelegate
+ /*package*/ static void nSetPathData(long outDataPtr, long fromDataPtr) {
+ PathParser_Delegate out = sManager.getDelegate(outDataPtr);
+ PathParser_Delegate from = sManager.getDelegate(fromDataPtr);
+ if (from == null || out == null) {
+ return;
+ }
+ out.mPathDataNodes = deepCopyNodes(from.mPathDataNodes);
+ }
+
+ /**
+ * @param pathData The string representing a path, the same as "d" string in svg file.
+ *
+ * @return an array of the PathDataNode.
+ */
+ @NonNull
+ public static PathDataNode[] createNodesFromPathData(@NonNull String pathData) {
+ int start = 0;
+ int end = 1;
+
+ ArrayList<PathDataNode> list = new ArrayList<PathDataNode>();
+ while (end < pathData.length()) {
+ end = nextStart(pathData, end);
+ String s = pathData.substring(start, end).trim();
+ if (s.length() > 0) {
+ float[] val = getFloats(s);
+ addNode(list, s.charAt(0), val);
+ }
+
+ start = end;
+ end++;
+ }
+ if ((end - start) == 1 && start < pathData.length()) {
+ addNode(list, pathData.charAt(start), new float[0]);
+ }
+ return list.toArray(new PathDataNode[list.size()]);
+ }
+
+ /**
+ * @param source The array of PathDataNode to be duplicated.
+ *
+ * @return a deep copy of the <code>source</code>.
+ */
+ @NonNull
+ public static PathDataNode[] deepCopyNodes(@NonNull PathDataNode[] source) {
+ PathDataNode[] copy = new PathDataNode[source.length];
+ for (int i = 0; i < source.length; i++) {
+ copy[i] = new PathDataNode(source[i]);
+ }
+ return copy;
+ }
+
+ /**
+ * @param nodesFrom The source path represented in an array of PathDataNode
+ * @param nodesTo The target path represented in an array of PathDataNode
+ * @return whether the <code>nodesFrom</code> can morph into <code>nodesTo</code>
+ */
+ public static boolean canMorph(PathDataNode[] nodesFrom, PathDataNode[] nodesTo) {
+ if (nodesFrom == null || nodesTo == null) {
+ return false;
+ }
+
+ if (nodesFrom.length != nodesTo.length) {
+ return false;
+ }
+
+ for (int i = 0; i < nodesFrom.length; i ++) {
+ if (nodesFrom[i].mType != nodesTo[i].mType
+ || nodesFrom[i].mParams.length != nodesTo[i].mParams.length) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Update the target's data to match the source.
+ * Before calling this, make sure canMorph(target, source) is true.
+ *
+ * @param target The target path represented in an array of PathDataNode
+ * @param source The source path represented in an array of PathDataNode
+ */
+ public static void updateNodes(PathDataNode[] target, PathDataNode[] source) {
+ for (int i = 0; i < source.length; i ++) {
+ target[i].mType = source[i].mType;
+ for (int j = 0; j < source[i].mParams.length; j ++) {
+ target[i].mParams[j] = source[i].mParams[j];
+ }
+ }
+ }
+
+ private static int nextStart(@NonNull String s, int end) {
+ char c;
+
+ while (end < s.length()) {
+ c = s.charAt(end);
+ // Note that 'e' or 'E' are not valid path commands, but could be
+ // used for floating point numbers' scientific notation.
+ // Therefore, when searching for next command, we should ignore 'e'
+ // and 'E'.
+ if ((((c - 'A') * (c - 'Z') <= 0) || ((c - 'a') * (c - 'z') <= 0))
+ && c != 'e' && c != 'E') {
+ return end;
+ }
+ end++;
+ }
+ return end;
+ }
+
+ /**
+ * Calculate the position of the next comma or space or negative sign
+ *
+ * @param s the string to search
+ * @param start the position to start searching
+ * @param result the result of the extraction, including the position of the the starting
+ * position of next number, whether it is ending with a '-'.
+ */
+ private static void extract(@NonNull String s, int start, @NonNull ExtractFloatResult result) {
+ // Now looking for ' ', ',', '.' or '-' from the start.
+ int currentIndex = start;
+ boolean foundSeparator = false;
+ result.mEndWithNegOrDot = false;
+ boolean secondDot = false;
+ boolean isExponential = false;
+ for (; currentIndex < s.length(); currentIndex++) {
+ boolean isPrevExponential = isExponential;
+ isExponential = false;
+ char currentChar = s.charAt(currentIndex);
+ switch (currentChar) {
+ case ' ':
+ case ',':
+ case '\t':
+ case '\n':
+ foundSeparator = true;
+ break;
+ case '-':
+ // The negative sign following a 'e' or 'E' is not a separator.
+ if (currentIndex != start && !isPrevExponential) {
+ foundSeparator = true;
+ result.mEndWithNegOrDot = true;
+ }
+ break;
+ case '.':
+ if (!secondDot) {
+ secondDot = true;
+ } else {
+ // This is the second dot, and it is considered as a separator.
+ foundSeparator = true;
+ result.mEndWithNegOrDot = true;
+ }
+ break;
+ case 'e':
+ case 'E':
+ isExponential = true;
+ break;
+ }
+ if (foundSeparator) {
+ break;
+ }
+ }
+ // When there is nothing found, then we put the end position to the end
+ // of the string.
+ result.mEndPosition = currentIndex;
+ }
+
+ /**
+ * Parse the floats in the string. This is an optimized version of
+ * parseFloat(s.split(",|\\s"));
+ *
+ * @param s the string containing a command and list of floats
+ *
+ * @return array of floats
+ */
+ @NonNull
+ private static float[] getFloats(@NonNull String s) {
+ if (s.charAt(0) == 'z' || s.charAt(0) == 'Z') {
+ return new float[0];
+ }
+ try {
+ float[] results = new float[s.length()];
+ int count = 0;
+ int startPosition = 1;
+ int endPosition;
+
+ ExtractFloatResult result = new ExtractFloatResult();
+ int totalLength = s.length();
+
+ // The startPosition should always be the first character of the
+ // current number, and endPosition is the character after the current
+ // number.
+ while (startPosition < totalLength) {
+ extract(s, startPosition, result);
+ endPosition = result.mEndPosition;
+
+ if (startPosition < endPosition) {
+ results[count++] = Float.parseFloat(
+ s.substring(startPosition, endPosition));
+ }
+
+ if (result.mEndWithNegOrDot) {
+ // Keep the '-' or '.' sign with next number.
+ startPosition = endPosition;
+ } else {
+ startPosition = endPosition + 1;
+ }
+ }
+ return Arrays.copyOf(results, count);
+ } catch (NumberFormatException e) {
+ assert false : "error in parsing \"" + s + "\"" + e;
+ return new float[0];
+ }
+ }
+
+
+ private static void addNode(@NonNull ArrayList<PathDataNode> list, char cmd,
+ @NonNull float[] val) {
+ list.add(new PathDataNode(cmd, val));
+ }
+
+ private static class ExtractFloatResult {
+ // We need to return the position of the next separator and whether the
+ // next float starts with a '-' or a '.'.
+ private int mEndPosition;
+ private boolean mEndWithNegOrDot;
+ }
+
+ /**
+ * Each PathDataNode represents one command in the "d" attribute of the svg file. An array of
+ * PathDataNode can represent the whole "d" attribute.
+ */
+ public static class PathDataNode {
+ private char mType;
+ @NonNull
+ private float[] mParams;
+
+ private PathDataNode(char type, @NonNull float[] params) {
+ mType = type;
+ mParams = params;
+ }
+
+ public char getType() {
+ return mType;
+ }
+
+ @NonNull
+ public float[] getParams() {
+ return mParams;
+ }
+
+ private PathDataNode(@NonNull PathDataNode n) {
+ mType = n.mType;
+ mParams = Arrays.copyOf(n.mParams, n.mParams.length);
+ }
+
+ /**
+ * Convert an array of PathDataNode to Path. Reset the passed path as needed before
+ * calling this method.
+ *
+ * @param node The source array of PathDataNode.
+ * @param path The target Path object.
+ */
+ public static void nodesToPath(@NonNull PathDataNode[] node, @NonNull Path_Delegate path) {
+ float[] current = new float[6];
+ char previousCommand = 'm';
+ //noinspection ForLoopReplaceableByForEach
+ for (int i = 0; i < node.length; i++) {
+ addCommand(path, current, previousCommand, node[i].mType, node[i].mParams);
+ previousCommand = node[i].mType;
+ }
+ }
+
+ /**
+ * The current PathDataNode will be interpolated between the <code>nodeFrom</code> and
+ * <code>nodeTo</code> according to the <code>fraction</code>.
+ *
+ * @param nodeFrom The start value as a PathDataNode.
+ * @param nodeTo The end value as a PathDataNode
+ * @param fraction The fraction to interpolate.
+ */
+ private void interpolatePathDataNode(@NonNull PathDataNode nodeFrom,
+ @NonNull PathDataNode nodeTo, float fraction) {
+ for (int i = 0; i < nodeFrom.mParams.length; i++) {
+ mParams[i] = nodeFrom.mParams[i] * (1 - fraction)
+ + nodeTo.mParams[i] * fraction;
+ }
+ }
+
+ @SuppressWarnings("PointlessArithmeticExpression")
+ private static void addCommand(@NonNull Path_Delegate path, float[] current,
+ char previousCmd, char cmd, @NonNull float[] val) {
+
+ int incr = 2;
+ float currentX = current[0];
+ float currentY = current[1];
+ float ctrlPointX = current[2];
+ float ctrlPointY = current[3];
+ float currentSegmentStartX = current[4];
+ float currentSegmentStartY = current[5];
+ float reflectiveCtrlPointX;
+ float reflectiveCtrlPointY;
+
+ switch (cmd) {
+ case 'z':
+ case 'Z':
+ path.close();
+ // Path is closed here, but we need to move the pen to the
+ // closed position. So we cache the segment's starting position,
+ // and restore it here.
+ currentX = currentSegmentStartX;
+ currentY = currentSegmentStartY;
+ ctrlPointX = currentSegmentStartX;
+ ctrlPointY = currentSegmentStartY;
+ path.moveTo(currentX, currentY);
+ break;
+ case 'm':
+ case 'M':
+ case 'l':
+ case 'L':
+ case 't':
+ case 'T':
+ incr = 2;
+ break;
+ case 'h':
+ case 'H':
+ case 'v':
+ case 'V':
+ incr = 1;
+ break;
+ case 'c':
+ case 'C':
+ incr = 6;
+ break;
+ case 's':
+ case 'S':
+ case 'q':
+ case 'Q':
+ incr = 4;
+ break;
+ case 'a':
+ case 'A':
+ incr = 7;
+ break;
+ }
+
+ for (int k = 0; k < val.length; k += incr) {
+ switch (cmd) {
+ case 'm': // moveto - Start a new sub-path (relative)
+ currentX += val[k + 0];
+ currentY += val[k + 1];
+
+ if (k > 0) {
+ // According to the spec, if a moveto is followed by multiple
+ // pairs of coordinates, the subsequent pairs are treated as
+ // implicit lineto commands.
+ path.rLineTo(val[k + 0], val[k + 1]);
+ } else {
+ path.rMoveTo(val[k + 0], val[k + 1]);
+ currentSegmentStartX = currentX;
+ currentSegmentStartY = currentY;
+ }
+ break;
+ case 'M': // moveto - Start a new sub-path
+ currentX = val[k + 0];
+ currentY = val[k + 1];
+
+ if (k > 0) {
+ // According to the spec, if a moveto is followed by multiple
+ // pairs of coordinates, the subsequent pairs are treated as
+ // implicit lineto commands.
+ path.lineTo(val[k + 0], val[k + 1]);
+ } else {
+ path.moveTo(val[k + 0], val[k + 1]);
+ currentSegmentStartX = currentX;
+ currentSegmentStartY = currentY;
+ }
+ break;
+ case 'l': // lineto - Draw a line from the current point (relative)
+ path.rLineTo(val[k + 0], val[k + 1]);
+ currentX += val[k + 0];
+ currentY += val[k + 1];
+ break;
+ case 'L': // lineto - Draw a line from the current point
+ path.lineTo(val[k + 0], val[k + 1]);
+ currentX = val[k + 0];
+ currentY = val[k + 1];
+ break;
+ case 'h': // horizontal lineto - Draws a horizontal line (relative)
+ path.rLineTo(val[k + 0], 0);
+ currentX += val[k + 0];
+ break;
+ case 'H': // horizontal lineto - Draws a horizontal line
+ path.lineTo(val[k + 0], currentY);
+ currentX = val[k + 0];
+ break;
+ case 'v': // vertical lineto - Draws a vertical line from the current point (r)
+ path.rLineTo(0, val[k + 0]);
+ currentY += val[k + 0];
+ break;
+ case 'V': // vertical lineto - Draws a vertical line from the current point
+ path.lineTo(currentX, val[k + 0]);
+ currentY = val[k + 0];
+ break;
+ case 'c': // curveto - Draws a cubic Bézier curve (relative)
+ path.rCubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3],
+ val[k + 4], val[k + 5]);
+
+ ctrlPointX = currentX + val[k + 2];
+ ctrlPointY = currentY + val[k + 3];
+ currentX += val[k + 4];
+ currentY += val[k + 5];
+
+ break;
+ case 'C': // curveto - Draws a cubic Bézier curve
+ path.cubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3],
+ val[k + 4], val[k + 5]);
+ currentX = val[k + 4];
+ currentY = val[k + 5];
+ ctrlPointX = val[k + 2];
+ ctrlPointY = val[k + 3];
+ break;
+ case 's': // smooth curveto - Draws a cubic Bézier curve (reflective cp)
+ reflectiveCtrlPointX = 0;
+ reflectiveCtrlPointY = 0;
+ if (previousCmd == 'c' || previousCmd == 's'
+ || previousCmd == 'C' || previousCmd == 'S') {
+ reflectiveCtrlPointX = currentX - ctrlPointX;
+ reflectiveCtrlPointY = currentY - ctrlPointY;
+ }
+ path.rCubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
+ val[k + 0], val[k + 1],
+ val[k + 2], val[k + 3]);
+
+ ctrlPointX = currentX + val[k + 0];
+ ctrlPointY = currentY + val[k + 1];
+ currentX += val[k + 2];
+ currentY += val[k + 3];
+ break;
+ case 'S': // shorthand/smooth curveto Draws a cubic Bézier curve(reflective cp)
+ reflectiveCtrlPointX = currentX;
+ reflectiveCtrlPointY = currentY;
+ if (previousCmd == 'c' || previousCmd == 's'
+ || previousCmd == 'C' || previousCmd == 'S') {
+ reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
+ reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
+ }
+ path.cubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
+ val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
+ ctrlPointX = val[k + 0];
+ ctrlPointY = val[k + 1];
+ currentX = val[k + 2];
+ currentY = val[k + 3];
+ break;
+ case 'q': // Draws a quadratic Bézier (relative)
+ path.rQuadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
+ ctrlPointX = currentX + val[k + 0];
+ ctrlPointY = currentY + val[k + 1];
+ currentX += val[k + 2];
+ currentY += val[k + 3];
+ break;
+ case 'Q': // Draws a quadratic Bézier
+ path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
+ ctrlPointX = val[k + 0];
+ ctrlPointY = val[k + 1];
+ currentX = val[k + 2];
+ currentY = val[k + 3];
+ break;
+ case 't': // Draws a quadratic Bézier curve(reflective control point)(relative)
+ reflectiveCtrlPointX = 0;
+ reflectiveCtrlPointY = 0;
+ if (previousCmd == 'q' || previousCmd == 't'
+ || previousCmd == 'Q' || previousCmd == 'T') {
+ reflectiveCtrlPointX = currentX - ctrlPointX;
+ reflectiveCtrlPointY = currentY - ctrlPointY;
+ }
+ path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
+ val[k + 0], val[k + 1]);
+ ctrlPointX = currentX + reflectiveCtrlPointX;
+ ctrlPointY = currentY + reflectiveCtrlPointY;
+ currentX += val[k + 0];
+ currentY += val[k + 1];
+ break;
+ case 'T': // Draws a quadratic Bézier curve (reflective control point)
+ reflectiveCtrlPointX = currentX;
+ reflectiveCtrlPointY = currentY;
+ if (previousCmd == 'q' || previousCmd == 't'
+ || previousCmd == 'Q' || previousCmd == 'T') {
+ reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
+ reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
+ }
+ path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
+ val[k + 0], val[k + 1]);
+ ctrlPointX = reflectiveCtrlPointX;
+ ctrlPointY = reflectiveCtrlPointY;
+ currentX = val[k + 0];
+ currentY = val[k + 1];
+ break;
+ case 'a': // Draws an elliptical arc
+ // (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
+ drawArc(path,
+ currentX,
+ currentY,
+ val[k + 5] + currentX,
+ val[k + 6] + currentY,
+ val[k + 0],
+ val[k + 1],
+ val[k + 2],
+ val[k + 3] != 0,
+ val[k + 4] != 0);
+ currentX += val[k + 5];
+ currentY += val[k + 6];
+ ctrlPointX = currentX;
+ ctrlPointY = currentY;
+ break;
+ case 'A': // Draws an elliptical arc
+ drawArc(path,
+ currentX,
+ currentY,
+ val[k + 5],
+ val[k + 6],
+ val[k + 0],
+ val[k + 1],
+ val[k + 2],
+ val[k + 3] != 0,
+ val[k + 4] != 0);
+ currentX = val[k + 5];
+ currentY = val[k + 6];
+ ctrlPointX = currentX;
+ ctrlPointY = currentY;
+ break;
+ }
+ previousCmd = cmd;
+ }
+ current[0] = currentX;
+ current[1] = currentY;
+ current[2] = ctrlPointX;
+ current[3] = ctrlPointY;
+ current[4] = currentSegmentStartX;
+ current[5] = currentSegmentStartY;
+ }
+
+ private static void drawArc(@NonNull Path_Delegate p, float x0, float y0, float x1,
+ float y1, float a, float b, float theta, boolean isMoreThanHalf,
+ boolean isPositiveArc) {
+
+ LOGGER.log(Level.FINE, "(" + x0 + "," + y0 + ")-(" + x1 + "," + y1
+ + ") {" + a + " " + b + "}");
+ /* Convert rotation angle from degrees to radians */
+ double thetaD = theta * Math.PI / 180.0f;
+ /* Pre-compute rotation matrix entries */
+ double cosTheta = Math.cos(thetaD);
+ double sinTheta = Math.sin(thetaD);
+ /* Transform (x0, y0) and (x1, y1) into unit space */
+ /* using (inverse) rotation, followed by (inverse) scale */
+ double x0p = (x0 * cosTheta + y0 * sinTheta) / a;
+ double y0p = (-x0 * sinTheta + y0 * cosTheta) / b;
+ double x1p = (x1 * cosTheta + y1 * sinTheta) / a;
+ double y1p = (-x1 * sinTheta + y1 * cosTheta) / b;
+ LOGGER.log(Level.FINE, "unit space (" + x0p + "," + y0p + ")-(" + x1p
+ + "," + y1p + ")");
+ /* Compute differences and averages */
+ double dx = x0p - x1p;
+ double dy = y0p - y1p;
+ double xm = (x0p + x1p) / 2;
+ double ym = (y0p + y1p) / 2;
+ /* Solve for intersecting unit circles */
+ double dsq = dx * dx + dy * dy;
+ if (dsq == 0.0) {
+ LOGGER.log(Level.FINE, " Points are coincident");
+ return; /* Points are coincident */
+ }
+ double disc = 1.0 / dsq - 1.0 / 4.0;
+ if (disc < 0.0) {
+ LOGGER.log(Level.FINE, "Points are too far apart " + dsq);
+ float adjust = (float) (Math.sqrt(dsq) / 1.99999);
+ drawArc(p, x0, y0, x1, y1, a * adjust, b * adjust, theta,
+ isMoreThanHalf, isPositiveArc);
+ return; /* Points are too far apart */
+ }
+ double s = Math.sqrt(disc);
+ double sdx = s * dx;
+ double sdy = s * dy;
+ double cx;
+ double cy;
+ if (isMoreThanHalf == isPositiveArc) {
+ cx = xm - sdy;
+ cy = ym + sdx;
+ } else {
+ cx = xm + sdy;
+ cy = ym - sdx;
+ }
+
+ double eta0 = Math.atan2((y0p - cy), (x0p - cx));
+ LOGGER.log(Level.FINE, "eta0 = Math.atan2( " + (y0p - cy) + " , "
+ + (x0p - cx) + ") = " + Math.toDegrees(eta0));
+
+ double eta1 = Math.atan2((y1p - cy), (x1p - cx));
+ LOGGER.log(Level.FINE, "eta1 = Math.atan2( " + (y1p - cy) + " , "
+ + (x1p - cx) + ") = " + Math.toDegrees(eta1));
+ double sweep = (eta1 - eta0);
+ if (isPositiveArc != (sweep >= 0)) {
+ if (sweep > 0) {
+ sweep -= 2 * Math.PI;
+ } else {
+ sweep += 2 * Math.PI;
+ }
+ }
+
+ cx *= a;
+ cy *= b;
+ double tcx = cx;
+ cx = cx * cosTheta - cy * sinTheta;
+ cy = tcx * sinTheta + cy * cosTheta;
+ LOGGER.log(
+ Level.FINE,
+ "cx, cy, a, b, x0, y0, thetaD, eta0, sweep = " + cx + " , "
+ + cy + " , " + a + " , " + b + " , " + x0 + " , " + y0
+ + " , " + Math.toDegrees(thetaD) + " , "
+ + Math.toDegrees(eta0) + " , " + Math.toDegrees(sweep));
+
+ arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep);
+ }
+
+ /**
+ * Converts an arc to cubic Bezier segments and records them in p.
+ *
+ * @param p The target for the cubic Bezier segments
+ * @param cx The x coordinate center of the ellipse
+ * @param cy The y coordinate center of the ellipse
+ * @param a The radius of the ellipse in the horizontal direction
+ * @param b The radius of the ellipse in the vertical direction
+ * @param e1x E(eta1) x coordinate of the starting point of the arc
+ * @param e1y E(eta2) y coordinate of the starting point of the arc
+ * @param theta The angle that the ellipse bounding rectangle makes with the horizontal
+ * plane
+ * @param start The start angle of the arc on the ellipse
+ * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse
+ */
+ private static void arcToBezier(@NonNull Path_Delegate p, double cx, double cy, double a,
+ double b, double e1x, double e1y, double theta, double start,
+ double sweep) {
+ // Taken from equations at:
+ // http://spaceroots.org/documents/ellipse/node8.html
+ // and http://www.spaceroots.org/documents/ellipse/node22.html
+ // Maximum of 45 degrees per cubic Bezier segment
+ int numSegments = (int) Math.ceil(Math.abs(sweep * 4 / Math.PI));
+
+
+ double eta1 = start;
+ double cosTheta = Math.cos(theta);
+ double sinTheta = Math.sin(theta);
+ double cosEta1 = Math.cos(eta1);
+ double sinEta1 = Math.sin(eta1);
+ double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1);
+ double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1);
+
+ double anglePerSegment = sweep / numSegments;
+ for (int i = 0; i < numSegments; i++) {
+ double eta2 = eta1 + anglePerSegment;
+ double sinEta2 = Math.sin(eta2);
+ double cosEta2 = Math.cos(eta2);
+ double e2x = cx + (a * cosTheta * cosEta2)
+ - (b * sinTheta * sinEta2);
+ double e2y = cy + (a * sinTheta * cosEta2)
+ + (b * cosTheta * sinEta2);
+ double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2;
+ double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2;
+ double tanDiff2 = Math.tan((eta2 - eta1) / 2);
+ double alpha = Math.sin(eta2 - eta1)
+ * (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3;
+ double q1x = e1x + alpha * ep1x;
+ double q1y = e1y + alpha * ep1y;
+ double q2x = e2x - alpha * ep2x;
+ double q2y = e2y - alpha * ep2y;
+
+ p.cubicTo((float) q1x,
+ (float) q1y,
+ (float) q2x,
+ (float) q2y,
+ (float) e2x,
+ (float) e2y);
+ eta1 = eta2;
+ e1x = e2x;
+ e1y = e2y;
+ ep1x = ep2x;
+ ep1y = ep2y;
+ }
+ }
+ }
+}
diff --git a/android/util/Patterns.java b/android/util/Patterns.java
new file mode 100644
index 0000000..50cd7b1
--- /dev/null
+++ b/android/util/Patterns.java
@@ -0,0 +1,512 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Commonly used regular expression patterns.
+ */
+public class Patterns {
+ /**
+ * Regular expression to match all IANA top-level domains.
+ * List accurate as of 2011/07/18. List taken from:
+ * http://data.iana.org/TLD/tlds-alpha-by-domain.txt
+ * This pattern is auto-generated by frameworks/ex/common/tools/make-iana-tld-pattern.py
+ *
+ * @deprecated Due to the recent profileration of gTLDs, this API is
+ * expected to become out-of-date very quickly. Therefore it is now
+ * deprecated.
+ */
+ @Deprecated
+ public static final String TOP_LEVEL_DOMAIN_STR =
+ "((aero|arpa|asia|a[cdefgilmnoqrstuwxz])"
+ + "|(biz|b[abdefghijmnorstvwyz])"
+ + "|(cat|com|coop|c[acdfghiklmnoruvxyz])"
+ + "|d[ejkmoz]"
+ + "|(edu|e[cegrstu])"
+ + "|f[ijkmor]"
+ + "|(gov|g[abdefghilmnpqrstuwy])"
+ + "|h[kmnrtu]"
+ + "|(info|int|i[delmnoqrst])"
+ + "|(jobs|j[emop])"
+ + "|k[eghimnprwyz]"
+ + "|l[abcikrstuvy]"
+ + "|(mil|mobi|museum|m[acdeghklmnopqrstuvwxyz])"
+ + "|(name|net|n[acefgilopruz])"
+ + "|(org|om)"
+ + "|(pro|p[aefghklmnrstwy])"
+ + "|qa"
+ + "|r[eosuw]"
+ + "|s[abcdeghijklmnortuvyz]"
+ + "|(tel|travel|t[cdfghjklmnoprtvwz])"
+ + "|u[agksyz]"
+ + "|v[aceginu]"
+ + "|w[fs]"
+ + "|(\u03b4\u03bf\u03ba\u03b9\u03bc\u03ae|\u0438\u0441\u043f\u044b\u0442\u0430\u043d\u0438\u0435|\u0440\u0444|\u0441\u0440\u0431|\u05d8\u05e2\u05e1\u05d8|\u0622\u0632\u0645\u0627\u06cc\u0634\u06cc|\u0625\u062e\u062a\u0628\u0627\u0631|\u0627\u0644\u0627\u0631\u062f\u0646|\u0627\u0644\u062c\u0632\u0627\u0626\u0631|\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629|\u0627\u0644\u0645\u063a\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062a|\u0628\u06be\u0627\u0631\u062a|\u062a\u0648\u0646\u0633|\u0633\u0648\u0631\u064a\u0629|\u0641\u0644\u0633\u0637\u064a\u0646|\u0642\u0637\u0631|\u0645\u0635\u0631|\u092a\u0930\u0940\u0915\u094d\u0937\u093e|\u092d\u093e\u0930\u0924|\u09ad\u09be\u09b0\u09a4|\u0a2d\u0a3e\u0a30\u0a24|\u0aad\u0abe\u0ab0\u0aa4|\u0b87\u0ba8\u0bcd\u0ba4\u0bbf\u0baf\u0bbe|\u0b87\u0bb2\u0b99\u0bcd\u0b95\u0bc8|\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc2\u0bb0\u0bcd|\u0baa\u0bb0\u0bbf\u0b9f\u0bcd\u0b9a\u0bc8|\u0c2d\u0c3e\u0c30\u0c24\u0c4d|\u0dbd\u0d82\u0d9a\u0dcf|\u0e44\u0e17\u0e22|\u30c6\u30b9\u30c8|\u4e2d\u56fd|\u4e2d\u570b|\u53f0\u6e7e|\u53f0\u7063|\u65b0\u52a0\u5761|\u6d4b\u8bd5|\u6e2c\u8a66|\u9999\u6e2f|\ud14c\uc2a4\ud2b8|\ud55c\uad6d|xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-3e0b707e|xn\\-\\-45brj9c|xn\\-\\-80akhbyknj4f|xn\\-\\-90a3ac|xn\\-\\-9t4b11yi5a|xn\\-\\-clchc0ea0b2g2a9gcd|xn\\-\\-deba0ad|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s|xn\\-\\-fpcrj9c3d|xn\\-\\-fzc2c9e2c|xn\\-\\-g6w251d|xn\\-\\-gecrj9c|xn\\-\\-h2brj9c|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-j6w193g|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-kprw13d|xn\\-\\-kpry57d|xn\\-\\-lgbbat1ad8j|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbayh7gpa|xn\\-\\-mgbbh1a71e|xn\\-\\-mgbc0a9azcg|xn\\-\\-mgberp4a5d4ar|xn\\-\\-o3cw4h|xn\\-\\-ogbpf8fl|xn\\-\\-p1ai|xn\\-\\-pgbs0dh|xn\\-\\-s9brj9c|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a|xn\\-\\-xkc2al3hye2a|xn\\-\\-xkc2dl3a5ee0h|xn\\-\\-yfro4i67o|xn\\-\\-ygbi2ammx|xn\\-\\-zckzah|xxx)"
+ + "|y[et]"
+ + "|z[amw])";
+
+ /**
+ * Regular expression pattern to match all IANA top-level domains.
+ * @deprecated This API is deprecated. See {@link #TOP_LEVEL_DOMAIN_STR}.
+ */
+ @Deprecated
+ public static final Pattern TOP_LEVEL_DOMAIN =
+ Pattern.compile(TOP_LEVEL_DOMAIN_STR);
+
+ /**
+ * Regular expression to match all IANA top-level domains for WEB_URL.
+ * List accurate as of 2011/07/18. List taken from:
+ * http://data.iana.org/TLD/tlds-alpha-by-domain.txt
+ * This pattern is auto-generated by frameworks/ex/common/tools/make-iana-tld-pattern.py
+ *
+ * @deprecated This API is deprecated. See {@link #TOP_LEVEL_DOMAIN_STR}.
+ */
+ @Deprecated
+ public static final String TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL =
+ "(?:"
+ + "(?:aero|arpa|asia|a[cdefgilmnoqrstuwxz])"
+ + "|(?:biz|b[abdefghijmnorstvwyz])"
+ + "|(?:cat|com|coop|c[acdfghiklmnoruvxyz])"
+ + "|d[ejkmoz]"
+ + "|(?:edu|e[cegrstu])"
+ + "|f[ijkmor]"
+ + "|(?:gov|g[abdefghilmnpqrstuwy])"
+ + "|h[kmnrtu]"
+ + "|(?:info|int|i[delmnoqrst])"
+ + "|(?:jobs|j[emop])"
+ + "|k[eghimnprwyz]"
+ + "|l[abcikrstuvy]"
+ + "|(?:mil|mobi|museum|m[acdeghklmnopqrstuvwxyz])"
+ + "|(?:name|net|n[acefgilopruz])"
+ + "|(?:org|om)"
+ + "|(?:pro|p[aefghklmnrstwy])"
+ + "|qa"
+ + "|r[eosuw]"
+ + "|s[abcdeghijklmnortuvyz]"
+ + "|(?:tel|travel|t[cdfghjklmnoprtvwz])"
+ + "|u[agksyz]"
+ + "|v[aceginu]"
+ + "|w[fs]"
+ + "|(?:\u03b4\u03bf\u03ba\u03b9\u03bc\u03ae|\u0438\u0441\u043f\u044b\u0442\u0430\u043d\u0438\u0435|\u0440\u0444|\u0441\u0440\u0431|\u05d8\u05e2\u05e1\u05d8|\u0622\u0632\u0645\u0627\u06cc\u0634\u06cc|\u0625\u062e\u062a\u0628\u0627\u0631|\u0627\u0644\u0627\u0631\u062f\u0646|\u0627\u0644\u062c\u0632\u0627\u0626\u0631|\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629|\u0627\u0644\u0645\u063a\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062a|\u0628\u06be\u0627\u0631\u062a|\u062a\u0648\u0646\u0633|\u0633\u0648\u0631\u064a\u0629|\u0641\u0644\u0633\u0637\u064a\u0646|\u0642\u0637\u0631|\u0645\u0635\u0631|\u092a\u0930\u0940\u0915\u094d\u0937\u093e|\u092d\u093e\u0930\u0924|\u09ad\u09be\u09b0\u09a4|\u0a2d\u0a3e\u0a30\u0a24|\u0aad\u0abe\u0ab0\u0aa4|\u0b87\u0ba8\u0bcd\u0ba4\u0bbf\u0baf\u0bbe|\u0b87\u0bb2\u0b99\u0bcd\u0b95\u0bc8|\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc2\u0bb0\u0bcd|\u0baa\u0bb0\u0bbf\u0b9f\u0bcd\u0b9a\u0bc8|\u0c2d\u0c3e\u0c30\u0c24\u0c4d|\u0dbd\u0d82\u0d9a\u0dcf|\u0e44\u0e17\u0e22|\u30c6\u30b9\u30c8|\u4e2d\u56fd|\u4e2d\u570b|\u53f0\u6e7e|\u53f0\u7063|\u65b0\u52a0\u5761|\u6d4b\u8bd5|\u6e2c\u8a66|\u9999\u6e2f|\ud14c\uc2a4\ud2b8|\ud55c\uad6d|xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-3e0b707e|xn\\-\\-45brj9c|xn\\-\\-80akhbyknj4f|xn\\-\\-90a3ac|xn\\-\\-9t4b11yi5a|xn\\-\\-clchc0ea0b2g2a9gcd|xn\\-\\-deba0ad|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s|xn\\-\\-fpcrj9c3d|xn\\-\\-fzc2c9e2c|xn\\-\\-g6w251d|xn\\-\\-gecrj9c|xn\\-\\-h2brj9c|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-j6w193g|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-kprw13d|xn\\-\\-kpry57d|xn\\-\\-lgbbat1ad8j|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbayh7gpa|xn\\-\\-mgbbh1a71e|xn\\-\\-mgbc0a9azcg|xn\\-\\-mgberp4a5d4ar|xn\\-\\-o3cw4h|xn\\-\\-ogbpf8fl|xn\\-\\-p1ai|xn\\-\\-pgbs0dh|xn\\-\\-s9brj9c|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a|xn\\-\\-xkc2al3hye2a|xn\\-\\-xkc2dl3a5ee0h|xn\\-\\-yfro4i67o|xn\\-\\-ygbi2ammx|xn\\-\\-zckzah|xxx)"
+ + "|y[et]"
+ + "|z[amw]))";
+
+ /**
+ * Regular expression to match all IANA top-level domains.
+ *
+ * List accurate as of 2015/11/24. List taken from:
+ * http://data.iana.org/TLD/tlds-alpha-by-domain.txt
+ * This pattern is auto-generated by frameworks/ex/common/tools/make-iana-tld-pattern.py
+ *
+ * @hide
+ */
+ static final String IANA_TOP_LEVEL_DOMAINS =
+ "(?:"
+ + "(?:aaa|aarp|abb|abbott|abogado|academy|accenture|accountant|accountants|aco|active"
+ + "|actor|ads|adult|aeg|aero|afl|agency|aig|airforce|airtel|allfinanz|alsace|amica|amsterdam"
+ + "|android|apartments|app|apple|aquarelle|aramco|archi|army|arpa|arte|asia|associates"
+ + "|attorney|auction|audio|auto|autos|axa|azure|a[cdefgilmoqrstuwxz])"
+ + "|(?:band|bank|bar|barcelona|barclaycard|barclays|bargains|bauhaus|bayern|bbc|bbva"
+ + "|bcn|beats|beer|bentley|berlin|best|bet|bharti|bible|bid|bike|bing|bingo|bio|biz|black"
+ + "|blackfriday|bloomberg|blue|bms|bmw|bnl|bnpparibas|boats|bom|bond|boo|boots|boutique"
+ + "|bradesco|bridgestone|broadway|broker|brother|brussels|budapest|build|builders|business"
+ + "|buzz|bzh|b[abdefghijmnorstvwyz])"
+ + "|(?:cab|cafe|cal|camera|camp|cancerresearch|canon|capetown|capital|car|caravan|cards"
+ + "|care|career|careers|cars|cartier|casa|cash|casino|cat|catering|cba|cbn|ceb|center|ceo"
+ + "|cern|cfa|cfd|chanel|channel|chat|cheap|chloe|christmas|chrome|church|cipriani|cisco"
+ + "|citic|city|cityeats|claims|cleaning|click|clinic|clothing|cloud|club|clubmed|coach"
+ + "|codes|coffee|college|cologne|com|commbank|community|company|computer|comsec|condos"
+ + "|construction|consulting|contractors|cooking|cool|coop|corsica|country|coupons|courses"
+ + "|credit|creditcard|creditunion|cricket|crown|crs|cruises|csc|cuisinella|cymru|cyou|c[acdfghiklmnoruvwxyz])"
+ + "|(?:dabur|dad|dance|date|dating|datsun|day|dclk|deals|degree|delivery|dell|delta"
+ + "|democrat|dental|dentist|desi|design|dev|diamonds|diet|digital|direct|directory|discount"
+ + "|dnp|docs|dog|doha|domains|doosan|download|drive|durban|dvag|d[ejkmoz])"
+ + "|(?:earth|eat|edu|education|email|emerck|energy|engineer|engineering|enterprises"
+ + "|epson|equipment|erni|esq|estate|eurovision|eus|events|everbank|exchange|expert|exposed"
+ + "|express|e[cegrstu])"
+ + "|(?:fage|fail|fairwinds|faith|family|fan|fans|farm|fashion|feedback|ferrero|film"
+ + "|final|finance|financial|firmdale|fish|fishing|fit|fitness|flights|florist|flowers|flsmidth"
+ + "|fly|foo|football|forex|forsale|forum|foundation|frl|frogans|fund|furniture|futbol|fyi"
+ + "|f[ijkmor])"
+ + "|(?:gal|gallery|game|garden|gbiz|gdn|gea|gent|genting|ggee|gift|gifts|gives|giving"
+ + "|glass|gle|global|globo|gmail|gmo|gmx|gold|goldpoint|golf|goo|goog|google|gop|gov|grainger"
+ + "|graphics|gratis|green|gripe|group|gucci|guge|guide|guitars|guru|g[abdefghilmnpqrstuwy])"
+ + "|(?:hamburg|hangout|haus|healthcare|help|here|hermes|hiphop|hitachi|hiv|hockey|holdings"
+ + "|holiday|homedepot|homes|honda|horse|host|hosting|hoteles|hotmail|house|how|hsbc|hyundai"
+ + "|h[kmnrtu])"
+ + "|(?:ibm|icbc|ice|icu|ifm|iinet|immo|immobilien|industries|infiniti|info|ing|ink|institute"
+ + "|insure|int|international|investments|ipiranga|irish|ist|istanbul|itau|iwc|i[delmnoqrst])"
+ + "|(?:jaguar|java|jcb|jetzt|jewelry|jlc|jll|jobs|joburg|jprs|juegos|j[emop])"
+ + "|(?:kaufen|kddi|kia|kim|kinder|kitchen|kiwi|koeln|komatsu|krd|kred|kyoto|k[eghimnprwyz])"
+ + "|(?:lacaixa|lancaster|land|landrover|lasalle|lat|latrobe|law|lawyer|lds|lease|leclerc"
+ + "|legal|lexus|lgbt|liaison|lidl|life|lifestyle|lighting|limited|limo|linde|link|live"
+ + "|lixil|loan|loans|lol|london|lotte|lotto|love|ltd|ltda|lupin|luxe|luxury|l[abcikrstuvy])"
+ + "|(?:madrid|maif|maison|man|management|mango|market|marketing|markets|marriott|mba"
+ + "|media|meet|melbourne|meme|memorial|men|menu|meo|miami|microsoft|mil|mini|mma|mobi|moda"
+ + "|moe|moi|mom|monash|money|montblanc|mormon|mortgage|moscow|motorcycles|mov|movie|movistar"
+ + "|mtn|mtpc|mtr|museum|mutuelle|m[acdeghklmnopqrstuvwxyz])"
+ + "|(?:nadex|nagoya|name|navy|nec|net|netbank|network|neustar|new|news|nexus|ngo|nhk"
+ + "|nico|ninja|nissan|nokia|nra|nrw|ntt|nyc|n[acefgilopruz])"
+ + "|(?:obi|office|okinawa|omega|one|ong|onl|online|ooo|oracle|orange|org|organic|osaka"
+ + "|otsuka|ovh|om)"
+ + "|(?:page|panerai|paris|partners|parts|party|pet|pharmacy|philips|photo|photography"
+ + "|photos|physio|piaget|pics|pictet|pictures|ping|pink|pizza|place|play|playstation|plumbing"
+ + "|plus|pohl|poker|porn|post|praxi|press|pro|prod|productions|prof|properties|property"
+ + "|protection|pub|p[aefghklmnrstwy])"
+ + "|(?:qpon|quebec|qa)"
+ + "|(?:racing|realtor|realty|recipes|red|redstone|rehab|reise|reisen|reit|ren|rent|rentals"
+ + "|repair|report|republican|rest|restaurant|review|reviews|rich|ricoh|rio|rip|rocher|rocks"
+ + "|rodeo|rsvp|ruhr|run|rwe|ryukyu|r[eosuw])"
+ + "|(?:saarland|sakura|sale|samsung|sandvik|sandvikcoromant|sanofi|sap|sapo|sarl|saxo"
+ + "|sbs|sca|scb|schmidt|scholarships|school|schule|schwarz|science|scor|scot|seat|security"
+ + "|seek|sener|services|seven|sew|sex|sexy|shiksha|shoes|show|shriram|singles|site|ski"
+ + "|sky|skype|sncf|soccer|social|software|sohu|solar|solutions|sony|soy|space|spiegel|spreadbetting"
+ + "|srl|stada|starhub|statoil|stc|stcgroup|stockholm|studio|study|style|sucks|supplies"
+ + "|supply|support|surf|surgery|suzuki|swatch|swiss|sydney|systems|s[abcdeghijklmnortuvxyz])"
+ + "|(?:tab|taipei|tatamotors|tatar|tattoo|tax|taxi|team|tech|technology|tel|telefonica"
+ + "|temasek|tennis|thd|theater|theatre|tickets|tienda|tips|tires|tirol|today|tokyo|tools"
+ + "|top|toray|toshiba|tours|town|toyota|toys|trade|trading|training|travel|trust|tui|t[cdfghjklmnortvwz])"
+ + "|(?:ubs|university|uno|uol|u[agksyz])"
+ + "|(?:vacations|vana|vegas|ventures|versicherung|vet|viajes|video|villas|vin|virgin"
+ + "|vision|vista|vistaprint|viva|vlaanderen|vodka|vote|voting|voto|voyage|v[aceginu])"
+ + "|(?:wales|walter|wang|watch|webcam|website|wed|wedding|weir|whoswho|wien|wiki|williamhill"
+ + "|win|windows|wine|wme|work|works|world|wtc|wtf|w[fs])"
+ + "|(?:\u03b5\u03bb|\u0431\u0435\u043b|\u0434\u0435\u0442\u0438|\u043a\u043e\u043c|\u043c\u043a\u0434"
+ + "|\u043c\u043e\u043d|\u043c\u043e\u0441\u043a\u0432\u0430|\u043e\u043d\u043b\u0430\u0439\u043d"
+ + "|\u043e\u0440\u0433|\u0440\u0443\u0441|\u0440\u0444|\u0441\u0430\u0439\u0442|\u0441\u0440\u0431"
+ + "|\u0443\u043a\u0440|\u049b\u0430\u0437|\u0570\u0561\u0575|\u05e7\u05d5\u05dd|\u0627\u0631\u0627\u0645\u0643\u0648"
+ + "|\u0627\u0644\u0627\u0631\u062f\u0646|\u0627\u0644\u062c\u0632\u0627\u0626\u0631|\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629"
+ + "|\u0627\u0644\u0645\u063a\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062a|\u0627\u06cc\u0631\u0627\u0646"
+ + "|\u0628\u0627\u0632\u0627\u0631|\u0628\u06be\u0627\u0631\u062a|\u062a\u0648\u0646\u0633"
+ + "|\u0633\u0648\u062f\u0627\u0646|\u0633\u0648\u0631\u064a\u0629|\u0634\u0628\u0643\u0629"
+ + "|\u0639\u0631\u0627\u0642|\u0639\u0645\u0627\u0646|\u0641\u0644\u0633\u0637\u064a\u0646"
+ + "|\u0642\u0637\u0631|\u0643\u0648\u0645|\u0645\u0635\u0631|\u0645\u0644\u064a\u0633\u064a\u0627"
+ + "|\u0645\u0648\u0642\u0639|\u0915\u0949\u092e|\u0928\u0947\u091f|\u092d\u093e\u0930\u0924"
+ + "|\u0938\u0902\u0917\u0920\u0928|\u09ad\u09be\u09b0\u09a4|\u0a2d\u0a3e\u0a30\u0a24|\u0aad\u0abe\u0ab0\u0aa4"
+ + "|\u0b87\u0ba8\u0bcd\u0ba4\u0bbf\u0baf\u0bbe|\u0b87\u0bb2\u0b99\u0bcd\u0b95\u0bc8|\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc2\u0bb0\u0bcd"
+ + "|\u0c2d\u0c3e\u0c30\u0c24\u0c4d|\u0dbd\u0d82\u0d9a\u0dcf|\u0e04\u0e2d\u0e21|\u0e44\u0e17\u0e22"
+ + "|\u10d2\u10d4|\u307f\u3093\u306a|\u30b0\u30fc\u30b0\u30eb|\u30b3\u30e0|\u4e16\u754c"
+ + "|\u4e2d\u4fe1|\u4e2d\u56fd|\u4e2d\u570b|\u4e2d\u6587\u7f51|\u4f01\u4e1a|\u4f5b\u5c71"
+ + "|\u4fe1\u606f|\u5065\u5eb7|\u516b\u5366|\u516c\u53f8|\u516c\u76ca|\u53f0\u6e7e|\u53f0\u7063"
+ + "|\u5546\u57ce|\u5546\u5e97|\u5546\u6807|\u5728\u7ebf|\u5927\u62ff|\u5a31\u4e50|\u5de5\u884c"
+ + "|\u5e7f\u4e1c|\u6148\u5584|\u6211\u7231\u4f60|\u624b\u673a|\u653f\u52a1|\u653f\u5e9c"
+ + "|\u65b0\u52a0\u5761|\u65b0\u95fb|\u65f6\u5c1a|\u673a\u6784|\u6de1\u9a6c\u9521|\u6e38\u620f"
+ + "|\u70b9\u770b|\u79fb\u52a8|\u7ec4\u7ec7\u673a\u6784|\u7f51\u5740|\u7f51\u5e97|\u7f51\u7edc"
+ + "|\u8c37\u6b4c|\u96c6\u56e2|\u98de\u5229\u6d66|\u9910\u5385|\u9999\u6e2f|\ub2f7\ub137"
+ + "|\ub2f7\ucef4|\uc0bc\uc131|\ud55c\uad6d|xbox"
+ + "|xerox|xin|xn\\-\\-11b4c3d|xn\\-\\-1qqw23a|xn\\-\\-30rr7y|xn\\-\\-3bst00m|xn\\-\\-3ds443g"
+ + "|xn\\-\\-3e0b707e|xn\\-\\-3pxu8k|xn\\-\\-42c2d9a|xn\\-\\-45brj9c|xn\\-\\-45q11c|xn\\-\\-4gbrim"
+ + "|xn\\-\\-55qw42g|xn\\-\\-55qx5d|xn\\-\\-6frz82g|xn\\-\\-6qq986b3xl|xn\\-\\-80adxhks"
+ + "|xn\\-\\-80ao21a|xn\\-\\-80asehdb|xn\\-\\-80aswg|xn\\-\\-90a3ac|xn\\-\\-90ais|xn\\-\\-9dbq2a"
+ + "|xn\\-\\-9et52u|xn\\-\\-b4w605ferd|xn\\-\\-c1avg|xn\\-\\-c2br7g|xn\\-\\-cg4bki|xn\\-\\-clchc0ea0b2g2a9gcd"
+ + "|xn\\-\\-czr694b|xn\\-\\-czrs0t|xn\\-\\-czru2d|xn\\-\\-d1acj3b|xn\\-\\-d1alf|xn\\-\\-efvy88h"
+ + "|xn\\-\\-estv75g|xn\\-\\-fhbei|xn\\-\\-fiq228c5hs|xn\\-\\-fiq64b|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s"
+ + "|xn\\-\\-fjq720a|xn\\-\\-flw351e|xn\\-\\-fpcrj9c3d|xn\\-\\-fzc2c9e2c|xn\\-\\-gecrj9c"
+ + "|xn\\-\\-h2brj9c|xn\\-\\-hxt814e|xn\\-\\-i1b6b1a6a2e|xn\\-\\-imr513n|xn\\-\\-io0a7i"
+ + "|xn\\-\\-j1aef|xn\\-\\-j1amh|xn\\-\\-j6w193g|xn\\-\\-kcrx77d1x4a|xn\\-\\-kprw13d|xn\\-\\-kpry57d"
+ + "|xn\\-\\-kput3i|xn\\-\\-l1acc|xn\\-\\-lgbbat1ad8j|xn\\-\\-mgb9awbf|xn\\-\\-mgba3a3ejt"
+ + "|xn\\-\\-mgba3a4f16a|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbab2bd|xn\\-\\-mgbayh7gpa|xn\\-\\-mgbbh1a71e"
+ + "|xn\\-\\-mgbc0a9azcg|xn\\-\\-mgberp4a5d4ar|xn\\-\\-mgbpl2fh|xn\\-\\-mgbtx2b|xn\\-\\-mgbx4cd0ab"
+ + "|xn\\-\\-mk1bu44c|xn\\-\\-mxtq1m|xn\\-\\-ngbc5azd|xn\\-\\-node|xn\\-\\-nqv7f|xn\\-\\-nqv7fs00ema"
+ + "|xn\\-\\-nyqy26a|xn\\-\\-o3cw4h|xn\\-\\-ogbpf8fl|xn\\-\\-p1acf|xn\\-\\-p1ai|xn\\-\\-pgbs0dh"
+ + "|xn\\-\\-pssy2u|xn\\-\\-q9jyb4c|xn\\-\\-qcka1pmc|xn\\-\\-qxam|xn\\-\\-rhqv96g|xn\\-\\-s9brj9c"
+ + "|xn\\-\\-ses554g|xn\\-\\-t60b56a|xn\\-\\-tckwe|xn\\-\\-unup4y|xn\\-\\-vermgensberater\\-ctb"
+ + "|xn\\-\\-vermgensberatung\\-pwb|xn\\-\\-vhquv|xn\\-\\-vuq861b|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a"
+ + "|xn\\-\\-xhq521b|xn\\-\\-xkc2al3hye2a|xn\\-\\-xkc2dl3a5ee0h|xn\\-\\-y9a3aq|xn\\-\\-yfro4i67o"
+ + "|xn\\-\\-ygbi2ammx|xn\\-\\-zfr164b|xperia|xxx|xyz)"
+ + "|(?:yachts|yamaxun|yandex|yodobashi|yoga|yokohama|youtube|y[et])"
+ + "|(?:zara|zip|zone|zuerich|z[amw]))";
+
+ /**
+ * Kept for backward compatibility reasons.
+ *
+ * @deprecated Deprecated since it does not include all IRI characters defined in RFC 3987
+ */
+ @Deprecated
+ public static final String GOOD_IRI_CHAR =
+ "a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF";
+
+ private static final String IP_ADDRESS_STRING =
+ "((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]"
+ + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]"
+ + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}"
+ + "|[1-9][0-9]|[0-9]))";
+ public static final Pattern IP_ADDRESS = Pattern.compile(IP_ADDRESS_STRING);
+
+ /**
+ * Valid UCS characters defined in RFC 3987. Excludes space characters.
+ */
+ private static final String UCS_CHAR = "[" +
+ "\u00A0-\uD7FF" +
+ "\uF900-\uFDCF" +
+ "\uFDF0-\uFFEF" +
+ "\uD800\uDC00-\uD83F\uDFFD" +
+ "\uD840\uDC00-\uD87F\uDFFD" +
+ "\uD880\uDC00-\uD8BF\uDFFD" +
+ "\uD8C0\uDC00-\uD8FF\uDFFD" +
+ "\uD900\uDC00-\uD93F\uDFFD" +
+ "\uD940\uDC00-\uD97F\uDFFD" +
+ "\uD980\uDC00-\uD9BF\uDFFD" +
+ "\uD9C0\uDC00-\uD9FF\uDFFD" +
+ "\uDA00\uDC00-\uDA3F\uDFFD" +
+ "\uDA40\uDC00-\uDA7F\uDFFD" +
+ "\uDA80\uDC00-\uDABF\uDFFD" +
+ "\uDAC0\uDC00-\uDAFF\uDFFD" +
+ "\uDB00\uDC00-\uDB3F\uDFFD" +
+ "\uDB44\uDC00-\uDB7F\uDFFD" +
+ "&&[^\u00A0[\u2000-\u200A]\u2028\u2029\u202F\u3000]]";
+
+ /**
+ * Valid characters for IRI label defined in RFC 3987.
+ */
+ private static final String LABEL_CHAR = "a-zA-Z0-9" + UCS_CHAR;
+
+ /**
+ * Valid characters for IRI TLD defined in RFC 3987.
+ */
+ private static final String TLD_CHAR = "a-zA-Z" + UCS_CHAR;
+
+ /**
+ * RFC 1035 Section 2.3.4 limits the labels to a maximum 63 octets.
+ */
+ private static final String IRI_LABEL =
+ "[" + LABEL_CHAR + "](?:[" + LABEL_CHAR + "_\\-]{0,61}[" + LABEL_CHAR + "]){0,1}";
+
+ /**
+ * RFC 3492 references RFC 1034 and limits Punycode algorithm output to 63 characters.
+ */
+ private static final String PUNYCODE_TLD = "xn\\-\\-[\\w\\-]{0,58}\\w";
+
+ private static final String TLD = "(" + PUNYCODE_TLD + "|" + "[" + TLD_CHAR + "]{2,63}" +")";
+
+ private static final String HOST_NAME = "(" + IRI_LABEL + "\\.)+" + TLD;
+
+ private static final String DOMAIN_NAME_STR = "(" + HOST_NAME + "|" + IP_ADDRESS_STRING + ")";
+ public static final Pattern DOMAIN_NAME = Pattern.compile(DOMAIN_NAME_STR);
+
+ private static final String PROTOCOL = "(?i:http|https|rtsp)://";
+
+ /* A word boundary or end of input. This is to stop foo.sure from matching as foo.su */
+ private static final String WORD_BOUNDARY = "(?:\\b|$|^)";
+
+ private static final String USER_INFO = "(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)"
+ + "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_"
+ + "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@";
+
+ private static final String PORT_NUMBER = "\\:\\d{1,5}";
+
+ private static final String PATH_AND_QUERY = "[/\\?](?:(?:[" + LABEL_CHAR
+ + ";/\\?:@&=#~" // plus optional query params
+ + "\\-\\.\\+!\\*'\\(\\),_\\$])|(?:%[a-fA-F0-9]{2}))*";
+
+ /**
+ * Regular expression pattern to match most part of RFC 3987
+ * Internationalized URLs, aka IRIs.
+ */
+ public static final Pattern WEB_URL = Pattern.compile("("
+ + "("
+ + "(?:" + PROTOCOL + "(?:" + USER_INFO + ")?" + ")?"
+ + "(?:" + DOMAIN_NAME_STR + ")"
+ + "(?:" + PORT_NUMBER + ")?"
+ + ")"
+ + "(" + PATH_AND_QUERY + ")?"
+ + WORD_BOUNDARY
+ + ")");
+
+ /**
+ * Regular expression that matches known TLDs and punycode TLDs
+ */
+ private static final String STRICT_TLD = "(?:" +
+ IANA_TOP_LEVEL_DOMAINS + "|" + PUNYCODE_TLD + ")";
+
+ /**
+ * Regular expression that matches host names using {@link #STRICT_TLD}
+ */
+ private static final String STRICT_HOST_NAME = "(?:(?:" + IRI_LABEL + "\\.)+"
+ + STRICT_TLD + ")";
+
+ /**
+ * Regular expression that matches domain names using either {@link #STRICT_HOST_NAME} or
+ * {@link #IP_ADDRESS}
+ */
+ private static final String STRICT_DOMAIN_NAME = "(?:" + STRICT_HOST_NAME + "|"
+ + IP_ADDRESS_STRING + ")";
+
+ /**
+ * Regular expression that matches domain names without a TLD
+ */
+ private static final String RELAXED_DOMAIN_NAME =
+ "(?:" + "(?:" + IRI_LABEL + "(?:\\.(?=\\S))" +"?)+" + "|" + IP_ADDRESS_STRING + ")";
+
+ /**
+ * Regular expression to match strings that do not start with a supported protocol. The TLDs
+ * are expected to be one of the known TLDs.
+ */
+ private static final String WEB_URL_WITHOUT_PROTOCOL = "("
+ + WORD_BOUNDARY
+ + "(?<!:\\/\\/)"
+ + "("
+ + "(?:" + STRICT_DOMAIN_NAME + ")"
+ + "(?:" + PORT_NUMBER + ")?"
+ + ")"
+ + "(?:" + PATH_AND_QUERY + ")?"
+ + WORD_BOUNDARY
+ + ")";
+
+ /**
+ * Regular expression to match strings that start with a supported protocol. Rules for domain
+ * names and TLDs are more relaxed. TLDs are optional.
+ */
+ private static final String WEB_URL_WITH_PROTOCOL = "("
+ + WORD_BOUNDARY
+ + "(?:"
+ + "(?:" + PROTOCOL + "(?:" + USER_INFO + ")?" + ")"
+ + "(?:" + RELAXED_DOMAIN_NAME + ")?"
+ + "(?:" + PORT_NUMBER + ")?"
+ + ")"
+ + "(?:" + PATH_AND_QUERY + ")?"
+ + WORD_BOUNDARY
+ + ")";
+
+ /**
+ * Regular expression pattern to match IRIs. If a string starts with http(s):// the expression
+ * tries to match the URL structure with a relaxed rule for TLDs. If the string does not start
+ * with http(s):// the TLDs are expected to be one of the known TLDs.
+ *
+ * @hide
+ */
+ public static final Pattern AUTOLINK_WEB_URL = Pattern.compile(
+ "(" + WEB_URL_WITH_PROTOCOL + "|" + WEB_URL_WITHOUT_PROTOCOL + ")");
+
+ /**
+ * Regular expression for valid email characters. Does not include some of the valid characters
+ * defined in RFC5321: #&~!^`{}/=$*?|
+ */
+ private static final String EMAIL_CHAR = LABEL_CHAR + "\\+\\-_%'";
+
+ /**
+ * Regular expression for local part of an email address. RFC5321 section 4.5.3.1.1 limits
+ * the local part to be at most 64 octets.
+ */
+ private static final String EMAIL_ADDRESS_LOCAL_PART =
+ "[" + EMAIL_CHAR + "]" + "(?:[" + EMAIL_CHAR + "\\.]{0,62}[" + EMAIL_CHAR + "])?";
+
+ /**
+ * Regular expression for the domain part of an email address. RFC5321 section 4.5.3.1.2 limits
+ * the domain to be at most 255 octets.
+ */
+ private static final String EMAIL_ADDRESS_DOMAIN =
+ "(?=.{1,255}(?:\\s|$|^))" + HOST_NAME;
+
+ /**
+ * Regular expression pattern to match email addresses. It excludes double quoted local parts
+ * and the special characters #&~!^`{}/=$*?| that are included in RFC5321.
+ * @hide
+ */
+ public static final Pattern AUTOLINK_EMAIL_ADDRESS = Pattern.compile("(" + WORD_BOUNDARY +
+ "(?:" + EMAIL_ADDRESS_LOCAL_PART + "@" + EMAIL_ADDRESS_DOMAIN + ")" +
+ WORD_BOUNDARY + ")"
+ );
+
+ public static final Pattern EMAIL_ADDRESS
+ = Pattern.compile(
+ "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" +
+ "\\@" +
+ "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" +
+ "(" +
+ "\\." +
+ "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" +
+ ")+"
+ );
+
+ /**
+ * This pattern is intended for searching for things that look like they
+ * might be phone numbers in arbitrary text, not for validating whether
+ * something is in fact a phone number. It will miss many things that
+ * are legitimate phone numbers.
+ *
+ * <p> The pattern matches the following:
+ * <ul>
+ * <li>Optionally, a + sign followed immediately by one or more digits. Spaces, dots, or dashes
+ * may follow.
+ * <li>Optionally, sets of digits in parentheses, separated by spaces, dots, or dashes.
+ * <li>A string starting and ending with a digit, containing digits, spaces, dots, and/or dashes.
+ * </ul>
+ */
+ public static final Pattern PHONE
+ = Pattern.compile( // sdd = space, dot, or dash
+ "(\\+[0-9]+[\\- \\.]*)?" // +<digits><sdd>*
+ + "(\\([0-9]+\\)[\\- \\.]*)?" // (<digits>)<sdd>*
+ + "([0-9][0-9\\- \\.]+[0-9])"); // <digit><digit|sdd>+<digit>
+
+ /**
+ * Convenience method to take all of the non-null matching groups in a
+ * regex Matcher and return them as a concatenated string.
+ *
+ * @param matcher The Matcher object from which grouped text will
+ * be extracted
+ *
+ * @return A String comprising all of the non-null matched
+ * groups concatenated together
+ */
+ public static final String concatGroups(Matcher matcher) {
+ StringBuilder b = new StringBuilder();
+ final int numGroups = matcher.groupCount();
+
+ for (int i = 1; i <= numGroups; i++) {
+ String s = matcher.group(i);
+
+ if (s != null) {
+ b.append(s);
+ }
+ }
+
+ return b.toString();
+ }
+
+ /**
+ * Convenience method to return only the digits and plus signs
+ * in the matching string.
+ *
+ * @param matcher The Matcher object from which digits and plus will
+ * be extracted
+ *
+ * @return A String comprising all of the digits and plus in
+ * the match
+ */
+ public static final String digitsAndPlusOnly(Matcher matcher) {
+ StringBuilder buffer = new StringBuilder();
+ String matchingRegion = matcher.group();
+
+ for (int i = 0, size = matchingRegion.length(); i < size; i++) {
+ char character = matchingRegion.charAt(i);
+
+ if (character == '+' || Character.isDigit(character)) {
+ buffer.append(character);
+ }
+ }
+ return buffer.toString();
+ }
+
+ /**
+ * Do not create this static utility class.
+ */
+ private Patterns() {}
+}
diff --git a/android/util/Pools.java b/android/util/Pools.java
new file mode 100644
index 0000000..7ae3244
--- /dev/null
+++ b/android/util/Pools.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.compat.annotation.UnsupportedAppUsage;
+
+/**
+ * Helper class for crating pools of objects. An example use looks like this:
+ * <pre>
+ * public class MyPooledClass {
+ *
+ * private static final SynchronizedPool<MyPooledClass> sPool =
+ * new SynchronizedPool<MyPooledClass>(10);
+ *
+ * public static MyPooledClass obtain() {
+ * MyPooledClass instance = sPool.acquire();
+ * return (instance != null) ? instance : new MyPooledClass();
+ * }
+ *
+ * public void recycle() {
+ * // Clear state if needed.
+ * sPool.release(this);
+ * }
+ *
+ * . . .
+ * }
+ * </pre>
+ *
+ * @hide
+ */
+public final class Pools {
+
+ /**
+ * Interface for managing a pool of objects.
+ *
+ * @param <T> The pooled type.
+ */
+ public static interface Pool<T> {
+
+ /**
+ * @return An instance from the pool if such, null otherwise.
+ */
+ @UnsupportedAppUsage
+ public T acquire();
+
+ /**
+ * Release an instance to the pool.
+ *
+ * @param instance The instance to release.
+ * @return Whether the instance was put in the pool.
+ *
+ * @throws IllegalStateException If the instance is already in the pool.
+ */
+ @UnsupportedAppUsage
+ public boolean release(T instance);
+ }
+
+ private Pools() {
+ /* do nothing - hiding constructor */
+ }
+
+ /**
+ * Simple (non-synchronized) pool of objects.
+ *
+ * @param <T> The pooled type.
+ */
+ public static class SimplePool<T> implements Pool<T> {
+ @UnsupportedAppUsage
+ private final Object[] mPool;
+
+ private int mPoolSize;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param maxPoolSize The max pool size.
+ *
+ * @throws IllegalArgumentException If the max pool size is less than zero.
+ */
+ @UnsupportedAppUsage
+ public SimplePool(int maxPoolSize) {
+ if (maxPoolSize <= 0) {
+ throw new IllegalArgumentException("The max pool size must be > 0");
+ }
+ mPool = new Object[maxPoolSize];
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ @UnsupportedAppUsage
+ public T acquire() {
+ if (mPoolSize > 0) {
+ final int lastPooledIndex = mPoolSize - 1;
+ T instance = (T) mPool[lastPooledIndex];
+ mPool[lastPooledIndex] = null;
+ mPoolSize--;
+ return instance;
+ }
+ return null;
+ }
+
+ @Override
+ @UnsupportedAppUsage
+ public boolean release(T instance) {
+ if (isInPool(instance)) {
+ throw new IllegalStateException("Already in the pool!");
+ }
+ if (mPoolSize < mPool.length) {
+ mPool[mPoolSize] = instance;
+ mPoolSize++;
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isInPool(T instance) {
+ for (int i = 0; i < mPoolSize; i++) {
+ if (mPool[i] == instance) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Synchronized pool of objects.
+ *
+ * @param <T> The pooled type.
+ */
+ public static class SynchronizedPool<T> extends SimplePool<T> {
+ private final Object mLock;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param maxPoolSize The max pool size.
+ * @param lock an optional custom object to synchronize on
+ *
+ * @throws IllegalArgumentException If the max pool size is less than zero.
+ */
+ public SynchronizedPool(int maxPoolSize, Object lock) {
+ super(maxPoolSize);
+ mLock = lock;
+ }
+
+ /** @see #SynchronizedPool(int, Object) */
+ @UnsupportedAppUsage
+ public SynchronizedPool(int maxPoolSize) {
+ this(maxPoolSize, new Object());
+ }
+
+ @Override
+ @UnsupportedAppUsage
+ public T acquire() {
+ synchronized (mLock) {
+ return super.acquire();
+ }
+ }
+
+ @Override
+ @UnsupportedAppUsage
+ public boolean release(T element) {
+ synchronized (mLock) {
+ return super.release(element);
+ }
+ }
+ }
+}
diff --git a/android/util/PrefixPrinter.java b/android/util/PrefixPrinter.java
new file mode 100644
index 0000000..62f7da1
--- /dev/null
+++ b/android/util/PrefixPrinter.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * PrefixPrinter is a Printer which prefixes all lines with a given
+ * prefix.
+ *
+ * @hide
+ */
+public class PrefixPrinter implements Printer {
+ private final Printer mPrinter;
+ private final String mPrefix;
+
+ /**
+ * Creates a new PrefixPrinter.
+ *
+ * <p>If prefix is null or empty, the provided printer is returned, rather
+ * than making a prefixing printer.
+ */
+ public static Printer create(Printer printer, String prefix) {
+ if (prefix == null || prefix.equals("")) {
+ return printer;
+ }
+ return new PrefixPrinter(printer, prefix);
+ }
+
+ private PrefixPrinter(Printer printer, String prefix) {
+ mPrinter = printer;
+ mPrefix = prefix;
+ }
+
+ public void println(String str) {
+ mPrinter.println(mPrefix + str);
+ }
+}
diff --git a/android/util/PrintStreamPrinter.java b/android/util/PrintStreamPrinter.java
new file mode 100644
index 0000000..1c11f15
--- /dev/null
+++ b/android/util/PrintStreamPrinter.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import java.io.PrintStream;
+
+/**
+ * Implementation of a {@link android.util.Printer} that sends its output
+ * to a {@link java.io.PrintStream}.
+ */
+public class PrintStreamPrinter implements Printer {
+ private final PrintStream mPS;
+
+ /**
+ * Create a new Printer that sends to a PrintWriter object.
+ *
+ * @param pw The PrintWriter where you would like output to go.
+ */
+ public PrintStreamPrinter(PrintStream pw) {
+ mPS = pw;
+ }
+
+ public void println(String x) {
+ mPS.println(x);
+ }
+}
diff --git a/android/util/PrintWriterPrinter.java b/android/util/PrintWriterPrinter.java
new file mode 100644
index 0000000..82c4d03
--- /dev/null
+++ b/android/util/PrintWriterPrinter.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import java.io.PrintWriter;
+
+/**
+ * Implementation of a {@link android.util.Printer} that sends its output
+ * to a {@link java.io.PrintWriter}.
+ */
+public class PrintWriterPrinter implements Printer {
+ private final PrintWriter mPW;
+
+ /**
+ * Create a new Printer that sends to a PrintWriter object.
+ *
+ * @param pw The PrintWriter where you would like output to go.
+ */
+ public PrintWriterPrinter(PrintWriter pw) {
+ mPW = pw;
+ }
+
+ public void println(String x) {
+ mPW.println(x);
+ }
+}
diff --git a/android/util/Printer.java b/android/util/Printer.java
new file mode 100644
index 0000000..595cf70
--- /dev/null
+++ b/android/util/Printer.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * Simple interface for printing text, allowing redirection to various
+ * targets. Standard implementations are {@link android.util.LogPrinter},
+ * {@link android.util.StringBuilderPrinter}, and
+ * {@link android.util.PrintWriterPrinter}.
+ */
+public interface Printer {
+ /**
+ * Write a line of text to the output. There is no need to terminate
+ * the given string with a newline.
+ */
+ void println(String x);
+}
diff --git a/android/util/Property.java b/android/util/Property.java
new file mode 100644
index 0000000..146db80
--- /dev/null
+++ b/android/util/Property.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.util;
+
+
+/**
+ * A property is an abstraction that can be used to represent a <emb>mutable</em> value that is held
+ * in a <em>host</em> object. The Property's {@link #set(Object, Object)} or {@link #get(Object)}
+ * methods can be implemented in terms of the private fields of the host object, or via "setter" and
+ * "getter" methods or by some other mechanism, as appropriate.
+ *
+ * @param <T> The class on which the property is declared.
+ * @param <V> The type that this property represents.
+ */
+public abstract class Property<T, V> {
+
+ private final String mName;
+ private final Class<V> mType;
+
+ /**
+ * This factory method creates and returns a Property given the <code>class</code> and
+ * <code>name</code> parameters, where the <code>"name"</code> parameter represents either:
+ * <ul>
+ * <li>a public <code>getName()</code> method on the class which takes no arguments, plus an
+ * optional public <code>setName()</code> method which takes a value of the same type
+ * returned by <code>getName()</code>
+ * <li>a public <code>isName()</code> method on the class which takes no arguments, plus an
+ * optional public <code>setName()</code> method which takes a value of the same type
+ * returned by <code>isName()</code>
+ * <li>a public <code>name</code> field on the class
+ * </ul>
+ *
+ * <p>If either of the get/is method alternatives is found on the class, but an appropriate
+ * <code>setName()</code> method is not found, the <code>Property</code> will be
+ * {@link #isReadOnly() readOnly}. Calling the {@link #set(Object, Object)} method on such
+ * a property is allowed, but will have no effect.</p>
+ *
+ * <p>If neither the methods nor the field are found on the class a
+ * {@link NoSuchPropertyException} exception will be thrown.</p>
+ */
+ public static <T, V> Property<T, V> of(Class<T> hostType, Class<V> valueType, String name) {
+ return new ReflectiveProperty<T, V>(hostType, valueType, name);
+ }
+
+ /**
+ * A constructor that takes an identifying name and {@link #getType() type} for the property.
+ */
+ public Property(Class<V> type, String name) {
+ mName = name;
+ mType = type;
+ }
+
+ /**
+ * Returns true if the {@link #set(Object, Object)} method does not set the value on the target
+ * object (in which case the {@link #set(Object, Object) set()} method should throw a {@link
+ * NoSuchPropertyException} exception). This may happen if the Property wraps functionality that
+ * allows querying the underlying value but not setting it. For example, the {@link #of(Class,
+ * Class, String)} factory method may return a Property with name "foo" for an object that has
+ * only a <code>getFoo()</code> or <code>isFoo()</code> method, but no matching
+ * <code>setFoo()</code> method.
+ */
+ public boolean isReadOnly() {
+ return false;
+ }
+
+ /**
+ * Sets the value on <code>object</code> which this property represents. If the method is unable
+ * to set the value on the target object it will throw an {@link UnsupportedOperationException}
+ * exception.
+ */
+ public void set(T object, V value) {
+ throw new UnsupportedOperationException("Property " + getName() +" is read-only");
+ }
+
+ /**
+ * Returns the current value that this property represents on the given <code>object</code>.
+ */
+ public abstract V get(T object);
+
+ /**
+ * Returns the name for this property.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the type for this property.
+ */
+ public Class<V> getType() {
+ return mType;
+ }
+}
diff --git a/android/util/Range.java b/android/util/Range.java
new file mode 100644
index 0000000..f31ddd9
--- /dev/null
+++ b/android/util/Range.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import static com.android.internal.util.Preconditions.*;
+
+import android.hardware.camera2.utils.HashCodeHelpers;
+
+/**
+ * Immutable class for describing the range of two numeric values.
+ * <p>
+ * A range (or "interval") defines the inclusive boundaries around a contiguous span of
+ * values of some {@link Comparable} type; for example,
+ * "integers from 1 to 100 inclusive."
+ * </p>
+ * <p>
+ * All ranges are bounded, and the left side of the range is always {@code <=}
+ * the right side of the range.
+ * </p>
+ *
+ * <p>Although the implementation itself is immutable, there is no restriction that objects
+ * stored must also be immutable. If mutable objects are stored here, then the range
+ * effectively becomes mutable. </p>
+ */
+public final class Range<T extends Comparable<? super T>> {
+ /**
+ * Create a new immutable range.
+ *
+ * <p>
+ * The endpoints are {@code [lower, upper]}; that
+ * is the range is bounded. {@code lower} must be {@link Comparable#compareTo lesser or equal}
+ * to {@code upper}.
+ * </p>
+ *
+ * @param lower The lower endpoint (inclusive)
+ * @param upper The upper endpoint (inclusive)
+ *
+ * @throws NullPointerException if {@code lower} or {@code upper} is {@code null}
+ */
+ public Range(final T lower, final T upper) {
+ mLower = checkNotNull(lower, "lower must not be null");
+ mUpper = checkNotNull(upper, "upper must not be null");
+
+ if (lower.compareTo(upper) > 0) {
+ throw new IllegalArgumentException("lower must be less than or equal to upper");
+ }
+ }
+
+ /**
+ * Create a new immutable range, with the argument types inferred.
+ *
+ * <p>
+ * The endpoints are {@code [lower, upper]}; that
+ * is the range is bounded. {@code lower} must be {@link Comparable#compareTo lesser or equal}
+ * to {@code upper}.
+ * </p>
+ *
+ * @param lower The lower endpoint (inclusive)
+ * @param upper The upper endpoint (inclusive)
+ *
+ * @throws NullPointerException if {@code lower} or {@code upper} is {@code null}
+ */
+ public static <T extends Comparable<? super T>> Range<T> create(final T lower, final T upper) {
+ return new Range<T>(lower, upper);
+ }
+
+ /**
+ * Get the lower endpoint.
+ *
+ * @return a non-{@code null} {@code T} reference
+ */
+ public T getLower() {
+ return mLower;
+ }
+
+ /**
+ * Get the upper endpoint.
+ *
+ * @return a non-{@code null} {@code T} reference
+ */
+ public T getUpper() {
+ return mUpper;
+ }
+
+ /**
+ * Checks if the {@code value} is within the bounds of this range.
+ *
+ * <p>A value is considered to be within this range if it's {@code >=}
+ * the lower endpoint <i>and</i> {@code <=} the upper endpoint (using the {@link Comparable}
+ * interface.)</p>
+ *
+ * @param value a non-{@code null} {@code T} reference
+ * @return {@code true} if the value is within this inclusive range, {@code false} otherwise
+ *
+ * @throws NullPointerException if {@code value} was {@code null}
+ */
+ public boolean contains(T value) {
+ checkNotNull(value, "value must not be null");
+
+ boolean gteLower = value.compareTo(mLower) >= 0;
+ boolean lteUpper = value.compareTo(mUpper) <= 0;
+
+ return gteLower && lteUpper;
+ }
+
+ /**
+ * Checks if another {@code range} is within the bounds of this range.
+ *
+ * <p>A range is considered to be within this range if both of its endpoints
+ * are within this range.</p>
+ *
+ * @param range a non-{@code null} {@code T} reference
+ * @return {@code true} if the range is within this inclusive range, {@code false} otherwise
+ *
+ * @throws NullPointerException if {@code range} was {@code null}
+ */
+ public boolean contains(Range<T> range) {
+ checkNotNull(range, "value must not be null");
+
+ boolean gteLower = range.mLower.compareTo(mLower) >= 0;
+ boolean lteUpper = range.mUpper.compareTo(mUpper) <= 0;
+
+ return gteLower && lteUpper;
+ }
+
+ /**
+ * Compare two ranges for equality.
+ *
+ * <p>A range is considered equal if and only if both the lower and upper endpoints
+ * are also equal.</p>
+ *
+ * @return {@code true} if the ranges are equal, {@code false} otherwise
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ } else if (this == obj) {
+ return true;
+ } else if (obj instanceof Range) {
+ @SuppressWarnings("rawtypes")
+ Range other = (Range) obj;
+ return mLower.equals(other.mLower) && mUpper.equals(other.mUpper);
+ }
+ return false;
+ }
+
+ /**
+ * Clamps {@code value} to this range.
+ *
+ * <p>If the value is within this range, it is returned. Otherwise, if it
+ * is {@code <} than the lower endpoint, the lower endpoint is returned,
+ * else the upper endpoint is returned. Comparisons are performed using the
+ * {@link Comparable} interface.</p>
+ *
+ * @param value a non-{@code null} {@code T} reference
+ * @return {@code value} clamped to this range.
+ */
+ public T clamp(T value) {
+ checkNotNull(value, "value must not be null");
+
+ if (value.compareTo(mLower) < 0) {
+ return mLower;
+ } else if (value.compareTo(mUpper) > 0) {
+ return mUpper;
+ } else {
+ return value;
+ }
+ }
+
+ /**
+ * Returns the intersection of this range and another {@code range}.
+ * <p>
+ * E.g. if a {@code <} b {@code <} c {@code <} d, the
+ * intersection of [a, c] and [b, d] ranges is [b, c].
+ * As the endpoints are object references, there is no guarantee
+ * which specific endpoint reference is used from the input ranges:</p>
+ * <p>
+ * E.g. if a {@code ==} a' {@code <} b {@code <} c, the
+ * intersection of [a, b] and [a', c] ranges could be either
+ * [a, b] or ['a, b], where [a, b] could be either the exact
+ * input range, or a newly created range with the same endpoints.</p>
+ *
+ * @param range a non-{@code null} {@code Range<T>} reference
+ * @return the intersection of this range and the other range.
+ *
+ * @throws NullPointerException if {@code range} was {@code null}
+ * @throws IllegalArgumentException if the ranges are disjoint.
+ */
+ public Range<T> intersect(Range<T> range) {
+ checkNotNull(range, "range must not be null");
+
+ int cmpLower = range.mLower.compareTo(mLower);
+ int cmpUpper = range.mUpper.compareTo(mUpper);
+
+ if (cmpLower <= 0 && cmpUpper >= 0) {
+ // range includes this
+ return this;
+ } else if (cmpLower >= 0 && cmpUpper <= 0) {
+ // this inludes range
+ return range;
+ } else {
+ return Range.create(
+ cmpLower <= 0 ? mLower : range.mLower,
+ cmpUpper >= 0 ? mUpper : range.mUpper);
+ }
+ }
+
+ /**
+ * Returns the intersection of this range and the inclusive range
+ * specified by {@code [lower, upper]}.
+ * <p>
+ * See {@link #intersect(Range)} for more details.</p>
+ *
+ * @param lower a non-{@code null} {@code T} reference
+ * @param upper a non-{@code null} {@code T} reference
+ * @return the intersection of this range and the other range
+ *
+ * @throws NullPointerException if {@code lower} or {@code upper} was {@code null}
+ * @throws IllegalArgumentException if the ranges are disjoint.
+ */
+ public Range<T> intersect(T lower, T upper) {
+ checkNotNull(lower, "lower must not be null");
+ checkNotNull(upper, "upper must not be null");
+
+ int cmpLower = lower.compareTo(mLower);
+ int cmpUpper = upper.compareTo(mUpper);
+
+ if (cmpLower <= 0 && cmpUpper >= 0) {
+ // [lower, upper] includes this
+ return this;
+ } else {
+ return Range.create(
+ cmpLower <= 0 ? mLower : lower,
+ cmpUpper >= 0 ? mUpper : upper);
+ }
+ }
+
+ /**
+ * Returns the smallest range that includes this range and
+ * another {@code range}.
+ * <p>
+ * E.g. if a {@code <} b {@code <} c {@code <} d, the
+ * extension of [a, c] and [b, d] ranges is [a, d].
+ * As the endpoints are object references, there is no guarantee
+ * which specific endpoint reference is used from the input ranges:</p>
+ * <p>
+ * E.g. if a {@code ==} a' {@code <} b {@code <} c, the
+ * extension of [a, b] and [a', c] ranges could be either
+ * [a, c] or ['a, c], where ['a, c] could be either the exact
+ * input range, or a newly created range with the same endpoints.</p>
+ *
+ * @param range a non-{@code null} {@code Range<T>} reference
+ * @return the extension of this range and the other range.
+ *
+ * @throws NullPointerException if {@code range} was {@code null}
+ */
+ public Range<T> extend(Range<T> range) {
+ checkNotNull(range, "range must not be null");
+
+ int cmpLower = range.mLower.compareTo(mLower);
+ int cmpUpper = range.mUpper.compareTo(mUpper);
+
+ if (cmpLower <= 0 && cmpUpper >= 0) {
+ // other includes this
+ return range;
+ } else if (cmpLower >= 0 && cmpUpper <= 0) {
+ // this inludes other
+ return this;
+ } else {
+ return Range.create(
+ cmpLower >= 0 ? mLower : range.mLower,
+ cmpUpper <= 0 ? mUpper : range.mUpper);
+ }
+ }
+
+ /**
+ * Returns the smallest range that includes this range and
+ * the inclusive range specified by {@code [lower, upper]}.
+ * <p>
+ * See {@link #extend(Range)} for more details.</p>
+ *
+ * @param lower a non-{@code null} {@code T} reference
+ * @param upper a non-{@code null} {@code T} reference
+ * @return the extension of this range and the other range.
+ *
+ * @throws NullPointerException if {@code lower} or {@code
+ * upper} was {@code null}
+ */
+ public Range<T> extend(T lower, T upper) {
+ checkNotNull(lower, "lower must not be null");
+ checkNotNull(upper, "upper must not be null");
+
+ int cmpLower = lower.compareTo(mLower);
+ int cmpUpper = upper.compareTo(mUpper);
+
+ if (cmpLower >= 0 && cmpUpper <= 0) {
+ // this inludes other
+ return this;
+ } else {
+ return Range.create(
+ cmpLower >= 0 ? mLower : lower,
+ cmpUpper <= 0 ? mUpper : upper);
+ }
+ }
+
+ /**
+ * Returns the smallest range that includes this range and
+ * the {@code value}.
+ * <p>
+ * See {@link #extend(Range)} for more details, as this method is
+ * equivalent to {@code extend(Range.create(value, value))}.</p>
+ *
+ * @param value a non-{@code null} {@code T} reference
+ * @return the extension of this range and the value.
+ *
+ * @throws NullPointerException if {@code value} was {@code null}
+ */
+ public Range<T> extend(T value) {
+ checkNotNull(value, "value must not be null");
+ return extend(value, value);
+ }
+
+ /**
+ * Return the range as a string representation {@code "[lower, upper]"}.
+ *
+ * @return string representation of the range
+ */
+ @Override
+ public String toString() {
+ return String.format("[%s, %s]", mLower, mUpper);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ return HashCodeHelpers.hashCodeGeneric(mLower, mUpper);
+ }
+
+ private final T mLower;
+ private final T mUpper;
+};
diff --git a/android/util/Rational.java b/android/util/Rational.java
new file mode 100644
index 0000000..5930000
--- /dev/null
+++ b/android/util/Rational.java
@@ -0,0 +1,606 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.util;
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.compat.annotation.UnsupportedAppUsage;
+
+import java.io.IOException;
+import java.io.InvalidObjectException;
+
+/**
+ * <p>An immutable data type representation a rational number.</p>
+ *
+ * <p>Contains a pair of {@code int}s representing the numerator and denominator of a
+ * Rational number. </p>
+ */
+public final class Rational extends Number implements Comparable<Rational> {
+ /**
+ * Constant for the <em>Not-a-Number (NaN)</em> value of the {@code Rational} type.
+ *
+ * <p>A {@code NaN} value is considered to be equal to itself (that is {@code NaN.equals(NaN)}
+ * will return {@code true}; it is always greater than any non-{@code NaN} value (that is
+ * {@code NaN.compareTo(notNaN)} will return a number greater than {@code 0}).</p>
+ *
+ * <p>Equivalent to constructing a new rational with both the numerator and denominator
+ * equal to {@code 0}.</p>
+ */
+ public static final Rational NaN = new Rational(0, 0);
+
+ /**
+ * Constant for the positive infinity value of the {@code Rational} type.
+ *
+ * <p>Equivalent to constructing a new rational with a positive numerator and a denominator
+ * equal to {@code 0}.</p>
+ */
+ public static final Rational POSITIVE_INFINITY = new Rational(1, 0);
+
+ /**
+ * Constant for the negative infinity value of the {@code Rational} type.
+ *
+ * <p>Equivalent to constructing a new rational with a negative numerator and a denominator
+ * equal to {@code 0}.</p>
+ */
+ public static final Rational NEGATIVE_INFINITY = new Rational(-1, 0);
+
+ /**
+ * Constant for the zero value of the {@code Rational} type.
+ *
+ * <p>Equivalent to constructing a new rational with a numerator equal to {@code 0} and
+ * any non-zero denominator.</p>
+ */
+ public static final Rational ZERO = new Rational(0, 1);
+
+ /**
+ * Unique version number per class to be compliant with {@link java.io.Serializable}.
+ *
+ * <p>Increment each time the fields change in any way.</p>
+ */
+ private static final long serialVersionUID = 1L;
+
+ /*
+ * Do not change the order of these fields or add new instance fields to maintain the
+ * Serializable compatibility across API revisions.
+ */
+ @UnsupportedAppUsage
+ private final int mNumerator;
+ @UnsupportedAppUsage
+ private final int mDenominator;
+
+ /**
+ * <p>Create a {@code Rational} with a given numerator and denominator.</p>
+ *
+ * <p>The signs of the numerator and the denominator may be flipped such that the denominator
+ * is always positive. Both the numerator and denominator will be converted to their reduced
+ * forms (see {@link #equals} for more details).</p>
+ *
+ * <p>For example,
+ * <ul>
+ * <li>a rational of {@code 2/4} will be reduced to {@code 1/2}.
+ * <li>a rational of {@code 1/-1} will be flipped to {@code -1/1}
+ * <li>a rational of {@code 5/0} will be reduced to {@code 1/0}
+ * <li>a rational of {@code 0/5} will be reduced to {@code 0/1}
+ * </ul>
+ * </p>
+ *
+ * @param numerator the numerator of the rational
+ * @param denominator the denominator of the rational
+ *
+ * @see #equals
+ */
+ public Rational(int numerator, int denominator) {
+
+ if (denominator < 0) {
+ numerator = -numerator;
+ denominator = -denominator;
+ }
+
+ // Convert to reduced form
+ if (denominator == 0 && numerator > 0) {
+ mNumerator = 1; // +Inf
+ mDenominator = 0;
+ } else if (denominator == 0 && numerator < 0) {
+ mNumerator = -1; // -Inf
+ mDenominator = 0;
+ } else if (denominator == 0 && numerator == 0) {
+ mNumerator = 0; // NaN
+ mDenominator = 0;
+ } else if (numerator == 0) {
+ mNumerator = 0;
+ mDenominator = 1;
+ } else {
+ int gcd = gcd(numerator, denominator);
+
+ mNumerator = numerator / gcd;
+ mDenominator = denominator / gcd;
+ }
+ }
+
+ /**
+ * Gets the numerator of the rational.
+ *
+ * <p>The numerator will always return {@code 1} if this rational represents
+ * infinity (that is, the denominator is {@code 0}).</p>
+ */
+ public int getNumerator() {
+ return mNumerator;
+ }
+
+ /**
+ * Gets the denominator of the rational
+ *
+ * <p>The denominator may return {@code 0}, in which case the rational may represent
+ * positive infinity (if the numerator was positive), negative infinity (if the numerator
+ * was negative), or {@code NaN} (if the numerator was {@code 0}).</p>
+ *
+ * <p>The denominator will always return {@code 1} if the numerator is {@code 0}.
+ */
+ public int getDenominator() {
+ return mDenominator;
+ }
+
+ /**
+ * Indicates whether this rational is a <em>Not-a-Number (NaN)</em> value.
+ *
+ * <p>A {@code NaN} value occurs when both the numerator and the denominator are {@code 0}.</p>
+ *
+ * @return {@code true} if this rational is a <em>Not-a-Number (NaN)</em> value;
+ * {@code false} if this is a (potentially infinite) number value
+ */
+ public boolean isNaN() {
+ return mDenominator == 0 && mNumerator == 0;
+ }
+
+ /**
+ * Indicates whether this rational represents an infinite value.
+ *
+ * <p>An infinite value occurs when the denominator is {@code 0} (but the numerator is not).</p>
+ *
+ * @return {@code true} if this rational is a (positive or negative) infinite value;
+ * {@code false} if this is a finite number value (or {@code NaN})
+ */
+ public boolean isInfinite() {
+ return mNumerator != 0 && mDenominator == 0;
+ }
+
+ /**
+ * Indicates whether this rational represents a finite value.
+ *
+ * <p>A finite value occurs when the denominator is not {@code 0}; in other words
+ * the rational is neither infinity or {@code NaN}.</p>
+ *
+ * @return {@code true} if this rational is a (positive or negative) infinite value;
+ * {@code false} if this is a finite number value (or {@code NaN})
+ */
+ public boolean isFinite() {
+ return mDenominator != 0;
+ }
+
+ /**
+ * Indicates whether this rational represents a zero value.
+ *
+ * <p>A zero value is a {@link #isFinite finite} rational with a numerator of {@code 0}.</p>
+ *
+ * @return {@code true} if this rational is finite zero value;
+ * {@code false} otherwise
+ */
+ public boolean isZero() {
+ return isFinite() && mNumerator == 0;
+ }
+
+ private boolean isPosInf() {
+ return mDenominator == 0 && mNumerator > 0;
+ }
+
+ private boolean isNegInf() {
+ return mDenominator == 0 && mNumerator < 0;
+ }
+
+ /**
+ * <p>Compare this Rational to another object and see if they are equal.</p>
+ *
+ * <p>A Rational object can only be equal to another Rational object (comparing against any
+ * other type will return {@code false}).</p>
+ *
+ * <p>A Rational object is considered equal to another Rational object if and only if one of
+ * the following holds:</p>
+ * <ul><li>Both are {@code NaN}</li>
+ * <li>Both are infinities of the same sign</li>
+ * <li>Both have the same numerator and denominator in their reduced form</li>
+ * </ul>
+ *
+ * <p>A reduced form of a Rational is calculated by dividing both the numerator and the
+ * denominator by their greatest common divisor.</p>
+ *
+ * <pre>{@code
+ * (new Rational(1, 2)).equals(new Rational(1, 2)) == true // trivially true
+ * (new Rational(2, 3)).equals(new Rational(1, 2)) == false // trivially false
+ * (new Rational(1, 2)).equals(new Rational(2, 4)) == true // true after reduction
+ * (new Rational(0, 0)).equals(new Rational(0, 0)) == true // NaN.equals(NaN)
+ * (new Rational(1, 0)).equals(new Rational(5, 0)) == true // both are +infinity
+ * (new Rational(1, 0)).equals(new Rational(-1, 0)) == false // +infinity != -infinity
+ * }</pre>
+ *
+ * @param obj a reference to another object
+ *
+ * @return A boolean that determines whether or not the two Rational objects are equal.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof Rational && equals((Rational) obj);
+ }
+
+ private boolean equals(Rational other) {
+ return (mNumerator == other.mNumerator && mDenominator == other.mDenominator);
+ }
+
+ /**
+ * Return a string representation of this rational, e.g. {@code "1/2"}.
+ *
+ * <p>The following rules of conversion apply:
+ * <ul>
+ * <li>{@code NaN} values will return {@code "NaN"}
+ * <li>Positive infinity values will return {@code "Infinity"}
+ * <li>Negative infinity values will return {@code "-Infinity"}
+ * <li>All other values will return {@code "numerator/denominator"} where {@code numerator}
+ * and {@code denominator} are substituted with the appropriate numerator and denominator
+ * values.
+ * </ul></p>
+ */
+ @Override
+ public String toString() {
+ if (isNaN()) {
+ return "NaN";
+ } else if (isPosInf()) {
+ return "Infinity";
+ } else if (isNegInf()) {
+ return "-Infinity";
+ } else {
+ return mNumerator + "/" + mDenominator;
+ }
+ }
+
+ /**
+ * <p>Convert to a floating point representation.</p>
+ *
+ * @return The floating point representation of this rational number.
+ * @hide
+ */
+ public float toFloat() {
+ // TODO: remove this duplicate function (used in CTS and the shim)
+ return floatValue();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ // Bias the hash code for the first (2^16) values for both numerator and denominator
+ int numeratorFlipped = mNumerator << 16 | mNumerator >>> 16;
+
+ return mDenominator ^ numeratorFlipped;
+ }
+
+ /**
+ * Calculates the greatest common divisor using Euclid's algorithm.
+ *
+ * <p><em>Visible for testing only.</em></p>
+ *
+ * @param numerator the numerator in a fraction
+ * @param denominator the denominator in a fraction
+ *
+ * @return An int value representing the gcd. Always positive.
+ * @hide
+ */
+ public static int gcd(int numerator, int denominator) {
+ /*
+ * Non-recursive implementation of Euclid's algorithm:
+ *
+ * gcd(a, 0) := a
+ * gcd(a, b) := gcd(b, a mod b)
+ *
+ */
+ int a = numerator;
+ int b = denominator;
+
+ while (b != 0) {
+ int oldB = b;
+
+ b = a % b;
+ a = oldB;
+ }
+
+ return Math.abs(a);
+ }
+
+ /**
+ * Returns the value of the specified number as a {@code double}.
+ *
+ * <p>The {@code double} is calculated by converting both the numerator and denominator
+ * to a {@code double}; then returning the result of dividing the numerator by the
+ * denominator.</p>
+ *
+ * @return the divided value of the numerator and denominator as a {@code double}.
+ */
+ @Override
+ public double doubleValue() {
+ double num = mNumerator;
+ double den = mDenominator;
+
+ return num / den;
+ }
+
+ /**
+ * Returns the value of the specified number as a {@code float}.
+ *
+ * <p>The {@code float} is calculated by converting both the numerator and denominator
+ * to a {@code float}; then returning the result of dividing the numerator by the
+ * denominator.</p>
+ *
+ * @return the divided value of the numerator and denominator as a {@code float}.
+ */
+ @Override
+ public float floatValue() {
+ float num = mNumerator;
+ float den = mDenominator;
+
+ return num / den;
+ }
+
+ /**
+ * Returns the value of the specified number as a {@code int}.
+ *
+ * <p>{@link #isInfinite Finite} rationals are converted to an {@code int} value
+ * by dividing the numerator by the denominator; conversion for non-finite values happens
+ * identically to casting a floating point value to an {@code int}, in particular:
+ *
+ * <p>
+ * <ul>
+ * <li>Positive infinity saturates to the largest maximum integer
+ * {@link Integer#MAX_VALUE}</li>
+ * <li>Negative infinity saturates to the smallest maximum integer
+ * {@link Integer#MIN_VALUE}</li>
+ * <li><em>Not-A-Number (NaN)</em> returns {@code 0}.</li>
+ * </ul>
+ * </p>
+ *
+ * @return the divided value of the numerator and denominator as a {@code int}.
+ */
+ @Override
+ public int intValue() {
+ // Mimic float to int conversion rules from JLS 5.1.3
+
+ if (isPosInf()) {
+ return Integer.MAX_VALUE;
+ } else if (isNegInf()) {
+ return Integer.MIN_VALUE;
+ } else if (isNaN()) {
+ return 0;
+ } else { // finite
+ return mNumerator / mDenominator;
+ }
+ }
+
+ /**
+ * Returns the value of the specified number as a {@code long}.
+ *
+ * <p>{@link #isInfinite Finite} rationals are converted to an {@code long} value
+ * by dividing the numerator by the denominator; conversion for non-finite values happens
+ * identically to casting a floating point value to a {@code long}, in particular:
+ *
+ * <p>
+ * <ul>
+ * <li>Positive infinity saturates to the largest maximum long
+ * {@link Long#MAX_VALUE}</li>
+ * <li>Negative infinity saturates to the smallest maximum long
+ * {@link Long#MIN_VALUE}</li>
+ * <li><em>Not-A-Number (NaN)</em> returns {@code 0}.</li>
+ * </ul>
+ * </p>
+ *
+ * @return the divided value of the numerator and denominator as a {@code long}.
+ */
+ @Override
+ public long longValue() {
+ // Mimic float to long conversion rules from JLS 5.1.3
+
+ if (isPosInf()) {
+ return Long.MAX_VALUE;
+ } else if (isNegInf()) {
+ return Long.MIN_VALUE;
+ } else if (isNaN()) {
+ return 0;
+ } else { // finite
+ return mNumerator / mDenominator;
+ }
+ }
+
+ /**
+ * Returns the value of the specified number as a {@code short}.
+ *
+ * <p>{@link #isInfinite Finite} rationals are converted to a {@code short} value
+ * identically to {@link #intValue}; the {@code int} result is then truncated to a
+ * {@code short} before returning the value.</p>
+ *
+ * @return the divided value of the numerator and denominator as a {@code short}.
+ */
+ @Override
+ public short shortValue() {
+ return (short) intValue();
+ }
+
+ /**
+ * Compare this rational to the specified rational to determine their natural order.
+ *
+ * <p>{@link #NaN} is considered to be equal to itself and greater than all other
+ * {@code Rational} values. Otherwise, if the objects are not {@link #equals equal}, then
+ * the following rules apply:</p>
+ *
+ * <ul>
+ * <li>Positive infinity is greater than any other finite number (or negative infinity)
+ * <li>Negative infinity is less than any other finite number (or positive infinity)
+ * <li>The finite number represented by this rational is checked numerically
+ * against the other finite number by converting both rationals to a common denominator multiple
+ * and comparing their numerators.
+ * </ul>
+ *
+ * @param another the rational to be compared
+ *
+ * @return a negative integer, zero, or a positive integer as this object is less than,
+ * equal to, or greater than the specified rational.
+ *
+ * @throws NullPointerException if {@code another} was {@code null}
+ */
+ @Override
+ public int compareTo(Rational another) {
+ checkNotNull(another, "another must not be null");
+
+ if (equals(another)) {
+ return 0;
+ } else if (isNaN()) { // NaN is greater than the other non-NaN value
+ return 1;
+ } else if (another.isNaN()) { // the other NaN is greater than this non-NaN value
+ return -1;
+ } else if (isPosInf() || another.isNegInf()) {
+ return 1; // positive infinity is greater than any non-NaN/non-posInf value
+ } else if (isNegInf() || another.isPosInf()) {
+ return -1; // negative infinity is less than any non-NaN/non-negInf value
+ }
+
+ // else both this and another are finite numbers
+
+ // make the denominators the same, then compare numerators
+ long thisNumerator = ((long)mNumerator) * another.mDenominator; // long to avoid overflow
+ long otherNumerator = ((long)another.mNumerator) * mDenominator; // long to avoid overflow
+
+ // avoid underflow from subtraction by doing comparisons
+ if (thisNumerator < otherNumerator) {
+ return -1;
+ } else if (thisNumerator > otherNumerator) {
+ return 1;
+ } else {
+ // This should be covered by #equals, but have this code path just in case
+ return 0;
+ }
+ }
+
+ /*
+ * Serializable implementation.
+ *
+ * The following methods are omitted:
+ * >> writeObject - the default is sufficient (field by field serialization)
+ * >> readObjectNoData - the default is sufficient (0s for both fields is a NaN)
+ */
+
+ /**
+ * writeObject with default serialized form - guards against
+ * deserializing non-reduced forms of the rational.
+ *
+ * @throws InvalidObjectException if the invariants were violated
+ */
+ private void readObject(java.io.ObjectInputStream in)
+ throws IOException, ClassNotFoundException {
+ in.defaultReadObject();
+
+ /*
+ * Guard against trying to deserialize illegal values (in this case, ones
+ * that don't have a standard reduced form).
+ *
+ * - Non-finite values must be one of [0, 1], [0, 0], [0, 1], [0, -1]
+ * - Finite values must always have their greatest common divisor as 1
+ */
+
+ if (mNumerator == 0) { // either zero or NaN
+ if (mDenominator == 1 || mDenominator == 0) {
+ return;
+ }
+ throw new InvalidObjectException(
+ "Rational must be deserialized from a reduced form for zero values");
+ } else if (mDenominator == 0) { // either positive or negative infinity
+ if (mNumerator == 1 || mNumerator == -1) {
+ return;
+ }
+ throw new InvalidObjectException(
+ "Rational must be deserialized from a reduced form for infinity values");
+ } else { // finite value
+ if (gcd(mNumerator, mDenominator) > 1) {
+ throw new InvalidObjectException(
+ "Rational must be deserialized from a reduced form for finite values");
+ }
+ }
+ }
+
+ private static NumberFormatException invalidRational(String s) {
+ throw new NumberFormatException("Invalid Rational: \"" + s + "\"");
+ }
+
+ /**
+ * Parses the specified string as a rational value.
+ * <p>The ASCII characters {@code \}{@code u003a} (':') and
+ * {@code \}{@code u002f} ('/') are recognized as separators between
+ * the numerator and denumerator.</p>
+ * <p>
+ * For any {@code Rational r}: {@code Rational.parseRational(r.toString()).equals(r)}.
+ * However, the method also handles rational numbers expressed in the
+ * following forms:</p>
+ * <p>
+ * "<i>num</i>{@code /}<i>den</i>" or
+ * "<i>num</i>{@code :}<i>den</i>" {@code => new Rational(num, den);},
+ * where <i>num</i> and <i>den</i> are string integers potentially
+ * containing a sign, such as "-10", "+7" or "5".</p>
+ *
+ * <pre>{@code
+ * Rational.parseRational("3:+6").equals(new Rational(1, 2)) == true
+ * Rational.parseRational("-3/-6").equals(new Rational(1, 2)) == true
+ * Rational.parseRational("4.56") => throws NumberFormatException
+ * }</pre>
+ *
+ * @param string the string representation of a rational value.
+ * @return the rational value represented by {@code string}.
+ *
+ * @throws NumberFormatException if {@code string} cannot be parsed
+ * as a rational value.
+ * @throws NullPointerException if {@code string} was {@code null}
+ */
+ public static Rational parseRational(String string)
+ throws NumberFormatException {
+ checkNotNull(string, "string must not be null");
+
+ if (string.equals("NaN")) {
+ return NaN;
+ } else if (string.equals("Infinity")) {
+ return POSITIVE_INFINITY;
+ } else if (string.equals("-Infinity")) {
+ return NEGATIVE_INFINITY;
+ }
+
+ int sep_ix = string.indexOf(':');
+ if (sep_ix < 0) {
+ sep_ix = string.indexOf('/');
+ }
+ if (sep_ix < 0) {
+ throw invalidRational(string);
+ }
+ try {
+ return new Rational(Integer.parseInt(string.substring(0, sep_ix)),
+ Integer.parseInt(string.substring(sep_ix + 1)));
+ } catch (NumberFormatException e) {
+ throw invalidRational(string);
+ }
+ }
+}
diff --git a/android/util/RecurrenceRule.java b/android/util/RecurrenceRule.java
new file mode 100644
index 0000000..a570e5e
--- /dev/null
+++ b/android/util/RecurrenceRule.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.net.ProtocolException;
+import java.time.Clock;
+import java.time.LocalTime;
+import java.time.Period;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Iterator;
+import java.util.Objects;
+
+/**
+ * Description of an event that should recur over time at a specific interval
+ * between two anchor points in time.
+ *
+ * @hide
+ */
+public class RecurrenceRule implements Parcelable {
+ private static final String TAG = "RecurrenceRule";
+ private static final boolean LOGD = Log.isLoggable(TAG, Log.DEBUG);
+
+ private static final int VERSION_INIT = 0;
+
+ /** {@hide} */
+ @VisibleForTesting
+ public static Clock sClock = Clock.systemDefaultZone();
+
+ @UnsupportedAppUsage
+ public final ZonedDateTime start;
+ public final ZonedDateTime end;
+ public final Period period;
+
+ public RecurrenceRule(ZonedDateTime start, ZonedDateTime end, Period period) {
+ this.start = start;
+ this.end = end;
+ this.period = period;
+ }
+
+ @Deprecated
+ public static RecurrenceRule buildNever() {
+ return new RecurrenceRule(null, null, null);
+ }
+
+ @Deprecated
+ @UnsupportedAppUsage
+ public static RecurrenceRule buildRecurringMonthly(int dayOfMonth, ZoneId zone) {
+ // Assume we started last January, since it has all possible days
+ final ZonedDateTime now = ZonedDateTime.now(sClock).withZoneSameInstant(zone);
+ final ZonedDateTime start = ZonedDateTime.of(
+ now.toLocalDate().minusYears(1).withMonth(1).withDayOfMonth(dayOfMonth),
+ LocalTime.MIDNIGHT, zone);
+ return new RecurrenceRule(start, null, Period.ofMonths(1));
+ }
+
+ private RecurrenceRule(Parcel source) {
+ start = convertZonedDateTime(source.readString());
+ end = convertZonedDateTime(source.readString());
+ period = convertPeriod(source.readString());
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(convertZonedDateTime(start));
+ dest.writeString(convertZonedDateTime(end));
+ dest.writeString(convertPeriod(period));
+ }
+
+ public RecurrenceRule(DataInputStream in) throws IOException {
+ final int version = in.readInt();
+ switch (version) {
+ case VERSION_INIT:
+ start = convertZonedDateTime(BackupUtils.readString(in));
+ end = convertZonedDateTime(BackupUtils.readString(in));
+ period = convertPeriod(BackupUtils.readString(in));
+ break;
+ default:
+ throw new ProtocolException("Unknown version " + version);
+ }
+ }
+
+ public void writeToStream(DataOutputStream out) throws IOException {
+ out.writeInt(VERSION_INIT);
+ BackupUtils.writeString(out, convertZonedDateTime(start));
+ BackupUtils.writeString(out, convertZonedDateTime(end));
+ BackupUtils.writeString(out, convertPeriod(period));
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder("RecurrenceRule{")
+ .append("start=").append(start)
+ .append(" end=").append(end)
+ .append(" period=").append(period)
+ .append("}").toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(start, end, period);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof RecurrenceRule) {
+ final RecurrenceRule other = (RecurrenceRule) obj;
+ return Objects.equals(start, other.start)
+ && Objects.equals(end, other.end)
+ && Objects.equals(period, other.period);
+ }
+ return false;
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<RecurrenceRule> CREATOR = new Parcelable.Creator<RecurrenceRule>() {
+ @Override
+ public RecurrenceRule createFromParcel(Parcel source) {
+ return new RecurrenceRule(source);
+ }
+
+ @Override
+ public RecurrenceRule[] newArray(int size) {
+ return new RecurrenceRule[size];
+ }
+ };
+
+ public boolean isRecurring() {
+ return period != null;
+ }
+
+ @Deprecated
+ public boolean isMonthly() {
+ return start != null
+ && period != null
+ && period.getYears() == 0
+ && period.getMonths() == 1
+ && period.getDays() == 0;
+ }
+
+ public Iterator<Range<ZonedDateTime>> cycleIterator() {
+ if (period != null) {
+ return new RecurringIterator();
+ } else {
+ return new NonrecurringIterator();
+ }
+ }
+
+ private class NonrecurringIterator implements Iterator<Range<ZonedDateTime>> {
+ boolean hasNext;
+
+ public NonrecurringIterator() {
+ hasNext = (start != null) && (end != null);
+ }
+
+ @Override
+ public boolean hasNext() {
+ return hasNext;
+ }
+
+ @Override
+ public Range<ZonedDateTime> next() {
+ hasNext = false;
+ return new Range<>(start, end);
+ }
+ }
+
+ private class RecurringIterator implements Iterator<Range<ZonedDateTime>> {
+ int i;
+ ZonedDateTime cycleStart;
+ ZonedDateTime cycleEnd;
+
+ public RecurringIterator() {
+ final ZonedDateTime anchor = (end != null) ? end
+ : ZonedDateTime.now(sClock).withZoneSameInstant(start.getZone());
+ if (LOGD) Log.d(TAG, "Resolving using anchor " + anchor);
+
+ updateCycle();
+
+ // Walk forwards until we find first cycle after now
+ while (anchor.toEpochSecond() > cycleEnd.toEpochSecond()) {
+ i++;
+ updateCycle();
+ }
+
+ // Walk backwards until we find first cycle before now
+ while (anchor.toEpochSecond() <= cycleStart.toEpochSecond()) {
+ i--;
+ updateCycle();
+ }
+ }
+
+ private void updateCycle() {
+ cycleStart = roundBoundaryTime(start.plus(period.multipliedBy(i)));
+ cycleEnd = roundBoundaryTime(start.plus(period.multipliedBy(i + 1)));
+ }
+
+ private ZonedDateTime roundBoundaryTime(ZonedDateTime boundary) {
+ if (isMonthly() && (boundary.getDayOfMonth() < start.getDayOfMonth())) {
+ // When forced to end a monthly cycle early, we want to count
+ // that entire day against the boundary.
+ return ZonedDateTime.of(boundary.toLocalDate(), LocalTime.MAX, start.getZone());
+ } else {
+ return boundary;
+ }
+ }
+
+ @Override
+ public boolean hasNext() {
+ return cycleStart.toEpochSecond() >= start.toEpochSecond();
+ }
+
+ @Override
+ public Range<ZonedDateTime> next() {
+ if (LOGD) Log.d(TAG, "Cycle " + i + " from " + cycleStart + " to " + cycleEnd);
+ Range<ZonedDateTime> r = new Range<>(cycleStart, cycleEnd);
+ i--;
+ updateCycle();
+ return r;
+ }
+ }
+
+ public static String convertZonedDateTime(ZonedDateTime time) {
+ return time != null ? time.toString() : null;
+ }
+
+ public static ZonedDateTime convertZonedDateTime(String time) {
+ return time != null ? ZonedDateTime.parse(time) : null;
+ }
+
+ public static String convertPeriod(Period period) {
+ return period != null ? period.toString() : null;
+ }
+
+ public static Period convertPeriod(String period) {
+ return period != null ? Period.parse(period) : null;
+ }
+}
diff --git a/android/util/ReflectiveProperty.java b/android/util/ReflectiveProperty.java
new file mode 100644
index 0000000..6832240
--- /dev/null
+++ b/android/util/ReflectiveProperty.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.util;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * Internal class to automatically generate a Property for a given class/name pair, given the
+ * specification of {@link Property#of(java.lang.Class, java.lang.Class, java.lang.String)}
+ */
+class ReflectiveProperty<T, V> extends Property<T, V> {
+
+ private static final String PREFIX_GET = "get";
+ private static final String PREFIX_IS = "is";
+ private static final String PREFIX_SET = "set";
+ private Method mSetter;
+ private Method mGetter;
+ private Field mField;
+
+ /**
+ * For given property name 'name', look for getName/isName method or 'name' field.
+ * Also look for setName method (optional - could be readonly). Failing method getters and
+ * field results in throwing NoSuchPropertyException.
+ *
+ * @param propertyHolder The class on which the methods or field are found
+ * @param name The name of the property, where this name is capitalized and appended to
+ * "get" and "is to search for the appropriate methods. If the get/is methods are not found,
+ * the constructor will search for a field with that exact name.
+ */
+ public ReflectiveProperty(Class<T> propertyHolder, Class<V> valueType, String name) {
+ // TODO: cache reflection info for each new class/name pair
+ super(valueType, name);
+ char firstLetter = Character.toUpperCase(name.charAt(0));
+ String theRest = name.substring(1);
+ String capitalizedName = firstLetter + theRest;
+ String getterName = PREFIX_GET + capitalizedName;
+ try {
+ mGetter = propertyHolder.getMethod(getterName, (Class<?>[])null);
+ } catch (NoSuchMethodException e) {
+ // getName() not available - try isName() instead
+ getterName = PREFIX_IS + capitalizedName;
+ try {
+ mGetter = propertyHolder.getMethod(getterName, (Class<?>[])null);
+ } catch (NoSuchMethodException e1) {
+ // Try public field instead
+ try {
+ mField = propertyHolder.getField(name);
+ Class fieldType = mField.getType();
+ if (!typesMatch(valueType, fieldType)) {
+ throw new NoSuchPropertyException("Underlying type (" + fieldType + ") " +
+ "does not match Property type (" + valueType + ")");
+ }
+ return;
+ } catch (NoSuchFieldException e2) {
+ // no way to access property - throw appropriate exception
+ throw new NoSuchPropertyException("No accessor method or field found for"
+ + " property with name " + name);
+ }
+ }
+ }
+ Class getterType = mGetter.getReturnType();
+ // Check to make sure our getter type matches our valueType
+ if (!typesMatch(valueType, getterType)) {
+ throw new NoSuchPropertyException("Underlying type (" + getterType + ") " +
+ "does not match Property type (" + valueType + ")");
+ }
+ String setterName = PREFIX_SET + capitalizedName;
+ try {
+ mSetter = propertyHolder.getMethod(setterName, getterType);
+ } catch (NoSuchMethodException ignored) {
+ // Okay to not have a setter - just a readonly property
+ }
+ }
+
+ /**
+ * Utility method to check whether the type of the underlying field/method on the target
+ * object matches the type of the Property. The extra checks for primitive types are because
+ * generics will force the Property type to be a class, whereas the type of the underlying
+ * method/field will probably be a primitive type instead. Accept float as matching Float,
+ * etc.
+ */
+ private boolean typesMatch(Class<V> valueType, Class getterType) {
+ if (getterType != valueType) {
+ if (getterType.isPrimitive()) {
+ return (getterType == float.class && valueType == Float.class) ||
+ (getterType == int.class && valueType == Integer.class) ||
+ (getterType == boolean.class && valueType == Boolean.class) ||
+ (getterType == long.class && valueType == Long.class) ||
+ (getterType == double.class && valueType == Double.class) ||
+ (getterType == short.class && valueType == Short.class) ||
+ (getterType == byte.class && valueType == Byte.class) ||
+ (getterType == char.class && valueType == Character.class);
+ }
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void set(T object, V value) {
+ if (mSetter != null) {
+ try {
+ mSetter.invoke(object, value);
+ } catch (IllegalAccessException e) {
+ throw new AssertionError();
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e.getCause());
+ }
+ } else if (mField != null) {
+ try {
+ mField.set(object, value);
+ } catch (IllegalAccessException e) {
+ throw new AssertionError();
+ }
+ } else {
+ throw new UnsupportedOperationException("Property " + getName() +" is read-only");
+ }
+ }
+
+ @Override
+ public V get(T object) {
+ if (mGetter != null) {
+ try {
+ return (V) mGetter.invoke(object, (Object[])null);
+ } catch (IllegalAccessException e) {
+ throw new AssertionError();
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e.getCause());
+ }
+ } else if (mField != null) {
+ try {
+ return (V) mField.get(object);
+ } catch (IllegalAccessException e) {
+ throw new AssertionError();
+ }
+ }
+ // Should not get here: there should always be a non-null getter or field
+ throw new AssertionError();
+ }
+
+ /**
+ * Returns false if there is no setter or public field underlying this Property.
+ */
+ @Override
+ public boolean isReadOnly() {
+ return (mSetter == null && mField == null);
+ }
+}
diff --git a/android/util/ResolvingAttributeSet.java b/android/util/ResolvingAttributeSet.java
new file mode 100644
index 0000000..6efd812
--- /dev/null
+++ b/android/util/ResolvingAttributeSet.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.util;
+
+import com.android.ide.common.rendering.api.ResourceValue;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+public interface ResolvingAttributeSet extends AttributeSet {
+ /**
+ * Returns the resolved value of the attribute with the given name and namespace
+ *
+ * @param namespace the namespace of the attribute
+ * @param name the name of the attribute
+ * @return the resolved value of the attribute, or null if the attribute does not exist
+ */
+ @Nullable
+ ResourceValue getResolvedAttributeValue(@Nullable String namespace, @NonNull String name);
+}
diff --git a/android/util/RotationUtils.java b/android/util/RotationUtils.java
new file mode 100644
index 0000000..a44ed59
--- /dev/null
+++ b/android/util/RotationUtils.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_180;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
+
+import android.graphics.Insets;
+import android.view.Surface.Rotation;
+
+/**
+ * A class containing utility methods related to rotation.
+ *
+ * @hide
+ */
+public class RotationUtils {
+
+ /**
+ * Rotates an Insets according to the given rotation.
+ */
+ public static Insets rotateInsets(Insets insets, @Rotation int rotation) {
+ if (insets == null || insets == Insets.NONE) {
+ return insets;
+ }
+ Insets rotated;
+ switch (rotation) {
+ case ROTATION_0:
+ rotated = insets;
+ break;
+ case ROTATION_90:
+ rotated = Insets.of(
+ insets.top,
+ insets.right,
+ insets.bottom,
+ insets.left);
+ break;
+ case ROTATION_180:
+ rotated = Insets.of(
+ insets.right,
+ insets.bottom,
+ insets.left,
+ insets.top);
+ break;
+ case ROTATION_270:
+ rotated = Insets.of(
+ insets.bottom,
+ insets.left,
+ insets.top,
+ insets.right);
+ break;
+ default:
+ throw new IllegalArgumentException("unknown rotation: " + rotation);
+ }
+ return rotated;
+ }
+}
diff --git a/android/util/Singleton.java b/android/util/Singleton.java
new file mode 100644
index 0000000..92646b4
--- /dev/null
+++ b/android/util/Singleton.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.compat.annotation.UnsupportedAppUsage;
+
+/**
+ * Singleton helper class for lazily initialization.
+ *
+ * Modeled after frameworks/base/include/utils/Singleton.h
+ *
+ * @hide
+ */
+public abstract class Singleton<T> {
+
+ @UnsupportedAppUsage
+ public Singleton() {
+ }
+
+ @UnsupportedAppUsage
+ private T mInstance;
+
+ protected abstract T create();
+
+ @UnsupportedAppUsage
+ public final T get() {
+ synchronized (this) {
+ if (mInstance == null) {
+ mInstance = create();
+ }
+ return mInstance;
+ }
+ }
+}
diff --git a/android/util/Size.java b/android/util/Size.java
new file mode 100644
index 0000000..62df564
--- /dev/null
+++ b/android/util/Size.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+/**
+ * Immutable class for describing width and height dimensions in pixels.
+ */
+public final class Size {
+ /**
+ * Create a new immutable Size instance.
+ *
+ * @param width The width of the size, in pixels
+ * @param height The height of the size, in pixels
+ */
+ public Size(int width, int height) {
+ mWidth = width;
+ mHeight = height;
+ }
+
+ /**
+ * Get the width of the size (in pixels).
+ * @return width
+ */
+ public int getWidth() {
+ return mWidth;
+ }
+
+ /**
+ * Get the height of the size (in pixels).
+ * @return height
+ */
+ public int getHeight() {
+ return mHeight;
+ }
+
+ /**
+ * Check if this size is equal to another size.
+ * <p>
+ * Two sizes are equal if and only if both their widths and heights are
+ * equal.
+ * </p>
+ * <p>
+ * A size object is never equal to any other type of object.
+ * </p>
+ *
+ * @return {@code true} if the objects were equal, {@code false} otherwise
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof Size) {
+ Size other = (Size) obj;
+ return mWidth == other.mWidth && mHeight == other.mHeight;
+ }
+ return false;
+ }
+
+ /**
+ * Return the size represented as a string with the format {@code "WxH"}
+ *
+ * @return string representation of the size
+ */
+ @Override
+ public String toString() {
+ return mWidth + "x" + mHeight;
+ }
+
+ private static NumberFormatException invalidSize(String s) {
+ throw new NumberFormatException("Invalid Size: \"" + s + "\"");
+ }
+
+ /**
+ * Parses the specified string as a size value.
+ * <p>
+ * The ASCII characters {@code \}{@code u002a} ('*') and
+ * {@code \}{@code u0078} ('x') are recognized as separators between
+ * the width and height.</p>
+ * <p>
+ * For any {@code Size s}: {@code Size.parseSize(s.toString()).equals(s)}.
+ * However, the method also handles sizes expressed in the
+ * following forms:</p>
+ * <p>
+ * "<i>width</i>{@code x}<i>height</i>" or
+ * "<i>width</i>{@code *}<i>height</i>" {@code => new Size(width, height)},
+ * where <i>width</i> and <i>height</i> are string integers potentially
+ * containing a sign, such as "-10", "+7" or "5".</p>
+ *
+ * <pre>{@code
+ * Size.parseSize("3*+6").equals(new Size(3, 6)) == true
+ * Size.parseSize("-3x-6").equals(new Size(-3, -6)) == true
+ * Size.parseSize("4 by 3") => throws NumberFormatException
+ * }</pre>
+ *
+ * @param string the string representation of a size value.
+ * @return the size value represented by {@code string}.
+ *
+ * @throws NumberFormatException if {@code string} cannot be parsed
+ * as a size value.
+ * @throws NullPointerException if {@code string} was {@code null}
+ */
+ public static Size parseSize(String string)
+ throws NumberFormatException {
+ checkNotNull(string, "string must not be null");
+
+ int sep_ix = string.indexOf('*');
+ if (sep_ix < 0) {
+ sep_ix = string.indexOf('x');
+ }
+ if (sep_ix < 0) {
+ throw invalidSize(string);
+ }
+ try {
+ return new Size(Integer.parseInt(string.substring(0, sep_ix)),
+ Integer.parseInt(string.substring(sep_ix + 1)));
+ } catch (NumberFormatException e) {
+ throw invalidSize(string);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ // assuming most sizes are <2^16, doing a rotate will give us perfect hashing
+ return mHeight ^ ((mWidth << (Integer.SIZE / 2)) | (mWidth >>> (Integer.SIZE / 2)));
+ }
+
+ private final int mWidth;
+ private final int mHeight;
+}
diff --git a/android/util/SizeF.java b/android/util/SizeF.java
new file mode 100644
index 0000000..2edc4a7
--- /dev/null
+++ b/android/util/SizeF.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+import static com.android.internal.util.Preconditions.checkArgumentFinite;
+
+/**
+ * Immutable class for describing width and height dimensions in some arbitrary
+ * unit.
+ * <p>
+ * Width and height are finite values stored as a floating point representation.
+ * </p>
+ */
+public final class SizeF {
+ /**
+ * Create a new immutable SizeF instance.
+ *
+ * <p>Both the {@code width} and the {@code height} must be a finite number.
+ * In particular, {@code NaN} and positive/negative infinity are illegal values.</p>
+ *
+ * @param width The width of the size
+ * @param height The height of the size
+ *
+ * @throws IllegalArgumentException
+ * if either {@code width} or {@code height} was not finite.
+ */
+ public SizeF(final float width, final float height) {
+ mWidth = checkArgumentFinite(width, "width");
+ mHeight = checkArgumentFinite(height, "height");
+ }
+
+ /**
+ * Get the width of the size (as an arbitrary unit).
+ * @return width
+ */
+ public float getWidth() {
+ return mWidth;
+ }
+
+ /**
+ * Get the height of the size (as an arbitrary unit).
+ * @return height
+ */
+ public float getHeight() {
+ return mHeight;
+ }
+
+ /**
+ * Check if this size is equal to another size.
+ *
+ * <p>Two sizes are equal if and only if both their widths and heights are the same.</p>
+ *
+ * <p>For this purpose, the width/height float values are considered to be the same if and only
+ * if the method {@link Float#floatToIntBits(float)} returns the identical {@code int} value
+ * when applied to each.</p>
+ *
+ * @return {@code true} if the objects were equal, {@code false} otherwise
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof SizeF) {
+ final SizeF other = (SizeF) obj;
+ return mWidth == other.mWidth && mHeight == other.mHeight;
+ }
+ return false;
+ }
+
+ /**
+ * Return the size represented as a string with the format {@code "WxH"}
+ *
+ * @return string representation of the size
+ */
+ @Override
+ public String toString() {
+ return mWidth + "x" + mHeight;
+ }
+
+ private static NumberFormatException invalidSizeF(String s) {
+ throw new NumberFormatException("Invalid SizeF: \"" + s + "\"");
+ }
+
+ /**
+ * Parses the specified string as a size value.
+ * <p>
+ * The ASCII characters {@code \}{@code u002a} ('*') and
+ * {@code \}{@code u0078} ('x') are recognized as separators between
+ * the width and height.</p>
+ * <p>
+ * For any {@code SizeF s}: {@code SizeF.parseSizeF(s.toString()).equals(s)}.
+ * However, the method also handles sizes expressed in the
+ * following forms:</p>
+ * <p>
+ * "<i>width</i>{@code x}<i>height</i>" or
+ * "<i>width</i>{@code *}<i>height</i>" {@code => new SizeF(width, height)},
+ * where <i>width</i> and <i>height</i> are string floats potentially
+ * containing a sign, such as "-10.3", "+7" or "5.2", but not containing
+ * an {@code 'x'} (such as a float in hexadecimal string format).</p>
+ *
+ * <pre>{@code
+ * SizeF.parseSizeF("3.2*+6").equals(new SizeF(3.2f, 6.0f)) == true
+ * SizeF.parseSizeF("-3x-6").equals(new SizeF(-3.0f, -6.0f)) == true
+ * SizeF.parseSizeF("4 by 3") => throws NumberFormatException
+ * }</pre>
+ *
+ * @param string the string representation of a size value.
+ * @return the size value represented by {@code string}.
+ *
+ * @throws NumberFormatException if {@code string} cannot be parsed
+ * as a size value.
+ * @throws NullPointerException if {@code string} was {@code null}
+ */
+ public static SizeF parseSizeF(String string)
+ throws NumberFormatException {
+ checkNotNull(string, "string must not be null");
+
+ int sep_ix = string.indexOf('*');
+ if (sep_ix < 0) {
+ sep_ix = string.indexOf('x');
+ }
+ if (sep_ix < 0) {
+ throw invalidSizeF(string);
+ }
+ try {
+ return new SizeF(Float.parseFloat(string.substring(0, sep_ix)),
+ Float.parseFloat(string.substring(sep_ix + 1)));
+ } catch (NumberFormatException e) {
+ throw invalidSizeF(string);
+ } catch (IllegalArgumentException e) {
+ throw invalidSizeF(string);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ return Float.floatToIntBits(mWidth) ^ Float.floatToIntBits(mHeight);
+ }
+
+ private final float mWidth;
+ private final float mHeight;
+}
diff --git a/android/util/Slog.java b/android/util/Slog.java
new file mode 100644
index 0000000..2c8bbbf
--- /dev/null
+++ b/android/util/Slog.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+
+/**
+ * @hide
+ */
+public final class Slog {
+
+ private Slog() {
+ }
+
+ @UnsupportedAppUsage
+ public static int v(String tag, String msg) {
+ return Log.println_native(Log.LOG_ID_SYSTEM, Log.VERBOSE, tag, msg);
+ }
+
+ public static int v(String tag, String msg, Throwable tr) {
+ return Log.println_native(Log.LOG_ID_SYSTEM, Log.VERBOSE, tag,
+ msg + '\n' + Log.getStackTraceString(tr));
+ }
+
+ @UnsupportedAppUsage
+ public static int d(String tag, String msg) {
+ return Log.println_native(Log.LOG_ID_SYSTEM, Log.DEBUG, tag, msg);
+ }
+
+ @UnsupportedAppUsage
+ public static int d(String tag, String msg, Throwable tr) {
+ return Log.println_native(Log.LOG_ID_SYSTEM, Log.DEBUG, tag,
+ msg + '\n' + Log.getStackTraceString(tr));
+ }
+
+ @UnsupportedAppUsage
+ public static int i(String tag, String msg) {
+ return Log.println_native(Log.LOG_ID_SYSTEM, Log.INFO, tag, msg);
+ }
+
+ public static int i(String tag, String msg, Throwable tr) {
+ return Log.println_native(Log.LOG_ID_SYSTEM, Log.INFO, tag,
+ msg + '\n' + Log.getStackTraceString(tr));
+ }
+
+ @UnsupportedAppUsage
+ public static int w(String tag, String msg) {
+ return Log.println_native(Log.LOG_ID_SYSTEM, Log.WARN, tag, msg);
+ }
+
+ @UnsupportedAppUsage
+ public static int w(String tag, String msg, Throwable tr) {
+ return Log.println_native(Log.LOG_ID_SYSTEM, Log.WARN, tag,
+ msg + '\n' + Log.getStackTraceString(tr));
+ }
+
+ public static int w(String tag, Throwable tr) {
+ return Log.println_native(Log.LOG_ID_SYSTEM, Log.WARN, tag, Log.getStackTraceString(tr));
+ }
+
+ @UnsupportedAppUsage
+ public static int e(String tag, String msg) {
+ return Log.println_native(Log.LOG_ID_SYSTEM, Log.ERROR, tag, msg);
+ }
+
+ @UnsupportedAppUsage
+ public static int e(String tag, String msg, Throwable tr) {
+ return Log.println_native(Log.LOG_ID_SYSTEM, Log.ERROR, tag,
+ msg + '\n' + Log.getStackTraceString(tr));
+ }
+
+ /**
+ * Like {@link Log#wtf(String, String)}, but will never cause the caller to crash, and
+ * will always be handled asynchronously. Primarily for use by coding running within
+ * the system process.
+ */
+ @UnsupportedAppUsage
+ public static int wtf(String tag, String msg) {
+ return Log.wtf(Log.LOG_ID_SYSTEM, tag, msg, null, false, true);
+ }
+
+ /**
+ * Like {@link #wtf(String, String)}, but does not output anything to the log.
+ */
+ public static void wtfQuiet(String tag, String msg) {
+ Log.wtfQuiet(Log.LOG_ID_SYSTEM, tag, msg, true);
+ }
+
+ /**
+ * Like {@link Log#wtfStack(String, String)}, but will never cause the caller to crash, and
+ * will always be handled asynchronously. Primarily for use by coding running within
+ * the system process.
+ */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ public static int wtfStack(String tag, String msg) {
+ return Log.wtf(Log.LOG_ID_SYSTEM, tag, msg, null, true, true);
+ }
+
+ /**
+ * Like {@link Log#wtf(String, Throwable)}, but will never cause the caller to crash,
+ * and will always be handled asynchronously. Primarily for use by coding running within
+ * the system process.
+ */
+ public static int wtf(String tag, Throwable tr) {
+ return Log.wtf(Log.LOG_ID_SYSTEM, tag, tr.getMessage(), tr, false, true);
+ }
+
+ /**
+ * Like {@link Log#wtf(String, String, Throwable)}, but will never cause the caller to crash,
+ * and will always be handled asynchronously. Primarily for use by coding running within
+ * the system process.
+ */
+ @UnsupportedAppUsage
+ public static int wtf(String tag, String msg, Throwable tr) {
+ return Log.wtf(Log.LOG_ID_SYSTEM, tag, msg, tr, false, true);
+ }
+
+ @UnsupportedAppUsage
+ public static int println(int priority, String tag, String msg) {
+ return Log.println_native(Log.LOG_ID_SYSTEM, priority, tag, msg);
+ }
+}
+
diff --git a/android/util/SparseArray.java b/android/util/SparseArray.java
new file mode 100644
index 0000000..dae760f
--- /dev/null
+++ b/android/util/SparseArray.java
@@ -0,0 +1,500 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.compat.annotation.UnsupportedAppUsage;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.GrowingArrayUtils;
+
+import libcore.util.EmptyArray;
+
+/**
+ * <code>SparseArray</code> maps integers to Objects and, unlike a normal array of Objects,
+ * its indices can contain gaps. <code>SparseArray</code> is intended to be more memory-efficient
+ * than a
+ * <a href="/reference/java/util/HashMap"><code>HashMap</code></a>, because it avoids
+ * auto-boxing keys and its data structure doesn't rely on an extra entry object
+ * for each mapping.
+ *
+ * <p>Note that this container keeps its mappings in an array data structure,
+ * using a binary search to find keys. The implementation is not intended to be appropriate for
+ * data structures
+ * that may contain large numbers of items. It is generally slower than a
+ * <code>HashMap</code> because lookups require a binary search,
+ * and adds and removes require inserting
+ * and deleting entries in the array. For containers holding up to hundreds of items,
+ * the performance difference is less than 50%.
+ *
+ * <p>To help with performance, the container includes an optimization when removing
+ * keys: instead of compacting its array immediately, it leaves the removed entry marked
+ * as deleted. The entry can then be re-used for the same key or compacted later in
+ * a single garbage collection of all removed entries. This garbage collection
+ * must be performed whenever the array needs to be grown, or when the map size or
+ * entry values are retrieved.
+ *
+ * <p>It is possible to iterate over the items in this container using
+ * {@link #keyAt(int)} and {@link #valueAt(int)}. Iterating over the keys using
+ * <code>keyAt(int)</code> with ascending values of the index returns the
+ * keys in ascending order. In the case of <code>valueAt(int)</code>, the
+ * values corresponding to the keys are returned in ascending order.
+ */
+public class SparseArray<E> implements Cloneable {
+ private static final Object DELETED = new Object();
+ private boolean mGarbage = false;
+
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Use keyAt(int)
+ private int[] mKeys;
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Use valueAt(int), setValueAt(int, E)
+ private Object[] mValues;
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Use size()
+ private int mSize;
+
+ /**
+ * Creates a new SparseArray containing no mappings.
+ */
+ public SparseArray() {
+ this(10);
+ }
+
+ /**
+ * Creates a new SparseArray containing no mappings that will not
+ * require any additional memory allocation to store the specified
+ * number of mappings. If you supply an initial capacity of 0, the
+ * sparse array will be initialized with a light-weight representation
+ * not requiring any additional array allocations.
+ */
+ public SparseArray(int initialCapacity) {
+ if (initialCapacity == 0) {
+ mKeys = EmptyArray.INT;
+ mValues = EmptyArray.OBJECT;
+ } else {
+ mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
+ mKeys = new int[mValues.length];
+ }
+ mSize = 0;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public SparseArray<E> clone() {
+ SparseArray<E> clone = null;
+ try {
+ clone = (SparseArray<E>) super.clone();
+ clone.mKeys = mKeys.clone();
+ clone.mValues = mValues.clone();
+ } catch (CloneNotSupportedException cnse) {
+ /* ignore */
+ }
+ return clone;
+ }
+
+ /**
+ * Returns true if the key exists in the array. This is equivalent to
+ * {@link #indexOfKey(int)} >= 0.
+ *
+ * @param key Potential key in the mapping
+ * @return true if the key is defined in the mapping
+ */
+ public boolean contains(int key) {
+ return indexOfKey(key) >= 0;
+ }
+
+ /**
+ * Gets the Object mapped from the specified key, or <code>null</code>
+ * if no such mapping has been made.
+ */
+ public E get(int key) {
+ return get(key, null);
+ }
+
+ /**
+ * Gets the Object mapped from the specified key, or the specified Object
+ * if no such mapping has been made.
+ */
+ @SuppressWarnings("unchecked")
+ public E get(int key, E valueIfKeyNotFound) {
+ int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+
+ if (i < 0 || mValues[i] == DELETED) {
+ return valueIfKeyNotFound;
+ } else {
+ return (E) mValues[i];
+ }
+ }
+
+ /**
+ * Removes the mapping from the specified key, if there was any.
+ */
+ public void delete(int key) {
+ int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+
+ if (i >= 0) {
+ if (mValues[i] != DELETED) {
+ mValues[i] = DELETED;
+ mGarbage = true;
+ }
+ }
+ }
+
+ /**
+ * @hide
+ * Removes the mapping from the specified key, if there was any, returning the old value.
+ */
+ public E removeReturnOld(int key) {
+ int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+
+ if (i >= 0) {
+ if (mValues[i] != DELETED) {
+ final E old = (E) mValues[i];
+ mValues[i] = DELETED;
+ mGarbage = true;
+ return old;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Alias for {@link #delete(int)}.
+ */
+ public void remove(int key) {
+ delete(key);
+ }
+
+ /**
+ * Removes the mapping at the specified index.
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>,
+ * the behavior is undefined for apps targeting {@link android.os.Build.VERSION_CODES#P} and
+ * earlier, and an {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ */
+ public void removeAt(int index) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ if (mValues[index] != DELETED) {
+ mValues[index] = DELETED;
+ mGarbage = true;
+ }
+ }
+
+ /**
+ * Remove a range of mappings as a batch.
+ *
+ * @param index Index to begin at
+ * @param size Number of mappings to remove
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>,
+ * the behavior is undefined.</p>
+ */
+ public void removeAtRange(int index, int size) {
+ final int end = Math.min(mSize, index + size);
+ for (int i = index; i < end; i++) {
+ removeAt(i);
+ }
+ }
+
+ private void gc() {
+ // Log.e("SparseArray", "gc start with " + mSize);
+
+ int n = mSize;
+ int o = 0;
+ int[] keys = mKeys;
+ Object[] values = mValues;
+
+ for (int i = 0; i < n; i++) {
+ Object val = values[i];
+
+ if (val != DELETED) {
+ if (i != o) {
+ keys[o] = keys[i];
+ values[o] = val;
+ values[i] = null;
+ }
+
+ o++;
+ }
+ }
+
+ mGarbage = false;
+ mSize = o;
+
+ // Log.e("SparseArray", "gc end with " + mSize);
+ }
+
+ /**
+ * Adds a mapping from the specified key to the specified value,
+ * replacing the previous mapping from the specified key if there
+ * was one.
+ */
+ public void put(int key, E value) {
+ int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+
+ if (i >= 0) {
+ mValues[i] = value;
+ } else {
+ i = ~i;
+
+ if (i < mSize && mValues[i] == DELETED) {
+ mKeys[i] = key;
+ mValues[i] = value;
+ return;
+ }
+
+ if (mGarbage && mSize >= mKeys.length) {
+ gc();
+
+ // Search again because indices may have changed.
+ i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
+ }
+
+ mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
+ mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
+ mSize++;
+ }
+ }
+
+ /**
+ * Returns the number of key-value mappings that this SparseArray
+ * currently stores.
+ */
+ public int size() {
+ if (mGarbage) {
+ gc();
+ }
+
+ return mSize;
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, returns
+ * the key from the <code>index</code>th key-value mapping that this
+ * SparseArray stores.
+ *
+ * <p>The keys corresponding to indices in ascending order are guaranteed to
+ * be in ascending order, e.g., <code>keyAt(0)</code> will return the
+ * smallest key and <code>keyAt(size()-1)</code> will return the largest
+ * key.</p>
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>,
+ * the behavior is undefined for apps targeting {@link android.os.Build.VERSION_CODES#P} and
+ * earlier, and an {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ */
+ public int keyAt(int index) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ if (mGarbage) {
+ gc();
+ }
+
+ return mKeys[index];
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, returns
+ * the value from the <code>index</code>th key-value mapping that this
+ * SparseArray stores.
+ *
+ * <p>The values corresponding to indices in ascending order are guaranteed
+ * to be associated with keys in ascending order, e.g.,
+ * <code>valueAt(0)</code> will return the value associated with the
+ * smallest key and <code>valueAt(size()-1)</code> will return the value
+ * associated with the largest key.</p>
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>,
+ * the behavior is undefined for apps targeting {@link android.os.Build.VERSION_CODES#P} and
+ * earlier, and an {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ */
+ @SuppressWarnings("unchecked")
+ public E valueAt(int index) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ if (mGarbage) {
+ gc();
+ }
+
+ return (E) mValues[index];
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, sets a new
+ * value for the <code>index</code>th key-value mapping that this
+ * SparseArray stores.
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ */
+ public void setValueAt(int index, E value) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ if (mGarbage) {
+ gc();
+ }
+
+ mValues[index] = value;
+ }
+
+ /**
+ * Returns the index for which {@link #keyAt} would return the
+ * specified key, or a negative number if the specified
+ * key is not mapped.
+ */
+ public int indexOfKey(int key) {
+ if (mGarbage) {
+ gc();
+ }
+
+ return ContainerHelpers.binarySearch(mKeys, mSize, key);
+ }
+
+ /**
+ * Returns an index for which {@link #valueAt} would return the
+ * specified value, or a negative number if no keys map to the
+ * specified value.
+ * <p>Beware that this is a linear search, unlike lookups by key,
+ * and that multiple keys can map to the same value and this will
+ * find only one of them.
+ * <p>Note also that unlike most collections' {@code indexOf} methods,
+ * this method compares values using {@code ==} rather than {@code equals}.
+ */
+ public int indexOfValue(E value) {
+ if (mGarbage) {
+ gc();
+ }
+
+ for (int i = 0; i < mSize; i++) {
+ if (mValues[i] == value) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Returns an index for which {@link #valueAt} would return the
+ * specified value, or a negative number if no keys map to the
+ * specified value.
+ * <p>Beware that this is a linear search, unlike lookups by key,
+ * and that multiple keys can map to the same value and this will
+ * find only one of them.
+ * <p>Note also that this method uses {@code equals} unlike {@code indexOfValue}.
+ * @hide
+ */
+ public int indexOfValueByValue(E value) {
+ if (mGarbage) {
+ gc();
+ }
+
+ for (int i = 0; i < mSize; i++) {
+ if (value == null) {
+ if (mValues[i] == null) {
+ return i;
+ }
+ } else {
+ if (value.equals(mValues[i])) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Removes all key-value mappings from this SparseArray.
+ */
+ public void clear() {
+ int n = mSize;
+ Object[] values = mValues;
+
+ for (int i = 0; i < n; i++) {
+ values[i] = null;
+ }
+
+ mSize = 0;
+ mGarbage = false;
+ }
+
+ /**
+ * Puts a key/value pair into the array, optimizing for the case where
+ * the key is greater than all existing keys in the array.
+ */
+ public void append(int key, E value) {
+ if (mSize != 0 && key <= mKeys[mSize - 1]) {
+ put(key, value);
+ return;
+ }
+
+ if (mGarbage && mSize >= mKeys.length) {
+ gc();
+ }
+
+ mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
+ mValues = GrowingArrayUtils.append(mValues, mSize, value);
+ mSize++;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>This implementation composes a string by iterating over its mappings. If
+ * this map contains itself as a value, the string "(this Map)"
+ * will appear in its place.
+ */
+ @Override
+ public String toString() {
+ if (size() <= 0) {
+ return "{}";
+ }
+
+ StringBuilder buffer = new StringBuilder(mSize * 28);
+ buffer.append('{');
+ for (int i=0; i<mSize; i++) {
+ if (i > 0) {
+ buffer.append(", ");
+ }
+ int key = keyAt(i);
+ buffer.append(key);
+ buffer.append('=');
+ Object value = valueAt(i);
+ if (value != this) {
+ buffer.append(value);
+ } else {
+ buffer.append("(this Map)");
+ }
+ }
+ buffer.append('}');
+ return buffer.toString();
+ }
+}
diff --git a/android/util/SparseArrayMap.java b/android/util/SparseArrayMap.java
new file mode 100644
index 0000000..3ec6b81
--- /dev/null
+++ b/android/util/SparseArrayMap.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+
+import java.util.function.Consumer;
+
+/**
+ * A sparse array of ArrayMaps, which is suitable for holding (userId, packageName)->object
+ * associations.
+ *
+ * @param <T> Any class
+ * @hide
+ */
+@TestApi
+public class SparseArrayMap<T> {
+ private final SparseArray<ArrayMap<String, T>> mData = new SparseArray<>();
+
+ /** Add an entry associating obj with the int-String pair. */
+ public void add(int key, @NonNull String mapKey, @Nullable T obj) {
+ ArrayMap<String, T> data = mData.get(key);
+ if (data == null) {
+ data = new ArrayMap<>();
+ mData.put(key, data);
+ }
+ data.put(mapKey, obj);
+ }
+
+ /** Remove all entries from the map. */
+ public void clear() {
+ for (int i = 0; i < mData.size(); ++i) {
+ mData.valueAt(i).clear();
+ }
+ }
+
+ /** Return true if the structure contains an explicit entry for the int-String pair. */
+ public boolean contains(int key, @NonNull String mapKey) {
+ return mData.contains(key) && mData.get(key).containsKey(mapKey);
+ }
+
+ /** Removes all the data for the key, if there was any. */
+ public void delete(int key) {
+ mData.delete(key);
+ }
+
+ /**
+ * Removes the data for the key and mapKey, if there was any.
+ *
+ * @return Returns the value that was stored under the keys, or null if there was none.
+ */
+ @Nullable
+ public T delete(int key, @NonNull String mapKey) {
+ ArrayMap<String, T> data = mData.get(key);
+ if (data != null) {
+ return data.remove(mapKey);
+ }
+ return null;
+ }
+
+ /**
+ * Get the value associated with the int-String pair.
+ */
+ @Nullable
+ public T get(int key, @NonNull String mapKey) {
+ ArrayMap<String, T> data = mData.get(key);
+ if (data != null) {
+ return data.get(mapKey);
+ }
+ return null;
+ }
+
+ /**
+ * Returns the value to which the specified key and mapKey are mapped, or defaultValue if this
+ * map contains no mapping for them.
+ */
+ @Nullable
+ public T getOrDefault(int key, @NonNull String mapKey, T defaultValue) {
+ if (mData.contains(key)) {
+ ArrayMap<String, T> data = mData.get(key);
+ if (data != null && data.containsKey(mapKey)) {
+ return data.get(mapKey);
+ }
+ }
+ return defaultValue;
+ }
+
+ /** @see SparseArray#indexOfKey */
+ public int indexOfKey(int key) {
+ return mData.indexOfKey(key);
+ }
+
+ /**
+ * Returns the index of the mapKey.
+ *
+ * @see SparseArray#indexOfKey
+ */
+ public int indexOfKey(int key, @NonNull String mapKey) {
+ ArrayMap<String, T> data = mData.get(key);
+ if (data != null) {
+ return data.indexOfKey(mapKey);
+ }
+ return -1;
+ }
+
+ /** Returns the key at the given index. */
+ public int keyAt(int index) {
+ return mData.keyAt(index);
+ }
+
+ /** Returns the map's key at the given mapIndex for the given keyIndex. */
+ @NonNull
+ public String keyAt(int keyIndex, int mapIndex) {
+ return mData.valueAt(keyIndex).keyAt(mapIndex);
+ }
+
+ /** Returns the size of the outer array. */
+ public int numMaps() {
+ return mData.size();
+ }
+
+ /** Returns the number of elements in the map of the given key. */
+ public int numElementsForKey(int key) {
+ ArrayMap<String, T> data = mData.get(key);
+ return data == null ? 0 : data.size();
+ }
+
+ /** Returns the value T at the given key and map index. */
+ @Nullable
+ public T valueAt(int keyIndex, int mapIndex) {
+ return mData.valueAt(keyIndex).valueAt(mapIndex);
+ }
+
+ /** Iterate through all int-String pairs and operate on all of the values. */
+ public void forEach(@NonNull Consumer<T> consumer) {
+ for (int i = numMaps() - 1; i >= 0; --i) {
+ ArrayMap<String, T> data = mData.valueAt(i);
+ for (int j = data.size() - 1; j >= 0; --j) {
+ consumer.accept(data.valueAt(j));
+ }
+ }
+ }
+}
diff --git a/android/util/SparseBooleanArray.java b/android/util/SparseBooleanArray.java
new file mode 100644
index 0000000..846df39
--- /dev/null
+++ b/android/util/SparseBooleanArray.java
@@ -0,0 +1,350 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.compat.annotation.UnsupportedAppUsage;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.GrowingArrayUtils;
+
+import libcore.util.EmptyArray;
+
+/**
+ * SparseBooleanArrays map integers to booleans.
+ * Unlike a normal array of booleans
+ * there can be gaps in the indices. It is intended to be more memory efficient
+ * than using a HashMap to map Integers to Booleans, both because it avoids
+ * auto-boxing keys and values and its data structure doesn't rely on an extra entry object
+ * for each mapping.
+ *
+ * <p>Note that this container keeps its mappings in an array data structure,
+ * using a binary search to find keys. The implementation is not intended to be appropriate for
+ * data structures
+ * that may contain large numbers of items. It is generally slower than a traditional
+ * HashMap, since lookups require a binary search and adds and removes require inserting
+ * and deleting entries in the array. For containers holding up to hundreds of items,
+ * the performance difference is not significant, less than 50%.</p>
+ *
+ * <p>It is possible to iterate over the items in this container using
+ * {@link #keyAt(int)} and {@link #valueAt(int)}. Iterating over the keys using
+ * <code>keyAt(int)</code> with ascending values of the index will return the
+ * keys in ascending order, or the values corresponding to the keys in ascending
+ * order in the case of <code>valueAt(int)</code>.</p>
+ */
+public class SparseBooleanArray implements Cloneable {
+ /**
+ * Creates a new SparseBooleanArray containing no mappings.
+ */
+ public SparseBooleanArray() {
+ this(10);
+ }
+
+ /**
+ * Creates a new SparseBooleanArray containing no mappings that will not
+ * require any additional memory allocation to store the specified
+ * number of mappings. If you supply an initial capacity of 0, the
+ * sparse array will be initialized with a light-weight representation
+ * not requiring any additional array allocations.
+ */
+ public SparseBooleanArray(int initialCapacity) {
+ if (initialCapacity == 0) {
+ mKeys = EmptyArray.INT;
+ mValues = EmptyArray.BOOLEAN;
+ } else {
+ mKeys = ArrayUtils.newUnpaddedIntArray(initialCapacity);
+ mValues = new boolean[mKeys.length];
+ }
+ mSize = 0;
+ }
+
+ @Override
+ public SparseBooleanArray clone() {
+ SparseBooleanArray clone = null;
+ try {
+ clone = (SparseBooleanArray) super.clone();
+ clone.mKeys = mKeys.clone();
+ clone.mValues = mValues.clone();
+ } catch (CloneNotSupportedException cnse) {
+ /* ignore */
+ }
+ return clone;
+ }
+
+ /**
+ * Gets the boolean mapped from the specified key, or <code>false</code>
+ * if no such mapping has been made.
+ */
+ public boolean get(int key) {
+ return get(key, false);
+ }
+
+ /**
+ * Gets the boolean mapped from the specified key, or the specified value
+ * if no such mapping has been made.
+ */
+ public boolean get(int key, boolean valueIfKeyNotFound) {
+ int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+
+ if (i < 0) {
+ return valueIfKeyNotFound;
+ } else {
+ return mValues[i];
+ }
+ }
+
+ /**
+ * Removes the mapping from the specified key, if there was any.
+ */
+ public void delete(int key) {
+ int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+
+ if (i >= 0) {
+ System.arraycopy(mKeys, i + 1, mKeys, i, mSize - (i + 1));
+ System.arraycopy(mValues, i + 1, mValues, i, mSize - (i + 1));
+ mSize--;
+ }
+ }
+
+ /**
+ * Removes the mapping at the specified index.
+ * <p>
+ * For indices outside of the range {@code 0...size()-1}, the behavior is undefined.
+ */
+ public void removeAt(int index) {
+ System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1));
+ System.arraycopy(mValues, index + 1, mValues, index, mSize - (index + 1));
+ mSize--;
+ }
+
+ /**
+ * Adds a mapping from the specified key to the specified value,
+ * replacing the previous mapping from the specified key if there
+ * was one.
+ */
+ public void put(int key, boolean value) {
+ int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+
+ if (i >= 0) {
+ mValues[i] = value;
+ } else {
+ i = ~i;
+
+ mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
+ mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
+ mSize++;
+ }
+ }
+
+ /**
+ * Returns the number of key-value mappings that this SparseBooleanArray
+ * currently stores.
+ */
+ public int size() {
+ return mSize;
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, returns
+ * the key from the <code>index</code>th key-value mapping that this
+ * SparseBooleanArray stores.
+ *
+ * <p>The keys corresponding to indices in ascending order are guaranteed to
+ * be in ascending order, e.g., <code>keyAt(0)</code> will return the
+ * smallest key and <code>keyAt(size()-1)</code> will return the largest
+ * key.</p>
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ */
+ public int keyAt(int index) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ return mKeys[index];
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, returns
+ * the value from the <code>index</code>th key-value mapping that this
+ * SparseBooleanArray stores.
+ *
+ * <p>The values corresponding to indices in ascending order are guaranteed
+ * to be associated with keys in ascending order, e.g.,
+ * <code>valueAt(0)</code> will return the value associated with the
+ * smallest key and <code>valueAt(size()-1)</code> will return the value
+ * associated with the largest key.</p>
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ */
+ public boolean valueAt(int index) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ return mValues[index];
+ }
+
+ /**
+ * Directly set the value at a particular index.
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ */
+ public void setValueAt(int index, boolean value) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ mValues[index] = value;
+ }
+
+ /** @hide */
+ public void setKeyAt(int index, int key) {
+ if (index >= mSize) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ mKeys[index] = key;
+ }
+
+ /**
+ * Returns the index for which {@link #keyAt} would return the
+ * specified key, or a negative number if the specified
+ * key is not mapped.
+ */
+ public int indexOfKey(int key) {
+ return ContainerHelpers.binarySearch(mKeys, mSize, key);
+ }
+
+ /**
+ * Returns an index for which {@link #valueAt} would return the
+ * specified key, or a negative number if no keys map to the
+ * specified value.
+ * Beware that this is a linear search, unlike lookups by key,
+ * and that multiple keys can map to the same value and this will
+ * find only one of them.
+ */
+ public int indexOfValue(boolean value) {
+ for (int i = 0; i < mSize; i++)
+ if (mValues[i] == value)
+ return i;
+
+ return -1;
+ }
+
+ /**
+ * Removes all key-value mappings from this SparseBooleanArray.
+ */
+ public void clear() {
+ mSize = 0;
+ }
+
+ /**
+ * Puts a key/value pair into the array, optimizing for the case where
+ * the key is greater than all existing keys in the array.
+ */
+ public void append(int key, boolean value) {
+ if (mSize != 0 && key <= mKeys[mSize - 1]) {
+ put(key, value);
+ return;
+ }
+
+ mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
+ mValues = GrowingArrayUtils.append(mValues, mSize, value);
+ mSize++;
+ }
+
+ @Override
+ public int hashCode() {
+ int hashCode = mSize;
+ for (int i = 0; i < mSize; i++) {
+ hashCode = 31 * hashCode + mKeys[i] | (mValues[i] ? 1 : 0);
+ }
+ return hashCode;
+ }
+
+ @Override
+ public boolean equals(Object that) {
+ if (this == that) {
+ return true;
+ }
+
+ if (!(that instanceof SparseBooleanArray)) {
+ return false;
+ }
+
+ SparseBooleanArray other = (SparseBooleanArray) that;
+ if (mSize != other.mSize) {
+ return false;
+ }
+
+ for (int i = 0; i < mSize; i++) {
+ if (mKeys[i] != other.mKeys[i]) {
+ return false;
+ }
+ if (mValues[i] != other.mValues[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>This implementation composes a string by iterating over its mappings.
+ */
+ @Override
+ public String toString() {
+ if (size() <= 0) {
+ return "{}";
+ }
+
+ StringBuilder buffer = new StringBuilder(mSize * 28);
+ buffer.append('{');
+ for (int i=0; i<mSize; i++) {
+ if (i > 0) {
+ buffer.append(", ");
+ }
+ int key = keyAt(i);
+ buffer.append(key);
+ buffer.append('=');
+ boolean value = valueAt(i);
+ buffer.append(value);
+ }
+ buffer.append('}');
+ return buffer.toString();
+ }
+
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Use keyAt(int)
+ private int[] mKeys;
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Use valueAt(int), setValueAt(int, boolean)
+ private boolean[] mValues;
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Use size()
+ private int mSize;
+}
diff --git a/android/util/SparseIntArray.java b/android/util/SparseIntArray.java
new file mode 100644
index 0000000..d4f6685
--- /dev/null
+++ b/android/util/SparseIntArray.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.compat.annotation.UnsupportedAppUsage;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.GrowingArrayUtils;
+
+import libcore.util.EmptyArray;
+
+import java.util.Arrays;
+
+/**
+ * SparseIntArrays map integers to integers. Unlike a normal array of integers,
+ * there can be gaps in the indices. It is intended to be more memory efficient
+ * than using a HashMap to map Integers to Integers, both because it avoids
+ * auto-boxing keys and values and its data structure doesn't rely on an extra entry object
+ * for each mapping.
+ *
+ * <p>Note that this container keeps its mappings in an array data structure,
+ * using a binary search to find keys. The implementation is not intended to be appropriate for
+ * data structures
+ * that may contain large numbers of items. It is generally slower than a traditional
+ * HashMap, since lookups require a binary search and adds and removes require inserting
+ * and deleting entries in the array. For containers holding up to hundreds of items,
+ * the performance difference is not significant, less than 50%.</p>
+ *
+ * <p>It is possible to iterate over the items in this container using
+ * {@link #keyAt(int)} and {@link #valueAt(int)}. Iterating over the keys using
+ * <code>keyAt(int)</code> with ascending values of the index will return the
+ * keys in ascending order, or the values corresponding to the keys in ascending
+ * order in the case of <code>valueAt(int)</code>.</p>
+ */
+public class SparseIntArray implements Cloneable {
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Use keyAt(int)
+ private int[] mKeys;
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Use valueAt(int), setValueAt(int, int)
+ private int[] mValues;
+ @UnsupportedAppUsage(maxTargetSdk = 28) // Use size()
+ private int mSize;
+
+ /**
+ * Creates a new SparseIntArray containing no mappings.
+ */
+ public SparseIntArray() {
+ this(10);
+ }
+
+ /**
+ * Creates a new SparseIntArray containing no mappings that will not
+ * require any additional memory allocation to store the specified
+ * number of mappings. If you supply an initial capacity of 0, the
+ * sparse array will be initialized with a light-weight representation
+ * not requiring any additional array allocations.
+ */
+ public SparseIntArray(int initialCapacity) {
+ if (initialCapacity == 0) {
+ mKeys = EmptyArray.INT;
+ mValues = EmptyArray.INT;
+ } else {
+ mKeys = ArrayUtils.newUnpaddedIntArray(initialCapacity);
+ mValues = new int[mKeys.length];
+ }
+ mSize = 0;
+ }
+
+ @Override
+ public SparseIntArray clone() {
+ SparseIntArray clone = null;
+ try {
+ clone = (SparseIntArray) super.clone();
+ clone.mKeys = mKeys.clone();
+ clone.mValues = mValues.clone();
+ } catch (CloneNotSupportedException cnse) {
+ /* ignore */
+ }
+ return clone;
+ }
+
+ /**
+ * Gets the int mapped from the specified key, or <code>0</code>
+ * if no such mapping has been made.
+ */
+ public int get(int key) {
+ return get(key, 0);
+ }
+
+ /**
+ * Gets the int mapped from the specified key, or the specified value
+ * if no such mapping has been made.
+ */
+ public int get(int key, int valueIfKeyNotFound) {
+ int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+
+ if (i < 0) {
+ return valueIfKeyNotFound;
+ } else {
+ return mValues[i];
+ }
+ }
+
+ /**
+ * Removes the mapping from the specified key, if there was any.
+ */
+ public void delete(int key) {
+ int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+
+ if (i >= 0) {
+ removeAt(i);
+ }
+ }
+
+ /**
+ * Removes the mapping at the given index.
+ */
+ public void removeAt(int index) {
+ System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1));
+ System.arraycopy(mValues, index + 1, mValues, index, mSize - (index + 1));
+ mSize--;
+ }
+
+ /**
+ * Adds a mapping from the specified key to the specified value,
+ * replacing the previous mapping from the specified key if there
+ * was one.
+ */
+ public void put(int key, int value) {
+ int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+
+ if (i >= 0) {
+ mValues[i] = value;
+ } else {
+ i = ~i;
+
+ mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
+ mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
+ mSize++;
+ }
+ }
+
+ /**
+ * Returns the number of key-value mappings that this SparseIntArray
+ * currently stores.
+ */
+ public int size() {
+ return mSize;
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, returns
+ * the key from the <code>index</code>th key-value mapping that this
+ * SparseIntArray stores.
+ *
+ * <p>The keys corresponding to indices in ascending order are guaranteed to
+ * be in ascending order, e.g., <code>keyAt(0)</code> will return the
+ * smallest key and <code>keyAt(size()-1)</code> will return the largest
+ * key.</p>
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ */
+ public int keyAt(int index) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ return mKeys[index];
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, returns
+ * the value from the <code>index</code>th key-value mapping that this
+ * SparseIntArray stores.
+ *
+ * <p>The values corresponding to indices in ascending order are guaranteed
+ * to be associated with keys in ascending order, e.g.,
+ * <code>valueAt(0)</code> will return the value associated with the
+ * smallest key and <code>valueAt(size()-1)</code> will return the value
+ * associated with the largest key.</p>
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ */
+ public int valueAt(int index) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ return mValues[index];
+ }
+
+ /**
+ * Directly set the value at a particular index.
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ */
+ public void setValueAt(int index, int value) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ mValues[index] = value;
+ }
+
+ /**
+ * Returns the index for which {@link #keyAt} would return the
+ * specified key, or a negative number if the specified
+ * key is not mapped.
+ */
+ public int indexOfKey(int key) {
+ return ContainerHelpers.binarySearch(mKeys, mSize, key);
+ }
+
+ /**
+ * Returns an index for which {@link #valueAt} would return the
+ * specified key, or a negative number if no keys map to the
+ * specified value.
+ * Beware that this is a linear search, unlike lookups by key,
+ * and that multiple keys can map to the same value and this will
+ * find only one of them.
+ */
+ public int indexOfValue(int value) {
+ for (int i = 0; i < mSize; i++)
+ if (mValues[i] == value)
+ return i;
+
+ return -1;
+ }
+
+ /**
+ * Removes all key-value mappings from this SparseIntArray.
+ */
+ public void clear() {
+ mSize = 0;
+ }
+
+ /**
+ * Puts a key/value pair into the array, optimizing for the case where
+ * the key is greater than all existing keys in the array.
+ */
+ public void append(int key, int value) {
+ if (mSize != 0 && key <= mKeys[mSize - 1]) {
+ put(key, value);
+ return;
+ }
+
+ mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
+ mValues = GrowingArrayUtils.append(mValues, mSize, value);
+ mSize++;
+ }
+
+ /**
+ * Provides a copy of keys.
+ *
+ * @hide
+ * */
+ public int[] copyKeys() {
+ if (size() == 0) {
+ return null;
+ }
+ return Arrays.copyOf(mKeys, size());
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>This implementation composes a string by iterating over its mappings.
+ */
+ @Override
+ public String toString() {
+ if (size() <= 0) {
+ return "{}";
+ }
+
+ StringBuilder buffer = new StringBuilder(mSize * 28);
+ buffer.append('{');
+ for (int i=0; i<mSize; i++) {
+ if (i > 0) {
+ buffer.append(", ");
+ }
+ int key = keyAt(i);
+ buffer.append(key);
+ buffer.append('=');
+ int value = valueAt(i);
+ buffer.append(value);
+ }
+ buffer.append('}');
+ return buffer.toString();
+ }
+}
diff --git a/android/util/SparseLongArray.java b/android/util/SparseLongArray.java
new file mode 100644
index 0000000..b0916d3
--- /dev/null
+++ b/android/util/SparseLongArray.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.GrowingArrayUtils;
+
+import libcore.util.EmptyArray;
+
+/**
+ * SparseLongArrays map integers to longs. Unlike a normal array of longs,
+ * there can be gaps in the indices. It is intended to be more memory efficient
+ * than using a HashMap to map Integers to Longs, both because it avoids
+ * auto-boxing keys and values and its data structure doesn't rely on an extra entry object
+ * for each mapping.
+ *
+ * <p>Note that this container keeps its mappings in an array data structure,
+ * using a binary search to find keys. The implementation is not intended to be appropriate for
+ * data structures
+ * that may contain large numbers of items. It is generally slower than a traditional
+ * HashMap, since lookups require a binary search and adds and removes require inserting
+ * and deleting entries in the array. For containers holding up to hundreds of items,
+ * the performance difference is not significant, less than 50%.</p>
+ *
+ * <p>It is possible to iterate over the items in this container using
+ * {@link #keyAt(int)} and {@link #valueAt(int)}. Iterating over the keys using
+ * <code>keyAt(int)</code> with ascending values of the index will return the
+ * keys in ascending order, or the values corresponding to the keys in ascending
+ * order in the case of <code>valueAt(int)</code>.</p>
+ */
+public class SparseLongArray implements Cloneable {
+ private int[] mKeys;
+ private long[] mValues;
+ private int mSize;
+
+ /**
+ * Creates a new SparseLongArray containing no mappings.
+ */
+ public SparseLongArray() {
+ this(10);
+ }
+
+ /**
+ * Creates a new SparseLongArray containing no mappings that will not
+ * require any additional memory allocation to store the specified
+ * number of mappings. If you supply an initial capacity of 0, the
+ * sparse array will be initialized with a light-weight representation
+ * not requiring any additional array allocations.
+ */
+ public SparseLongArray(int initialCapacity) {
+ if (initialCapacity == 0) {
+ mKeys = EmptyArray.INT;
+ mValues = EmptyArray.LONG;
+ } else {
+ mValues = ArrayUtils.newUnpaddedLongArray(initialCapacity);
+ mKeys = new int[mValues.length];
+ }
+ mSize = 0;
+ }
+
+ @Override
+ public SparseLongArray clone() {
+ SparseLongArray clone = null;
+ try {
+ clone = (SparseLongArray) super.clone();
+ clone.mKeys = mKeys.clone();
+ clone.mValues = mValues.clone();
+ } catch (CloneNotSupportedException cnse) {
+ /* ignore */
+ }
+ return clone;
+ }
+
+ /**
+ * Gets the long mapped from the specified key, or <code>0</code>
+ * if no such mapping has been made.
+ */
+ public long get(int key) {
+ return get(key, 0);
+ }
+
+ /**
+ * Gets the long mapped from the specified key, or the specified value
+ * if no such mapping has been made.
+ */
+ public long get(int key, long valueIfKeyNotFound) {
+ int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+
+ if (i < 0) {
+ return valueIfKeyNotFound;
+ } else {
+ return mValues[i];
+ }
+ }
+
+ /**
+ * Removes the mapping from the specified key, if there was any.
+ */
+ public void delete(int key) {
+ int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+
+ if (i >= 0) {
+ removeAt(i);
+ }
+ }
+
+ /**
+ * @hide
+ * Remove a range of mappings as a batch.
+ *
+ * @param index Index to begin at
+ * @param size Number of mappings to remove
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>,
+ * the behavior is undefined.</p>
+ */
+ public void removeAtRange(int index, int size) {
+ size = Math.min(size, mSize - index);
+ System.arraycopy(mKeys, index + size, mKeys, index, mSize - (index + size));
+ System.arraycopy(mValues, index + size, mValues, index, mSize - (index + size));
+ mSize -= size;
+ }
+
+ /**
+ * Removes the mapping at the given index.
+ */
+ public void removeAt(int index) {
+ System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1));
+ System.arraycopy(mValues, index + 1, mValues, index, mSize - (index + 1));
+ mSize--;
+ }
+
+ /**
+ * Adds a mapping from the specified key to the specified value,
+ * replacing the previous mapping from the specified key if there
+ * was one.
+ */
+ public void put(int key, long value) {
+ int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+
+ if (i >= 0) {
+ mValues[i] = value;
+ } else {
+ i = ~i;
+
+ mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
+ mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
+ mSize++;
+ }
+ }
+
+ /**
+ * Returns the number of key-value mappings that this SparseIntArray
+ * currently stores.
+ */
+ public int size() {
+ return mSize;
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, returns
+ * the key from the <code>index</code>th key-value mapping that this
+ * SparseLongArray stores.
+ *
+ * <p>The keys corresponding to indices in ascending order are guaranteed to
+ * be in ascending order, e.g., <code>keyAt(0)</code> will return the
+ * smallest key and <code>keyAt(size()-1)</code> will return the largest
+ * key.</p>
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ */
+ public int keyAt(int index) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ return mKeys[index];
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, returns
+ * the value from the <code>index</code>th key-value mapping that this
+ * SparseLongArray stores.
+ *
+ * <p>The values corresponding to indices in ascending order are guaranteed
+ * to be associated with keys in ascending order, e.g.,
+ * <code>valueAt(0)</code> will return the value associated with the
+ * smallest key and <code>valueAt(size()-1)</code> will return the value
+ * associated with the largest key.</p>
+ *
+ * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.</p>
+ */
+ public long valueAt(int index) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ return mValues[index];
+ }
+
+ /**
+ * Returns the index for which {@link #keyAt} would return the
+ * specified key, or a negative number if the specified
+ * key is not mapped.
+ */
+ public int indexOfKey(int key) {
+ return ContainerHelpers.binarySearch(mKeys, mSize, key);
+ }
+
+ /**
+ * Returns an index for which {@link #valueAt} would return the
+ * specified key, or a negative number if no keys map to the
+ * specified value.
+ * Beware that this is a linear search, unlike lookups by key,
+ * and that multiple keys can map to the same value and this will
+ * find only one of them.
+ */
+ public int indexOfValue(long value) {
+ for (int i = 0; i < mSize; i++)
+ if (mValues[i] == value)
+ return i;
+
+ return -1;
+ }
+
+ /**
+ * Removes all key-value mappings from this SparseIntArray.
+ */
+ public void clear() {
+ mSize = 0;
+ }
+
+ /**
+ * Puts a key/value pair into the array, optimizing for the case where
+ * the key is greater than all existing keys in the array.
+ */
+ public void append(int key, long value) {
+ if (mSize != 0 && key <= mKeys[mSize - 1]) {
+ put(key, value);
+ return;
+ }
+
+ mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
+ mValues = GrowingArrayUtils.append(mValues, mSize, value);
+ mSize++;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>This implementation composes a string by iterating over its mappings.
+ */
+ @Override
+ public String toString() {
+ if (size() <= 0) {
+ return "{}";
+ }
+
+ StringBuilder buffer = new StringBuilder(mSize * 28);
+ buffer.append('{');
+ for (int i=0; i<mSize; i++) {
+ if (i > 0) {
+ buffer.append(", ");
+ }
+ int key = keyAt(i);
+ buffer.append(key);
+ buffer.append('=');
+ long value = valueAt(i);
+ buffer.append(value);
+ }
+ buffer.append('}');
+ return buffer.toString();
+ }
+}
diff --git a/android/util/SparseSetArray.java b/android/util/SparseSetArray.java
new file mode 100644
index 0000000..f5025f7
--- /dev/null
+++ b/android/util/SparseSetArray.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.util;
+
+/**
+ * A sparse array of ArraySets, which is suitable to hold userid->packages association.
+ *
+ * @hide
+ */
+public class SparseSetArray<T> {
+ private final SparseArray<ArraySet<T>> mData = new SparseArray<>();
+
+ public SparseSetArray() {
+ }
+
+ /**
+ * Add a value for key n.
+ * @return FALSE when the value already existed for the given key, TRUE otherwise.
+ */
+ public boolean add(int n, T value) {
+ ArraySet<T> set = mData.get(n);
+ if (set == null) {
+ set = new ArraySet<>();
+ mData.put(n, set);
+ }
+ if (set.contains(value)) {
+ return false;
+ }
+ set.add(value);
+ return true;
+ }
+
+ /**
+ * Removes all mappings from this SparseSetArray.
+ */
+ public void clear() {
+ mData.clear();
+ }
+
+ /**
+ * @return whether the value exists for the key n.
+ */
+ public boolean contains(int n, T value) {
+ final ArraySet<T> set = mData.get(n);
+ if (set == null) {
+ return false;
+ }
+ return set.contains(value);
+ }
+
+ /**
+ * @return the set of items of key n
+ */
+ public ArraySet<T> get(int n) {
+ return mData.get(n);
+ }
+
+ /**
+ * Remove a value for key n.
+ * @return TRUE when the value existed for the given key and removed, FALSE otherwise.
+ */
+ public boolean remove(int n, T value) {
+ final ArraySet<T> set = mData.get(n);
+ if (set == null) {
+ return false;
+ }
+ final boolean ret = set.remove(value);
+ if (set.size() == 0) {
+ mData.remove(n);
+ }
+ return ret;
+ }
+
+ /**
+ * Remove all values for key n.
+ */
+ public void remove(int n) {
+ mData.remove(n);
+ }
+ public int size() {
+ return mData.size();
+ }
+
+ public int keyAt(int index) {
+ return mData.keyAt(index);
+ }
+
+ public int sizeAt(int index) {
+ final ArraySet<T> set = mData.valueAt(index);
+ if (set == null) {
+ return 0;
+ }
+ return set.size();
+ }
+
+ public T valueAt(int intIndex, int valueIndex) {
+ return mData.valueAt(intIndex).valueAt(valueIndex);
+ }
+}
diff --git a/android/util/Spline.java b/android/util/Spline.java
new file mode 100644
index 0000000..1037096
--- /dev/null
+++ b/android/util/Spline.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * Performs spline interpolation given a set of control points.
+ * @hide
+ */
+public abstract class Spline {
+
+ /**
+ * Interpolates the value of Y = f(X) for given X.
+ * Clamps X to the domain of the spline.
+ *
+ * @param x The X value.
+ * @return The interpolated Y = f(X) value.
+ */
+ public abstract float interpolate(float x);
+
+ /**
+ * Creates an appropriate spline based on the properties of the control points.
+ *
+ * If the control points are monotonic then the resulting spline will preserve that and
+ * otherwise optimize for error bounds.
+ */
+ public static Spline createSpline(float[] x, float[] y) {
+ if (!isStrictlyIncreasing(x)) {
+ throw new IllegalArgumentException("The control points must all have strictly "
+ + "increasing X values.");
+ }
+
+ if (isMonotonic(y)) {
+ return createMonotoneCubicSpline(x, y);
+ } else {
+ return createLinearSpline(x, y);
+ }
+ }
+
+ /**
+ * Creates a monotone cubic spline from a given set of control points.
+ *
+ * The spline is guaranteed to pass through each control point exactly.
+ * Moreover, assuming the control points are monotonic (Y is non-decreasing or
+ * non-increasing) then the interpolated values will also be monotonic.
+ *
+ * This function uses the Fritsch-Carlson method for computing the spline parameters.
+ * http://en.wikipedia.org/wiki/Monotone_cubic_interpolation
+ *
+ * @param x The X component of the control points, strictly increasing.
+ * @param y The Y component of the control points, monotonic.
+ * @return
+ *
+ * @throws IllegalArgumentException if the X or Y arrays are null, have
+ * different lengths or have fewer than 2 values.
+ * @throws IllegalArgumentException if the control points are not monotonic.
+ */
+ public static Spline createMonotoneCubicSpline(float[] x, float[] y) {
+ return new MonotoneCubicSpline(x, y);
+ }
+
+ /**
+ * Creates a linear spline from a given set of control points.
+ *
+ * Like a monotone cubic spline, the interpolated curve will be monotonic if the control points
+ * are monotonic.
+ *
+ * @param x The X component of the control points, strictly increasing.
+ * @param y The Y component of the control points.
+ * @return
+ *
+ * @throws IllegalArgumentException if the X or Y arrays are null, have
+ * different lengths or have fewer than 2 values.
+ * @throws IllegalArgumentException if the X components of the control points are not strictly
+ * increasing.
+ */
+ public static Spline createLinearSpline(float[] x, float[] y) {
+ return new LinearSpline(x, y);
+ }
+
+ private static boolean isStrictlyIncreasing(float[] x) {
+ if (x == null || x.length < 2) {
+ throw new IllegalArgumentException("There must be at least two control points.");
+ }
+ float prev = x[0];
+ for (int i = 1; i < x.length; i++) {
+ float curr = x[i];
+ if (curr <= prev) {
+ return false;
+ }
+ prev = curr;
+ }
+ return true;
+ }
+
+ private static boolean isMonotonic(float[] x) {
+ if (x == null || x.length < 2) {
+ throw new IllegalArgumentException("There must be at least two control points.");
+ }
+ float prev = x[0];
+ for (int i = 1; i < x.length; i++) {
+ float curr = x[i];
+ if (curr < prev) {
+ return false;
+ }
+ prev = curr;
+ }
+ return true;
+ }
+
+ public static class MonotoneCubicSpline extends Spline {
+ private float[] mX;
+ private float[] mY;
+ private float[] mM;
+
+ public MonotoneCubicSpline(float[] x, float[] y) {
+ if (x == null || y == null || x.length != y.length || x.length < 2) {
+ throw new IllegalArgumentException("There must be at least two control "
+ + "points and the arrays must be of equal length.");
+ }
+
+ final int n = x.length;
+ float[] d = new float[n - 1]; // could optimize this out
+ float[] m = new float[n];
+
+ // Compute slopes of secant lines between successive points.
+ for (int i = 0; i < n - 1; i++) {
+ float h = x[i + 1] - x[i];
+ if (h <= 0f) {
+ throw new IllegalArgumentException("The control points must all "
+ + "have strictly increasing X values.");
+ }
+ d[i] = (y[i + 1] - y[i]) / h;
+ }
+
+ // Initialize the tangents as the average of the secants.
+ m[0] = d[0];
+ for (int i = 1; i < n - 1; i++) {
+ m[i] = (d[i - 1] + d[i]) * 0.5f;
+ }
+ m[n - 1] = d[n - 2];
+
+ // Update the tangents to preserve monotonicity.
+ for (int i = 0; i < n - 1; i++) {
+ if (d[i] == 0f) { // successive Y values are equal
+ m[i] = 0f;
+ m[i + 1] = 0f;
+ } else {
+ float a = m[i] / d[i];
+ float b = m[i + 1] / d[i];
+ if (a < 0f || b < 0f) {
+ throw new IllegalArgumentException("The control points must have "
+ + "monotonic Y values.");
+ }
+ float h = (float) Math.hypot(a, b);
+ if (h > 3f) {
+ float t = 3f / h;
+ m[i] *= t;
+ m[i + 1] *= t;
+ }
+ }
+ }
+
+ mX = x;
+ mY = y;
+ mM = m;
+ }
+
+ @Override
+ public float interpolate(float x) {
+ // Handle the boundary cases.
+ final int n = mX.length;
+ if (Float.isNaN(x)) {
+ return x;
+ }
+ if (x <= mX[0]) {
+ return mY[0];
+ }
+ if (x >= mX[n - 1]) {
+ return mY[n - 1];
+ }
+
+ // Find the index 'i' of the last point with smaller X.
+ // We know this will be within the spline due to the boundary tests.
+ int i = 0;
+ while (x >= mX[i + 1]) {
+ i += 1;
+ if (x == mX[i]) {
+ return mY[i];
+ }
+ }
+
+ // Perform cubic Hermite spline interpolation.
+ float h = mX[i + 1] - mX[i];
+ float t = (x - mX[i]) / h;
+ return (mY[i] * (1 + 2 * t) + h * mM[i] * t) * (1 - t) * (1 - t)
+ + (mY[i + 1] * (3 - 2 * t) + h * mM[i + 1] * (t - 1)) * t * t;
+ }
+
+ // For debugging.
+ @Override
+ public String toString() {
+ StringBuilder str = new StringBuilder();
+ final int n = mX.length;
+ str.append("MonotoneCubicSpline{[");
+ for (int i = 0; i < n; i++) {
+ if (i != 0) {
+ str.append(", ");
+ }
+ str.append("(").append(mX[i]);
+ str.append(", ").append(mY[i]);
+ str.append(": ").append(mM[i]).append(")");
+ }
+ str.append("]}");
+ return str.toString();
+ }
+ }
+
+ public static class LinearSpline extends Spline {
+ private final float[] mX;
+ private final float[] mY;
+ private final float[] mM;
+
+ public LinearSpline(float[] x, float[] y) {
+ if (x == null || y == null || x.length != y.length || x.length < 2) {
+ throw new IllegalArgumentException("There must be at least two control "
+ + "points and the arrays must be of equal length.");
+ }
+ final int N = x.length;
+ mM = new float[N-1];
+ for (int i = 0; i < N-1; i++) {
+ mM[i] = (y[i+1] - y[i]) / (x[i+1] - x[i]);
+ }
+ mX = x;
+ mY = y;
+ }
+
+ @Override
+ public float interpolate(float x) {
+ // Handle the boundary cases.
+ final int n = mX.length;
+ if (Float.isNaN(x)) {
+ return x;
+ }
+ if (x <= mX[0]) {
+ return mY[0];
+ }
+ if (x >= mX[n - 1]) {
+ return mY[n - 1];
+ }
+
+ // Find the index 'i' of the last point with smaller X.
+ // We know this will be within the spline due to the boundary tests.
+ int i = 0;
+ while (x >= mX[i + 1]) {
+ i += 1;
+ if (x == mX[i]) {
+ return mY[i];
+ }
+ }
+ return mY[i] + mM[i] * (x - mX[i]);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder str = new StringBuilder();
+ final int n = mX.length;
+ str.append("LinearSpline{[");
+ for (int i = 0; i < n; i++) {
+ if (i != 0) {
+ str.append(", ");
+ }
+ str.append("(").append(mX[i]);
+ str.append(", ").append(mY[i]);
+ if (i < n-1) {
+ str.append(": ").append(mM[i]);
+ }
+ str.append(")");
+ }
+ str.append("]}");
+ return str.toString();
+ }
+ }
+}
diff --git a/android/util/StateSet.java b/android/util/StateSet.java
new file mode 100644
index 0000000..4bbc0f8
--- /dev/null
+++ b/android/util/StateSet.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import com.android.internal.R;
+
+/**
+ * State sets are arrays of positive ints where each element
+ * represents the state of a {@link android.view.View} (e.g. focused,
+ * selected, visible, etc.). A {@link android.view.View} may be in
+ * one or more of those states.
+ *
+ * A state spec is an array of signed ints where each element
+ * represents a required (if positive) or an undesired (if negative)
+ * {@link android.view.View} state.
+ *
+ * Utils dealing with state sets.
+ *
+ * In theory we could encapsulate the state set and state spec arrays
+ * and not have static methods here but there is some concern about
+ * performance since these methods are called during view drawing.
+ */
+
+public class StateSet {
+ /**
+ * The order here is very important to
+ * {@link android.view.View#getDrawableState()}
+ */
+ private static final int[][] VIEW_STATE_SETS;
+
+ /** @hide */
+ public static final int VIEW_STATE_WINDOW_FOCUSED = 1;
+ /** @hide */
+ public static final int VIEW_STATE_SELECTED = 1 << 1;
+ /** @hide */
+ public static final int VIEW_STATE_FOCUSED = 1 << 2;
+ /** @hide */
+ public static final int VIEW_STATE_ENABLED = 1 << 3;
+ /** @hide */
+ public static final int VIEW_STATE_PRESSED = 1 << 4;
+ /** @hide */
+ public static final int VIEW_STATE_ACTIVATED = 1 << 5;
+ /** @hide */
+ public static final int VIEW_STATE_ACCELERATED = 1 << 6;
+ /** @hide */
+ public static final int VIEW_STATE_HOVERED = 1 << 7;
+ /** @hide */
+ public static final int VIEW_STATE_DRAG_CAN_ACCEPT = 1 << 8;
+ /** @hide */
+ public static final int VIEW_STATE_DRAG_HOVERED = 1 << 9;
+
+ static final int[] VIEW_STATE_IDS = new int[] {
+ R.attr.state_window_focused, VIEW_STATE_WINDOW_FOCUSED,
+ R.attr.state_selected, VIEW_STATE_SELECTED,
+ R.attr.state_focused, VIEW_STATE_FOCUSED,
+ R.attr.state_enabled, VIEW_STATE_ENABLED,
+ R.attr.state_pressed, VIEW_STATE_PRESSED,
+ R.attr.state_activated, VIEW_STATE_ACTIVATED,
+ R.attr.state_accelerated, VIEW_STATE_ACCELERATED,
+ R.attr.state_hovered, VIEW_STATE_HOVERED,
+ R.attr.state_drag_can_accept, VIEW_STATE_DRAG_CAN_ACCEPT,
+ R.attr.state_drag_hovered, VIEW_STATE_DRAG_HOVERED
+ };
+
+ static {
+ if ((VIEW_STATE_IDS.length / 2) != R.styleable.ViewDrawableStates.length) {
+ throw new IllegalStateException(
+ "VIEW_STATE_IDs array length does not match ViewDrawableStates style array");
+ }
+
+ final int[] orderedIds = new int[VIEW_STATE_IDS.length];
+ for (int i = 0; i < R.styleable.ViewDrawableStates.length; i++) {
+ final int viewState = R.styleable.ViewDrawableStates[i];
+ for (int j = 0; j < VIEW_STATE_IDS.length; j += 2) {
+ if (VIEW_STATE_IDS[j] == viewState) {
+ orderedIds[i * 2] = viewState;
+ orderedIds[i * 2 + 1] = VIEW_STATE_IDS[j + 1];
+ }
+ }
+ }
+
+ final int NUM_BITS = VIEW_STATE_IDS.length / 2;
+ VIEW_STATE_SETS = new int[1 << NUM_BITS][];
+ for (int i = 0; i < VIEW_STATE_SETS.length; i++) {
+ final int numBits = Integer.bitCount(i);
+ final int[] set = new int[numBits];
+ int pos = 0;
+ for (int j = 0; j < orderedIds.length; j += 2) {
+ if ((i & orderedIds[j + 1]) != 0) {
+ set[pos++] = orderedIds[j];
+ }
+ }
+ VIEW_STATE_SETS[i] = set;
+ }
+ }
+
+ /** @hide */
+ public static int[] get(int mask) {
+ if (mask >= VIEW_STATE_SETS.length) {
+ throw new IllegalArgumentException("Invalid state set mask");
+ }
+ return VIEW_STATE_SETS[mask];
+ }
+
+ /** @hide */
+ public StateSet() {}
+
+ /**
+ * A state specification that will be matched by all StateSets.
+ */
+ public static final int[] WILD_CARD = new int[0];
+
+ /**
+ * A state set that does not contain any valid states.
+ */
+ public static final int[] NOTHING = new int[] { 0 };
+
+ /**
+ * Return whether the stateSetOrSpec is matched by all StateSets.
+ *
+ * @param stateSetOrSpec a state set or state spec.
+ */
+ public static boolean isWildCard(int[] stateSetOrSpec) {
+ return stateSetOrSpec.length == 0 || stateSetOrSpec[0] == 0;
+ }
+
+ /**
+ * Return whether the stateSet matches the desired stateSpec.
+ *
+ * @param stateSpec an array of required (if positive) or
+ * prohibited (if negative) {@link android.view.View} states.
+ * @param stateSet an array of {@link android.view.View} states
+ */
+ public static boolean stateSetMatches(int[] stateSpec, int[] stateSet) {
+ if (stateSet == null) {
+ return (stateSpec == null || isWildCard(stateSpec));
+ }
+ int stateSpecSize = stateSpec.length;
+ int stateSetSize = stateSet.length;
+ for (int i = 0; i < stateSpecSize; i++) {
+ int stateSpecState = stateSpec[i];
+ if (stateSpecState == 0) {
+ // We've reached the end of the cases to match against.
+ return true;
+ }
+ final boolean mustMatch;
+ if (stateSpecState > 0) {
+ mustMatch = true;
+ } else {
+ // We use negative values to indicate must-NOT-match states.
+ mustMatch = false;
+ stateSpecState = -stateSpecState;
+ }
+ boolean found = false;
+ for (int j = 0; j < stateSetSize; j++) {
+ final int state = stateSet[j];
+ if (state == 0) {
+ // We've reached the end of states to match.
+ if (mustMatch) {
+ // We didn't find this must-match state.
+ return false;
+ } else {
+ // Continue checking other must-not-match states.
+ break;
+ }
+ }
+ if (state == stateSpecState) {
+ if (mustMatch) {
+ found = true;
+ // Continue checking other other must-match states.
+ break;
+ } else {
+ // Any match of a must-not-match state returns false.
+ return false;
+ }
+ }
+ }
+ if (mustMatch && !found) {
+ // We've reached the end of states to match and we didn't
+ // find a must-match state.
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Return whether the state matches the desired stateSpec.
+ *
+ * @param stateSpec an array of required (if positive) or
+ * prohibited (if negative) {@link android.view.View} states.
+ * @param state a {@link android.view.View} state
+ */
+ public static boolean stateSetMatches(int[] stateSpec, int state) {
+ int stateSpecSize = stateSpec.length;
+ for (int i = 0; i < stateSpecSize; i++) {
+ int stateSpecState = stateSpec[i];
+ if (stateSpecState == 0) {
+ // We've reached the end of the cases to match against.
+ return true;
+ }
+ if (stateSpecState > 0) {
+ if (state != stateSpecState) {
+ return false;
+ }
+ } else {
+ // We use negative values to indicate must-NOT-match states.
+ if (state == -stateSpecState) {
+ // We matched a must-not-match case.
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Check whether a list of state specs has an attribute specified.
+ * @param stateSpecs a list of state specs we're checking.
+ * @param attr an attribute we're looking for.
+ * @return {@code true} if the attribute is contained in the state specs.
+ * @hide
+ */
+ public static boolean containsAttribute(int[][] stateSpecs, int attr) {
+ if (stateSpecs != null) {
+ for (int[] spec : stateSpecs) {
+ if (spec == null) {
+ break;
+ }
+ for (int specAttr : spec) {
+ if (specAttr == attr || -specAttr == attr) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ public static int[] trimStateSet(int[] states, int newSize) {
+ if (states.length == newSize) {
+ return states;
+ }
+
+ int[] trimmedStates = new int[newSize];
+ System.arraycopy(states, 0, trimmedStates, 0, newSize);
+ return trimmedStates;
+ }
+
+ public static String dump(int[] states) {
+ StringBuilder sb = new StringBuilder();
+
+ int count = states.length;
+ for (int i = 0; i < count; i++) {
+
+ switch (states[i]) {
+ case R.attr.state_window_focused:
+ sb.append("W ");
+ break;
+ case R.attr.state_pressed:
+ sb.append("P ");
+ break;
+ case R.attr.state_selected:
+ sb.append("S ");
+ break;
+ case R.attr.state_focused:
+ sb.append("F ");
+ break;
+ case R.attr.state_enabled:
+ sb.append("E ");
+ break;
+ case R.attr.state_checked:
+ sb.append("C ");
+ break;
+ case R.attr.state_activated:
+ sb.append("A ");
+ break;
+ }
+ }
+
+ return sb.toString();
+ }
+}
diff --git a/android/util/StatsEvent.java b/android/util/StatsEvent.java
new file mode 100644
index 0000000..8bd36a5
--- /dev/null
+++ b/android/util/StatsEvent.java
@@ -0,0 +1,845 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.SystemClock;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * StatsEvent builds and stores the buffer sent over the statsd socket.
+ * This class defines and encapsulates the socket protocol.
+ *
+ * <p>Usage:</p>
+ * <pre>
+ * // Pushed event
+ * StatsEvent statsEvent = StatsEvent.newBuilder()
+ * .setAtomId(atomId)
+ * .writeBoolean(false)
+ * .writeString("annotated String field")
+ * .addBooleanAnnotation(annotationId, true)
+ * .usePooledBuffer()
+ * .build();
+ * StatsLog.write(statsEvent);
+ *
+ * // Pulled event
+ * StatsEvent statsEvent = StatsEvent.newBuilder()
+ * .setAtomId(atomId)
+ * .writeBoolean(false)
+ * .writeString("annotated String field")
+ * .addBooleanAnnotation(annotationId, true)
+ * .build();
+ * </pre>
+ * @hide
+ **/
+@SystemApi
+public final class StatsEvent {
+ // Type Ids.
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final byte TYPE_INT = 0x00;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final byte TYPE_LONG = 0x01;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final byte TYPE_STRING = 0x02;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final byte TYPE_LIST = 0x03;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final byte TYPE_FLOAT = 0x04;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final byte TYPE_BOOLEAN = 0x05;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final byte TYPE_BYTE_ARRAY = 0x06;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final byte TYPE_OBJECT = 0x07;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final byte TYPE_KEY_VALUE_PAIRS = 0x08;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final byte TYPE_ATTRIBUTION_CHAIN = 0x09;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final byte TYPE_ERRORS = 0x0F;
+
+ // Error flags.
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int ERROR_NO_TIMESTAMP = 0x1;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int ERROR_NO_ATOM_ID = 0x2;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int ERROR_OVERFLOW = 0x4;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int ERROR_ATTRIBUTION_CHAIN_TOO_LONG = 0x8;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int ERROR_TOO_MANY_KEY_VALUE_PAIRS = 0x10;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int ERROR_ANNOTATION_DOES_NOT_FOLLOW_FIELD = 0x20;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int ERROR_INVALID_ANNOTATION_ID = 0x40;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int ERROR_ANNOTATION_ID_TOO_LARGE = 0x80;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int ERROR_TOO_MANY_ANNOTATIONS = 0x100;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int ERROR_TOO_MANY_FIELDS = 0x200;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int ERROR_ATTRIBUTION_UIDS_TAGS_SIZES_NOT_EQUAL = 0x1000;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int ERROR_ATOM_ID_INVALID_POSITION = 0x2000;
+
+ // Size limits.
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int MAX_ANNOTATION_COUNT = 15;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int MAX_ATTRIBUTION_NODES = 127;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int MAX_NUM_ELEMENTS = 127;
+
+ /**
+ * @hide
+ **/
+ @VisibleForTesting
+ public static final int MAX_KEY_VALUE_PAIRS = 127;
+
+ private static final int LOGGER_ENTRY_MAX_PAYLOAD = 4068;
+
+ // Max payload size is 4 bytes less as 4 bytes are reserved for statsEventTag.
+ // See android_util_StatsLog.cpp.
+ private static final int MAX_PAYLOAD_SIZE = LOGGER_ENTRY_MAX_PAYLOAD - 4;
+
+ private final int mAtomId;
+ private final byte[] mPayload;
+ private Buffer mBuffer;
+ private final int mNumBytes;
+
+ private StatsEvent(final int atomId, @Nullable final Buffer buffer,
+ @NonNull final byte[] payload, final int numBytes) {
+ mAtomId = atomId;
+ mBuffer = buffer;
+ mPayload = payload;
+ mNumBytes = numBytes;
+ }
+
+ /**
+ * Returns a new StatsEvent.Builder for building StatsEvent object.
+ **/
+ @NonNull
+ public static StatsEvent.Builder newBuilder() {
+ return new StatsEvent.Builder(Buffer.obtain());
+ }
+
+ /**
+ * Get the atom Id of the atom encoded in this StatsEvent object.
+ *
+ * @hide
+ **/
+ public int getAtomId() {
+ return mAtomId;
+ }
+
+ /**
+ * Get the byte array that contains the encoded payload that can be sent to statsd.
+ *
+ * @hide
+ **/
+ @NonNull
+ public byte[] getBytes() {
+ return mPayload;
+ }
+
+ /**
+ * Get the number of bytes used to encode the StatsEvent payload.
+ *
+ * @hide
+ **/
+ public int getNumBytes() {
+ return mNumBytes;
+ }
+
+ /**
+ * Recycle resources used by this StatsEvent object.
+ * No actions should be taken on this StatsEvent after release() is called.
+ *
+ * @hide
+ **/
+ public void release() {
+ if (mBuffer != null) {
+ mBuffer.release();
+ mBuffer = null;
+ }
+ }
+
+ /**
+ * Builder for constructing a StatsEvent object.
+ *
+ * <p>This class defines and encapsulates the socket encoding for the buffer.
+ * The write methods must be called in the same order as the order of fields in the
+ * atom definition.</p>
+ *
+ * <p>setAtomId() can be called anytime before build().</p>
+ *
+ * <p>Example:</p>
+ * <pre>
+ * // Atom definition.
+ * message MyAtom {
+ * optional int32 field1 = 1;
+ * optional int64 field2 = 2;
+ * optional string field3 = 3 [(annotation1) = true];
+ * }
+ *
+ * // StatsEvent construction for pushed event.
+ * StatsEvent.newBuilder()
+ * StatsEvent statsEvent = StatsEvent.newBuilder()
+ * .setAtomId(atomId)
+ * .writeInt(3) // field1
+ * .writeLong(8L) // field2
+ * .writeString("foo") // field 3
+ * .addBooleanAnnotation(annotation1Id, true)
+ * .usePooledBuffer()
+ * .build();
+ *
+ * // StatsEvent construction for pulled event.
+ * StatsEvent.newBuilder()
+ * StatsEvent statsEvent = StatsEvent.newBuilder()
+ * .setAtomId(atomId)
+ * .writeInt(3) // field1
+ * .writeLong(8L) // field2
+ * .writeString("foo") // field 3
+ * .addBooleanAnnotation(annotation1Id, true)
+ * .build();
+ * </pre>
+ **/
+ public static final class Builder {
+ // Fixed positions.
+ private static final int POS_NUM_ELEMENTS = 1;
+ private static final int POS_TIMESTAMP_NS = POS_NUM_ELEMENTS + Byte.BYTES;
+ private static final int POS_ATOM_ID = POS_TIMESTAMP_NS + Byte.BYTES + Long.BYTES;
+
+ private final Buffer mBuffer;
+ private long mTimestampNs;
+ private int mAtomId;
+ private byte mCurrentAnnotationCount;
+ private int mPos;
+ private int mPosLastField;
+ private byte mLastType;
+ private int mNumElements;
+ private int mErrorMask;
+ private boolean mUsePooledBuffer = false;
+
+ private Builder(final Buffer buffer) {
+ mBuffer = buffer;
+ mCurrentAnnotationCount = 0;
+ mAtomId = 0;
+ mTimestampNs = SystemClock.elapsedRealtimeNanos();
+ mNumElements = 0;
+
+ // Set mPos to 0 for writing TYPE_OBJECT at 0th position.
+ mPos = 0;
+ writeTypeId(TYPE_OBJECT);
+
+ // Write timestamp.
+ mPos = POS_TIMESTAMP_NS;
+ writeLong(mTimestampNs);
+ }
+
+ /**
+ * Sets the atom id for this StatsEvent.
+ *
+ * This should be called immediately after StatsEvent.newBuilder()
+ * and should only be called once.
+ * Not calling setAtomId will result in ERROR_NO_ATOM_ID.
+ * Calling setAtomId out of order will result in ERROR_ATOM_ID_INVALID_POSITION.
+ **/
+ @NonNull
+ public Builder setAtomId(final int atomId) {
+ if (0 == mAtomId) {
+ mAtomId = atomId;
+
+ if (1 == mNumElements) { // Only timestamp is written so far.
+ writeInt(atomId);
+ } else {
+ // setAtomId called out of order.
+ mErrorMask |= ERROR_ATOM_ID_INVALID_POSITION;
+ }
+ }
+
+ return this;
+ }
+
+ /**
+ * Write a boolean field to this StatsEvent.
+ **/
+ @NonNull
+ public Builder writeBoolean(final boolean value) {
+ // Write boolean typeId byte followed by boolean byte representation.
+ writeTypeId(TYPE_BOOLEAN);
+ mPos += mBuffer.putBoolean(mPos, value);
+ mNumElements++;
+ return this;
+ }
+
+ /**
+ * Write an integer field to this StatsEvent.
+ **/
+ @NonNull
+ public Builder writeInt(final int value) {
+ // Write integer typeId byte followed by 4-byte representation of value.
+ writeTypeId(TYPE_INT);
+ mPos += mBuffer.putInt(mPos, value);
+ mNumElements++;
+ return this;
+ }
+
+ /**
+ * Write a long field to this StatsEvent.
+ **/
+ @NonNull
+ public Builder writeLong(final long value) {
+ // Write long typeId byte followed by 8-byte representation of value.
+ writeTypeId(TYPE_LONG);
+ mPos += mBuffer.putLong(mPos, value);
+ mNumElements++;
+ return this;
+ }
+
+ /**
+ * Write a float field to this StatsEvent.
+ **/
+ @NonNull
+ public Builder writeFloat(final float value) {
+ // Write float typeId byte followed by 4-byte representation of value.
+ writeTypeId(TYPE_FLOAT);
+ mPos += mBuffer.putFloat(mPos, value);
+ mNumElements++;
+ return this;
+ }
+
+ /**
+ * Write a String field to this StatsEvent.
+ **/
+ @NonNull
+ public Builder writeString(@NonNull final String value) {
+ // Write String typeId byte, followed by 4-byte representation of number of bytes
+ // in the UTF-8 encoding, followed by the actual UTF-8 byte encoding of value.
+ final byte[] valueBytes = stringToBytes(value);
+ writeByteArray(valueBytes, TYPE_STRING);
+ return this;
+ }
+
+ /**
+ * Write a byte array field to this StatsEvent.
+ **/
+ @NonNull
+ public Builder writeByteArray(@NonNull final byte[] value) {
+ // Write byte array typeId byte, followed by 4-byte representation of number of bytes
+ // in value, followed by the actual byte array.
+ writeByteArray(value, TYPE_BYTE_ARRAY);
+ return this;
+ }
+
+ private void writeByteArray(@NonNull final byte[] value, final byte typeId) {
+ writeTypeId(typeId);
+ final int numBytes = value.length;
+ mPos += mBuffer.putInt(mPos, numBytes);
+ mPos += mBuffer.putByteArray(mPos, value);
+ mNumElements++;
+ }
+
+ /**
+ * Write an attribution chain field to this StatsEvent.
+ *
+ * The sizes of uids and tags must be equal. The AttributionNode at position i is
+ * made up of uids[i] and tags[i].
+ *
+ * @param uids array of uids in the attribution nodes.
+ * @param tags array of tags in the attribution nodes.
+ **/
+ @NonNull
+ public Builder writeAttributionChain(
+ @NonNull final int[] uids, @NonNull final String[] tags) {
+ final byte numUids = (byte) uids.length;
+ final byte numTags = (byte) tags.length;
+
+ if (numUids != numTags) {
+ mErrorMask |= ERROR_ATTRIBUTION_UIDS_TAGS_SIZES_NOT_EQUAL;
+ } else if (numUids > MAX_ATTRIBUTION_NODES) {
+ mErrorMask |= ERROR_ATTRIBUTION_CHAIN_TOO_LONG;
+ } else {
+ // Write attribution chain typeId byte, followed by 1-byte representation of
+ // number of attribution nodes, followed by encoding of each attribution node.
+ writeTypeId(TYPE_ATTRIBUTION_CHAIN);
+ mPos += mBuffer.putByte(mPos, numUids);
+ for (int i = 0; i < numUids; i++) {
+ // Each uid is encoded as 4-byte representation of its int value.
+ mPos += mBuffer.putInt(mPos, uids[i]);
+
+ // Each tag is encoded as 4-byte representation of number of bytes in its
+ // UTF-8 encoding, followed by the actual UTF-8 bytes.
+ final byte[] tagBytes = stringToBytes(tags[i]);
+ mPos += mBuffer.putInt(mPos, tagBytes.length);
+ mPos += mBuffer.putByteArray(mPos, tagBytes);
+ }
+ mNumElements++;
+ }
+ return this;
+ }
+
+ /**
+ * Write KeyValuePairsAtom entries to this StatsEvent.
+ *
+ * @param intMap Integer key-value pairs.
+ * @param longMap Long key-value pairs.
+ * @param stringMap String key-value pairs.
+ * @param floatMap Float key-value pairs.
+ **/
+ @NonNull
+ public Builder writeKeyValuePairs(
+ @Nullable final SparseIntArray intMap,
+ @Nullable final SparseLongArray longMap,
+ @Nullable final SparseArray<String> stringMap,
+ @Nullable final SparseArray<Float> floatMap) {
+ final int intMapSize = null == intMap ? 0 : intMap.size();
+ final int longMapSize = null == longMap ? 0 : longMap.size();
+ final int stringMapSize = null == stringMap ? 0 : stringMap.size();
+ final int floatMapSize = null == floatMap ? 0 : floatMap.size();
+ final int totalCount = intMapSize + longMapSize + stringMapSize + floatMapSize;
+
+ if (totalCount > MAX_KEY_VALUE_PAIRS) {
+ mErrorMask |= ERROR_TOO_MANY_KEY_VALUE_PAIRS;
+ } else {
+ writeTypeId(TYPE_KEY_VALUE_PAIRS);
+ mPos += mBuffer.putByte(mPos, (byte) totalCount);
+
+ for (int i = 0; i < intMapSize; i++) {
+ final int key = intMap.keyAt(i);
+ final int value = intMap.valueAt(i);
+ mPos += mBuffer.putInt(mPos, key);
+ writeTypeId(TYPE_INT);
+ mPos += mBuffer.putInt(mPos, value);
+ }
+
+ for (int i = 0; i < longMapSize; i++) {
+ final int key = longMap.keyAt(i);
+ final long value = longMap.valueAt(i);
+ mPos += mBuffer.putInt(mPos, key);
+ writeTypeId(TYPE_LONG);
+ mPos += mBuffer.putLong(mPos, value);
+ }
+
+ for (int i = 0; i < stringMapSize; i++) {
+ final int key = stringMap.keyAt(i);
+ final String value = stringMap.valueAt(i);
+ mPos += mBuffer.putInt(mPos, key);
+ writeTypeId(TYPE_STRING);
+ final byte[] valueBytes = stringToBytes(value);
+ mPos += mBuffer.putInt(mPos, valueBytes.length);
+ mPos += mBuffer.putByteArray(mPos, valueBytes);
+ }
+
+ for (int i = 0; i < floatMapSize; i++) {
+ final int key = floatMap.keyAt(i);
+ final float value = floatMap.valueAt(i);
+ mPos += mBuffer.putInt(mPos, key);
+ writeTypeId(TYPE_FLOAT);
+ mPos += mBuffer.putFloat(mPos, value);
+ }
+
+ mNumElements++;
+ }
+
+ return this;
+ }
+
+ /**
+ * Write a boolean annotation for the last field written.
+ **/
+ @NonNull
+ public Builder addBooleanAnnotation(
+ final byte annotationId, final boolean value) {
+ // Ensure there's a field written to annotate.
+ if (mNumElements < 2) {
+ mErrorMask |= ERROR_ANNOTATION_DOES_NOT_FOLLOW_FIELD;
+ } else if (mCurrentAnnotationCount >= MAX_ANNOTATION_COUNT) {
+ mErrorMask |= ERROR_TOO_MANY_ANNOTATIONS;
+ } else {
+ mPos += mBuffer.putByte(mPos, annotationId);
+ mPos += mBuffer.putByte(mPos, TYPE_BOOLEAN);
+ mPos += mBuffer.putBoolean(mPos, value);
+ mCurrentAnnotationCount++;
+ writeAnnotationCount();
+ }
+
+ return this;
+ }
+
+ /**
+ * Write an integer annotation for the last field written.
+ **/
+ @NonNull
+ public Builder addIntAnnotation(final byte annotationId, final int value) {
+ if (mNumElements < 2) {
+ mErrorMask |= ERROR_ANNOTATION_DOES_NOT_FOLLOW_FIELD;
+ } else if (mCurrentAnnotationCount >= MAX_ANNOTATION_COUNT) {
+ mErrorMask |= ERROR_TOO_MANY_ANNOTATIONS;
+ } else {
+ mPos += mBuffer.putByte(mPos, annotationId);
+ mPos += mBuffer.putByte(mPos, TYPE_INT);
+ mPos += mBuffer.putInt(mPos, value);
+ mCurrentAnnotationCount++;
+ writeAnnotationCount();
+ }
+
+ return this;
+ }
+
+ /**
+ * Indicates to reuse Buffer's byte array as the underlying payload in StatsEvent.
+ * This should be called for pushed events to reduce memory allocations and garbage
+ * collections.
+ **/
+ @NonNull
+ public Builder usePooledBuffer() {
+ mUsePooledBuffer = true;
+ return this;
+ }
+
+ /**
+ * Builds a StatsEvent object with values entered in this Builder.
+ **/
+ @NonNull
+ public StatsEvent build() {
+ if (0L == mTimestampNs) {
+ mErrorMask |= ERROR_NO_TIMESTAMP;
+ }
+ if (0 == mAtomId) {
+ mErrorMask |= ERROR_NO_ATOM_ID;
+ }
+ if (mBuffer.hasOverflowed()) {
+ mErrorMask |= ERROR_OVERFLOW;
+ }
+ if (mNumElements > MAX_NUM_ELEMENTS) {
+ mErrorMask |= ERROR_TOO_MANY_FIELDS;
+ }
+
+ if (0 == mErrorMask) {
+ mBuffer.putByte(POS_NUM_ELEMENTS, (byte) mNumElements);
+ } else {
+ // Write atom id and error mask. Overwrite any annotations for atom Id.
+ mPos = POS_ATOM_ID;
+ mPos += mBuffer.putByte(mPos, TYPE_INT);
+ mPos += mBuffer.putInt(mPos, mAtomId);
+ mPos += mBuffer.putByte(mPos, TYPE_ERRORS);
+ mPos += mBuffer.putInt(mPos, mErrorMask);
+ mBuffer.putByte(POS_NUM_ELEMENTS, (byte) 3);
+ }
+
+ final int size = mPos;
+
+ if (mUsePooledBuffer) {
+ return new StatsEvent(mAtomId, mBuffer, mBuffer.getBytes(), size);
+ } else {
+ // Create a copy of the buffer with the required number of bytes.
+ final byte[] payload = new byte[size];
+ System.arraycopy(mBuffer.getBytes(), 0, payload, 0, size);
+
+ // Return Buffer instance to the pool.
+ mBuffer.release();
+
+ return new StatsEvent(mAtomId, null, payload, size);
+ }
+ }
+
+ private void writeTypeId(final byte typeId) {
+ mPosLastField = mPos;
+ mLastType = typeId;
+ mCurrentAnnotationCount = 0;
+ final byte encodedId = (byte) (typeId & 0x0F);
+ mPos += mBuffer.putByte(mPos, encodedId);
+ }
+
+ private void writeAnnotationCount() {
+ // Use first 4 bits for annotation count and last 4 bits for typeId.
+ final byte encodedId = (byte) ((mCurrentAnnotationCount << 4) | (mLastType & 0x0F));
+ mBuffer.putByte(mPosLastField, encodedId);
+ }
+
+ @NonNull
+ private static byte[] stringToBytes(@Nullable final String value) {
+ return (null == value ? "" : value).getBytes(UTF_8);
+ }
+ }
+
+ private static final class Buffer {
+ private static Object sLock = new Object();
+
+ @GuardedBy("sLock")
+ private static Buffer sPool;
+
+ private final byte[] mBytes = new byte[MAX_PAYLOAD_SIZE];
+ private boolean mOverflow = false;
+
+ @NonNull
+ private static Buffer obtain() {
+ final Buffer buffer;
+ synchronized (sLock) {
+ buffer = null == sPool ? new Buffer() : sPool;
+ sPool = null;
+ }
+ buffer.reset();
+ return buffer;
+ }
+
+ private Buffer() {
+ }
+
+ @NonNull
+ private byte[] getBytes() {
+ return mBytes;
+ }
+
+ private void release() {
+ synchronized (sLock) {
+ if (null == sPool) {
+ sPool = this;
+ }
+ }
+ }
+
+ private void reset() {
+ mOverflow = false;
+ }
+
+ private boolean hasOverflowed() {
+ return mOverflow;
+ }
+
+ /**
+ * Checks for available space in the byte array.
+ *
+ * @param index starting position in the buffer to start the check.
+ * @param numBytes number of bytes to check from index.
+ * @return true if space is available, false otherwise.
+ **/
+ private boolean hasEnoughSpace(final int index, final int numBytes) {
+ final boolean result = index + numBytes < MAX_PAYLOAD_SIZE;
+ if (!result) {
+ mOverflow = true;
+ }
+ return result;
+ }
+
+ /**
+ * Writes a byte into the buffer.
+ *
+ * @param index position in the buffer where the byte is written.
+ * @param value the byte to write.
+ * @return number of bytes written to buffer from this write operation.
+ **/
+ private int putByte(final int index, final byte value) {
+ if (hasEnoughSpace(index, Byte.BYTES)) {
+ mBytes[index] = (byte) (value);
+ return Byte.BYTES;
+ }
+ return 0;
+ }
+
+ /**
+ * Writes a boolean into the buffer.
+ *
+ * @param index position in the buffer where the boolean is written.
+ * @param value the boolean to write.
+ * @return number of bytes written to buffer from this write operation.
+ **/
+ private int putBoolean(final int index, final boolean value) {
+ return putByte(index, (byte) (value ? 1 : 0));
+ }
+
+ /**
+ * Writes an integer into the buffer.
+ *
+ * @param index position in the buffer where the integer is written.
+ * @param value the integer to write.
+ * @return number of bytes written to buffer from this write operation.
+ **/
+ private int putInt(final int index, final int value) {
+ if (hasEnoughSpace(index, Integer.BYTES)) {
+ // Use little endian byte order.
+ mBytes[index] = (byte) (value);
+ mBytes[index + 1] = (byte) (value >> 8);
+ mBytes[index + 2] = (byte) (value >> 16);
+ mBytes[index + 3] = (byte) (value >> 24);
+ return Integer.BYTES;
+ }
+ return 0;
+ }
+
+ /**
+ * Writes a long into the buffer.
+ *
+ * @param index position in the buffer where the long is written.
+ * @param value the long to write.
+ * @return number of bytes written to buffer from this write operation.
+ **/
+ private int putLong(final int index, final long value) {
+ if (hasEnoughSpace(index, Long.BYTES)) {
+ // Use little endian byte order.
+ mBytes[index] = (byte) (value);
+ mBytes[index + 1] = (byte) (value >> 8);
+ mBytes[index + 2] = (byte) (value >> 16);
+ mBytes[index + 3] = (byte) (value >> 24);
+ mBytes[index + 4] = (byte) (value >> 32);
+ mBytes[index + 5] = (byte) (value >> 40);
+ mBytes[index + 6] = (byte) (value >> 48);
+ mBytes[index + 7] = (byte) (value >> 56);
+ return Long.BYTES;
+ }
+ return 0;
+ }
+
+ /**
+ * Writes a float into the buffer.
+ *
+ * @param index position in the buffer where the float is written.
+ * @param value the float to write.
+ * @return number of bytes written to buffer from this write operation.
+ **/
+ private int putFloat(final int index, final float value) {
+ return putInt(index, Float.floatToIntBits(value));
+ }
+
+ /**
+ * Copies a byte array into the buffer.
+ *
+ * @param index position in the buffer where the byte array is copied.
+ * @param value the byte array to copy.
+ * @return number of bytes written to buffer from this write operation.
+ **/
+ private int putByteArray(final int index, @NonNull final byte[] value) {
+ final int numBytes = value.length;
+ if (hasEnoughSpace(index, numBytes)) {
+ System.arraycopy(value, 0, mBytes, index, numBytes);
+ return numBytes;
+ }
+ return 0;
+ }
+ }
+}
diff --git a/android/util/StatsEventTest.java b/android/util/StatsEventTest.java
new file mode 100644
index 0000000..7b51155
--- /dev/null
+++ b/android/util/StatsEventTest.java
@@ -0,0 +1,658 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.os.SystemClock;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.common.collect.Range;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Internal tests for {@link StatsEvent}.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class StatsEventTest {
+
+ @Test
+ public void testNoFields() {
+ final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+ final StatsEvent statsEvent = StatsEvent.newBuilder().usePooledBuffer().build();
+ final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+ final int expectedAtomId = 0;
+ assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+ final ByteBuffer buffer =
+ ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+ assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT);
+
+ assertWithMessage("Incorrect number of elements in root object")
+ .that(buffer.get()).isEqualTo(3);
+
+ assertWithMessage("First element is not timestamp")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG);
+
+ assertWithMessage("Incorrect timestamp")
+ .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp));
+
+ assertWithMessage("Second element is not atom id")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect atom id")
+ .that(buffer.getInt()).isEqualTo(expectedAtomId);
+
+ assertWithMessage("Third element is not errors type")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_ERRORS);
+
+ final int errorMask = buffer.getInt();
+
+ assertWithMessage("ERROR_NO_ATOM_ID should be the only error in the error mask")
+ .that(errorMask).isEqualTo(StatsEvent.ERROR_NO_ATOM_ID);
+
+ assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+ statsEvent.release();
+ }
+
+ @Test
+ public void testOnlyAtomId() {
+ final int expectedAtomId = 109;
+
+ final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+ final StatsEvent statsEvent = StatsEvent.newBuilder()
+ .setAtomId(expectedAtomId)
+ .usePooledBuffer()
+ .build();
+ final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+ assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+ final ByteBuffer buffer =
+ ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+ assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT);
+
+ assertWithMessage("Incorrect number of elements in root object")
+ .that(buffer.get()).isEqualTo(2);
+
+ assertWithMessage("First element is not timestamp")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG);
+
+ assertWithMessage("Incorrect timestamp")
+ .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp));
+
+ assertWithMessage("Second element is not atom id")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect atom id")
+ .that(buffer.getInt()).isEqualTo(expectedAtomId);
+
+ assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+ statsEvent.release();
+ }
+
+ @Test
+ public void testIntBooleanIntInt() {
+ final int expectedAtomId = 109;
+ final int field1 = 1;
+ final boolean field2 = true;
+ final int field3 = 3;
+ final int field4 = 4;
+
+ final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+ final StatsEvent statsEvent = StatsEvent.newBuilder()
+ .setAtomId(expectedAtomId)
+ .writeInt(field1)
+ .writeBoolean(field2)
+ .writeInt(field3)
+ .writeInt(field4)
+ .usePooledBuffer()
+ .build();
+ final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+ assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+ final ByteBuffer buffer =
+ ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+ assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT);
+
+ assertWithMessage("Incorrect number of elements in root object")
+ .that(buffer.get()).isEqualTo(6);
+
+ assertWithMessage("First element is not timestamp")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG);
+
+ assertWithMessage("Incorrect timestamp")
+ .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp));
+
+ assertWithMessage("Second element is not atom id")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect atom id")
+ .that(buffer.getInt()).isEqualTo(expectedAtomId);
+
+ assertWithMessage("First field is not Int")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect field 1")
+ .that(buffer.getInt()).isEqualTo(field1);
+
+ assertWithMessage("Second field is not Boolean")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_BOOLEAN);
+
+ assertWithMessage("Incorrect field 2")
+ .that(buffer.get()).isEqualTo(1);
+
+ assertWithMessage("Third field is not Int")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect field 3")
+ .that(buffer.getInt()).isEqualTo(field3);
+
+ assertWithMessage("Fourth field is not Int")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect field 4")
+ .that(buffer.getInt()).isEqualTo(field4);
+
+ assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+ statsEvent.release();
+ }
+
+ @Test
+ public void testStringFloatByteArray() {
+ final int expectedAtomId = 109;
+ final String field1 = "Str 1";
+ final float field2 = 9.334f;
+ final byte[] field3 = new byte[] { 56, 23, 89, -120 };
+
+ final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+ final StatsEvent statsEvent = StatsEvent.newBuilder()
+ .setAtomId(expectedAtomId)
+ .writeString(field1)
+ .writeFloat(field2)
+ .writeByteArray(field3)
+ .usePooledBuffer()
+ .build();
+ final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+ assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+ final ByteBuffer buffer =
+ ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+ assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT);
+
+ assertWithMessage("Incorrect number of elements in root object")
+ .that(buffer.get()).isEqualTo(5);
+
+ assertWithMessage("First element is not timestamp")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG);
+
+ assertWithMessage("Incorrect timestamp")
+ .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp));
+
+ assertWithMessage("Second element is not atom id")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect atom id")
+ .that(buffer.getInt()).isEqualTo(expectedAtomId);
+
+ assertWithMessage("First field is not String")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_STRING);
+
+ final String field1Actual = getStringFromByteBuffer(buffer);
+ assertWithMessage("Incorrect field 1")
+ .that(field1Actual).isEqualTo(field1);
+
+ assertWithMessage("Second field is not Float")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_FLOAT);
+
+ assertWithMessage("Incorrect field 2")
+ .that(buffer.getFloat()).isEqualTo(field2);
+
+ assertWithMessage("Third field is not byte array")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_BYTE_ARRAY);
+
+ final byte[] field3Actual = getByteArrayFromByteBuffer(buffer);
+ assertWithMessage("Incorrect field 3")
+ .that(field3Actual).isEqualTo(field3);
+
+ assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+ statsEvent.release();
+ }
+
+ @Test
+ public void testAttributionChainLong() {
+ final int expectedAtomId = 109;
+ final int[] uids = new int[] { 1, 2, 3, 4, 5 };
+ final String[] tags = new String[] { "1", "2", "3", "4", "5" };
+ final long field2 = -230909823L;
+
+ final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+ final StatsEvent statsEvent = StatsEvent.newBuilder()
+ .setAtomId(expectedAtomId)
+ .writeAttributionChain(uids, tags)
+ .writeLong(field2)
+ .usePooledBuffer()
+ .build();
+ final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+ assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+ final ByteBuffer buffer =
+ ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+ assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT);
+
+ assertWithMessage("Incorrect number of elements in root object")
+ .that(buffer.get()).isEqualTo(4);
+
+ assertWithMessage("First element is not timestamp")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG);
+
+ assertWithMessage("Incorrect timestamp")
+ .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp));
+
+ assertWithMessage("Second element is not atom id")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect atom id")
+ .that(buffer.getInt()).isEqualTo(expectedAtomId);
+
+ assertWithMessage("First field is not Attribution Chain")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_ATTRIBUTION_CHAIN);
+
+ assertWithMessage("Incorrect number of attribution nodes")
+ .that(buffer.get()).isEqualTo((byte) uids.length);
+
+ for (int i = 0; i < tags.length; i++) {
+ assertWithMessage("Incorrect uid in Attribution Chain")
+ .that(buffer.getInt()).isEqualTo(uids[i]);
+
+ final String tag = getStringFromByteBuffer(buffer);
+ assertWithMessage("Incorrect tag in Attribution Chain")
+ .that(tag).isEqualTo(tags[i]);
+ }
+
+ assertWithMessage("Second field is not Long")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG);
+
+ assertWithMessage("Incorrect field 2")
+ .that(buffer.getLong()).isEqualTo(field2);
+
+ assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+ statsEvent.release();
+ }
+
+ @Test
+ public void testKeyValuePairs() {
+ final int expectedAtomId = 109;
+ final SparseIntArray intMap = new SparseIntArray();
+ final SparseLongArray longMap = new SparseLongArray();
+ final SparseArray<String> stringMap = new SparseArray<>();
+ final SparseArray<Float> floatMap = new SparseArray<>();
+ intMap.put(1, -1);
+ intMap.put(2, -2);
+ stringMap.put(3, "abc");
+ stringMap.put(4, "2h");
+ floatMap.put(9, -234.344f);
+
+ final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+ final StatsEvent statsEvent = StatsEvent.newBuilder()
+ .setAtomId(expectedAtomId)
+ .writeKeyValuePairs(intMap, longMap, stringMap, floatMap)
+ .usePooledBuffer()
+ .build();
+ final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+ assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+ final ByteBuffer buffer =
+ ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+ assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT);
+
+ assertWithMessage("Incorrect number of elements in root object")
+ .that(buffer.get()).isEqualTo(3);
+
+ assertWithMessage("First element is not timestamp")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG);
+
+ assertWithMessage("Incorrect timestamp")
+ .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp));
+
+ assertWithMessage("Second element is not atom id")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect atom id")
+ .that(buffer.getInt()).isEqualTo(expectedAtomId);
+
+ assertWithMessage("First field is not KeyValuePairs")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_KEY_VALUE_PAIRS);
+
+ assertWithMessage("Incorrect number of key value pairs")
+ .that(buffer.get()).isEqualTo(
+ (byte) (intMap.size() + longMap.size() + stringMap.size()
+ + floatMap.size()));
+
+ for (int i = 0; i < intMap.size(); i++) {
+ assertWithMessage("Incorrect key in intMap")
+ .that(buffer.getInt()).isEqualTo(intMap.keyAt(i));
+ assertWithMessage("The type id of the value should be TYPE_INT in intMap")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+ assertWithMessage("Incorrect value in intMap")
+ .that(buffer.getInt()).isEqualTo(intMap.valueAt(i));
+ }
+
+ for (int i = 0; i < longMap.size(); i++) {
+ assertWithMessage("Incorrect key in longMap")
+ .that(buffer.getInt()).isEqualTo(longMap.keyAt(i));
+ assertWithMessage("The type id of the value should be TYPE_LONG in longMap")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG);
+ assertWithMessage("Incorrect value in longMap")
+ .that(buffer.getLong()).isEqualTo(longMap.valueAt(i));
+ }
+
+ for (int i = 0; i < stringMap.size(); i++) {
+ assertWithMessage("Incorrect key in stringMap")
+ .that(buffer.getInt()).isEqualTo(stringMap.keyAt(i));
+ assertWithMessage("The type id of the value should be TYPE_STRING in stringMap")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_STRING);
+ final String value = getStringFromByteBuffer(buffer);
+ assertWithMessage("Incorrect value in stringMap")
+ .that(value).isEqualTo(stringMap.valueAt(i));
+ }
+
+ for (int i = 0; i < floatMap.size(); i++) {
+ assertWithMessage("Incorrect key in floatMap")
+ .that(buffer.getInt()).isEqualTo(floatMap.keyAt(i));
+ assertWithMessage("The type id of the value should be TYPE_FLOAT in floatMap")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_FLOAT);
+ assertWithMessage("Incorrect value in floatMap")
+ .that(buffer.getFloat()).isEqualTo(floatMap.valueAt(i));
+ }
+
+ assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+ statsEvent.release();
+ }
+
+ @Test
+ public void testSingleAnnotations() {
+ final int expectedAtomId = 109;
+ final int field1 = 1;
+ final byte field1AnnotationId = 45;
+ final boolean field1AnnotationValue = false;
+ final boolean field2 = true;
+ final byte field2AnnotationId = 1;
+ final int field2AnnotationValue = 23;
+
+ final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+ final StatsEvent statsEvent = StatsEvent.newBuilder()
+ .setAtomId(expectedAtomId)
+ .writeInt(field1)
+ .addBooleanAnnotation(field1AnnotationId, field1AnnotationValue)
+ .writeBoolean(field2)
+ .addIntAnnotation(field2AnnotationId, field2AnnotationValue)
+ .usePooledBuffer()
+ .build();
+ final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+ assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+ final ByteBuffer buffer =
+ ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+ assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT);
+
+ assertWithMessage("Incorrect number of elements in root object")
+ .that(buffer.get()).isEqualTo(4);
+
+ assertWithMessage("First element is not timestamp")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG);
+
+ assertWithMessage("Incorrect timestamp")
+ .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp));
+
+ assertWithMessage("Second element is not atom id")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect atom id")
+ .that(buffer.getInt()).isEqualTo(expectedAtomId);
+
+ final byte field1Header = buffer.get();
+ final int field1AnnotationValueCount = field1Header >> 4;
+ final byte field1Type = (byte) (field1Header & 0x0F);
+ assertWithMessage("First field is not Int")
+ .that(field1Type).isEqualTo(StatsEvent.TYPE_INT);
+ assertWithMessage("First field annotation count is wrong")
+ .that(field1AnnotationValueCount).isEqualTo(1);
+ assertWithMessage("Incorrect field 1")
+ .that(buffer.getInt()).isEqualTo(field1);
+ assertWithMessage("First field's annotation id is wrong")
+ .that(buffer.get()).isEqualTo(field1AnnotationId);
+ assertWithMessage("First field's annotation type is wrong")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_BOOLEAN);
+ assertWithMessage("First field's annotation value is wrong")
+ .that(buffer.get()).isEqualTo(field1AnnotationValue ? 1 : 0);
+
+ final byte field2Header = buffer.get();
+ final int field2AnnotationValueCount = field2Header >> 4;
+ final byte field2Type = (byte) (field2Header & 0x0F);
+ assertWithMessage("Second field is not boolean")
+ .that(field2Type).isEqualTo(StatsEvent.TYPE_BOOLEAN);
+ assertWithMessage("Second field annotation count is wrong")
+ .that(field2AnnotationValueCount).isEqualTo(1);
+ assertWithMessage("Incorrect field 2")
+ .that(buffer.get()).isEqualTo(field2 ? 1 : 0);
+ assertWithMessage("Second field's annotation id is wrong")
+ .that(buffer.get()).isEqualTo(field2AnnotationId);
+ assertWithMessage("Second field's annotation type is wrong")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+ assertWithMessage("Second field's annotation value is wrong")
+ .that(buffer.getInt()).isEqualTo(field2AnnotationValue);
+
+ assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+ statsEvent.release();
+ }
+
+ @Test
+ public void testAtomIdAnnotations() {
+ final int expectedAtomId = 109;
+ final byte atomAnnotationId = 84;
+ final int atomAnnotationValue = 9;
+ final int field1 = 1;
+ final byte field1AnnotationId = 45;
+ final boolean field1AnnotationValue = false;
+ final boolean field2 = true;
+ final byte field2AnnotationId = 1;
+ final int field2AnnotationValue = 23;
+
+ final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+ final StatsEvent statsEvent = StatsEvent.newBuilder()
+ .setAtomId(expectedAtomId)
+ .addIntAnnotation(atomAnnotationId, atomAnnotationValue)
+ .writeInt(field1)
+ .addBooleanAnnotation(field1AnnotationId, field1AnnotationValue)
+ .writeBoolean(field2)
+ .addIntAnnotation(field2AnnotationId, field2AnnotationValue)
+ .usePooledBuffer()
+ .build();
+ final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+ assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+ final ByteBuffer buffer =
+ ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+ assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT);
+
+ assertWithMessage("Incorrect number of elements in root object")
+ .that(buffer.get()).isEqualTo(4);
+
+ assertWithMessage("First element is not timestamp")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG);
+
+ assertWithMessage("Incorrect timestamp")
+ .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp));
+
+ final byte atomIdHeader = buffer.get();
+ final int atomIdAnnotationValueCount = atomIdHeader >> 4;
+ final byte atomIdValueType = (byte) (atomIdHeader & 0x0F);
+ assertWithMessage("Second element is not atom id")
+ .that(atomIdValueType).isEqualTo(StatsEvent.TYPE_INT);
+ assertWithMessage("Atom id annotation count is wrong")
+ .that(atomIdAnnotationValueCount).isEqualTo(1);
+ assertWithMessage("Incorrect atom id")
+ .that(buffer.getInt()).isEqualTo(expectedAtomId);
+ assertWithMessage("Atom id's annotation id is wrong")
+ .that(buffer.get()).isEqualTo(atomAnnotationId);
+ assertWithMessage("Atom id's annotation type is wrong")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+ assertWithMessage("Atom id's annotation value is wrong")
+ .that(buffer.getInt()).isEqualTo(atomAnnotationValue);
+
+ final byte field1Header = buffer.get();
+ final int field1AnnotationValueCount = field1Header >> 4;
+ final byte field1Type = (byte) (field1Header & 0x0F);
+ assertWithMessage("First field is not Int")
+ .that(field1Type).isEqualTo(StatsEvent.TYPE_INT);
+ assertWithMessage("First field annotation count is wrong")
+ .that(field1AnnotationValueCount).isEqualTo(1);
+ assertWithMessage("Incorrect field 1")
+ .that(buffer.getInt()).isEqualTo(field1);
+ assertWithMessage("First field's annotation id is wrong")
+ .that(buffer.get()).isEqualTo(field1AnnotationId);
+ assertWithMessage("First field's annotation type is wrong")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_BOOLEAN);
+ assertWithMessage("First field's annotation value is wrong")
+ .that(buffer.get()).isEqualTo(field1AnnotationValue ? 1 : 0);
+
+ final byte field2Header = buffer.get();
+ final int field2AnnotationValueCount = field2Header >> 4;
+ final byte field2Type = (byte) (field2Header & 0x0F);
+ assertWithMessage("Second field is not boolean")
+ .that(field2Type).isEqualTo(StatsEvent.TYPE_BOOLEAN);
+ assertWithMessage("Second field annotation count is wrong")
+ .that(field2AnnotationValueCount).isEqualTo(1);
+ assertWithMessage("Incorrect field 2")
+ .that(buffer.get()).isEqualTo(field2 ? 1 : 0);
+ assertWithMessage("Second field's annotation id is wrong")
+ .that(buffer.get()).isEqualTo(field2AnnotationId);
+ assertWithMessage("Second field's annotation type is wrong")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+ assertWithMessage("Second field's annotation value is wrong")
+ .that(buffer.getInt()).isEqualTo(field2AnnotationValue);
+
+ assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+ statsEvent.release();
+ }
+
+ @Test
+ public void testSetAtomIdNotCalledImmediately() {
+ final int expectedAtomId = 109;
+ final int field1 = 25;
+ final boolean field2 = true;
+
+ final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+ final StatsEvent statsEvent = StatsEvent.newBuilder()
+ .writeInt(field1)
+ .setAtomId(expectedAtomId)
+ .writeBoolean(field2)
+ .usePooledBuffer()
+ .build();
+ final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+ assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+ final ByteBuffer buffer =
+ ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+ assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_OBJECT);
+
+ assertWithMessage("Incorrect number of elements in root object")
+ .that(buffer.get()).isEqualTo(3);
+
+ assertWithMessage("First element is not timestamp")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_LONG);
+
+ assertWithMessage("Incorrect timestamp")
+ .that(buffer.getLong()).isIn(Range.closed(minTimestamp, maxTimestamp));
+
+ assertWithMessage("Second element is not atom id")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_INT);
+
+ assertWithMessage("Incorrect atom id")
+ .that(buffer.getInt()).isEqualTo(expectedAtomId);
+
+ assertWithMessage("Third element is not errors type")
+ .that(buffer.get()).isEqualTo(StatsEvent.TYPE_ERRORS);
+
+ final int errorMask = buffer.getInt();
+
+ assertWithMessage("ERROR_ATOM_ID_INVALID_POSITION should be the only error in the mask")
+ .that(errorMask).isEqualTo(StatsEvent.ERROR_ATOM_ID_INVALID_POSITION);
+
+ assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+ statsEvent.release();
+ }
+
+ private static byte[] getByteArrayFromByteBuffer(final ByteBuffer buffer) {
+ final int numBytes = buffer.getInt();
+ byte[] bytes = new byte[numBytes];
+ buffer.get(bytes);
+ return bytes;
+ }
+
+ private static String getStringFromByteBuffer(final ByteBuffer buffer) {
+ final byte[] bytes = getByteArrayFromByteBuffer(buffer);
+ return new String(bytes, UTF_8);
+ }
+}
diff --git a/android/util/StatsLog.java b/android/util/StatsLog.java
new file mode 100644
index 0000000..4eeae57
--- /dev/null
+++ b/android/util/StatsLog.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import static android.Manifest.permission.DUMP;
+import static android.Manifest.permission.PACKAGE_USAGE_STATS;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.content.Context;
+import android.os.IStatsd;
+import android.os.Process;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.util.StatsdStatsLog;
+
+/**
+ * StatsLog provides an API for developers to send events to statsd. The events can be used to
+ * define custom metrics inside statsd.
+ */
+public final class StatsLog {
+
+ // Load JNI library
+ static {
+ System.loadLibrary("stats_jni");
+ }
+ private static final String TAG = "StatsLog";
+ private static final boolean DEBUG = false;
+ private static final int EXPERIMENT_IDS_FIELD_ID = 1;
+
+ private StatsLog() {
+ }
+
+ /**
+ * Logs a start event.
+ *
+ * @param label developer-chosen label.
+ * @return True if the log request was sent to statsd.
+ */
+ public static boolean logStart(int label) {
+ int callingUid = Process.myUid();
+ StatsdStatsLog.write(
+ StatsdStatsLog.APP_BREADCRUMB_REPORTED,
+ callingUid,
+ label,
+ StatsdStatsLog.APP_BREADCRUMB_REPORTED__STATE__START);
+ return true;
+ }
+
+ /**
+ * Logs a stop event.
+ *
+ * @param label developer-chosen label.
+ * @return True if the log request was sent to statsd.
+ */
+ public static boolean logStop(int label) {
+ int callingUid = Process.myUid();
+ StatsdStatsLog.write(
+ StatsdStatsLog.APP_BREADCRUMB_REPORTED,
+ callingUid,
+ label,
+ StatsdStatsLog.APP_BREADCRUMB_REPORTED__STATE__STOP);
+ return true;
+ }
+
+ /**
+ * Logs an event that does not represent a start or stop boundary.
+ *
+ * @param label developer-chosen label.
+ * @return True if the log request was sent to statsd.
+ */
+ public static boolean logEvent(int label) {
+ int callingUid = Process.myUid();
+ StatsdStatsLog.write(
+ StatsdStatsLog.APP_BREADCRUMB_REPORTED,
+ callingUid,
+ label,
+ StatsdStatsLog.APP_BREADCRUMB_REPORTED__STATE__UNSPECIFIED);
+ return true;
+ }
+
+ /**
+ * Logs an event for binary push for module updates.
+ *
+ * @param trainName name of install train.
+ * @param trainVersionCode version code of the train.
+ * @param options optional flags about this install.
+ * The last 3 bits indicate options:
+ * 0x01: FLAG_REQUIRE_STAGING
+ * 0x02: FLAG_ROLLBACK_ENABLED
+ * 0x04: FLAG_REQUIRE_LOW_LATENCY_MONITOR
+ * @param state current install state. Defined as State enums in
+ * BinaryPushStateChanged atom in
+ * frameworks/base/cmds/statsd/src/atoms.proto
+ * @param experimentIds experiment ids.
+ * @return True if the log request was sent to statsd.
+ */
+ @RequiresPermission(allOf = {DUMP, PACKAGE_USAGE_STATS})
+ public static boolean logBinaryPushStateChanged(@NonNull String trainName,
+ long trainVersionCode, int options, int state,
+ @NonNull long[] experimentIds) {
+ ProtoOutputStream proto = new ProtoOutputStream();
+ for (long id : experimentIds) {
+ proto.write(
+ ProtoOutputStream.FIELD_TYPE_INT64
+ | ProtoOutputStream.FIELD_COUNT_REPEATED
+ | EXPERIMENT_IDS_FIELD_ID,
+ id);
+ }
+ StatsdStatsLog.write(StatsdStatsLog.BINARY_PUSH_STATE_CHANGED,
+ trainName,
+ trainVersionCode,
+ (options & IStatsd.FLAG_REQUIRE_STAGING) > 0,
+ (options & IStatsd.FLAG_ROLLBACK_ENABLED) > 0,
+ (options & IStatsd.FLAG_REQUIRE_LOW_LATENCY_MONITOR) > 0,
+ state,
+ proto.getBytes(),
+ 0,
+ 0,
+ false);
+ return true;
+ }
+
+ /**
+ * Write an event to stats log using the raw format.
+ *
+ * @param buffer The encoded buffer of data to write.
+ * @param size The number of bytes from the buffer to write.
+ * @hide
+ */
+ // TODO(b/144935988): Mark deprecated.
+ @SystemApi
+ public static void writeRaw(@NonNull byte[] buffer, int size) {
+ // TODO(b/144935988): make this no-op once clients have migrated to StatsEvent.
+ writeImpl(buffer, size, 0);
+ }
+
+ /**
+ * Write an event to stats log using the raw format.
+ *
+ * @param buffer The encoded buffer of data to write.
+ * @param size The number of bytes from the buffer to write.
+ * @param atomId The id of the atom to which the event belongs.
+ */
+ private static native void writeImpl(@NonNull byte[] buffer, int size, int atomId);
+
+ /**
+ * Write an event to stats log using the raw format encapsulated in StatsEvent.
+ * After writing to stats log, release() is called on the StatsEvent object.
+ * No further action should be taken on the StatsEvent object following this call.
+ *
+ * @param statsEvent The StatsEvent object containing the encoded buffer of data to write.
+ * @hide
+ */
+ @SystemApi
+ public static void write(@NonNull final StatsEvent statsEvent) {
+ writeImpl(statsEvent.getBytes(), statsEvent.getNumBytes(), statsEvent.getAtomId());
+ statsEvent.release();
+ }
+
+ private static void enforceDumpCallingPermission(Context context) {
+ context.enforceCallingPermission(android.Manifest.permission.DUMP, "Need DUMP permission.");
+ }
+
+ private static void enforcesageStatsCallingPermission(Context context) {
+ context.enforceCallingPermission(Manifest.permission.PACKAGE_USAGE_STATS,
+ "Need PACKAGE_USAGE_STATS permission.");
+ }
+}
diff --git a/android/util/StringBuilderPrinter.java b/android/util/StringBuilderPrinter.java
new file mode 100644
index 0000000..d0fc1e7
--- /dev/null
+++ b/android/util/StringBuilderPrinter.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * Implementation of a {@link android.util.Printer} that sends its output
+ * to a {@link StringBuilder}.
+ */
+public class StringBuilderPrinter implements Printer {
+ private final StringBuilder mBuilder;
+
+ /**
+ * Create a new Printer that sends to a StringBuilder object.
+ *
+ * @param builder The StringBuilder where you would like output to go.
+ */
+ public StringBuilderPrinter(StringBuilder builder) {
+ mBuilder = builder;
+ }
+
+ public void println(String x) {
+ mBuilder.append(x);
+ int len = x.length();
+ if (len <= 0 || x.charAt(len-1) != '\n') {
+ mBuilder.append('\n');
+ }
+ }
+}
diff --git a/android/util/SuperNotCalledException.java b/android/util/SuperNotCalledException.java
new file mode 100644
index 0000000..1836142
--- /dev/null
+++ b/android/util/SuperNotCalledException.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package android.util;
+
+/**
+ * @hide
+ */
+public final class SuperNotCalledException extends AndroidRuntimeException {
+ public SuperNotCalledException(String msg) {
+ super(msg);
+ }
+}
diff --git a/android/util/TimeFormatException.java b/android/util/TimeFormatException.java
new file mode 100644
index 0000000..f520523
--- /dev/null
+++ b/android/util/TimeFormatException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+public class TimeFormatException extends RuntimeException
+{
+
+ /**
+ * @hide
+ */
+ public TimeFormatException(String s)
+ {
+ super(s);
+ }
+}
+
diff --git a/android/util/TimeUtils.java b/android/util/TimeUtils.java
new file mode 100644
index 0000000..e8d3459
--- /dev/null
+++ b/android/util/TimeUtils.java
@@ -0,0 +1,431 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.SystemClock;
+
+import libcore.timezone.CountryTimeZones;
+import libcore.timezone.CountryTimeZones.TimeZoneMapping;
+import libcore.timezone.TimeZoneFinder;
+import libcore.timezone.ZoneInfoDb;
+
+import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
+import java.time.LocalTime;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * A class containing utility methods related to time zones.
+ */
+public class TimeUtils {
+ /** @hide */ public TimeUtils() {}
+ /** {@hide} */
+ private static SimpleDateFormat sLoggingFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+ /** @hide */
+ public static final SimpleDateFormat sDumpDateFormat =
+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+ /**
+ * Tries to return a time zone that would have had the specified offset
+ * and DST value at the specified moment in the specified country.
+ * Returns null if no suitable zone could be found.
+ */
+ public static java.util.TimeZone getTimeZone(
+ int offset, boolean dst, long when, String country) {
+
+ android.icu.util.TimeZone icuTimeZone = getIcuTimeZone(offset, dst, when, country);
+ // We must expose a java.util.TimeZone here for API compatibility because this is a public
+ // API method.
+ return icuTimeZone != null ? java.util.TimeZone.getTimeZone(icuTimeZone.getID()) : null;
+ }
+
+ /**
+ * Returns a frozen ICU time zone that has / would have had the specified offset and DST value
+ * at the specified moment in the specified country. Returns null if no suitable zone could be
+ * found.
+ */
+ private static android.icu.util.TimeZone getIcuTimeZone(
+ int offsetMillis, boolean isDst, long whenMillis, String countryIso) {
+ if (countryIso == null) {
+ return null;
+ }
+
+ android.icu.util.TimeZone bias = android.icu.util.TimeZone.getDefault();
+ CountryTimeZones countryTimeZones =
+ TimeZoneFinder.getInstance().lookupCountryTimeZones(countryIso);
+ if (countryTimeZones == null) {
+ return null;
+ }
+ CountryTimeZones.OffsetResult offsetResult = countryTimeZones.lookupByOffsetWithBias(
+ whenMillis, bias, offsetMillis, isDst);
+ return offsetResult != null ? offsetResult.getTimeZone() : null;
+ }
+
+ /**
+ * Returns time zone IDs for time zones known to be associated with a country.
+ *
+ * <p>The list returned may be different from other on-device sources like
+ * {@link android.icu.util.TimeZone#getRegion(String)} as it can be curated to avoid
+ * contentious or obsolete mappings.
+ *
+ * @param countryCode the ISO 3166-1 alpha-2 code for the country as can be obtained using
+ * {@link java.util.Locale#getCountry()}
+ * @return IDs that can be passed to {@link java.util.TimeZone#getTimeZone(String)} or similar
+ * methods, or {@code null} if the countryCode is unrecognized
+ */
+ public static @Nullable List<String> getTimeZoneIdsForCountryCode(@NonNull String countryCode) {
+ if (countryCode == null) {
+ throw new NullPointerException("countryCode == null");
+ }
+ TimeZoneFinder timeZoneFinder = TimeZoneFinder.getInstance();
+ CountryTimeZones countryTimeZones =
+ timeZoneFinder.lookupCountryTimeZones(countryCode.toLowerCase());
+ if (countryTimeZones == null) {
+ return null;
+ }
+
+ List<String> timeZoneIds = new ArrayList<>();
+ for (TimeZoneMapping timeZoneMapping : countryTimeZones.getTimeZoneMappings()) {
+ if (timeZoneMapping.isShownInPicker()) {
+ timeZoneIds.add(timeZoneMapping.getTimeZoneId());
+ }
+ }
+ return Collections.unmodifiableList(timeZoneIds);
+ }
+
+ /**
+ * Returns a String indicating the version of the time zone database currently
+ * in use. The format of the string is dependent on the underlying time zone
+ * database implementation, but will typically contain the year in which the database
+ * was updated plus a letter from a to z indicating changes made within that year.
+ *
+ * <p>Time zone database updates should be expected to occur periodically due to
+ * political and legal changes that cannot be anticipated in advance. Therefore,
+ * when computing the UTC time for a future event, applications should be aware that
+ * the results may differ following a time zone database update. This method allows
+ * applications to detect that a database change has occurred, and to recalculate any
+ * cached times accordingly.
+ *
+ * <p>The time zone database may be assumed to change only when the device runtime
+ * is restarted. Therefore, it is not necessary to re-query the database version
+ * during the lifetime of an activity.
+ */
+ public static String getTimeZoneDatabaseVersion() {
+ return ZoneInfoDb.getInstance().getVersion();
+ }
+
+ /** @hide Field length that can hold 999 days of time */
+ public static final int HUNDRED_DAY_FIELD_LEN = 19;
+
+ private static final int SECONDS_PER_MINUTE = 60;
+ private static final int SECONDS_PER_HOUR = 60 * 60;
+ private static final int SECONDS_PER_DAY = 24 * 60 * 60;
+
+ /** @hide */
+ public static final long NANOS_PER_MS = 1000000;
+
+ private static final Object sFormatSync = new Object();
+ private static char[] sFormatStr = new char[HUNDRED_DAY_FIELD_LEN+10];
+ private static char[] sTmpFormatStr = new char[HUNDRED_DAY_FIELD_LEN+10];
+
+ static private int accumField(int amt, int suffix, boolean always, int zeropad) {
+ if (amt > 999) {
+ int num = 0;
+ while (amt != 0) {
+ num++;
+ amt /= 10;
+ }
+ return num + suffix;
+ } else {
+ if (amt > 99 || (always && zeropad >= 3)) {
+ return 3+suffix;
+ }
+ if (amt > 9 || (always && zeropad >= 2)) {
+ return 2+suffix;
+ }
+ if (always || amt > 0) {
+ return 1+suffix;
+ }
+ }
+ return 0;
+ }
+
+ static private int printFieldLocked(char[] formatStr, int amt, char suffix, int pos,
+ boolean always, int zeropad) {
+ if (always || amt > 0) {
+ final int startPos = pos;
+ if (amt > 999) {
+ int tmp = 0;
+ while (amt != 0 && tmp < sTmpFormatStr.length) {
+ int dig = amt % 10;
+ sTmpFormatStr[tmp] = (char)(dig + '0');
+ tmp++;
+ amt /= 10;
+ }
+ tmp--;
+ while (tmp >= 0) {
+ formatStr[pos] = sTmpFormatStr[tmp];
+ pos++;
+ tmp--;
+ }
+ } else {
+ if ((always && zeropad >= 3) || amt > 99) {
+ int dig = amt/100;
+ formatStr[pos] = (char)(dig + '0');
+ pos++;
+ amt -= (dig*100);
+ }
+ if ((always && zeropad >= 2) || amt > 9 || startPos != pos) {
+ int dig = amt/10;
+ formatStr[pos] = (char)(dig + '0');
+ pos++;
+ amt -= (dig*10);
+ }
+ formatStr[pos] = (char)(amt + '0');
+ pos++;
+ }
+ formatStr[pos] = suffix;
+ pos++;
+ }
+ return pos;
+ }
+
+ private static int formatDurationLocked(long duration, int fieldLen) {
+ if (sFormatStr.length < fieldLen) {
+ sFormatStr = new char[fieldLen];
+ }
+
+ char[] formatStr = sFormatStr;
+
+ if (duration == 0) {
+ int pos = 0;
+ fieldLen -= 1;
+ while (pos < fieldLen) {
+ formatStr[pos++] = ' ';
+ }
+ formatStr[pos] = '0';
+ return pos+1;
+ }
+
+ char prefix;
+ if (duration > 0) {
+ prefix = '+';
+ } else {
+ prefix = '-';
+ duration = -duration;
+ }
+
+ int millis = (int)(duration%1000);
+ int seconds = (int) Math.floor(duration / 1000);
+ int days = 0, hours = 0, minutes = 0;
+
+ if (seconds >= SECONDS_PER_DAY) {
+ days = seconds / SECONDS_PER_DAY;
+ seconds -= days * SECONDS_PER_DAY;
+ }
+ if (seconds >= SECONDS_PER_HOUR) {
+ hours = seconds / SECONDS_PER_HOUR;
+ seconds -= hours * SECONDS_PER_HOUR;
+ }
+ if (seconds >= SECONDS_PER_MINUTE) {
+ minutes = seconds / SECONDS_PER_MINUTE;
+ seconds -= minutes * SECONDS_PER_MINUTE;
+ }
+
+ int pos = 0;
+
+ if (fieldLen != 0) {
+ int myLen = accumField(days, 1, false, 0);
+ myLen += accumField(hours, 1, myLen > 0, 2);
+ myLen += accumField(minutes, 1, myLen > 0, 2);
+ myLen += accumField(seconds, 1, myLen > 0, 2);
+ myLen += accumField(millis, 2, true, myLen > 0 ? 3 : 0) + 1;
+ while (myLen < fieldLen) {
+ formatStr[pos] = ' ';
+ pos++;
+ myLen++;
+ }
+ }
+
+ formatStr[pos] = prefix;
+ pos++;
+
+ int start = pos;
+ boolean zeropad = fieldLen != 0;
+ pos = printFieldLocked(formatStr, days, 'd', pos, false, 0);
+ pos = printFieldLocked(formatStr, hours, 'h', pos, pos != start, zeropad ? 2 : 0);
+ pos = printFieldLocked(formatStr, minutes, 'm', pos, pos != start, zeropad ? 2 : 0);
+ pos = printFieldLocked(formatStr, seconds, 's', pos, pos != start, zeropad ? 2 : 0);
+ pos = printFieldLocked(formatStr, millis, 'm', pos, true, (zeropad && pos != start) ? 3 : 0);
+ formatStr[pos] = 's';
+ return pos + 1;
+ }
+
+ /** @hide Just for debugging; not internationalized. */
+ public static void formatDuration(long duration, StringBuilder builder) {
+ synchronized (sFormatSync) {
+ int len = formatDurationLocked(duration, 0);
+ builder.append(sFormatStr, 0, len);
+ }
+ }
+
+ /** @hide Just for debugging; not internationalized. */
+ public static void formatDuration(long duration, StringBuilder builder, int fieldLen) {
+ synchronized (sFormatSync) {
+ int len = formatDurationLocked(duration, fieldLen);
+ builder.append(sFormatStr, 0, len);
+ }
+ }
+
+ /** @hide Just for debugging; not internationalized. */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ public static void formatDuration(long duration, PrintWriter pw, int fieldLen) {
+ synchronized (sFormatSync) {
+ int len = formatDurationLocked(duration, fieldLen);
+ pw.print(new String(sFormatStr, 0, len));
+ }
+ }
+
+ /** @hide Just for debugging; not internationalized. */
+ @TestApi
+ public static String formatDuration(long duration) {
+ synchronized (sFormatSync) {
+ int len = formatDurationLocked(duration, 0);
+ return new String(sFormatStr, 0, len);
+ }
+ }
+
+ /** @hide Just for debugging; not internationalized. */
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+ public static void formatDuration(long duration, PrintWriter pw) {
+ formatDuration(duration, pw, 0);
+ }
+
+ /** @hide Just for debugging; not internationalized. */
+ public static void formatDuration(long time, long now, PrintWriter pw) {
+ if (time == 0) {
+ pw.print("--");
+ return;
+ }
+ formatDuration(time-now, pw, 0);
+ }
+
+ /** @hide Just for debugging; not internationalized. */
+ public static String formatUptime(long time) {
+ return formatTime(time, SystemClock.uptimeMillis());
+ }
+
+ /** @hide Just for debugging; not internationalized. */
+ public static String formatRealtime(long time) {
+ return formatTime(time, SystemClock.elapsedRealtime());
+ }
+
+ /** @hide Just for debugging; not internationalized. */
+ public static String formatTime(long time, long referenceTime) {
+ long diff = time - referenceTime;
+ if (diff > 0) {
+ return time + " (in " + diff + " ms)";
+ }
+ if (diff < 0) {
+ return time + " (" + -diff + " ms ago)";
+ }
+ return time + " (now)";
+ }
+
+ /**
+ * Convert a System.currentTimeMillis() value to a time of day value like
+ * that printed in logs. MM-DD HH:MM:SS.MMM
+ *
+ * @param millis since the epoch (1/1/1970)
+ * @return String representation of the time.
+ * @hide
+ */
+ @UnsupportedAppUsage
+ public static String logTimeOfDay(long millis) {
+ Calendar c = Calendar.getInstance();
+ if (millis >= 0) {
+ c.setTimeInMillis(millis);
+ return String.format("%tm-%td %tH:%tM:%tS.%tL", c, c, c, c, c, c);
+ } else {
+ return Long.toString(millis);
+ }
+ }
+
+ /** {@hide} */
+ public static String formatForLogging(long millis) {
+ if (millis <= 0) {
+ return "unknown";
+ } else {
+ return sLoggingFormat.format(new Date(millis));
+ }
+ }
+
+ /**
+ * Dump a currentTimeMillis style timestamp for dumpsys.
+ *
+ * @hide
+ */
+ public static void dumpTime(PrintWriter pw, long time) {
+ pw.print(sDumpDateFormat.format(new Date(time)));
+ }
+
+ /**
+ * This method is used to find if a clock time is inclusively between two other clock times
+ * @param reference The time of the day we want check if it is between start and end
+ * @param start The start time reference
+ * @param end The end time
+ * @return true if the reference time is between the two clock times, and false otherwise.
+ */
+ public static boolean isTimeBetween(@NonNull LocalTime reference,
+ @NonNull LocalTime start,
+ @NonNull LocalTime end) {
+ // ////////E----+-----S////////
+ if ((reference.isBefore(start) && reference.isAfter(end)
+ // -----+----S//////////E------
+ || (reference.isBefore(end) && reference.isBefore(start) && start.isBefore(end))
+ // ---------S//////////E---+---
+ || (reference.isAfter(end) && reference.isAfter(start)) && start.isBefore(end))) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * Dump a currentTimeMillis style timestamp for dumpsys, with the delta time from now.
+ *
+ * @hide
+ */
+ public static void dumpTimeWithDelta(PrintWriter pw, long time, long now) {
+ pw.print(sDumpDateFormat.format(new Date(time)));
+ if (time == now) {
+ pw.print(" (now)");
+ } else {
+ pw.print(" (");
+ TimeUtils.formatDuration(time, now, pw);
+ pw.print(")");
+ }
+ }}
diff --git a/android/util/TimedRemoteCaller.java b/android/util/TimedRemoteCaller.java
new file mode 100644
index 0000000..100c16e
--- /dev/null
+++ b/android/util/TimedRemoteCaller.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.os.SystemClock;
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.concurrent.TimeoutException;
+
+/**
+ * This is a helper class for making an async one way call and
+ * its async one way response response in a sync fashion within
+ * a timeout. The key idea is to call the remote method with a
+ * sequence number and a callback and then starting to wait for
+ * the response. The remote method calls back with the result and
+ * the sequence number. If the response comes within the timeout
+ * and its sequence number is the one sent in the method invocation,
+ * then the call succeeded. If the response does not come within
+ * the timeout then the call failed.
+ * <p>
+ * Typical usage is:
+ * </p>
+ * <p><pre><code>
+ * public class MyMethodCaller extends TimeoutRemoteCallHelper<Object> {
+ * // The one way remote method to call.
+ * private final IRemoteInterface mTarget;
+ *
+ * // One way callback invoked when the remote method is done.
+ * private final IRemoteCallback mCallback = new IRemoteCallback.Stub() {
+ * public void onCompleted(Object result, int sequence) {
+ * onRemoteMethodResult(result, sequence);
+ * }
+ * };
+ *
+ * public MyMethodCaller(IRemoteInterface target) {
+ * mTarget = target;
+ * }
+ *
+ * public Object onCallMyMethod(Object arg) throws RemoteException {
+ * final int sequence = onBeforeRemoteCall();
+ * mTarget.myMethod(arg, sequence);
+ * return getResultTimed(sequence);
+ * }
+ * }
+ * </code></pre></p>
+ *
+ * @param <T> The type of the expected result.
+ *
+ * @hide
+ */
+public abstract class TimedRemoteCaller<T> {
+
+ public static final long DEFAULT_CALL_TIMEOUT_MILLIS = 5000;
+
+ private final Object mLock = new Object();
+
+ private final long mCallTimeoutMillis;
+
+ /** The callbacks we are waiting for, key == sequence id, value == 1 */
+ @GuardedBy("mLock")
+ private final SparseIntArray mAwaitedCalls = new SparseIntArray(1);
+
+ /** The callbacks we received but for which the result has not yet been reported */
+ @GuardedBy("mLock")
+ private final SparseArray<T> mReceivedCalls = new SparseArray<>(1);
+
+ @GuardedBy("mLock")
+ private int mSequenceCounter;
+
+ /**
+ * Create a new timed caller.
+ *
+ * @param callTimeoutMillis The time to wait in {@link #getResultTimed} before a timed call will
+ * be declared timed out
+ */
+ public TimedRemoteCaller(long callTimeoutMillis) {
+ mCallTimeoutMillis = callTimeoutMillis;
+ }
+
+ /**
+ * Indicate that a timed call will be made.
+ *
+ * @return The sequence id for the call
+ */
+ protected final int onBeforeRemoteCall() {
+ synchronized (mLock) {
+ int sequenceId;
+ do {
+ sequenceId = mSequenceCounter++;
+ } while (mAwaitedCalls.get(sequenceId) != 0);
+
+ mAwaitedCalls.put(sequenceId, 1);
+
+ return sequenceId;
+ }
+ }
+
+ /**
+ * Indicate that the timed call has returned.
+ *
+ * @param result The result of the timed call
+ * @param sequence The sequence id of the call (from {@link #onBeforeRemoteCall()})
+ */
+ protected final void onRemoteMethodResult(T result, int sequence) {
+ synchronized (mLock) {
+ // Do nothing if we do not await the call anymore as it must have timed out
+ boolean containedSequenceId = mAwaitedCalls.get(sequence) != 0;
+ if (containedSequenceId) {
+ mAwaitedCalls.delete(sequence);
+ mReceivedCalls.put(sequence, result);
+ mLock.notifyAll();
+ }
+ }
+ }
+
+ /**
+ * Wait until the timed call has returned.
+ *
+ * @param sequence The sequence id of the call (from {@link #onBeforeRemoteCall()})
+ *
+ * @return The result of the timed call (set in {@link #onRemoteMethodResult(Object, int)})
+ */
+ protected final T getResultTimed(int sequence) throws TimeoutException {
+ final long startMillis = SystemClock.uptimeMillis();
+ while (true) {
+ try {
+ synchronized (mLock) {
+ if (mReceivedCalls.indexOfKey(sequence) >= 0) {
+ return mReceivedCalls.removeReturnOld(sequence);
+ }
+ final long elapsedMillis = SystemClock.uptimeMillis() - startMillis;
+ final long waitMillis = mCallTimeoutMillis - elapsedMillis;
+ if (waitMillis <= 0) {
+ mAwaitedCalls.delete(sequence);
+ throw new TimeoutException("No response for sequence: " + sequence);
+ }
+ mLock.wait(waitMillis);
+ }
+ } catch (InterruptedException ie) {
+ /* ignore */
+ }
+ }
+ }
+}
diff --git a/android/util/TimingLogger.java b/android/util/TimingLogger.java
new file mode 100644
index 0000000..5a4a512
--- /dev/null
+++ b/android/util/TimingLogger.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import java.util.ArrayList;
+
+import android.os.SystemClock;
+
+/**
+ * A utility class to help log timings splits throughout a method call.
+ * Typical usage is:
+ *
+ * <pre>
+ * TimingLogger timings = new TimingLogger(TAG, "methodA");
+ * // ... do some work A ...
+ * timings.addSplit("work A");
+ * // ... do some work B ...
+ * timings.addSplit("work B");
+ * // ... do some work C ...
+ * timings.addSplit("work C");
+ * timings.dumpToLog();
+ * </pre>
+ *
+ * <p>The dumpToLog call would add the following to the log:</p>
+ *
+ * <pre>
+ * D/TAG ( 3459): methodA: begin
+ * D/TAG ( 3459): methodA: 9 ms, work A
+ * D/TAG ( 3459): methodA: 1 ms, work B
+ * D/TAG ( 3459): methodA: 6 ms, work C
+ * D/TAG ( 3459): methodA: end, 16 ms
+ * </pre>
+ *
+ * @deprecated Use {@link android.os.Trace}, or
+ * <a href="https://developer.android.com/studio/profile/benchmark">Android Studio</a>. In
+ * general, milliseconds is the wrong granularity for method-level tracing. Rounding errors
+ * can overemphasize cheap operations, or underemphasize repeated operations. This timing
+ * system also does not take CPU scheduling or frequency into account.
+ */
+@Deprecated
+public class TimingLogger {
+
+ /**
+ * The Log tag to use for checking Log.isLoggable and for
+ * logging the timings.
+ */
+ private String mTag;
+
+ /** A label to be included in every log. */
+ private String mLabel;
+
+ /** Used to track whether Log.isLoggable was enabled at reset time. */
+ private boolean mDisabled;
+
+ /** Stores the time of each split. */
+ ArrayList<Long> mSplits;
+
+ /** Stores the labels for each split. */
+ ArrayList<String> mSplitLabels;
+
+ /**
+ * Create and initialize a TimingLogger object that will log using
+ * the specific tag. If the Log.isLoggable is not enabled to at
+ * least the Log.VERBOSE level for that tag at creation time then
+ * the addSplit and dumpToLog call will do nothing.
+ * @param tag the log tag to use while logging the timings
+ * @param label a string to be displayed with each log
+ */
+ public TimingLogger(String tag, String label) {
+ reset(tag, label);
+ }
+
+ /**
+ * Clear and initialize a TimingLogger object that will log using
+ * the specific tag. If the Log.isLoggable is not enabled to at
+ * least the Log.VERBOSE level for that tag at creation time then
+ * the addSplit and dumpToLog call will do nothing.
+ * @param tag the log tag to use while logging the timings
+ * @param label a string to be displayed with each log
+ */
+ public void reset(String tag, String label) {
+ mTag = tag;
+ mLabel = label;
+ reset();
+ }
+
+ /**
+ * Clear and initialize a TimingLogger object that will log using
+ * the tag and label that was specified previously, either via
+ * the constructor or a call to reset(tag, label). If the
+ * Log.isLoggable is not enabled to at least the Log.VERBOSE
+ * level for that tag at creation time then the addSplit and
+ * dumpToLog call will do nothing.
+ */
+ public void reset() {
+ mDisabled = !Log.isLoggable(mTag, Log.VERBOSE);
+ if (mDisabled) return;
+ if (mSplits == null) {
+ mSplits = new ArrayList<Long>();
+ mSplitLabels = new ArrayList<String>();
+ } else {
+ mSplits.clear();
+ mSplitLabels.clear();
+ }
+ addSplit(null);
+ }
+
+ /**
+ * Add a split for the current time, labeled with splitLabel. If
+ * Log.isLoggable was not enabled to at least the Log.VERBOSE for
+ * the specified tag at construction or reset() time then this
+ * call does nothing.
+ * @param splitLabel a label to associate with this split.
+ */
+ public void addSplit(String splitLabel) {
+ if (mDisabled) return;
+ long now = SystemClock.elapsedRealtime();
+ mSplits.add(now);
+ mSplitLabels.add(splitLabel);
+ }
+
+ /**
+ * Dumps the timings to the log using Log.d(). If Log.isLoggable was
+ * not enabled to at least the Log.VERBOSE for the specified tag at
+ * construction or reset() time then this call does nothing.
+ */
+ public void dumpToLog() {
+ if (mDisabled) return;
+ Log.d(mTag, mLabel + ": begin");
+ final long first = mSplits.get(0);
+ long now = first;
+ for (int i = 1; i < mSplits.size(); i++) {
+ now = mSplits.get(i);
+ final String splitLabel = mSplitLabels.get(i);
+ final long prev = mSplits.get(i - 1);
+
+ Log.d(mTag, mLabel + ": " + (now - prev) + " ms, " + splitLabel);
+ }
+ Log.d(mTag, mLabel + ": end, " + (now - first) + " ms");
+ }
+}
diff --git a/android/util/TimingsTraceLog.java b/android/util/TimingsTraceLog.java
new file mode 100644
index 0000000..5370645
--- /dev/null
+++ b/android/util/TimingsTraceLog.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.annotation.NonNull;
+import android.os.Build;
+import android.os.SystemClock;
+import android.os.Trace;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Helper class for reporting boot and shutdown timing metrics.
+ *
+ * <p><b>NOTE:</b> This class is not thread-safe. Use a separate copy for other threads.
+ *
+ * @hide
+ */
+public class TimingsTraceLog {
+ // Debug boot time for every step if it's non-user build.
+ private static final boolean DEBUG_BOOT_TIME = !Build.IS_USER;
+
+ // Maximum number of nested calls that are stored
+ private static final int MAX_NESTED_CALLS = 10;
+
+ private final String[] mStartNames;
+ private final long[] mStartTimes;
+
+ private final String mTag;
+ private final long mTraceTag;
+ private final long mThreadId;
+ private final int mMaxNestedCalls;
+
+ private int mCurrentLevel = -1;
+
+ public TimingsTraceLog(String tag, long traceTag) {
+ this(tag, traceTag, DEBUG_BOOT_TIME ? MAX_NESTED_CALLS : -1);
+ }
+
+ @VisibleForTesting
+ public TimingsTraceLog(String tag, long traceTag, int maxNestedCalls) {
+ mTag = tag;
+ mTraceTag = traceTag;
+ mThreadId = Thread.currentThread().getId();
+ mMaxNestedCalls = maxNestedCalls;
+ if (maxNestedCalls > 0) {
+ mStartNames = new String[maxNestedCalls];
+ mStartTimes = new long[maxNestedCalls];
+ } else {
+ mStartNames = null;
+ mStartTimes = null;
+ }
+ }
+
+ /**
+ * Begin tracing named section
+ * @param name name to appear in trace
+ */
+ public void traceBegin(String name) {
+ assertSameThread();
+ Trace.traceBegin(mTraceTag, name);
+
+ if (!DEBUG_BOOT_TIME) return;
+
+ if (mCurrentLevel + 1 >= mMaxNestedCalls) {
+ Slog.w(mTag, "not tracing duration of '" + name + "' because already reached "
+ + mMaxNestedCalls + " levels");
+ return;
+ }
+
+ mCurrentLevel++;
+ mStartNames[mCurrentLevel] = name;
+ mStartTimes[mCurrentLevel] = SystemClock.elapsedRealtime();
+ }
+
+ /**
+ * End tracing previously {@link #traceBegin(String) started} section.
+ *
+ * <p>Also {@link #logDuration logs} the duration.
+ */
+ public void traceEnd() {
+ assertSameThread();
+ Trace.traceEnd(mTraceTag);
+
+ if (!DEBUG_BOOT_TIME) return;
+
+ if (mCurrentLevel < 0) {
+ Slog.w(mTag, "traceEnd called more times than traceBegin");
+ return;
+ }
+
+ final String name = mStartNames[mCurrentLevel];
+ final long duration = SystemClock.elapsedRealtime() - mStartTimes[mCurrentLevel];
+ mCurrentLevel--;
+
+ logDuration(name, duration);
+ }
+
+ private void assertSameThread() {
+ final Thread currentThread = Thread.currentThread();
+ if (currentThread.getId() != mThreadId) {
+ throw new IllegalStateException("Instance of TimingsTraceLog can only be called from "
+ + "the thread it was created on (tid: " + mThreadId + "), but was from "
+ + currentThread.getName() + " (tid: " + currentThread.getId() + ")");
+ }
+ }
+
+ /**
+ * Logs a duration so it can be parsed by external tools for performance reporting.
+ */
+ public void logDuration(String name, long timeMs) {
+ Slog.d(mTag, name + " took to complete: " + timeMs + "ms");
+ }
+
+ /**
+ * Gets the names of the traces that {@link #traceBegin(String) have begun} but
+ * {@link #traceEnd() have not finished} yet.
+ *
+ * <p><b>NOTE:</b> this method is expensive and it should not be used in "production" - it
+ * should only be used for debugging purposes during development (and/or guarded by
+ * static {@code DEBUG} constants that are {@code false}).
+ */
+ @NonNull
+ public final List<String> getUnfinishedTracesForDebug() {
+ if (mStartTimes == null || mCurrentLevel < 0) return Collections.emptyList();
+ final ArrayList<String> list = new ArrayList<>(mCurrentLevel + 1);
+ for (int i = 0; i <= mCurrentLevel; i++) {
+ list.add(mStartNames[i]);
+ }
+ return list;
+ }
+}
diff --git a/android/util/TrustedTime.java b/android/util/TrustedTime.java
new file mode 100644
index 0000000..f41fe85
--- /dev/null
+++ b/android/util/TrustedTime.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.compat.annotation.UnsupportedAppUsage;
+
+/**
+ * Interface that provides trusted time information, possibly coming from an NTP
+ * server.
+ *
+ * @hide
+ * @deprecated Only kept for UnsupportedAppUsage. Do not use. See {@link NtpTrustedTime}
+ */
+public interface TrustedTime {
+ /**
+ * Force update with an external trusted time source, returning {@code true}
+ * when successful.
+ *
+ * @deprecated Only kept for UnsupportedAppUsage. Do not use. See {@link NtpTrustedTime}
+ */
+ @Deprecated
+ @UnsupportedAppUsage
+ public boolean forceRefresh();
+
+ /**
+ * Check if this instance has cached a response from a trusted time source.
+ *
+ * @deprecated Only kept for UnsupportedAppUsage. Do not use. See {@link NtpTrustedTime}
+ */
+ @Deprecated
+ @UnsupportedAppUsage
+ boolean hasCache();
+
+ /**
+ * Return time since last trusted time source contact, or
+ * {@link Long#MAX_VALUE} if never contacted.
+ *
+ * @deprecated Only kept for UnsupportedAppUsage. Do not use. See {@link NtpTrustedTime}
+ */
+ @Deprecated
+ @UnsupportedAppUsage
+ public long getCacheAge();
+
+ /**
+ * Return current time similar to {@link System#currentTimeMillis()},
+ * possibly using a cached authoritative time source.
+ *
+ * @deprecated Only kept for UnsupportedAppUsage. Do not use. See {@link NtpTrustedTime}
+ */
+ @Deprecated
+ @UnsupportedAppUsage
+ long currentTimeMillis();
+}
diff --git a/android/util/TypedValue.java b/android/util/TypedValue.java
new file mode 100644
index 0000000..7f1ee30
--- /dev/null
+++ b/android/util/TypedValue.java
@@ -0,0 +1,558 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.annotation.AnyRes;
+import android.content.pm.ActivityInfo.Config;
+
+/**
+ * Container for a dynamically typed data value. Primarily used with
+ * {@link android.content.res.Resources} for holding resource values.
+ */
+public class TypedValue {
+ /** The value contains no data. */
+ public static final int TYPE_NULL = 0x00;
+
+ /** The <var>data</var> field holds a resource identifier. */
+ public static final int TYPE_REFERENCE = 0x01;
+ /** The <var>data</var> field holds an attribute resource
+ * identifier (referencing an attribute in the current theme
+ * style, not a resource entry). */
+ public static final int TYPE_ATTRIBUTE = 0x02;
+ /** The <var>string</var> field holds string data. In addition, if
+ * <var>data</var> is non-zero then it is the string block
+ * index of the string and <var>assetCookie</var> is the set of
+ * assets the string came from. */
+ public static final int TYPE_STRING = 0x03;
+ /** The <var>data</var> field holds an IEEE 754 floating point number. */
+ public static final int TYPE_FLOAT = 0x04;
+ /** The <var>data</var> field holds a complex number encoding a
+ * dimension value. */
+ public static final int TYPE_DIMENSION = 0x05;
+ /** The <var>data</var> field holds a complex number encoding a fraction
+ * of a container. */
+ public static final int TYPE_FRACTION = 0x06;
+
+ /** Identifies the start of plain integer values. Any type value
+ * from this to {@link #TYPE_LAST_INT} means the
+ * <var>data</var> field holds a generic integer value. */
+ public static final int TYPE_FIRST_INT = 0x10;
+
+ /** The <var>data</var> field holds a number that was
+ * originally specified in decimal. */
+ public static final int TYPE_INT_DEC = 0x10;
+ /** The <var>data</var> field holds a number that was
+ * originally specified in hexadecimal (0xn). */
+ public static final int TYPE_INT_HEX = 0x11;
+ /** The <var>data</var> field holds 0 or 1 that was originally
+ * specified as "false" or "true". */
+ public static final int TYPE_INT_BOOLEAN = 0x12;
+
+ /** Identifies the start of integer values that were specified as
+ * color constants (starting with '#'). */
+ public static final int TYPE_FIRST_COLOR_INT = 0x1c;
+
+ /** The <var>data</var> field holds a color that was originally
+ * specified as #aarrggbb. */
+ public static final int TYPE_INT_COLOR_ARGB8 = 0x1c;
+ /** The <var>data</var> field holds a color that was originally
+ * specified as #rrggbb. */
+ public static final int TYPE_INT_COLOR_RGB8 = 0x1d;
+ /** The <var>data</var> field holds a color that was originally
+ * specified as #argb. */
+ public static final int TYPE_INT_COLOR_ARGB4 = 0x1e;
+ /** The <var>data</var> field holds a color that was originally
+ * specified as #rgb. */
+ public static final int TYPE_INT_COLOR_RGB4 = 0x1f;
+
+ /** Identifies the end of integer values that were specified as color
+ * constants. */
+ public static final int TYPE_LAST_COLOR_INT = 0x1f;
+
+ /** Identifies the end of plain integer values. */
+ public static final int TYPE_LAST_INT = 0x1f;
+
+ /* ------------------------------------------------------------ */
+
+ /** Complex data: bit location of unit information. */
+ public static final int COMPLEX_UNIT_SHIFT = 0;
+ /** Complex data: mask to extract unit information (after shifting by
+ * {@link #COMPLEX_UNIT_SHIFT}). This gives us 16 possible types, as
+ * defined below. */
+ public static final int COMPLEX_UNIT_MASK = 0xf;
+
+ /** {@link #TYPE_DIMENSION} complex unit: Value is raw pixels. */
+ public static final int COMPLEX_UNIT_PX = 0;
+ /** {@link #TYPE_DIMENSION} complex unit: Value is Device Independent
+ * Pixels. */
+ public static final int COMPLEX_UNIT_DIP = 1;
+ /** {@link #TYPE_DIMENSION} complex unit: Value is a scaled pixel. */
+ public static final int COMPLEX_UNIT_SP = 2;
+ /** {@link #TYPE_DIMENSION} complex unit: Value is in points. */
+ public static final int COMPLEX_UNIT_PT = 3;
+ /** {@link #TYPE_DIMENSION} complex unit: Value is in inches. */
+ public static final int COMPLEX_UNIT_IN = 4;
+ /** {@link #TYPE_DIMENSION} complex unit: Value is in millimeters. */
+ public static final int COMPLEX_UNIT_MM = 5;
+
+ /** {@link #TYPE_FRACTION} complex unit: A basic fraction of the overall
+ * size. */
+ public static final int COMPLEX_UNIT_FRACTION = 0;
+ /** {@link #TYPE_FRACTION} complex unit: A fraction of the parent size. */
+ public static final int COMPLEX_UNIT_FRACTION_PARENT = 1;
+
+ /** Complex data: where the radix information is, telling where the decimal
+ * place appears in the mantissa. */
+ public static final int COMPLEX_RADIX_SHIFT = 4;
+ /** Complex data: mask to extract radix information (after shifting by
+ * {@link #COMPLEX_RADIX_SHIFT}). This give us 4 possible fixed point
+ * representations as defined below. */
+ public static final int COMPLEX_RADIX_MASK = 0x3;
+
+ /** Complex data: the mantissa is an integral number -- i.e., 0xnnnnnn.0 */
+ public static final int COMPLEX_RADIX_23p0 = 0;
+ /** Complex data: the mantissa magnitude is 16 bits -- i.e, 0xnnnn.nn */
+ public static final int COMPLEX_RADIX_16p7 = 1;
+ /** Complex data: the mantissa magnitude is 8 bits -- i.e, 0xnn.nnnn */
+ public static final int COMPLEX_RADIX_8p15 = 2;
+ /** Complex data: the mantissa magnitude is 0 bits -- i.e, 0x0.nnnnnn */
+ public static final int COMPLEX_RADIX_0p23 = 3;
+
+ /** Complex data: bit location of mantissa information. */
+ public static final int COMPLEX_MANTISSA_SHIFT = 8;
+ /** Complex data: mask to extract mantissa information (after shifting by
+ * {@link #COMPLEX_MANTISSA_SHIFT}). This gives us 23 bits of precision;
+ * the top bit is the sign. */
+ public static final int COMPLEX_MANTISSA_MASK = 0xffffff;
+
+ /* ------------------------------------------------------------ */
+
+ /**
+ * {@link #TYPE_NULL} data indicating the value was not specified.
+ */
+ public static final int DATA_NULL_UNDEFINED = 0;
+ /**
+ * {@link #TYPE_NULL} data indicating the value was explicitly set to null.
+ */
+ public static final int DATA_NULL_EMPTY = 1;
+
+ /* ------------------------------------------------------------ */
+
+ /**
+ * If {@link #density} is equal to this value, then the density should be
+ * treated as the system's default density value: {@link DisplayMetrics#DENSITY_DEFAULT}.
+ */
+ public static final int DENSITY_DEFAULT = 0;
+
+ /**
+ * If {@link #density} is equal to this value, then there is no density
+ * associated with the resource and it should not be scaled.
+ */
+ public static final int DENSITY_NONE = 0xffff;
+
+ /* ------------------------------------------------------------ */
+
+ /** The type held by this value, as defined by the constants here.
+ * This tells you how to interpret the other fields in the object. */
+ public int type;
+
+ /** If the value holds a string, this is it. */
+ public CharSequence string;
+
+ /** Basic data in the value, interpreted according to {@link #type} */
+ public int data;
+
+ /** Additional information about where the value came from; only
+ * set for strings. */
+ public int assetCookie;
+
+ /** If Value came from a resource, this holds the corresponding resource id. */
+ @AnyRes
+ public int resourceId;
+
+ /**
+ * If the value came from a resource, these are the configurations for
+ * which its contents can change.
+ *
+ * <p>For example, if a resource has a value defined for the -land resource qualifier,
+ * this field will have the {@link android.content.pm.ActivityInfo#CONFIG_ORIENTATION} bit set.
+ * </p>
+ *
+ * @see android.content.pm.ActivityInfo#CONFIG_MCC
+ * @see android.content.pm.ActivityInfo#CONFIG_MNC
+ * @see android.content.pm.ActivityInfo#CONFIG_LOCALE
+ * @see android.content.pm.ActivityInfo#CONFIG_TOUCHSCREEN
+ * @see android.content.pm.ActivityInfo#CONFIG_KEYBOARD
+ * @see android.content.pm.ActivityInfo#CONFIG_KEYBOARD_HIDDEN
+ * @see android.content.pm.ActivityInfo#CONFIG_NAVIGATION
+ * @see android.content.pm.ActivityInfo#CONFIG_ORIENTATION
+ * @see android.content.pm.ActivityInfo#CONFIG_SCREEN_LAYOUT
+ * @see android.content.pm.ActivityInfo#CONFIG_UI_MODE
+ * @see android.content.pm.ActivityInfo#CONFIG_SCREEN_SIZE
+ * @see android.content.pm.ActivityInfo#CONFIG_SMALLEST_SCREEN_SIZE
+ * @see android.content.pm.ActivityInfo#CONFIG_DENSITY
+ * @see android.content.pm.ActivityInfo#CONFIG_LAYOUT_DIRECTION
+ * @see android.content.pm.ActivityInfo#CONFIG_COLOR_MODE
+ *
+ */
+ public @Config int changingConfigurations = -1;
+
+ /**
+ * If the Value came from a resource, this holds the corresponding pixel density.
+ * */
+ public int density;
+
+ /**
+ * If the Value came from a style resource or a layout resource (set in an XML layout), this
+ * holds the corresponding style or layout resource id against which the attribute was resolved.
+ */
+ public int sourceResourceId;
+
+ /* ------------------------------------------------------------ */
+
+ /** Return the data for this value as a float. Only use for values
+ * whose type is {@link #TYPE_FLOAT}. */
+ public final float getFloat() {
+ return Float.intBitsToFloat(data);
+ }
+
+ private static final float MANTISSA_MULT =
+ 1.0f / (1<<TypedValue.COMPLEX_MANTISSA_SHIFT);
+ private static final float[] RADIX_MULTS = new float[] {
+ 1.0f*MANTISSA_MULT, 1.0f/(1<<7)*MANTISSA_MULT,
+ 1.0f/(1<<15)*MANTISSA_MULT, 1.0f/(1<<23)*MANTISSA_MULT
+ };
+
+ /**
+ * Determine if a value is a color.
+ *
+ * This works by comparing {@link #type} to {@link #TYPE_FIRST_COLOR_INT}
+ * and {@link #TYPE_LAST_COLOR_INT}.
+ *
+ * @return true if this value is a color
+ */
+ public boolean isColorType() {
+ return (type >= TYPE_FIRST_COLOR_INT && type <= TYPE_LAST_COLOR_INT);
+ }
+
+ /**
+ * Retrieve the base value from a complex data integer. This uses the
+ * {@link #COMPLEX_MANTISSA_MASK} and {@link #COMPLEX_RADIX_MASK} fields of
+ * the data to compute a floating point representation of the number they
+ * describe. The units are ignored.
+ *
+ * @param complex A complex data value.
+ *
+ * @return A floating point value corresponding to the complex data.
+ */
+ public static float complexToFloat(int complex)
+ {
+ return (complex&(TypedValue.COMPLEX_MANTISSA_MASK
+ <<TypedValue.COMPLEX_MANTISSA_SHIFT))
+ * RADIX_MULTS[(complex>>TypedValue.COMPLEX_RADIX_SHIFT)
+ & TypedValue.COMPLEX_RADIX_MASK];
+ }
+
+ /**
+ * Converts a complex data value holding a dimension to its final floating
+ * point value. The given <var>data</var> must be structured as a
+ * {@link #TYPE_DIMENSION}.
+ *
+ * @param data A complex data value holding a unit, magnitude, and
+ * mantissa.
+ * @param metrics Current display metrics to use in the conversion --
+ * supplies display density and scaling information.
+ *
+ * @return The complex floating point value multiplied by the appropriate
+ * metrics depending on its unit.
+ */
+ public static float complexToDimension(int data, DisplayMetrics metrics)
+ {
+ return applyDimension(
+ (data>>COMPLEX_UNIT_SHIFT)&COMPLEX_UNIT_MASK,
+ complexToFloat(data),
+ metrics);
+ }
+
+ /**
+ * Converts a complex data value holding a dimension to its final value
+ * as an integer pixel offset. This is the same as
+ * {@link #complexToDimension}, except the raw floating point value is
+ * truncated to an integer (pixel) value.
+ * The given <var>data</var> must be structured as a
+ * {@link #TYPE_DIMENSION}.
+ *
+ * @param data A complex data value holding a unit, magnitude, and
+ * mantissa.
+ * @param metrics Current display metrics to use in the conversion --
+ * supplies display density and scaling information.
+ *
+ * @return The number of pixels specified by the data and its desired
+ * multiplier and units.
+ */
+ public static int complexToDimensionPixelOffset(int data,
+ DisplayMetrics metrics)
+ {
+ return (int)applyDimension(
+ (data>>COMPLEX_UNIT_SHIFT)&COMPLEX_UNIT_MASK,
+ complexToFloat(data),
+ metrics);
+ }
+
+ /**
+ * Converts a complex data value holding a dimension to its final value
+ * as an integer pixel size. This is the same as
+ * {@link #complexToDimension}, except the raw floating point value is
+ * converted to an integer (pixel) value for use as a size. A size
+ * conversion involves rounding the base value, and ensuring that a
+ * non-zero base value is at least one pixel in size.
+ * The given <var>data</var> must be structured as a
+ * {@link #TYPE_DIMENSION}.
+ *
+ * @param data A complex data value holding a unit, magnitude, and
+ * mantissa.
+ * @param metrics Current display metrics to use in the conversion --
+ * supplies display density and scaling information.
+ *
+ * @return The number of pixels specified by the data and its desired
+ * multiplier and units.
+ */
+ public static int complexToDimensionPixelSize(int data,
+ DisplayMetrics metrics)
+ {
+ final float value = complexToFloat(data);
+ final float f = applyDimension(
+ (data>>COMPLEX_UNIT_SHIFT)&COMPLEX_UNIT_MASK,
+ value,
+ metrics);
+ final int res = (int) ((f >= 0) ? (f + 0.5f) : (f - 0.5f));
+ if (res != 0) return res;
+ if (value == 0) return 0;
+ if (value > 0) return 1;
+ return -1;
+ }
+
+ /**
+ * @hide Was accidentally exposed in API level 1 for debugging purposes.
+ * Kept for compatibility just in case although the debugging code has been removed.
+ */
+ @Deprecated
+ public static float complexToDimensionNoisy(int data, DisplayMetrics metrics)
+ {
+ return complexToDimension(data, metrics);
+ }
+
+ /**
+ * Return the complex unit type for this value. For example, a dimen type
+ * with value 12sp will return {@link #COMPLEX_UNIT_SP}. Only use for values
+ * whose type is {@link #TYPE_DIMENSION}.
+ *
+ * @return The complex unit type.
+ */
+ public int getComplexUnit()
+ {
+ return COMPLEX_UNIT_MASK & (data>>TypedValue.COMPLEX_UNIT_SHIFT);
+ }
+
+ /**
+ * Converts an unpacked complex data value holding a dimension to its final floating
+ * point value. The two parameters <var>unit</var> and <var>value</var>
+ * are as in {@link #TYPE_DIMENSION}.
+ *
+ * @param unit The unit to convert from.
+ * @param value The value to apply the unit to.
+ * @param metrics Current display metrics to use in the conversion --
+ * supplies display density and scaling information.
+ *
+ * @return The complex floating point value multiplied by the appropriate
+ * metrics depending on its unit.
+ */
+ public static float applyDimension(int unit, float value,
+ DisplayMetrics metrics)
+ {
+ switch (unit) {
+ case COMPLEX_UNIT_PX:
+ return value;
+ case COMPLEX_UNIT_DIP:
+ return value * metrics.density;
+ case COMPLEX_UNIT_SP:
+ return value * metrics.scaledDensity;
+ case COMPLEX_UNIT_PT:
+ return value * metrics.xdpi * (1.0f/72);
+ case COMPLEX_UNIT_IN:
+ return value * metrics.xdpi;
+ case COMPLEX_UNIT_MM:
+ return value * metrics.xdpi * (1.0f/25.4f);
+ }
+ return 0;
+ }
+
+ /**
+ * Return the data for this value as a dimension. Only use for values
+ * whose type is {@link #TYPE_DIMENSION}.
+ *
+ * @param metrics Current display metrics to use in the conversion --
+ * supplies display density and scaling information.
+ *
+ * @return The complex floating point value multiplied by the appropriate
+ * metrics depending on its unit.
+ */
+ public float getDimension(DisplayMetrics metrics)
+ {
+ return complexToDimension(data, metrics);
+ }
+
+ /**
+ * Converts a complex data value holding a fraction to its final floating
+ * point value. The given <var>data</var> must be structured as a
+ * {@link #TYPE_FRACTION}.
+ *
+ * @param data A complex data value holding a unit, magnitude, and
+ * mantissa.
+ * @param base The base value of this fraction. In other words, a
+ * standard fraction is multiplied by this value.
+ * @param pbase The parent base value of this fraction. In other
+ * words, a parent fraction (nn%p) is multiplied by this
+ * value.
+ *
+ * @return The complex floating point value multiplied by the appropriate
+ * base value depending on its unit.
+ */
+ public static float complexToFraction(int data, float base, float pbase)
+ {
+ switch ((data>>COMPLEX_UNIT_SHIFT)&COMPLEX_UNIT_MASK) {
+ case COMPLEX_UNIT_FRACTION:
+ return complexToFloat(data) * base;
+ case COMPLEX_UNIT_FRACTION_PARENT:
+ return complexToFloat(data) * pbase;
+ }
+ return 0;
+ }
+
+ /**
+ * Return the data for this value as a fraction. Only use for values whose
+ * type is {@link #TYPE_FRACTION}.
+ *
+ * @param base The base value of this fraction. In other words, a
+ * standard fraction is multiplied by this value.
+ * @param pbase The parent base value of this fraction. In other
+ * words, a parent fraction (nn%p) is multiplied by this
+ * value.
+ *
+ * @return The complex floating point value multiplied by the appropriate
+ * base value depending on its unit.
+ */
+ public float getFraction(float base, float pbase)
+ {
+ return complexToFraction(data, base, pbase);
+ }
+
+ /**
+ * Regardless of the actual type of the value, try to convert it to a
+ * string value. For example, a color type will be converted to a
+ * string of the form #aarrggbb.
+ *
+ * @return CharSequence The coerced string value. If the value is
+ * null or the type is not known, null is returned.
+ */
+ public final CharSequence coerceToString()
+ {
+ int t = type;
+ if (t == TYPE_STRING) {
+ return string;
+ }
+ return coerceToString(t, data);
+ }
+
+ private static final String[] DIMENSION_UNIT_STRS = new String[] {
+ "px", "dip", "sp", "pt", "in", "mm"
+ };
+ private static final String[] FRACTION_UNIT_STRS = new String[] {
+ "%", "%p"
+ };
+
+ /**
+ * Perform type conversion as per {@link #coerceToString()} on an
+ * explicitly supplied type and data.
+ *
+ * @param type The data type identifier.
+ * @param data The data value.
+ *
+ * @return String The coerced string value. If the value is
+ * null or the type is not known, null is returned.
+ */
+ public static final String coerceToString(int type, int data)
+ {
+ switch (type) {
+ case TYPE_NULL:
+ return null;
+ case TYPE_REFERENCE:
+ return "@" + data;
+ case TYPE_ATTRIBUTE:
+ return "?" + data;
+ case TYPE_FLOAT:
+ return Float.toString(Float.intBitsToFloat(data));
+ case TYPE_DIMENSION:
+ return Float.toString(complexToFloat(data)) + DIMENSION_UNIT_STRS[
+ (data>>COMPLEX_UNIT_SHIFT)&COMPLEX_UNIT_MASK];
+ case TYPE_FRACTION:
+ return Float.toString(complexToFloat(data)*100) + FRACTION_UNIT_STRS[
+ (data>>COMPLEX_UNIT_SHIFT)&COMPLEX_UNIT_MASK];
+ case TYPE_INT_HEX:
+ return "0x" + Integer.toHexString(data);
+ case TYPE_INT_BOOLEAN:
+ return data != 0 ? "true" : "false";
+ }
+
+ if (type >= TYPE_FIRST_COLOR_INT && type <= TYPE_LAST_COLOR_INT) {
+ return "#" + Integer.toHexString(data);
+ } else if (type >= TYPE_FIRST_INT && type <= TYPE_LAST_INT) {
+ return Integer.toString(data);
+ }
+
+ return null;
+ }
+
+ public void setTo(TypedValue other)
+ {
+ type = other.type;
+ string = other.string;
+ data = other.data;
+ assetCookie = other.assetCookie;
+ resourceId = other.resourceId;
+ density = other.density;
+ }
+
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.append("TypedValue{t=0x").append(Integer.toHexString(type));
+ sb.append("/d=0x").append(Integer.toHexString(data));
+ if (type == TYPE_STRING) {
+ sb.append(" \"").append(string != null ? string : "<null>").append("\"");
+ }
+ if (assetCookie != 0) {
+ sb.append(" a=").append(assetCookie);
+ }
+ if (resourceId != 0) {
+ sb.append(" r=0x").append(Integer.toHexString(resourceId));
+ }
+ sb.append("}");
+ return sb.toString();
+ }
+};
+
diff --git a/android/util/UtilConfig.java b/android/util/UtilConfig.java
new file mode 100644
index 0000000..7658c40
--- /dev/null
+++ b/android/util/UtilConfig.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+/**
+ * Class to configure several of the util classes.
+ *
+ * @hide
+ */
+public class UtilConfig {
+ static boolean sThrowExceptionForUpperArrayOutOfBounds = true;
+
+ public static void setThrowExceptionForUpperArrayOutOfBounds(boolean check) {
+ sThrowExceptionForUpperArrayOutOfBounds = check;
+ }
+}
diff --git a/android/util/Xml.java b/android/util/Xml.java
new file mode 100644
index 0000000..e3b8fec
--- /dev/null
+++ b/android/util/Xml.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import libcore.util.XmlObjectFactory;
+
+import org.xml.sax.ContentHandler;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * XML utility methods.
+ */
+public class Xml {
+ private Xml() {}
+
+ /**
+ * {@link org.xmlpull.v1.XmlPullParser} "relaxed" feature name.
+ *
+ * @see <a href="http://xmlpull.org/v1/doc/features.html#relaxed">
+ * specification</a>
+ */
+ public static String FEATURE_RELAXED = "http://xmlpull.org/v1/doc/features.html#relaxed";
+
+ /**
+ * Parses the given xml string and fires events on the given SAX handler.
+ */
+ public static void parse(String xml, ContentHandler contentHandler)
+ throws SAXException {
+ try {
+ XMLReader reader = XmlObjectFactory.newXMLReader();
+ reader.setContentHandler(contentHandler);
+ reader.parse(new InputSource(new StringReader(xml)));
+ } catch (IOException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Parses xml from the given reader and fires events on the given SAX
+ * handler.
+ */
+ public static void parse(Reader in, ContentHandler contentHandler)
+ throws IOException, SAXException {
+ XMLReader reader = XmlObjectFactory.newXMLReader();
+ reader.setContentHandler(contentHandler);
+ reader.parse(new InputSource(in));
+ }
+
+ /**
+ * Parses xml from the given input stream and fires events on the given SAX
+ * handler.
+ */
+ public static void parse(InputStream in, Encoding encoding,
+ ContentHandler contentHandler) throws IOException, SAXException {
+ XMLReader reader = XmlObjectFactory.newXMLReader();
+ reader.setContentHandler(contentHandler);
+ InputSource source = new InputSource(in);
+ source.setEncoding(encoding.expatName);
+ reader.parse(source);
+ }
+
+ /**
+ * Returns a new pull parser with namespace support.
+ */
+ public static XmlPullParser newPullParser() {
+ try {
+ XmlPullParser parser = XmlObjectFactory.newXmlPullParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, true);
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ return parser;
+ } catch (XmlPullParserException e) {
+ throw new AssertionError();
+ }
+ }
+
+ /**
+ * Creates a new xml serializer.
+ */
+ public static XmlSerializer newSerializer() {
+ return XmlObjectFactory.newXmlSerializer();
+ }
+
+ /**
+ * Supported character encodings.
+ */
+ public enum Encoding {
+
+ US_ASCII("US-ASCII"),
+ UTF_8("UTF-8"),
+ UTF_16("UTF-16"),
+ ISO_8859_1("ISO-8859-1");
+
+ final String expatName;
+
+ Encoding(String expatName) {
+ this.expatName = expatName;
+ }
+ }
+
+ /**
+ * Finds an encoding by name. Returns UTF-8 if you pass {@code null}.
+ */
+ public static Encoding findEncodingByName(String encodingName)
+ throws UnsupportedEncodingException {
+ if (encodingName == null) {
+ return Encoding.UTF_8;
+ }
+
+ for (Encoding encoding : Encoding.values()) {
+ if (encoding.expatName.equalsIgnoreCase(encodingName))
+ return encoding;
+ }
+ throw new UnsupportedEncodingException(encodingName);
+ }
+
+ /**
+ * Return an AttributeSet interface for use with the given XmlPullParser.
+ * If the given parser itself implements AttributeSet, that implementation
+ * is simply returned. Otherwise a wrapper class is
+ * instantiated on top of the XmlPullParser, as a proxy for retrieving its
+ * attributes, and returned to you.
+ *
+ * @param parser The existing parser for which you would like an
+ * AttributeSet.
+ *
+ * @return An AttributeSet you can use to retrieve the
+ * attribute values at each of the tags as the parser moves
+ * through its XML document.
+ *
+ * @see AttributeSet
+ */
+ public static AttributeSet asAttributeSet(XmlPullParser parser) {
+ return (parser instanceof AttributeSet)
+ ? (AttributeSet) parser
+ : new XmlPullAttributes(parser);
+ }
+}
diff --git a/android/util/XmlPullAttributes.java b/android/util/XmlPullAttributes.java
new file mode 100644
index 0000000..d83b355
--- /dev/null
+++ b/android/util/XmlPullAttributes.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import android.compat.annotation.UnsupportedAppUsage;
+
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Provides an implementation of AttributeSet on top of an XmlPullParser.
+ */
+class XmlPullAttributes implements AttributeSet {
+ @UnsupportedAppUsage
+ public XmlPullAttributes(XmlPullParser parser) {
+ mParser = parser;
+ }
+
+ public int getAttributeCount() {
+ return mParser.getAttributeCount();
+ }
+
+ public String getAttributeNamespace (int index) {
+ return mParser.getAttributeNamespace(index);
+ }
+
+ public String getAttributeName(int index) {
+ return mParser.getAttributeName(index);
+ }
+
+ public String getAttributeValue(int index) {
+ return mParser.getAttributeValue(index);
+ }
+
+ public String getAttributeValue(String namespace, String name) {
+ return mParser.getAttributeValue(namespace, name);
+ }
+
+ public String getPositionDescription() {
+ return mParser.getPositionDescription();
+ }
+
+ public int getAttributeNameResource(int index) {
+ return 0;
+ }
+
+ public int getAttributeListValue(String namespace, String attribute,
+ String[] options, int defaultValue) {
+ return XmlUtils.convertValueToList(
+ getAttributeValue(namespace, attribute), options, defaultValue);
+ }
+
+ public boolean getAttributeBooleanValue(String namespace, String attribute,
+ boolean defaultValue) {
+ return XmlUtils.convertValueToBoolean(
+ getAttributeValue(namespace, attribute), defaultValue);
+ }
+
+ public int getAttributeResourceValue(String namespace, String attribute,
+ int defaultValue) {
+ return XmlUtils.convertValueToInt(
+ getAttributeValue(namespace, attribute), defaultValue);
+ }
+
+ public int getAttributeIntValue(String namespace, String attribute,
+ int defaultValue) {
+ return XmlUtils.convertValueToInt(
+ getAttributeValue(namespace, attribute), defaultValue);
+ }
+
+ public int getAttributeUnsignedIntValue(String namespace, String attribute,
+ int defaultValue) {
+ return XmlUtils.convertValueToUnsignedInt(
+ getAttributeValue(namespace, attribute), defaultValue);
+ }
+
+ public float getAttributeFloatValue(String namespace, String attribute,
+ float defaultValue) {
+ String s = getAttributeValue(namespace, attribute);
+ if (s != null) {
+ return Float.parseFloat(s);
+ }
+ return defaultValue;
+ }
+
+ public int getAttributeListValue(int index,
+ String[] options, int defaultValue) {
+ return XmlUtils.convertValueToList(
+ getAttributeValue(index), options, defaultValue);
+ }
+
+ public boolean getAttributeBooleanValue(int index, boolean defaultValue) {
+ return XmlUtils.convertValueToBoolean(
+ getAttributeValue(index), defaultValue);
+ }
+
+ public int getAttributeResourceValue(int index, int defaultValue) {
+ return XmlUtils.convertValueToInt(
+ getAttributeValue(index), defaultValue);
+ }
+
+ public int getAttributeIntValue(int index, int defaultValue) {
+ return XmlUtils.convertValueToInt(
+ getAttributeValue(index), defaultValue);
+ }
+
+ public int getAttributeUnsignedIntValue(int index, int defaultValue) {
+ return XmlUtils.convertValueToUnsignedInt(
+ getAttributeValue(index), defaultValue);
+ }
+
+ public float getAttributeFloatValue(int index, float defaultValue) {
+ String s = getAttributeValue(index);
+ if (s != null) {
+ return Float.parseFloat(s);
+ }
+ return defaultValue;
+ }
+
+ public String getIdAttribute() {
+ return getAttributeValue(null, "id");
+ }
+
+ public String getClassAttribute() {
+ return getAttributeValue(null, "class");
+ }
+
+ public int getIdAttributeResourceValue(int defaultValue) {
+ return getAttributeResourceValue(null, "id", defaultValue);
+ }
+
+ public int getStyleAttribute() {
+ return getAttributeResourceValue(null, "style", 0);
+ }
+
+ @UnsupportedAppUsage
+ /*package*/ XmlPullParser mParser;
+}
diff --git a/android/util/Xml_Delegate.java b/android/util/Xml_Delegate.java
new file mode 100644
index 0000000..e309dc6
--- /dev/null
+++ b/android/util/Xml_Delegate.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import com.android.layoutlib.bridge.impl.DelegateManager;
+import com.android.layoutlib.bridge.impl.ParserFactory;
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+/**
+ * Delegate overriding some methods of android.util.Xml
+ *
+ * Through the layoutlib_create tool, the original methods of Xml have been replaced
+ * by calls to methods of the same name in this delegate class.
+ *
+ * Because it's a stateless class to start with, there's no need to keep a {@link DelegateManager}
+ * around to map int to instance of the delegate.
+ */
+public class Xml_Delegate {
+ @LayoutlibDelegate
+ /*package*/ static XmlPullParser newPullParser() {
+ try {
+ return ParserFactory.create();
+ } catch (XmlPullParserException e) {
+ throw new AssertionError();
+ }
+ }
+}
diff --git a/android/util/apk/ApkSignatureSchemeV2Verifier.java b/android/util/apk/ApkSignatureSchemeV2Verifier.java
new file mode 100644
index 0000000..346fe29
--- /dev/null
+++ b/android/util/apk/ApkSignatureSchemeV2Verifier.java
@@ -0,0 +1,446 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.apk;
+
+import static android.util.apk.ApkSigningBlockUtils.CONTENT_DIGEST_VERITY_CHUNKED_SHA256;
+import static android.util.apk.ApkSigningBlockUtils.compareSignatureAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.getContentDigestAlgorithmJcaDigestAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
+import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmContentDigestAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaKeyAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaSignatureAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.isSupportedSignatureAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.pickBestDigestForV4;
+import static android.util.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray;
+
+import android.util.ArrayMap;
+import android.util.Pair;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.DigestException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * APK Signature Scheme v2 verifier.
+ *
+ * <p>APK Signature Scheme v2 is a whole-file signature scheme which aims to protect every single
+ * bit of the APK, as opposed to the JAR Signature Scheme which protects only the names and
+ * uncompressed contents of ZIP entries.
+ *
+ * @see <a href="https://source.android.com/security/apksigning/v2.html">APK Signature Scheme v2</a>
+ *
+ * @hide for internal use only.
+ */
+public class ApkSignatureSchemeV2Verifier {
+
+ /**
+ * ID of this signature scheme as used in X-Android-APK-Signed header used in JAR signing.
+ */
+ public static final int SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID = 2;
+
+ private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
+
+ /**
+ * Returns {@code true} if the provided APK contains an APK Signature Scheme V2 signature.
+ *
+ * <p><b>NOTE: This method does not verify the signature.</b>
+ */
+ public static boolean hasSignature(String apkFile) throws IOException {
+ try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) {
+ findSignature(apk);
+ return true;
+ } catch (SignatureNotFoundException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Verifies APK Signature Scheme v2 signatures of the provided APK and returns the certificates
+ * associated with each signer.
+ *
+ * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2.
+ * @throws SecurityException if a APK Signature Scheme v2 signature of this APK does not verify.
+ * @throws IOException if an I/O error occurs while reading the APK file.
+ */
+ public static X509Certificate[][] verify(String apkFile)
+ throws SignatureNotFoundException, SecurityException, IOException {
+ VerifiedSigner vSigner = verify(apkFile, true);
+ return vSigner.certs;
+ }
+
+ /**
+ * Returns the certificates associated with each signer for the given APK without verification.
+ * This method is dangerous and should not be used, unless the caller is absolutely certain the
+ * APK is trusted. Specifically, verification is only done for the APK Signature Scheme v2
+ * Block while gathering signer information. The APK contents are not verified.
+ *
+ * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2.
+ * @throws IOException if an I/O error occurs while reading the APK file.
+ */
+ public static X509Certificate[][] unsafeGetCertsWithoutVerification(String apkFile)
+ throws SignatureNotFoundException, SecurityException, IOException {
+ VerifiedSigner vSigner = verify(apkFile, false);
+ return vSigner.certs;
+ }
+
+ /**
+ * Same as above returns the full signer object, containing additional info e.g. digest.
+ */
+ public static VerifiedSigner verify(String apkFile, boolean verifyIntegrity)
+ throws SignatureNotFoundException, SecurityException, IOException {
+ try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) {
+ return verify(apk, verifyIntegrity);
+ }
+ }
+
+ /**
+ * Verifies APK Signature Scheme v2 signatures of the provided APK and returns the certificates
+ * associated with each signer.
+ *
+ * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2.
+ * @throws SecurityException if an APK Signature Scheme v2 signature of this APK does not
+ * verify.
+ * @throws IOException if an I/O error occurs while reading the APK file.
+ */
+ private static VerifiedSigner verify(RandomAccessFile apk, boolean verifyIntegrity)
+ throws SignatureNotFoundException, SecurityException, IOException {
+ SignatureInfo signatureInfo = findSignature(apk);
+ return verify(apk, signatureInfo, verifyIntegrity);
+ }
+
+ /**
+ * Returns the APK Signature Scheme v2 block contained in the provided APK file and the
+ * additional information relevant for verifying the block against the file.
+ *
+ * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v2.
+ * @throws IOException if an I/O error occurs while reading the APK file.
+ */
+ private static SignatureInfo findSignature(RandomAccessFile apk)
+ throws IOException, SignatureNotFoundException {
+ return ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
+ }
+
+ /**
+ * Verifies the contents of the provided APK file against the provided APK Signature Scheme v2
+ * Block.
+ *
+ * @param signatureInfo APK Signature Scheme v2 Block and information relevant for verifying it
+ * against the APK file.
+ */
+ private static VerifiedSigner verify(
+ RandomAccessFile apk,
+ SignatureInfo signatureInfo,
+ boolean doVerifyIntegrity) throws SecurityException, IOException {
+ int signerCount = 0;
+ Map<Integer, byte[]> contentDigests = new ArrayMap<>();
+ List<X509Certificate[]> signerCerts = new ArrayList<>();
+ CertificateFactory certFactory;
+ try {
+ certFactory = CertificateFactory.getInstance("X.509");
+ } catch (CertificateException e) {
+ throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
+ }
+ ByteBuffer signers;
+ try {
+ signers = getLengthPrefixedSlice(signatureInfo.signatureBlock);
+ } catch (IOException e) {
+ throw new SecurityException("Failed to read list of signers", e);
+ }
+ while (signers.hasRemaining()) {
+ signerCount++;
+ try {
+ ByteBuffer signer = getLengthPrefixedSlice(signers);
+ X509Certificate[] certs = verifySigner(signer, contentDigests, certFactory);
+ signerCerts.add(certs);
+ } catch (IOException | BufferUnderflowException | SecurityException e) {
+ throw new SecurityException(
+ "Failed to parse/verify signer #" + signerCount + " block",
+ e);
+ }
+ }
+
+ if (signerCount < 1) {
+ throw new SecurityException("No signers found");
+ }
+
+ if (contentDigests.isEmpty()) {
+ throw new SecurityException("No content digests found");
+ }
+
+ if (doVerifyIntegrity) {
+ ApkSigningBlockUtils.verifyIntegrity(contentDigests, apk, signatureInfo);
+ }
+
+ byte[] verityRootHash = null;
+ if (contentDigests.containsKey(CONTENT_DIGEST_VERITY_CHUNKED_SHA256)) {
+ byte[] verityDigest = contentDigests.get(CONTENT_DIGEST_VERITY_CHUNKED_SHA256);
+ verityRootHash = ApkSigningBlockUtils.parseVerityDigestAndVerifySourceLength(
+ verityDigest, apk.length(), signatureInfo);
+ }
+
+ byte[] digest = pickBestDigestForV4(contentDigests);
+
+ return new VerifiedSigner(
+ signerCerts.toArray(new X509Certificate[signerCerts.size()][]),
+ verityRootHash, digest);
+ }
+
+ private static X509Certificate[] verifySigner(
+ ByteBuffer signerBlock,
+ Map<Integer, byte[]> contentDigests,
+ CertificateFactory certFactory) throws SecurityException, IOException {
+ ByteBuffer signedData = getLengthPrefixedSlice(signerBlock);
+ ByteBuffer signatures = getLengthPrefixedSlice(signerBlock);
+ byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock);
+
+ int signatureCount = 0;
+ int bestSigAlgorithm = -1;
+ byte[] bestSigAlgorithmSignatureBytes = null;
+ List<Integer> signaturesSigAlgorithms = new ArrayList<>();
+ while (signatures.hasRemaining()) {
+ signatureCount++;
+ try {
+ ByteBuffer signature = getLengthPrefixedSlice(signatures);
+ if (signature.remaining() < 8) {
+ throw new SecurityException("Signature record too short");
+ }
+ int sigAlgorithm = signature.getInt();
+ signaturesSigAlgorithms.add(sigAlgorithm);
+ if (!isSupportedSignatureAlgorithm(sigAlgorithm)) {
+ continue;
+ }
+ if ((bestSigAlgorithm == -1)
+ || (compareSignatureAlgorithm(sigAlgorithm, bestSigAlgorithm) > 0)) {
+ bestSigAlgorithm = sigAlgorithm;
+ bestSigAlgorithmSignatureBytes = readLengthPrefixedByteArray(signature);
+ }
+ } catch (IOException | BufferUnderflowException e) {
+ throw new SecurityException(
+ "Failed to parse signature record #" + signatureCount,
+ e);
+ }
+ }
+ if (bestSigAlgorithm == -1) {
+ if (signatureCount == 0) {
+ throw new SecurityException("No signatures found");
+ } else {
+ throw new SecurityException("No supported signatures found");
+ }
+ }
+
+ String keyAlgorithm = getSignatureAlgorithmJcaKeyAlgorithm(bestSigAlgorithm);
+ Pair<String, ? extends AlgorithmParameterSpec> signatureAlgorithmParams =
+ getSignatureAlgorithmJcaSignatureAlgorithm(bestSigAlgorithm);
+ String jcaSignatureAlgorithm = signatureAlgorithmParams.first;
+ AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureAlgorithmParams.second;
+ boolean sigVerified;
+ try {
+ PublicKey publicKey =
+ KeyFactory.getInstance(keyAlgorithm)
+ .generatePublic(new X509EncodedKeySpec(publicKeyBytes));
+ Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
+ sig.initVerify(publicKey);
+ if (jcaSignatureAlgorithmParams != null) {
+ sig.setParameter(jcaSignatureAlgorithmParams);
+ }
+ sig.update(signedData);
+ sigVerified = sig.verify(bestSigAlgorithmSignatureBytes);
+ } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException
+ | InvalidAlgorithmParameterException | SignatureException e) {
+ throw new SecurityException(
+ "Failed to verify " + jcaSignatureAlgorithm + " signature", e);
+ }
+ if (!sigVerified) {
+ throw new SecurityException(jcaSignatureAlgorithm + " signature did not verify");
+ }
+
+ // Signature over signedData has verified.
+
+ byte[] contentDigest = null;
+ signedData.clear();
+ ByteBuffer digests = getLengthPrefixedSlice(signedData);
+ List<Integer> digestsSigAlgorithms = new ArrayList<>();
+ int digestCount = 0;
+ while (digests.hasRemaining()) {
+ digestCount++;
+ try {
+ ByteBuffer digest = getLengthPrefixedSlice(digests);
+ if (digest.remaining() < 8) {
+ throw new IOException("Record too short");
+ }
+ int sigAlgorithm = digest.getInt();
+ digestsSigAlgorithms.add(sigAlgorithm);
+ if (sigAlgorithm == bestSigAlgorithm) {
+ contentDigest = readLengthPrefixedByteArray(digest);
+ }
+ } catch (IOException | BufferUnderflowException e) {
+ throw new IOException("Failed to parse digest record #" + digestCount, e);
+ }
+ }
+
+ if (!signaturesSigAlgorithms.equals(digestsSigAlgorithms)) {
+ throw new SecurityException(
+ "Signature algorithms don't match between digests and signatures records");
+ }
+ int digestAlgorithm = getSignatureAlgorithmContentDigestAlgorithm(bestSigAlgorithm);
+ byte[] previousSignerDigest = contentDigests.put(digestAlgorithm, contentDigest);
+ if ((previousSignerDigest != null)
+ && (!MessageDigest.isEqual(previousSignerDigest, contentDigest))) {
+ throw new SecurityException(
+ getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm)
+ + " contents digest does not match the digest specified by a preceding signer");
+ }
+
+ ByteBuffer certificates = getLengthPrefixedSlice(signedData);
+ List<X509Certificate> certs = new ArrayList<>();
+ int certificateCount = 0;
+ while (certificates.hasRemaining()) {
+ certificateCount++;
+ byte[] encodedCert = readLengthPrefixedByteArray(certificates);
+ X509Certificate certificate;
+ try {
+ certificate = (X509Certificate)
+ certFactory.generateCertificate(new ByteArrayInputStream(encodedCert));
+ } catch (CertificateException e) {
+ throw new SecurityException("Failed to decode certificate #" + certificateCount, e);
+ }
+ certificate = new VerbatimX509Certificate(
+ certificate, encodedCert);
+ certs.add(certificate);
+ }
+
+ if (certs.isEmpty()) {
+ throw new SecurityException("No certificates listed");
+ }
+ X509Certificate mainCertificate = certs.get(0);
+ byte[] certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
+ if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
+ throw new SecurityException(
+ "Public key mismatch between certificate and signature record");
+ }
+
+ ByteBuffer additionalAttrs = getLengthPrefixedSlice(signedData);
+ verifyAdditionalAttributes(additionalAttrs);
+
+ return certs.toArray(new X509Certificate[certs.size()]);
+ }
+
+ // Attribute to check whether a newer APK Signature Scheme signature was stripped
+ private static final int STRIPPING_PROTECTION_ATTR_ID = 0xbeeff00d;
+
+ private static void verifyAdditionalAttributes(ByteBuffer attrs)
+ throws SecurityException, IOException {
+ while (attrs.hasRemaining()) {
+ ByteBuffer attr = getLengthPrefixedSlice(attrs);
+ if (attr.remaining() < 4) {
+ throw new IOException("Remaining buffer too short to contain additional attribute "
+ + "ID. Remaining: " + attr.remaining());
+ }
+ int id = attr.getInt();
+ switch (id) {
+ case STRIPPING_PROTECTION_ATTR_ID:
+ if (attr.remaining() < 4) {
+ throw new IOException("V2 Signature Scheme Stripping Protection Attribute "
+ + " value too small. Expected 4 bytes, but found "
+ + attr.remaining());
+ }
+ int vers = attr.getInt();
+ if (vers == ApkSignatureSchemeV3Verifier.SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID) {
+ throw new SecurityException("V2 signature indicates APK is signed using APK"
+ + " Signature Scheme v3, but none was found. Signature stripped?");
+ }
+ break;
+ default:
+ // not the droid we're looking for, move along, move along.
+ break;
+ }
+ }
+ return;
+ }
+
+ static byte[] getVerityRootHash(String apkPath)
+ throws IOException, SignatureNotFoundException, SecurityException {
+ try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
+ SignatureInfo signatureInfo = findSignature(apk);
+ VerifiedSigner vSigner = verify(apk, false);
+ return vSigner.verityRootHash;
+ }
+ }
+
+ static byte[] generateApkVerity(String apkPath, ByteBufferFactory bufferFactory)
+ throws IOException, SignatureNotFoundException, SecurityException, DigestException,
+ NoSuchAlgorithmException {
+ try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
+ SignatureInfo signatureInfo = findSignature(apk);
+ return VerityBuilder.generateApkVerity(apkPath, bufferFactory, signatureInfo);
+ }
+ }
+
+ static byte[] generateApkVerityRootHash(String apkPath)
+ throws IOException, SignatureNotFoundException, DigestException,
+ NoSuchAlgorithmException {
+ try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
+ SignatureInfo signatureInfo = findSignature(apk);
+ VerifiedSigner vSigner = verify(apk, false);
+ if (vSigner.verityRootHash == null) {
+ return null;
+ }
+ return VerityBuilder.generateApkVerityRootHash(
+ apk, ByteBuffer.wrap(vSigner.verityRootHash), signatureInfo);
+ }
+ }
+
+ /**
+ * Verified APK Signature Scheme v2 signer.
+ *
+ * @hide for internal use only.
+ */
+ public static class VerifiedSigner {
+ public final X509Certificate[][] certs;
+
+ public final byte[] verityRootHash;
+ public final byte[] digest;
+
+ public VerifiedSigner(X509Certificate[][] certs, byte[] verityRootHash, byte[] digest) {
+ this.certs = certs;
+ this.verityRootHash = verityRootHash;
+ this.digest = digest;
+ }
+
+ }
+}
diff --git a/android/util/apk/ApkSignatureSchemeV3Verifier.java b/android/util/apk/ApkSignatureSchemeV3Verifier.java
new file mode 100644
index 0000000..4ab541b
--- /dev/null
+++ b/android/util/apk/ApkSignatureSchemeV3Verifier.java
@@ -0,0 +1,589 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.apk;
+
+import static android.util.apk.ApkSigningBlockUtils.CONTENT_DIGEST_VERITY_CHUNKED_SHA256;
+import static android.util.apk.ApkSigningBlockUtils.compareSignatureAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.getContentDigestAlgorithmJcaDigestAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
+import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmContentDigestAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaKeyAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaSignatureAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.isSupportedSignatureAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.pickBestDigestForV4;
+import static android.util.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray;
+
+import android.os.Build;
+import android.util.ArrayMap;
+import android.util.Pair;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.security.DigestException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * APK Signature Scheme v3 verifier.
+ *
+ * @hide for internal use only.
+ */
+public class ApkSignatureSchemeV3Verifier {
+
+ /**
+ * ID of this signature scheme as used in X-Android-APK-Signed header used in JAR signing.
+ */
+ public static final int SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID = 3;
+
+ private static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0;
+
+ /**
+ * Returns {@code true} if the provided APK contains an APK Signature Scheme V3 signature.
+ *
+ * <p><b>NOTE: This method does not verify the signature.</b>
+ */
+ public static boolean hasSignature(String apkFile) throws IOException {
+ try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) {
+ findSignature(apk);
+ return true;
+ } catch (SignatureNotFoundException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Verifies APK Signature Scheme v3 signatures of the provided APK and returns the certificates
+ * associated with each signer.
+ *
+ * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v3.
+ * @throws SecurityException if the APK Signature Scheme v3 signature of this APK does not
+ * verify.
+ * @throws IOException if an I/O error occurs while reading the APK file.
+ */
+ public static VerifiedSigner verify(String apkFile)
+ throws SignatureNotFoundException, SecurityException, IOException {
+ return verify(apkFile, true);
+ }
+
+ /**
+ * Returns the certificates associated with each signer for the given APK without verification.
+ * This method is dangerous and should not be used, unless the caller is absolutely certain the
+ * APK is trusted. Specifically, verification is only done for the APK Signature Scheme v3
+ * Block while gathering signer information. The APK contents are not verified.
+ *
+ * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v3.
+ * @throws IOException if an I/O error occurs while reading the APK file.
+ */
+ public static VerifiedSigner unsafeGetCertsWithoutVerification(String apkFile)
+ throws SignatureNotFoundException, SecurityException, IOException {
+ return verify(apkFile, false);
+ }
+
+ private static VerifiedSigner verify(String apkFile, boolean verifyIntegrity)
+ throws SignatureNotFoundException, SecurityException, IOException {
+ try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) {
+ return verify(apk, verifyIntegrity);
+ }
+ }
+
+ /**
+ * Verifies APK Signature Scheme v3 signatures of the provided APK and returns the certificates
+ * associated with each signer.
+ *
+ * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v3.
+ * @throws SecurityException if an APK Signature Scheme v3 signature of this APK does not
+ * verify.
+ * @throws IOException if an I/O error occurs while reading the APK file.
+ */
+ private static VerifiedSigner verify(RandomAccessFile apk, boolean verifyIntegrity)
+ throws SignatureNotFoundException, SecurityException, IOException {
+ SignatureInfo signatureInfo = findSignature(apk);
+ return verify(apk, signatureInfo, verifyIntegrity);
+ }
+
+ /**
+ * Returns the APK Signature Scheme v3 block contained in the provided APK file and the
+ * additional information relevant for verifying the block against the file.
+ *
+ * @throws SignatureNotFoundException if the APK is not signed using APK Signature Scheme v3.
+ * @throws IOException if an I/O error occurs while reading the APK file.
+ */
+ private static SignatureInfo findSignature(RandomAccessFile apk)
+ throws IOException, SignatureNotFoundException {
+ return ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+ }
+
+ /**
+ * Verifies the contents of the provided APK file against the provided APK Signature Scheme v3
+ * Block.
+ *
+ * @param signatureInfo APK Signature Scheme v3 Block and information relevant for verifying it
+ * against the APK file.
+ */
+ private static VerifiedSigner verify(
+ RandomAccessFile apk,
+ SignatureInfo signatureInfo,
+ boolean doVerifyIntegrity) throws SecurityException, IOException {
+ int signerCount = 0;
+ Map<Integer, byte[]> contentDigests = new ArrayMap<>();
+ VerifiedSigner result = null;
+ CertificateFactory certFactory;
+ try {
+ certFactory = CertificateFactory.getInstance("X.509");
+ } catch (CertificateException e) {
+ throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
+ }
+ ByteBuffer signers;
+ try {
+ signers = getLengthPrefixedSlice(signatureInfo.signatureBlock);
+ } catch (IOException e) {
+ throw new SecurityException("Failed to read list of signers", e);
+ }
+ while (signers.hasRemaining()) {
+ try {
+ ByteBuffer signer = getLengthPrefixedSlice(signers);
+ result = verifySigner(signer, contentDigests, certFactory);
+ signerCount++;
+ } catch (PlatformNotSupportedException e) {
+ // this signer is for a different platform, ignore it.
+ continue;
+ } catch (IOException | BufferUnderflowException | SecurityException e) {
+ throw new SecurityException(
+ "Failed to parse/verify signer #" + signerCount + " block",
+ e);
+ }
+ }
+
+ if (signerCount < 1 || result == null) {
+ throw new SecurityException("No signers found");
+ }
+
+ if (signerCount != 1) {
+ throw new SecurityException("APK Signature Scheme V3 only supports one signer: "
+ + "multiple signers found.");
+ }
+
+ if (contentDigests.isEmpty()) {
+ throw new SecurityException("No content digests found");
+ }
+
+ if (doVerifyIntegrity) {
+ ApkSigningBlockUtils.verifyIntegrity(contentDigests, apk, signatureInfo);
+ }
+
+ if (contentDigests.containsKey(CONTENT_DIGEST_VERITY_CHUNKED_SHA256)) {
+ byte[] verityDigest = contentDigests.get(CONTENT_DIGEST_VERITY_CHUNKED_SHA256);
+ result.verityRootHash = ApkSigningBlockUtils.parseVerityDigestAndVerifySourceLength(
+ verityDigest, apk.length(), signatureInfo);
+ }
+
+ result.digest = pickBestDigestForV4(contentDigests);
+
+ return result;
+ }
+
+ private static VerifiedSigner verifySigner(
+ ByteBuffer signerBlock,
+ Map<Integer, byte[]> contentDigests,
+ CertificateFactory certFactory)
+ throws SecurityException, IOException, PlatformNotSupportedException {
+ ByteBuffer signedData = getLengthPrefixedSlice(signerBlock);
+ int minSdkVersion = signerBlock.getInt();
+ int maxSdkVersion = signerBlock.getInt();
+
+ if (Build.VERSION.SDK_INT < minSdkVersion || Build.VERSION.SDK_INT > maxSdkVersion) {
+ // this signature isn't meant to be used with this platform, skip it.
+ throw new PlatformNotSupportedException(
+ "Signer not supported by this platform "
+ + "version. This platform: " + Build.VERSION.SDK_INT
+ + ", signer minSdkVersion: " + minSdkVersion
+ + ", maxSdkVersion: " + maxSdkVersion);
+ }
+
+ ByteBuffer signatures = getLengthPrefixedSlice(signerBlock);
+ byte[] publicKeyBytes = readLengthPrefixedByteArray(signerBlock);
+
+ int signatureCount = 0;
+ int bestSigAlgorithm = -1;
+ byte[] bestSigAlgorithmSignatureBytes = null;
+ List<Integer> signaturesSigAlgorithms = new ArrayList<>();
+ while (signatures.hasRemaining()) {
+ signatureCount++;
+ try {
+ ByteBuffer signature = getLengthPrefixedSlice(signatures);
+ if (signature.remaining() < 8) {
+ throw new SecurityException("Signature record too short");
+ }
+ int sigAlgorithm = signature.getInt();
+ signaturesSigAlgorithms.add(sigAlgorithm);
+ if (!isSupportedSignatureAlgorithm(sigAlgorithm)) {
+ continue;
+ }
+ if ((bestSigAlgorithm == -1)
+ || (compareSignatureAlgorithm(sigAlgorithm, bestSigAlgorithm) > 0)) {
+ bestSigAlgorithm = sigAlgorithm;
+ bestSigAlgorithmSignatureBytes = readLengthPrefixedByteArray(signature);
+ }
+ } catch (IOException | BufferUnderflowException e) {
+ throw new SecurityException(
+ "Failed to parse signature record #" + signatureCount,
+ e);
+ }
+ }
+ if (bestSigAlgorithm == -1) {
+ if (signatureCount == 0) {
+ throw new SecurityException("No signatures found");
+ } else {
+ throw new SecurityException("No supported signatures found");
+ }
+ }
+
+ String keyAlgorithm = getSignatureAlgorithmJcaKeyAlgorithm(bestSigAlgorithm);
+ Pair<String, ? extends AlgorithmParameterSpec> signatureAlgorithmParams =
+ getSignatureAlgorithmJcaSignatureAlgorithm(bestSigAlgorithm);
+ String jcaSignatureAlgorithm = signatureAlgorithmParams.first;
+ AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureAlgorithmParams.second;
+ boolean sigVerified;
+ try {
+ PublicKey publicKey =
+ KeyFactory.getInstance(keyAlgorithm)
+ .generatePublic(new X509EncodedKeySpec(publicKeyBytes));
+ Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
+ sig.initVerify(publicKey);
+ if (jcaSignatureAlgorithmParams != null) {
+ sig.setParameter(jcaSignatureAlgorithmParams);
+ }
+ sig.update(signedData);
+ sigVerified = sig.verify(bestSigAlgorithmSignatureBytes);
+ } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException
+ | InvalidAlgorithmParameterException | SignatureException e) {
+ throw new SecurityException(
+ "Failed to verify " + jcaSignatureAlgorithm + " signature", e);
+ }
+ if (!sigVerified) {
+ throw new SecurityException(jcaSignatureAlgorithm + " signature did not verify");
+ }
+
+ // Signature over signedData has verified.
+
+ byte[] contentDigest = null;
+ signedData.clear();
+ ByteBuffer digests = getLengthPrefixedSlice(signedData);
+ List<Integer> digestsSigAlgorithms = new ArrayList<>();
+ int digestCount = 0;
+ while (digests.hasRemaining()) {
+ digestCount++;
+ try {
+ ByteBuffer digest = getLengthPrefixedSlice(digests);
+ if (digest.remaining() < 8) {
+ throw new IOException("Record too short");
+ }
+ int sigAlgorithm = digest.getInt();
+ digestsSigAlgorithms.add(sigAlgorithm);
+ if (sigAlgorithm == bestSigAlgorithm) {
+ contentDigest = readLengthPrefixedByteArray(digest);
+ }
+ } catch (IOException | BufferUnderflowException e) {
+ throw new IOException("Failed to parse digest record #" + digestCount, e);
+ }
+ }
+
+ if (!signaturesSigAlgorithms.equals(digestsSigAlgorithms)) {
+ throw new SecurityException(
+ "Signature algorithms don't match between digests and signatures records");
+ }
+ int digestAlgorithm = getSignatureAlgorithmContentDigestAlgorithm(bestSigAlgorithm);
+ byte[] previousSignerDigest = contentDigests.put(digestAlgorithm, contentDigest);
+ if ((previousSignerDigest != null)
+ && (!MessageDigest.isEqual(previousSignerDigest, contentDigest))) {
+ throw new SecurityException(
+ getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm)
+ + " contents digest does not match the digest specified by a preceding signer");
+ }
+
+ ByteBuffer certificates = getLengthPrefixedSlice(signedData);
+ List<X509Certificate> certs = new ArrayList<>();
+ int certificateCount = 0;
+ while (certificates.hasRemaining()) {
+ certificateCount++;
+ byte[] encodedCert = readLengthPrefixedByteArray(certificates);
+ X509Certificate certificate;
+ try {
+ certificate = (X509Certificate)
+ certFactory.generateCertificate(new ByteArrayInputStream(encodedCert));
+ } catch (CertificateException e) {
+ throw new SecurityException("Failed to decode certificate #" + certificateCount, e);
+ }
+ certificate = new VerbatimX509Certificate(
+ certificate, encodedCert);
+ certs.add(certificate);
+ }
+
+ if (certs.isEmpty()) {
+ throw new SecurityException("No certificates listed");
+ }
+ X509Certificate mainCertificate = certs.get(0);
+ byte[] certificatePublicKeyBytes = mainCertificate.getPublicKey().getEncoded();
+ if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
+ throw new SecurityException(
+ "Public key mismatch between certificate and signature record");
+ }
+
+ int signedMinSDK = signedData.getInt();
+ if (signedMinSDK != minSdkVersion) {
+ throw new SecurityException(
+ "minSdkVersion mismatch between signed and unsigned in v3 signer block.");
+ }
+
+ int signedMaxSDK = signedData.getInt();
+ if (signedMaxSDK != maxSdkVersion) {
+ throw new SecurityException(
+ "maxSdkVersion mismatch between signed and unsigned in v3 signer block.");
+ }
+
+ ByteBuffer additionalAttrs = getLengthPrefixedSlice(signedData);
+ return verifyAdditionalAttributes(additionalAttrs, certs, certFactory);
+ }
+
+ private static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c;
+
+ private static VerifiedSigner verifyAdditionalAttributes(ByteBuffer attrs,
+ List<X509Certificate> certs, CertificateFactory certFactory) throws IOException {
+ X509Certificate[] certChain = certs.toArray(new X509Certificate[certs.size()]);
+ VerifiedProofOfRotation por = null;
+
+ while (attrs.hasRemaining()) {
+ ByteBuffer attr = getLengthPrefixedSlice(attrs);
+ if (attr.remaining() < 4) {
+ throw new IOException("Remaining buffer too short to contain additional attribute "
+ + "ID. Remaining: " + attr.remaining());
+ }
+ int id = attr.getInt();
+ switch(id) {
+ case PROOF_OF_ROTATION_ATTR_ID:
+ if (por != null) {
+ throw new SecurityException("Encountered multiple Proof-of-rotation records"
+ + " when verifying APK Signature Scheme v3 signature");
+ }
+ por = verifyProofOfRotationStruct(attr, certFactory);
+ // make sure that the last certificate in the Proof-of-rotation record matches
+ // the one used to sign this APK.
+ try {
+ if (por.certs.size() > 0
+ && !Arrays.equals(por.certs.get(por.certs.size() - 1).getEncoded(),
+ certChain[0].getEncoded())) {
+ throw new SecurityException("Terminal certificate in Proof-of-rotation"
+ + " record does not match APK signing certificate");
+ }
+ } catch (CertificateEncodingException e) {
+ throw new SecurityException("Failed to encode certificate when comparing"
+ + " Proof-of-rotation record and signing certificate", e);
+ }
+
+ break;
+ default:
+ // not the droid we're looking for, move along, move along.
+ break;
+ }
+ }
+ return new VerifiedSigner(certChain, por);
+ }
+
+ private static VerifiedProofOfRotation verifyProofOfRotationStruct(
+ ByteBuffer porBuf,
+ CertificateFactory certFactory)
+ throws SecurityException, IOException {
+ int levelCount = 0;
+ int lastSigAlgorithm = -1;
+ X509Certificate lastCert = null;
+ List<X509Certificate> certs = new ArrayList<>();
+ List<Integer> flagsList = new ArrayList<>();
+
+ // Proof-of-rotation struct:
+ // A uint32 version code followed by basically a singly linked list of nodes, called levels
+ // here, each of which have the following structure:
+ // * length-prefix for the entire level
+ // - length-prefixed signed data (if previous level exists)
+ // * length-prefixed X509 Certificate
+ // * uint32 signature algorithm ID describing how this signed data was signed
+ // - uint32 flags describing how to treat the cert contained in this level
+ // - uint32 signature algorithm ID to use to verify the signature of the next level. The
+ // algorithm here must match the one in the signed data section of the next level.
+ // - length-prefixed signature over the signed data in this level. The signature here
+ // is verified using the certificate from the previous level.
+ // The linking is provided by the certificate of each level signing the one of the next.
+
+ try {
+
+ // get the version code, but don't do anything with it: creator knew about all our flags
+ porBuf.getInt();
+ HashSet<X509Certificate> certHistorySet = new HashSet<>();
+ while (porBuf.hasRemaining()) {
+ levelCount++;
+ ByteBuffer level = getLengthPrefixedSlice(porBuf);
+ ByteBuffer signedData = getLengthPrefixedSlice(level);
+ int flags = level.getInt();
+ int sigAlgorithm = level.getInt();
+ byte[] signature = readLengthPrefixedByteArray(level);
+
+ if (lastCert != null) {
+ // Use previous level cert to verify current level
+ Pair<String, ? extends AlgorithmParameterSpec> sigAlgParams =
+ getSignatureAlgorithmJcaSignatureAlgorithm(lastSigAlgorithm);
+ PublicKey publicKey = lastCert.getPublicKey();
+ Signature sig = Signature.getInstance(sigAlgParams.first);
+ sig.initVerify(publicKey);
+ if (sigAlgParams.second != null) {
+ sig.setParameter(sigAlgParams.second);
+ }
+ sig.update(signedData);
+ if (!sig.verify(signature)) {
+ throw new SecurityException("Unable to verify signature of certificate #"
+ + levelCount + " using " + sigAlgParams.first + " when verifying"
+ + " Proof-of-rotation record");
+ }
+ }
+
+ signedData.rewind();
+ byte[] encodedCert = readLengthPrefixedByteArray(signedData);
+ int signedSigAlgorithm = signedData.getInt();
+ if (lastCert != null && lastSigAlgorithm != signedSigAlgorithm) {
+ throw new SecurityException("Signing algorithm ID mismatch for certificate #"
+ + levelCount + " when verifying Proof-of-rotation record");
+ }
+ lastCert = (X509Certificate)
+ certFactory.generateCertificate(new ByteArrayInputStream(encodedCert));
+ lastCert = new VerbatimX509Certificate(lastCert, encodedCert);
+
+ lastSigAlgorithm = sigAlgorithm;
+ if (certHistorySet.contains(lastCert)) {
+ throw new SecurityException("Encountered duplicate entries in "
+ + "Proof-of-rotation record at certificate #" + levelCount + ". All "
+ + "signing certificates should be unique");
+ }
+ certHistorySet.add(lastCert);
+ certs.add(lastCert);
+ flagsList.add(flags);
+ }
+ } catch (IOException | BufferUnderflowException e) {
+ throw new IOException("Failed to parse Proof-of-rotation record", e);
+ } catch (NoSuchAlgorithmException | InvalidKeyException
+ | InvalidAlgorithmParameterException | SignatureException e) {
+ throw new SecurityException(
+ "Failed to verify signature over signed data for certificate #"
+ + levelCount + " when verifying Proof-of-rotation record", e);
+ } catch (CertificateException e) {
+ throw new SecurityException("Failed to decode certificate #" + levelCount
+ + " when verifying Proof-of-rotation record", e);
+ }
+ return new VerifiedProofOfRotation(certs, flagsList);
+ }
+
+ static byte[] getVerityRootHash(String apkPath)
+ throws IOException, SignatureNotFoundException, SecurityException {
+ try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
+ SignatureInfo signatureInfo = findSignature(apk);
+ VerifiedSigner vSigner = verify(apk, false);
+ return vSigner.verityRootHash;
+ }
+ }
+
+ static byte[] generateApkVerity(String apkPath, ByteBufferFactory bufferFactory)
+ throws IOException, SignatureNotFoundException, SecurityException, DigestException,
+ NoSuchAlgorithmException {
+ try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
+ SignatureInfo signatureInfo = findSignature(apk);
+ return VerityBuilder.generateApkVerity(apkPath, bufferFactory, signatureInfo);
+ }
+ }
+
+ static byte[] generateApkVerityRootHash(String apkPath)
+ throws NoSuchAlgorithmException, DigestException, IOException,
+ SignatureNotFoundException {
+ try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
+ SignatureInfo signatureInfo = findSignature(apk);
+ VerifiedSigner vSigner = verify(apk, false);
+ if (vSigner.verityRootHash == null) {
+ return null;
+ }
+ return VerityBuilder.generateApkVerityRootHash(
+ apk, ByteBuffer.wrap(vSigner.verityRootHash), signatureInfo);
+ }
+ }
+
+ /**
+ * Verified processed proof of rotation.
+ *
+ * @hide for internal use only.
+ */
+ public static class VerifiedProofOfRotation {
+ public final List<X509Certificate> certs;
+ public final List<Integer> flagsList;
+
+ public VerifiedProofOfRotation(List<X509Certificate> certs, List<Integer> flagsList) {
+ this.certs = certs;
+ this.flagsList = flagsList;
+ }
+ }
+
+ /**
+ * Verified APK Signature Scheme v3 signer, including the proof of rotation structure.
+ *
+ * @hide for internal use only.
+ */
+ public static class VerifiedSigner {
+ public final X509Certificate[] certs;
+ public final VerifiedProofOfRotation por;
+
+ public byte[] verityRootHash;
+ public byte[] digest;
+
+ public VerifiedSigner(X509Certificate[] certs, VerifiedProofOfRotation por) {
+ this.certs = certs;
+ this.por = por;
+ }
+
+ }
+
+ private static class PlatformNotSupportedException extends Exception {
+
+ PlatformNotSupportedException(String s) {
+ super(s);
+ }
+ }
+}
diff --git a/android/util/apk/ApkSignatureSchemeV4Verifier.java b/android/util/apk/ApkSignatureSchemeV4Verifier.java
new file mode 100644
index 0000000..d40efce
--- /dev/null
+++ b/android/util/apk/ApkSignatureSchemeV4Verifier.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.apk;
+
+import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaKeyAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaSignatureAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.isSupportedSignatureAlgorithm;
+
+import android.os.incremental.IncrementalManager;
+import android.os.incremental.V4Signature;
+import android.util.Pair;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Arrays;
+
+/**
+ * APK Signature Scheme v4 verifier.
+ *
+ * @hide for internal use only.
+ */
+public class ApkSignatureSchemeV4Verifier {
+ /**
+ * Extracts and verifies APK Signature Scheme v4 signatures of the provided APK and returns the
+ * certificates associated with each signer.
+ */
+ public static VerifiedSigner extractCertificates(String apkFile)
+ throws SignatureNotFoundException, SecurityException {
+ final File apk = new File(apkFile);
+ final byte[] signatureBytes = IncrementalManager.unsafeGetFileSignature(
+ apk.getAbsolutePath());
+ if (signatureBytes == null || signatureBytes.length == 0) {
+ throw new SignatureNotFoundException("Failed to obtain signature bytes from IncFS.");
+ }
+
+ final V4Signature signature;
+ final V4Signature.HashingInfo hashingInfo;
+ final V4Signature.SigningInfo signingInfo;
+ try {
+ signature = V4Signature.readFrom(signatureBytes);
+
+ if (!signature.isVersionSupported()) {
+ throw new SecurityException(
+ "v4 signature version " + signature.version + " is not supported");
+ }
+
+ hashingInfo = V4Signature.HashingInfo.fromByteArray(signature.hashingInfo);
+ signingInfo = V4Signature.SigningInfo.fromByteArray(signature.signingInfo);
+ } catch (IOException e) {
+ throw new SignatureNotFoundException("Failed to read V4 signature.", e);
+ }
+
+ final byte[] signedData = V4Signature.getSigningData(apk.length(), hashingInfo,
+ signingInfo);
+
+ return verifySigner(signingInfo, signedData);
+ }
+
+ private static VerifiedSigner verifySigner(V4Signature.SigningInfo signingInfo,
+ final byte[] signedData) throws SecurityException {
+ if (!isSupportedSignatureAlgorithm(signingInfo.signatureAlgorithmId)) {
+ throw new SecurityException("No supported signatures found");
+ }
+
+ final int signatureAlgorithmId = signingInfo.signatureAlgorithmId;
+ final byte[] signatureBytes = signingInfo.signature;
+ final byte[] publicKeyBytes = signingInfo.publicKey;
+ final byte[] encodedCert = signingInfo.certificate;
+
+ String keyAlgorithm = getSignatureAlgorithmJcaKeyAlgorithm(signatureAlgorithmId);
+ Pair<String, ? extends AlgorithmParameterSpec> signatureAlgorithmParams =
+ getSignatureAlgorithmJcaSignatureAlgorithm(signatureAlgorithmId);
+ String jcaSignatureAlgorithm = signatureAlgorithmParams.first;
+ AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureAlgorithmParams.second;
+ boolean sigVerified;
+ try {
+ PublicKey publicKey =
+ KeyFactory.getInstance(keyAlgorithm)
+ .generatePublic(new X509EncodedKeySpec(publicKeyBytes));
+ Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
+ sig.initVerify(publicKey);
+ if (jcaSignatureAlgorithmParams != null) {
+ sig.setParameter(jcaSignatureAlgorithmParams);
+ }
+ sig.update(signedData);
+ sigVerified = sig.verify(signatureBytes);
+ } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException
+ | InvalidAlgorithmParameterException | SignatureException e) {
+ throw new SecurityException(
+ "Failed to verify " + jcaSignatureAlgorithm + " signature", e);
+ }
+ if (!sigVerified) {
+ throw new SecurityException(jcaSignatureAlgorithm + " signature did not verify");
+ }
+
+ // Signature over signedData has verified.
+ CertificateFactory certFactory;
+ try {
+ certFactory = CertificateFactory.getInstance("X.509");
+ } catch (CertificateException e) {
+ throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
+ }
+
+ X509Certificate certificate;
+ try {
+ certificate = (X509Certificate)
+ certFactory.generateCertificate(new ByteArrayInputStream(encodedCert));
+ } catch (CertificateException e) {
+ throw new SecurityException("Failed to decode certificate", e);
+ }
+ certificate = new VerbatimX509Certificate(certificate, encodedCert);
+
+ byte[] certificatePublicKeyBytes = certificate.getPublicKey().getEncoded();
+ if (!Arrays.equals(publicKeyBytes, certificatePublicKeyBytes)) {
+ throw new SecurityException(
+ "Public key mismatch between certificate and signature record");
+ }
+
+ return new VerifiedSigner(new Certificate[]{certificate}, signingInfo.apkDigest);
+ }
+
+ /**
+ * Verified APK Signature Scheme v4 signer, including V3 digest.
+ *
+ * @hide for internal use only.
+ */
+ public static class VerifiedSigner {
+ public final Certificate[] certs;
+ public byte[] apkDigest;
+
+ public VerifiedSigner(Certificate[] certs, byte[] apkDigest) {
+ this.certs = certs;
+ this.apkDigest = apkDigest;
+ }
+
+ }
+}
diff --git a/android/util/apk/ApkSignatureVerifier.java b/android/util/apk/ApkSignatureVerifier.java
new file mode 100644
index 0000000..ab8f80d
--- /dev/null
+++ b/android/util/apk/ApkSignatureVerifier.java
@@ -0,0 +1,537 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.apk;
+
+import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST;
+import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING;
+import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES;
+import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES;
+import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION;
+import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;
+
+import android.content.pm.PackageParser;
+import android.content.pm.PackageParser.PackageParserException;
+import android.content.pm.PackageParser.SigningDetails.SignatureSchemeVersion;
+import android.content.pm.Signature;
+import android.os.Build;
+import android.os.Trace;
+import android.util.jar.StrictJarFile;
+
+import com.android.internal.util.ArrayUtils;
+
+import libcore.io.IoUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.DigestException;
+import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.zip.ZipEntry;
+
+/**
+ * Facade class that takes care of the details of APK verification on
+ * behalf of PackageParser.
+ *
+ * @hide for internal use only.
+ */
+public class ApkSignatureVerifier {
+
+ private static final AtomicReference<byte[]> sBuffer = new AtomicReference<>();
+
+ /**
+ * Verifies the provided APK and returns the certificates associated with each signer.
+ *
+ * @throws PackageParserException if the APK's signature failed to verify.
+ */
+ public static PackageParser.SigningDetails verify(String apkPath,
+ @SignatureSchemeVersion int minSignatureSchemeVersion)
+ throws PackageParserException {
+ return verifySignatures(apkPath, minSignatureSchemeVersion, true);
+ }
+
+ /**
+ * Returns the certificates associated with each signer for the given APK without verification.
+ * This method is dangerous and should not be used, unless the caller is absolutely certain the
+ * APK is trusted.
+ *
+ * @throws PackageParserException if there was a problem collecting certificates.
+ */
+ public static PackageParser.SigningDetails unsafeGetCertsWithoutVerification(
+ String apkPath, int minSignatureSchemeVersion)
+ throws PackageParserException {
+ return verifySignatures(apkPath, minSignatureSchemeVersion, false);
+ }
+
+ /**
+ * Verifies the provided APK using all allowed signing schemas.
+ * @return the certificates associated with each signer.
+ * @param verifyFull whether to verify all contents of this APK or just collect certificates.
+ * @throws PackageParserException if there was a problem collecting certificates
+ */
+ private static PackageParser.SigningDetails verifySignatures(String apkPath,
+ @SignatureSchemeVersion int minSignatureSchemeVersion, boolean verifyFull)
+ throws PackageParserException {
+
+ if (minSignatureSchemeVersion > SignatureSchemeVersion.SIGNING_BLOCK_V4) {
+ // V3 and before are older than the requested minimum signing version
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "No signature found in package of version " + minSignatureSchemeVersion
+ + " or newer for package " + apkPath);
+ }
+
+ // first try v4
+ try {
+ return verifyV4Signature(apkPath, minSignatureSchemeVersion, verifyFull);
+ } catch (SignatureNotFoundException e) {
+ // not signed with v4, try older if allowed
+ if (minSignatureSchemeVersion >= SignatureSchemeVersion.SIGNING_BLOCK_V4) {
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "No APK Signature Scheme v4 signature in package " + apkPath, e);
+ }
+ }
+
+ if (minSignatureSchemeVersion > SignatureSchemeVersion.SIGNING_BLOCK_V3) {
+ // V3 and before are older than the requested minimum signing version
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "No signature found in package of version " + minSignatureSchemeVersion
+ + " or newer for package " + apkPath);
+ }
+
+ return verifyV3AndBelowSignatures(apkPath, minSignatureSchemeVersion, verifyFull);
+ }
+
+ private static PackageParser.SigningDetails verifyV3AndBelowSignatures(String apkPath,
+ @SignatureSchemeVersion int minSignatureSchemeVersion, boolean verifyFull)
+ throws PackageParserException {
+ // try v3
+ try {
+ return verifyV3Signature(apkPath, verifyFull);
+ } catch (SignatureNotFoundException e) {
+ // not signed with v3, try older if allowed
+ if (minSignatureSchemeVersion >= SignatureSchemeVersion.SIGNING_BLOCK_V3) {
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "No APK Signature Scheme v3 signature in package " + apkPath, e);
+ }
+ }
+
+ // redundant, protective version check
+ if (minSignatureSchemeVersion > SignatureSchemeVersion.SIGNING_BLOCK_V2) {
+ // V2 and before are older than the requested minimum signing version
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "No signature found in package of version " + minSignatureSchemeVersion
+ + " or newer for package " + apkPath);
+ }
+
+ // try v2
+ try {
+ return verifyV2Signature(apkPath, verifyFull);
+ } catch (SignatureNotFoundException e) {
+ // not signed with v2, try older if allowed
+ if (minSignatureSchemeVersion >= SignatureSchemeVersion.SIGNING_BLOCK_V2) {
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "No APK Signature Scheme v2 signature in package " + apkPath, e);
+ }
+ }
+
+ // redundant, protective version check
+ if (minSignatureSchemeVersion > SignatureSchemeVersion.JAR) {
+ // V1 and is older than the requested minimum signing version
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "No signature found in package of version " + minSignatureSchemeVersion
+ + " or newer for package " + apkPath);
+ }
+
+ // v2 didn't work, try jarsigner
+ return verifyV1Signature(apkPath, verifyFull);
+ }
+
+ /**
+ * Verifies the provided APK using V4 schema.
+ *
+ * @param verifyFull whether to verify (V4 vs V3) or just collect certificates.
+ * @return the certificates associated with each signer.
+ * @throws SignatureNotFoundException if there are no V4 signatures in the APK
+ * @throws PackageParserException if there was a problem collecting certificates
+ */
+ private static PackageParser.SigningDetails verifyV4Signature(String apkPath,
+ @SignatureSchemeVersion int minSignatureSchemeVersion, boolean verifyFull)
+ throws SignatureNotFoundException, PackageParserException {
+ Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, verifyFull ? "verifyV4" : "certsOnlyV4");
+ try {
+ ApkSignatureSchemeV4Verifier.VerifiedSigner vSigner =
+ ApkSignatureSchemeV4Verifier.extractCertificates(apkPath);
+ Certificate[][] signerCerts = new Certificate[][]{vSigner.certs};
+ Signature[] signerSigs = convertToSignatures(signerCerts);
+
+ if (verifyFull) {
+ byte[] nonstreamingDigest = null;
+ Certificate[][] nonstreamingCerts = null;
+
+ try {
+ // v4 is an add-on and requires v2 or v3 signature to validate against its
+ // certificate and digest
+ ApkSignatureSchemeV3Verifier.VerifiedSigner v3Signer =
+ ApkSignatureSchemeV3Verifier.unsafeGetCertsWithoutVerification(apkPath);
+ nonstreamingDigest = v3Signer.digest;
+ nonstreamingCerts = new Certificate[][]{v3Signer.certs};
+ } catch (SignatureNotFoundException e) {
+ try {
+ ApkSignatureSchemeV2Verifier.VerifiedSigner v2Signer =
+ ApkSignatureSchemeV2Verifier.verify(apkPath, false);
+ nonstreamingDigest = v2Signer.digest;
+ nonstreamingCerts = v2Signer.certs;
+ } catch (SignatureNotFoundException ee) {
+ throw new SecurityException(
+ "V4 verification failed to collect V2/V3 certificates from : "
+ + apkPath, ee);
+ }
+ }
+
+ Signature[] nonstreamingSigs = convertToSignatures(nonstreamingCerts);
+ if (nonstreamingSigs.length != signerSigs.length) {
+ throw new SecurityException(
+ "Invalid number of certificates: " + nonstreamingSigs.length);
+ }
+
+ for (int i = 0, size = signerSigs.length; i < size; ++i) {
+ if (!nonstreamingSigs[i].equals(signerSigs[i])) {
+ throw new SecurityException(
+ "V4 signature certificate does not match V2/V3");
+ }
+ }
+
+ if (!ArrayUtils.equals(vSigner.apkDigest, nonstreamingDigest,
+ vSigner.apkDigest.length)) {
+ throw new SecurityException("APK digest in V4 signature does not match V2/V3");
+ }
+ }
+
+ return new PackageParser.SigningDetails(signerSigs,
+ SignatureSchemeVersion.SIGNING_BLOCK_V4);
+ } catch (SignatureNotFoundException e) {
+ throw e;
+ } catch (Exception e) {
+ // APK Signature Scheme v4 signature found but did not verify
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "Failed to collect certificates from " + apkPath
+ + " using APK Signature Scheme v4", e);
+ } finally {
+ Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
+ }
+ }
+
+ /**
+ * Verifies the provided APK using V3 schema.
+ *
+ * @param verifyFull whether to verify all contents of this APK or just collect certificates.
+ * @return the certificates associated with each signer.
+ * @throws SignatureNotFoundException if there are no V3 signatures in the APK
+ * @throws PackageParserException if there was a problem collecting certificates
+ */
+ private static PackageParser.SigningDetails verifyV3Signature(String apkPath,
+ boolean verifyFull) throws SignatureNotFoundException, PackageParserException {
+ Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, verifyFull ? "verifyV3" : "certsOnlyV3");
+ try {
+ ApkSignatureSchemeV3Verifier.VerifiedSigner vSigner =
+ verifyFull ? ApkSignatureSchemeV3Verifier.verify(apkPath)
+ : ApkSignatureSchemeV3Verifier.unsafeGetCertsWithoutVerification(
+ apkPath);
+ Certificate[][] signerCerts = new Certificate[][]{vSigner.certs};
+ Signature[] signerSigs = convertToSignatures(signerCerts);
+ Signature[] pastSignerSigs = null;
+ if (vSigner.por != null) {
+ // populate proof-of-rotation information
+ pastSignerSigs = new Signature[vSigner.por.certs.size()];
+ for (int i = 0; i < pastSignerSigs.length; i++) {
+ pastSignerSigs[i] = new Signature(vSigner.por.certs.get(i).getEncoded());
+ pastSignerSigs[i].setFlags(vSigner.por.flagsList.get(i));
+ }
+ }
+ return new PackageParser.SigningDetails(signerSigs,
+ SignatureSchemeVersion.SIGNING_BLOCK_V3, pastSignerSigs);
+ } catch (SignatureNotFoundException e) {
+ throw e;
+ } catch (Exception e) {
+ // APK Signature Scheme v3 signature found but did not verify
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "Failed to collect certificates from " + apkPath
+ + " using APK Signature Scheme v3", e);
+ } finally {
+ Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
+ }
+ }
+
+ /**
+ * Verifies the provided APK using V2 schema.
+ *
+ * @param verifyFull whether to verify all contents of this APK or just collect certificates.
+ * @return the certificates associated with each signer.
+ * @throws SignatureNotFoundException if there are no V2 signatures in the APK
+ * @throws PackageParserException if there was a problem collecting certificates
+ */
+ private static PackageParser.SigningDetails verifyV2Signature(String apkPath,
+ boolean verifyFull) throws SignatureNotFoundException, PackageParserException {
+ Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, verifyFull ? "verifyV2" : "certsOnlyV2");
+ try {
+ Certificate[][] signerCerts = verifyFull ? ApkSignatureSchemeV2Verifier.verify(apkPath)
+ : ApkSignatureSchemeV2Verifier.unsafeGetCertsWithoutVerification(apkPath);
+ Signature[] signerSigs = convertToSignatures(signerCerts);
+ return new PackageParser.SigningDetails(signerSigs,
+ SignatureSchemeVersion.SIGNING_BLOCK_V2);
+ } catch (SignatureNotFoundException e) {
+ throw e;
+ } catch (Exception e) {
+ // APK Signature Scheme v2 signature found but did not verify
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "Failed to collect certificates from " + apkPath
+ + " using APK Signature Scheme v2", e);
+ } finally {
+ Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
+ }
+ }
+
+ /**
+ * Verifies the provided APK using JAR schema.
+ * @return the certificates associated with each signer.
+ * @param verifyFull whether to verify all contents of this APK or just collect certificates.
+ * @throws PackageParserException if there was a problem collecting certificates
+ */
+ private static PackageParser.SigningDetails verifyV1Signature(
+ String apkPath, boolean verifyFull)
+ throws PackageParserException {
+ StrictJarFile jarFile = null;
+
+ try {
+ final Certificate[][] lastCerts;
+ final Signature[] lastSigs;
+
+ Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "strictJarFileCtor");
+
+ // we still pass verify = true to ctor to collect certs, even though we're not checking
+ // the whole jar.
+ jarFile = new StrictJarFile(
+ apkPath,
+ true, // collect certs
+ verifyFull); // whether to reject APK with stripped v2 signatures (b/27887819)
+ final List<ZipEntry> toVerify = new ArrayList<>();
+
+ // Gather certs from AndroidManifest.xml, which every APK must have, as an optimization
+ // to not need to verify the whole APK when verifyFUll == false.
+ final ZipEntry manifestEntry = jarFile.findEntry(
+ PackageParser.ANDROID_MANIFEST_FILENAME);
+ if (manifestEntry == null) {
+ throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST,
+ "Package " + apkPath + " has no manifest");
+ }
+ lastCerts = loadCertificates(jarFile, manifestEntry);
+ if (ArrayUtils.isEmpty(lastCerts)) {
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, "Package "
+ + apkPath + " has no certificates at entry "
+ + PackageParser.ANDROID_MANIFEST_FILENAME);
+ }
+ lastSigs = convertToSignatures(lastCerts);
+
+ // fully verify all contents, except for AndroidManifest.xml and the META-INF/ files.
+ if (verifyFull) {
+ final Iterator<ZipEntry> i = jarFile.iterator();
+ while (i.hasNext()) {
+ final ZipEntry entry = i.next();
+ if (entry.isDirectory()) continue;
+
+ final String entryName = entry.getName();
+ if (entryName.startsWith("META-INF/")) continue;
+ if (entryName.equals(PackageParser.ANDROID_MANIFEST_FILENAME)) continue;
+
+ toVerify.add(entry);
+ }
+
+ for (ZipEntry entry : toVerify) {
+ final Certificate[][] entryCerts = loadCertificates(jarFile, entry);
+ if (ArrayUtils.isEmpty(entryCerts)) {
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "Package " + apkPath + " has no certificates at entry "
+ + entry.getName());
+ }
+
+ // make sure all entries use the same signing certs
+ final Signature[] entrySigs = convertToSignatures(entryCerts);
+ if (!Signature.areExactMatch(lastSigs, entrySigs)) {
+ throw new PackageParserException(
+ INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES,
+ "Package " + apkPath + " has mismatched certificates at entry "
+ + entry.getName());
+ }
+ }
+ }
+ return new PackageParser.SigningDetails(lastSigs, SignatureSchemeVersion.JAR);
+ } catch (GeneralSecurityException e) {
+ throw new PackageParserException(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING,
+ "Failed to collect certificates from " + apkPath, e);
+ } catch (IOException | RuntimeException e) {
+ throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
+ "Failed to collect certificates from " + apkPath, e);
+ } finally {
+ Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
+ closeQuietly(jarFile);
+ }
+ }
+
+ private static Certificate[][] loadCertificates(StrictJarFile jarFile, ZipEntry entry)
+ throws PackageParserException {
+ InputStream is = null;
+ try {
+ // We must read the stream for the JarEntry to retrieve
+ // its certificates.
+ is = jarFile.getInputStream(entry);
+ readFullyIgnoringContents(is);
+ return jarFile.getCertificateChains(entry);
+ } catch (IOException | RuntimeException e) {
+ throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
+ "Failed reading " + entry.getName() + " in " + jarFile, e);
+ } finally {
+ IoUtils.closeQuietly(is);
+ }
+ }
+
+ private static void readFullyIgnoringContents(InputStream in) throws IOException {
+ byte[] buffer = sBuffer.getAndSet(null);
+ if (buffer == null) {
+ buffer = new byte[4096];
+ }
+
+ int n = 0;
+ int count = 0;
+ while ((n = in.read(buffer, 0, buffer.length)) != -1) {
+ count += n;
+ }
+
+ sBuffer.set(buffer);
+ return;
+ }
+
+ /**
+ * Converts an array of certificate chains into the {@code Signature} equivalent used by the
+ * PackageManager.
+ *
+ * @throws CertificateEncodingException if it is unable to create a Signature object.
+ */
+ private static Signature[] convertToSignatures(Certificate[][] certs)
+ throws CertificateEncodingException {
+ final Signature[] res = new Signature[certs.length];
+ for (int i = 0; i < certs.length; i++) {
+ res[i] = new Signature(certs[i]);
+ }
+ return res;
+ }
+
+ private static void closeQuietly(StrictJarFile jarFile) {
+ if (jarFile != null) {
+ try {
+ jarFile.close();
+ } catch (Exception ignored) {
+ }
+ }
+ }
+
+ /**
+ * Returns the minimum signature scheme version required for an app targeting the specified
+ * {@code targetSdk}.
+ */
+ public static int getMinimumSignatureSchemeVersionForTargetSdk(int targetSdk) {
+ if (targetSdk >= Build.VERSION_CODES.R) {
+ return SignatureSchemeVersion.SIGNING_BLOCK_V2;
+ }
+ return SignatureSchemeVersion.JAR;
+ }
+
+ /**
+ * Result of a successful APK verification operation.
+ */
+ public static class Result {
+ public final Certificate[][] certs;
+ public final Signature[] sigs;
+ public final int signatureSchemeVersion;
+
+ public Result(Certificate[][] certs, Signature[] sigs, int signingVersion) {
+ this.certs = certs;
+ this.sigs = sigs;
+ this.signatureSchemeVersion = signingVersion;
+ }
+ }
+
+ /**
+ * @return the verity root hash in the Signing Block.
+ */
+ public static byte[] getVerityRootHash(String apkPath) throws IOException, SecurityException {
+ // first try v3
+ try {
+ return ApkSignatureSchemeV3Verifier.getVerityRootHash(apkPath);
+ } catch (SignatureNotFoundException e) {
+ // try older version
+ }
+ try {
+ return ApkSignatureSchemeV2Verifier.getVerityRootHash(apkPath);
+ } catch (SignatureNotFoundException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Generates the Merkle tree and verity metadata to the buffer allocated by the {@code
+ * ByteBufferFactory}.
+ *
+ * @return the verity root hash of the generated Merkle tree.
+ */
+ public static byte[] generateApkVerity(String apkPath, ByteBufferFactory bufferFactory)
+ throws IOException, SignatureNotFoundException, SecurityException, DigestException,
+ NoSuchAlgorithmException {
+ // first try v3
+ try {
+ return ApkSignatureSchemeV3Verifier.generateApkVerity(apkPath, bufferFactory);
+ } catch (SignatureNotFoundException e) {
+ // try older version
+ }
+ return ApkSignatureSchemeV2Verifier.generateApkVerity(apkPath, bufferFactory);
+ }
+
+ /**
+ * Generates the FSVerity root hash from FSVerity header, extensions and Merkle tree root hash
+ * in Signing Block.
+ *
+ * @return FSverity root hash
+ */
+ public static byte[] generateApkVerityRootHash(String apkPath)
+ throws NoSuchAlgorithmException, DigestException, IOException {
+ // first try v3
+ try {
+ return ApkSignatureSchemeV3Verifier.generateApkVerityRootHash(apkPath);
+ } catch (SignatureNotFoundException e) {
+ // try older version
+ }
+ try {
+ return ApkSignatureSchemeV2Verifier.generateApkVerityRootHash(apkPath);
+ } catch (SignatureNotFoundException e) {
+ return null;
+ }
+ }
+}
diff --git a/android/util/apk/ApkSigningBlockUtils.java b/android/util/apk/ApkSigningBlockUtils.java
new file mode 100644
index 0000000..2a4b65d
--- /dev/null
+++ b/android/util/apk/ApkSigningBlockUtils.java
@@ -0,0 +1,818 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.apk;
+
+import android.util.ArrayMap;
+import android.util.Pair;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.DigestException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.MGF1ParameterSpec;
+import java.security.spec.PSSParameterSpec;
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * Utility class for an APK Signature Scheme using the APK Signing Block.
+ *
+ * @hide for internal use only.
+ */
+final class ApkSigningBlockUtils {
+
+ private ApkSigningBlockUtils() {
+ }
+
+ /**
+ * Returns the APK Signature Scheme block contained in the provided APK file and the
+ * additional information relevant for verifying the block against the file.
+ *
+ * @param blockId the ID value in the APK Signing Block's sequence of ID-value pairs
+ * identifying the appropriate block to find, e.g. the APK Signature Scheme v2
+ * block ID.
+ *
+ * @throws SignatureNotFoundException if the APK is not signed using this scheme.
+ * @throws IOException if an I/O error occurs while reading the APK file.
+ */
+ static SignatureInfo findSignature(RandomAccessFile apk, int blockId)
+ throws IOException, SignatureNotFoundException {
+ // Find the ZIP End of Central Directory (EoCD) record.
+ Pair<ByteBuffer, Long> eocdAndOffsetInFile = getEocd(apk);
+ ByteBuffer eocd = eocdAndOffsetInFile.first;
+ long eocdOffset = eocdAndOffsetInFile.second;
+ if (ZipUtils.isZip64EndOfCentralDirectoryLocatorPresent(apk, eocdOffset)) {
+ throw new SignatureNotFoundException("ZIP64 APK not supported");
+ }
+
+ // Find the APK Signing Block. The block immediately precedes the Central Directory.
+ long centralDirOffset = getCentralDirOffset(eocd, eocdOffset);
+ Pair<ByteBuffer, Long> apkSigningBlockAndOffsetInFile =
+ findApkSigningBlock(apk, centralDirOffset);
+ ByteBuffer apkSigningBlock = apkSigningBlockAndOffsetInFile.first;
+ long apkSigningBlockOffset = apkSigningBlockAndOffsetInFile.second;
+
+ // Find the APK Signature Scheme Block inside the APK Signing Block.
+ ByteBuffer apkSignatureSchemeBlock = findApkSignatureSchemeBlock(apkSigningBlock,
+ blockId);
+
+ return new SignatureInfo(
+ apkSignatureSchemeBlock,
+ apkSigningBlockOffset,
+ centralDirOffset,
+ eocdOffset,
+ eocd);
+ }
+
+ static void verifyIntegrity(
+ Map<Integer, byte[]> expectedDigests,
+ RandomAccessFile apk,
+ SignatureInfo signatureInfo) throws SecurityException {
+ if (expectedDigests.isEmpty()) {
+ throw new SecurityException("No digests provided");
+ }
+
+ boolean neverVerified = true;
+
+ Map<Integer, byte[]> expected1MbChunkDigests = new ArrayMap<>();
+ if (expectedDigests.containsKey(CONTENT_DIGEST_CHUNKED_SHA256)) {
+ expected1MbChunkDigests.put(CONTENT_DIGEST_CHUNKED_SHA256,
+ expectedDigests.get(CONTENT_DIGEST_CHUNKED_SHA256));
+ }
+ if (expectedDigests.containsKey(CONTENT_DIGEST_CHUNKED_SHA512)) {
+ expected1MbChunkDigests.put(CONTENT_DIGEST_CHUNKED_SHA512,
+ expectedDigests.get(CONTENT_DIGEST_CHUNKED_SHA512));
+ }
+ if (!expected1MbChunkDigests.isEmpty()) {
+ try {
+ verifyIntegrityFor1MbChunkBasedAlgorithm(expected1MbChunkDigests, apk.getFD(),
+ signatureInfo);
+ neverVerified = false;
+ } catch (IOException e) {
+ throw new SecurityException("Cannot get FD", e);
+ }
+ }
+
+ if (expectedDigests.containsKey(CONTENT_DIGEST_VERITY_CHUNKED_SHA256)) {
+ verifyIntegrityForVerityBasedAlgorithm(
+ expectedDigests.get(CONTENT_DIGEST_VERITY_CHUNKED_SHA256), apk, signatureInfo);
+ neverVerified = false;
+ }
+
+ if (neverVerified) {
+ throw new SecurityException("No known digest exists for integrity check");
+ }
+ }
+
+ static boolean isSupportedSignatureAlgorithm(int sigAlgorithm) {
+ switch (sigAlgorithm) {
+ case SIGNATURE_RSA_PSS_WITH_SHA256:
+ case SIGNATURE_RSA_PSS_WITH_SHA512:
+ case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
+ case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
+ case SIGNATURE_ECDSA_WITH_SHA256:
+ case SIGNATURE_ECDSA_WITH_SHA512:
+ case SIGNATURE_DSA_WITH_SHA256:
+ case SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256:
+ case SIGNATURE_VERITY_ECDSA_WITH_SHA256:
+ case SIGNATURE_VERITY_DSA_WITH_SHA256:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private static void verifyIntegrityFor1MbChunkBasedAlgorithm(
+ Map<Integer, byte[]> expectedDigests,
+ FileDescriptor apkFileDescriptor,
+ SignatureInfo signatureInfo) throws SecurityException {
+ // We need to verify the integrity of the following three sections of the file:
+ // 1. Everything up to the start of the APK Signing Block.
+ // 2. ZIP Central Directory.
+ // 3. ZIP End of Central Directory (EoCD).
+ // Each of these sections is represented as a separate DataSource instance below.
+
+ // To handle large APKs, these sections are read in 1 MB chunks using memory-mapped I/O to
+ // avoid wasting physical memory. In most APK verification scenarios, the contents of the
+ // APK are already there in the OS's page cache and thus mmap does not use additional
+ // physical memory.
+ DataSource beforeApkSigningBlock =
+ new MemoryMappedFileDataSource(apkFileDescriptor, 0,
+ signatureInfo.apkSigningBlockOffset);
+ DataSource centralDir =
+ new MemoryMappedFileDataSource(
+ apkFileDescriptor, signatureInfo.centralDirOffset,
+ signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
+
+ // For the purposes of integrity verification, ZIP End of Central Directory's field Start of
+ // Central Directory must be considered to point to the offset of the APK Signing Block.
+ ByteBuffer eocdBuf = signatureInfo.eocd.duplicate();
+ eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
+ ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, signatureInfo.apkSigningBlockOffset);
+ DataSource eocd = new ByteBufferDataSource(eocdBuf);
+
+ int[] digestAlgorithms = new int[expectedDigests.size()];
+ int digestAlgorithmCount = 0;
+ for (int digestAlgorithm : expectedDigests.keySet()) {
+ digestAlgorithms[digestAlgorithmCount] = digestAlgorithm;
+ digestAlgorithmCount++;
+ }
+ byte[][] actualDigests;
+ try {
+ actualDigests =
+ computeContentDigestsPer1MbChunk(
+ digestAlgorithms,
+ new DataSource[] {beforeApkSigningBlock, centralDir, eocd});
+ } catch (DigestException e) {
+ throw new SecurityException("Failed to compute digest(s) of contents", e);
+ }
+ for (int i = 0; i < digestAlgorithms.length; i++) {
+ int digestAlgorithm = digestAlgorithms[i];
+ byte[] expectedDigest = expectedDigests.get(digestAlgorithm);
+ byte[] actualDigest = actualDigests[i];
+ if (!MessageDigest.isEqual(expectedDigest, actualDigest)) {
+ throw new SecurityException(
+ getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm)
+ + " digest of contents did not verify");
+ }
+ }
+ }
+
+ private static byte[][] computeContentDigestsPer1MbChunk(
+ int[] digestAlgorithms,
+ DataSource[] contents) throws DigestException {
+ // For each digest algorithm the result is computed as follows:
+ // 1. Each segment of contents is split into consecutive chunks of 1 MB in size.
+ // The final chunk will be shorter iff the length of segment is not a multiple of 1 MB.
+ // No chunks are produced for empty (zero length) segments.
+ // 2. The digest of each chunk is computed over the concatenation of byte 0xa5, the chunk's
+ // length in bytes (uint32 little-endian) and the chunk's contents.
+ // 3. The output digest is computed over the concatenation of the byte 0x5a, the number of
+ // chunks (uint32 little-endian) and the concatenation of digests of chunks of all
+ // segments in-order.
+
+ long totalChunkCountLong = 0;
+ for (DataSource input : contents) {
+ totalChunkCountLong += getChunkCount(input.size());
+ }
+ if (totalChunkCountLong >= Integer.MAX_VALUE / 1024) {
+ throw new DigestException("Too many chunks: " + totalChunkCountLong);
+ }
+ int totalChunkCount = (int) totalChunkCountLong;
+
+ byte[][] digestsOfChunks = new byte[digestAlgorithms.length][];
+ for (int i = 0; i < digestAlgorithms.length; i++) {
+ int digestAlgorithm = digestAlgorithms[i];
+ int digestOutputSizeBytes = getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
+ byte[] concatenationOfChunkCountAndChunkDigests =
+ new byte[5 + totalChunkCount * digestOutputSizeBytes];
+ concatenationOfChunkCountAndChunkDigests[0] = 0x5a;
+ setUnsignedInt32LittleEndian(
+ totalChunkCount,
+ concatenationOfChunkCountAndChunkDigests,
+ 1);
+ digestsOfChunks[i] = concatenationOfChunkCountAndChunkDigests;
+ }
+
+ byte[] chunkContentPrefix = new byte[5];
+ chunkContentPrefix[0] = (byte) 0xa5;
+ int chunkIndex = 0;
+ MessageDigest[] mds = new MessageDigest[digestAlgorithms.length];
+ for (int i = 0; i < digestAlgorithms.length; i++) {
+ String jcaAlgorithmName =
+ getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithms[i]);
+ try {
+ mds[i] = MessageDigest.getInstance(jcaAlgorithmName);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(jcaAlgorithmName + " digest not supported", e);
+ }
+ }
+ // TODO: Compute digests of chunks in parallel when beneficial. This requires some research
+ // into how to parallelize (if at all) based on the capabilities of the hardware on which
+ // this code is running and based on the size of input.
+ DataDigester digester = new MultipleDigestDataDigester(mds);
+ int dataSourceIndex = 0;
+ for (DataSource input : contents) {
+ long inputOffset = 0;
+ long inputRemaining = input.size();
+ while (inputRemaining > 0) {
+ int chunkSize = (int) Math.min(inputRemaining, CHUNK_SIZE_BYTES);
+ setUnsignedInt32LittleEndian(chunkSize, chunkContentPrefix, 1);
+ for (int i = 0; i < mds.length; i++) {
+ mds[i].update(chunkContentPrefix);
+ }
+ try {
+ input.feedIntoDataDigester(digester, inputOffset, chunkSize);
+ } catch (IOException e) {
+ throw new DigestException(
+ "Failed to digest chunk #" + chunkIndex + " of section #"
+ + dataSourceIndex,
+ e);
+ }
+ for (int i = 0; i < digestAlgorithms.length; i++) {
+ int digestAlgorithm = digestAlgorithms[i];
+ byte[] concatenationOfChunkCountAndChunkDigests = digestsOfChunks[i];
+ int expectedDigestSizeBytes =
+ getContentDigestAlgorithmOutputSizeBytes(digestAlgorithm);
+ MessageDigest md = mds[i];
+ int actualDigestSizeBytes =
+ md.digest(
+ concatenationOfChunkCountAndChunkDigests,
+ 5 + chunkIndex * expectedDigestSizeBytes,
+ expectedDigestSizeBytes);
+ if (actualDigestSizeBytes != expectedDigestSizeBytes) {
+ throw new RuntimeException(
+ "Unexpected output size of " + md.getAlgorithm() + " digest: "
+ + actualDigestSizeBytes);
+ }
+ }
+ inputOffset += chunkSize;
+ inputRemaining -= chunkSize;
+ chunkIndex++;
+ }
+ dataSourceIndex++;
+ }
+
+ byte[][] result = new byte[digestAlgorithms.length][];
+ for (int i = 0; i < digestAlgorithms.length; i++) {
+ int digestAlgorithm = digestAlgorithms[i];
+ byte[] input = digestsOfChunks[i];
+ String jcaAlgorithmName = getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm);
+ MessageDigest md;
+ try {
+ md = MessageDigest.getInstance(jcaAlgorithmName);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(jcaAlgorithmName + " digest not supported", e);
+ }
+ byte[] output = md.digest(input);
+ result[i] = output;
+ }
+ return result;
+ }
+
+ /**
+ * Return the verity digest only if the length of digest content looks correct.
+ * When verity digest is generated, the last incomplete 4k chunk is padded with 0s before
+ * hashing. This means two almost identical APKs with different number of 0 at the end will have
+ * the same verity digest. To avoid this problem, the length of the source content (excluding
+ * Signing Block) is appended to the verity digest, and the digest is returned only if the
+ * length is consistent to the current APK.
+ */
+ static byte[] parseVerityDigestAndVerifySourceLength(
+ byte[] data, long fileSize, SignatureInfo signatureInfo) throws SecurityException {
+ // FORMAT:
+ // OFFSET DATA TYPE DESCRIPTION
+ // * @+0 bytes uint8[32] Merkle tree root hash of SHA-256
+ // * @+32 bytes int64 Length of source data
+ int kRootHashSize = 32;
+ int kSourceLengthSize = 8;
+
+ if (data.length != kRootHashSize + kSourceLengthSize) {
+ throw new SecurityException("Verity digest size is wrong: " + data.length);
+ }
+ ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
+ buffer.position(kRootHashSize);
+ long expectedSourceLength = buffer.getLong();
+
+ long signingBlockSize = signatureInfo.centralDirOffset
+ - signatureInfo.apkSigningBlockOffset;
+ if (expectedSourceLength != fileSize - signingBlockSize) {
+ throw new SecurityException("APK content size did not verify");
+ }
+
+ return Arrays.copyOfRange(data, 0, kRootHashSize);
+ }
+
+ private static void verifyIntegrityForVerityBasedAlgorithm(
+ byte[] expectedDigest,
+ RandomAccessFile apk,
+ SignatureInfo signatureInfo) throws SecurityException {
+ try {
+ byte[] expectedRootHash = parseVerityDigestAndVerifySourceLength(expectedDigest,
+ apk.length(), signatureInfo);
+ VerityBuilder.VerityResult verity = VerityBuilder.generateApkVerityTree(apk,
+ signatureInfo, new ByteBufferFactory() {
+ @Override
+ public ByteBuffer create(int capacity) {
+ return ByteBuffer.allocate(capacity);
+ }
+ });
+ if (!Arrays.equals(expectedRootHash, verity.rootHash)) {
+ throw new SecurityException("APK verity digest of contents did not verify");
+ }
+ } catch (DigestException | IOException | NoSuchAlgorithmException e) {
+ throw new SecurityException("Error during verification", e);
+ }
+ }
+
+ /**
+ * Returns the ZIP End of Central Directory (EoCD) and its offset in the file.
+ *
+ * @throws IOException if an I/O error occurs while reading the file.
+ * @throws SignatureNotFoundException if the EoCD could not be found.
+ */
+ static Pair<ByteBuffer, Long> getEocd(RandomAccessFile apk)
+ throws IOException, SignatureNotFoundException {
+ Pair<ByteBuffer, Long> eocdAndOffsetInFile =
+ ZipUtils.findZipEndOfCentralDirectoryRecord(apk);
+ if (eocdAndOffsetInFile == null) {
+ throw new SignatureNotFoundException(
+ "Not an APK file: ZIP End of Central Directory record not found");
+ }
+ return eocdAndOffsetInFile;
+ }
+
+ static long getCentralDirOffset(ByteBuffer eocd, long eocdOffset)
+ throws SignatureNotFoundException {
+ // Look up the offset of ZIP Central Directory.
+ long centralDirOffset = ZipUtils.getZipEocdCentralDirectoryOffset(eocd);
+ if (centralDirOffset > eocdOffset) {
+ throw new SignatureNotFoundException(
+ "ZIP Central Directory offset out of range: " + centralDirOffset
+ + ". ZIP End of Central Directory offset: " + eocdOffset);
+ }
+ long centralDirSize = ZipUtils.getZipEocdCentralDirectorySizeBytes(eocd);
+ if (centralDirOffset + centralDirSize != eocdOffset) {
+ throw new SignatureNotFoundException(
+ "ZIP Central Directory is not immediately followed by End of Central"
+ + " Directory");
+ }
+ return centralDirOffset;
+ }
+
+ private static long getChunkCount(long inputSizeBytes) {
+ return (inputSizeBytes + CHUNK_SIZE_BYTES - 1) / CHUNK_SIZE_BYTES;
+ }
+
+ private static final int CHUNK_SIZE_BYTES = 1024 * 1024;
+
+ static final int SIGNATURE_RSA_PSS_WITH_SHA256 = 0x0101;
+ static final int SIGNATURE_RSA_PSS_WITH_SHA512 = 0x0102;
+ static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256 = 0x0103;
+ static final int SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512 = 0x0104;
+ static final int SIGNATURE_ECDSA_WITH_SHA256 = 0x0201;
+ static final int SIGNATURE_ECDSA_WITH_SHA512 = 0x0202;
+ static final int SIGNATURE_DSA_WITH_SHA256 = 0x0301;
+ static final int SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256 = 0x0421;
+ static final int SIGNATURE_VERITY_ECDSA_WITH_SHA256 = 0x0423;
+ static final int SIGNATURE_VERITY_DSA_WITH_SHA256 = 0x0425;
+
+ static final int CONTENT_DIGEST_CHUNKED_SHA256 = 1;
+ static final int CONTENT_DIGEST_CHUNKED_SHA512 = 2;
+ static final int CONTENT_DIGEST_VERITY_CHUNKED_SHA256 = 3;
+
+ private static final int[] V4_CONTENT_DIGEST_ALGORITHMS =
+ {CONTENT_DIGEST_CHUNKED_SHA512, CONTENT_DIGEST_VERITY_CHUNKED_SHA256,
+ CONTENT_DIGEST_CHUNKED_SHA256};
+
+ static int compareSignatureAlgorithm(int sigAlgorithm1, int sigAlgorithm2) {
+ int digestAlgorithm1 = getSignatureAlgorithmContentDigestAlgorithm(sigAlgorithm1);
+ int digestAlgorithm2 = getSignatureAlgorithmContentDigestAlgorithm(sigAlgorithm2);
+ return compareContentDigestAlgorithm(digestAlgorithm1, digestAlgorithm2);
+ }
+
+ private static int compareContentDigestAlgorithm(int digestAlgorithm1, int digestAlgorithm2) {
+ switch (digestAlgorithm1) {
+ case CONTENT_DIGEST_CHUNKED_SHA256:
+ switch (digestAlgorithm2) {
+ case CONTENT_DIGEST_CHUNKED_SHA256:
+ return 0;
+ case CONTENT_DIGEST_CHUNKED_SHA512:
+ case CONTENT_DIGEST_VERITY_CHUNKED_SHA256:
+ return -1;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown digestAlgorithm2: " + digestAlgorithm2);
+ }
+ case CONTENT_DIGEST_CHUNKED_SHA512:
+ switch (digestAlgorithm2) {
+ case CONTENT_DIGEST_CHUNKED_SHA256:
+ case CONTENT_DIGEST_VERITY_CHUNKED_SHA256:
+ return 1;
+ case CONTENT_DIGEST_CHUNKED_SHA512:
+ return 0;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown digestAlgorithm2: " + digestAlgorithm2);
+ }
+ case CONTENT_DIGEST_VERITY_CHUNKED_SHA256:
+ switch (digestAlgorithm2) {
+ case CONTENT_DIGEST_CHUNKED_SHA512:
+ return -1;
+ case CONTENT_DIGEST_VERITY_CHUNKED_SHA256:
+ return 0;
+ case CONTENT_DIGEST_CHUNKED_SHA256:
+ return 1;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown digestAlgorithm2: " + digestAlgorithm2);
+ }
+ default:
+ throw new IllegalArgumentException("Unknown digestAlgorithm1: " + digestAlgorithm1);
+ }
+ }
+
+ static int getSignatureAlgorithmContentDigestAlgorithm(int sigAlgorithm) {
+ switch (sigAlgorithm) {
+ case SIGNATURE_RSA_PSS_WITH_SHA256:
+ case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
+ case SIGNATURE_ECDSA_WITH_SHA256:
+ case SIGNATURE_DSA_WITH_SHA256:
+ return CONTENT_DIGEST_CHUNKED_SHA256;
+ case SIGNATURE_RSA_PSS_WITH_SHA512:
+ case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
+ case SIGNATURE_ECDSA_WITH_SHA512:
+ return CONTENT_DIGEST_CHUNKED_SHA512;
+ case SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256:
+ case SIGNATURE_VERITY_ECDSA_WITH_SHA256:
+ case SIGNATURE_VERITY_DSA_WITH_SHA256:
+ return CONTENT_DIGEST_VERITY_CHUNKED_SHA256;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown signature algorithm: 0x"
+ + Long.toHexString(sigAlgorithm & 0xffffffff));
+ }
+ }
+
+ static String getContentDigestAlgorithmJcaDigestAlgorithm(int digestAlgorithm) {
+ switch (digestAlgorithm) {
+ case CONTENT_DIGEST_CHUNKED_SHA256:
+ case CONTENT_DIGEST_VERITY_CHUNKED_SHA256:
+ return "SHA-256";
+ case CONTENT_DIGEST_CHUNKED_SHA512:
+ return "SHA-512";
+ default:
+ throw new IllegalArgumentException(
+ "Unknown content digest algorthm: " + digestAlgorithm);
+ }
+ }
+
+ private static int getContentDigestAlgorithmOutputSizeBytes(int digestAlgorithm) {
+ switch (digestAlgorithm) {
+ case CONTENT_DIGEST_CHUNKED_SHA256:
+ case CONTENT_DIGEST_VERITY_CHUNKED_SHA256:
+ return 256 / 8;
+ case CONTENT_DIGEST_CHUNKED_SHA512:
+ return 512 / 8;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown content digest algorthm: " + digestAlgorithm);
+ }
+ }
+
+ static String getSignatureAlgorithmJcaKeyAlgorithm(int sigAlgorithm) {
+ switch (sigAlgorithm) {
+ case SIGNATURE_RSA_PSS_WITH_SHA256:
+ case SIGNATURE_RSA_PSS_WITH_SHA512:
+ case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
+ case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
+ case SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256:
+ return "RSA";
+ case SIGNATURE_ECDSA_WITH_SHA256:
+ case SIGNATURE_ECDSA_WITH_SHA512:
+ case SIGNATURE_VERITY_ECDSA_WITH_SHA256:
+ return "EC";
+ case SIGNATURE_DSA_WITH_SHA256:
+ case SIGNATURE_VERITY_DSA_WITH_SHA256:
+ return "DSA";
+ default:
+ throw new IllegalArgumentException(
+ "Unknown signature algorithm: 0x"
+ + Long.toHexString(sigAlgorithm & 0xffffffff));
+ }
+ }
+
+ static Pair<String, ? extends AlgorithmParameterSpec>
+ getSignatureAlgorithmJcaSignatureAlgorithm(int sigAlgorithm) {
+ switch (sigAlgorithm) {
+ case SIGNATURE_RSA_PSS_WITH_SHA256:
+ return Pair.create(
+ "SHA256withRSA/PSS",
+ new PSSParameterSpec(
+ "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 256 / 8, 1));
+ case SIGNATURE_RSA_PSS_WITH_SHA512:
+ return Pair.create(
+ "SHA512withRSA/PSS",
+ new PSSParameterSpec(
+ "SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1));
+ case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
+ case SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256:
+ return Pair.create("SHA256withRSA", null);
+ case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
+ return Pair.create("SHA512withRSA", null);
+ case SIGNATURE_ECDSA_WITH_SHA256:
+ case SIGNATURE_VERITY_ECDSA_WITH_SHA256:
+ return Pair.create("SHA256withECDSA", null);
+ case SIGNATURE_ECDSA_WITH_SHA512:
+ return Pair.create("SHA512withECDSA", null);
+ case SIGNATURE_DSA_WITH_SHA256:
+ case SIGNATURE_VERITY_DSA_WITH_SHA256:
+ return Pair.create("SHA256withDSA", null);
+ default:
+ throw new IllegalArgumentException(
+ "Unknown signature algorithm: 0x"
+ + Long.toHexString(sigAlgorithm & 0xffffffff));
+ }
+ }
+
+ /**
+ * Returns the best digest from the map of available digests.
+ * similarly to compareContentDigestAlgorithm.
+ *
+ * Keep in sync with pickBestDigestForV4 in apksigner's ApkSigningBlockUtils.
+ */
+ static byte[] pickBestDigestForV4(Map<Integer, byte[]> contentDigests) {
+ for (int algo : V4_CONTENT_DIGEST_ALGORITHMS) {
+ if (contentDigests.containsKey(algo)) {
+ return contentDigests.get(algo);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns new byte buffer whose content is a shared subsequence of this buffer's content
+ * between the specified start (inclusive) and end (exclusive) positions. As opposed to
+ * {@link ByteBuffer#slice()}, the returned buffer's byte order is the same as the source
+ * buffer's byte order.
+ */
+ static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) {
+ if (start < 0) {
+ throw new IllegalArgumentException("start: " + start);
+ }
+ if (end < start) {
+ throw new IllegalArgumentException("end < start: " + end + " < " + start);
+ }
+ int capacity = source.capacity();
+ if (end > source.capacity()) {
+ throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity);
+ }
+ int originalLimit = source.limit();
+ int originalPosition = source.position();
+ try {
+ source.position(0);
+ source.limit(end);
+ source.position(start);
+ ByteBuffer result = source.slice();
+ result.order(source.order());
+ return result;
+ } finally {
+ source.position(0);
+ source.limit(originalLimit);
+ source.position(originalPosition);
+ }
+ }
+
+ /**
+ * Relative <em>get</em> method for reading {@code size} number of bytes from the current
+ * position of this buffer.
+ *
+ * <p>This method reads the next {@code size} bytes at this buffer's current position,
+ * returning them as a {@code ByteBuffer} with start set to 0, limit and capacity set to
+ * {@code size}, byte order set to this buffer's byte order; and then increments the position by
+ * {@code size}.
+ */
+ static ByteBuffer getByteBuffer(ByteBuffer source, int size)
+ throws BufferUnderflowException {
+ if (size < 0) {
+ throw new IllegalArgumentException("size: " + size);
+ }
+ int originalLimit = source.limit();
+ int position = source.position();
+ int limit = position + size;
+ if ((limit < position) || (limit > originalLimit)) {
+ throw new BufferUnderflowException();
+ }
+ source.limit(limit);
+ try {
+ ByteBuffer result = source.slice();
+ result.order(source.order());
+ source.position(limit);
+ return result;
+ } finally {
+ source.limit(originalLimit);
+ }
+ }
+
+ static ByteBuffer getLengthPrefixedSlice(ByteBuffer source) throws IOException {
+ if (source.remaining() < 4) {
+ throw new IOException(
+ "Remaining buffer too short to contain length of length-prefixed field."
+ + " Remaining: " + source.remaining());
+ }
+ int len = source.getInt();
+ if (len < 0) {
+ throw new IllegalArgumentException("Negative length");
+ } else if (len > source.remaining()) {
+ throw new IOException("Length-prefixed field longer than remaining buffer."
+ + " Field length: " + len + ", remaining: " + source.remaining());
+ }
+ return getByteBuffer(source, len);
+ }
+
+ static byte[] readLengthPrefixedByteArray(ByteBuffer buf) throws IOException {
+ int len = buf.getInt();
+ if (len < 0) {
+ throw new IOException("Negative length");
+ } else if (len > buf.remaining()) {
+ throw new IOException("Underflow while reading length-prefixed value. Length: " + len
+ + ", available: " + buf.remaining());
+ }
+ byte[] result = new byte[len];
+ buf.get(result);
+ return result;
+ }
+
+ static void setUnsignedInt32LittleEndian(int value, byte[] result, int offset) {
+ result[offset] = (byte) (value & 0xff);
+ result[offset + 1] = (byte) ((value >>> 8) & 0xff);
+ result[offset + 2] = (byte) ((value >>> 16) & 0xff);
+ result[offset + 3] = (byte) ((value >>> 24) & 0xff);
+ }
+
+ private static final long APK_SIG_BLOCK_MAGIC_HI = 0x3234206b636f6c42L;
+ private static final long APK_SIG_BLOCK_MAGIC_LO = 0x20676953204b5041L;
+ private static final int APK_SIG_BLOCK_MIN_SIZE = 32;
+
+ static Pair<ByteBuffer, Long> findApkSigningBlock(
+ RandomAccessFile apk, long centralDirOffset)
+ throws IOException, SignatureNotFoundException {
+ // FORMAT:
+ // OFFSET DATA TYPE DESCRIPTION
+ // * @+0 bytes uint64: size in bytes (excluding this field)
+ // * @+8 bytes payload
+ // * @-24 bytes uint64: size in bytes (same as the one above)
+ // * @-16 bytes uint128: magic
+
+ if (centralDirOffset < APK_SIG_BLOCK_MIN_SIZE) {
+ throw new SignatureNotFoundException(
+ "APK too small for APK Signing Block. ZIP Central Directory offset: "
+ + centralDirOffset);
+ }
+ // Read the magic and offset in file from the footer section of the block:
+ // * uint64: size of block
+ // * 16 bytes: magic
+ ByteBuffer footer = ByteBuffer.allocate(24);
+ footer.order(ByteOrder.LITTLE_ENDIAN);
+ apk.seek(centralDirOffset - footer.capacity());
+ apk.readFully(footer.array(), footer.arrayOffset(), footer.capacity());
+ if ((footer.getLong(8) != APK_SIG_BLOCK_MAGIC_LO)
+ || (footer.getLong(16) != APK_SIG_BLOCK_MAGIC_HI)) {
+ throw new SignatureNotFoundException(
+ "No APK Signing Block before ZIP Central Directory");
+ }
+ // Read and compare size fields
+ long apkSigBlockSizeInFooter = footer.getLong(0);
+ if ((apkSigBlockSizeInFooter < footer.capacity())
+ || (apkSigBlockSizeInFooter > Integer.MAX_VALUE - 8)) {
+ throw new SignatureNotFoundException(
+ "APK Signing Block size out of range: " + apkSigBlockSizeInFooter);
+ }
+ int totalSize = (int) (apkSigBlockSizeInFooter + 8);
+ long apkSigBlockOffset = centralDirOffset - totalSize;
+ if (apkSigBlockOffset < 0) {
+ throw new SignatureNotFoundException(
+ "APK Signing Block offset out of range: " + apkSigBlockOffset);
+ }
+ ByteBuffer apkSigBlock = ByteBuffer.allocate(totalSize);
+ apkSigBlock.order(ByteOrder.LITTLE_ENDIAN);
+ apk.seek(apkSigBlockOffset);
+ apk.readFully(apkSigBlock.array(), apkSigBlock.arrayOffset(), apkSigBlock.capacity());
+ long apkSigBlockSizeInHeader = apkSigBlock.getLong(0);
+ if (apkSigBlockSizeInHeader != apkSigBlockSizeInFooter) {
+ throw new SignatureNotFoundException(
+ "APK Signing Block sizes in header and footer do not match: "
+ + apkSigBlockSizeInHeader + " vs " + apkSigBlockSizeInFooter);
+ }
+ return Pair.create(apkSigBlock, apkSigBlockOffset);
+ }
+
+ static ByteBuffer findApkSignatureSchemeBlock(ByteBuffer apkSigningBlock, int blockId)
+ throws SignatureNotFoundException {
+ checkByteOrderLittleEndian(apkSigningBlock);
+ // FORMAT:
+ // OFFSET DATA TYPE DESCRIPTION
+ // * @+0 bytes uint64: size in bytes (excluding this field)
+ // * @+8 bytes pairs
+ // * @-24 bytes uint64: size in bytes (same as the one above)
+ // * @-16 bytes uint128: magic
+ ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
+
+ int entryCount = 0;
+ while (pairs.hasRemaining()) {
+ entryCount++;
+ if (pairs.remaining() < 8) {
+ throw new SignatureNotFoundException(
+ "Insufficient data to read size of APK Signing Block entry #" + entryCount);
+ }
+ long lenLong = pairs.getLong();
+ if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
+ throw new SignatureNotFoundException(
+ "APK Signing Block entry #" + entryCount
+ + " size out of range: " + lenLong);
+ }
+ int len = (int) lenLong;
+ int nextEntryPos = pairs.position() + len;
+ if (len > pairs.remaining()) {
+ throw new SignatureNotFoundException(
+ "APK Signing Block entry #" + entryCount + " size out of range: " + len
+ + ", available: " + pairs.remaining());
+ }
+ int id = pairs.getInt();
+ if (id == blockId) {
+ return getByteBuffer(pairs, len - 4);
+ }
+ pairs.position(nextEntryPos);
+ }
+
+ throw new SignatureNotFoundException(
+ "No block with ID " + blockId + " in APK Signing Block.");
+ }
+
+ private static void checkByteOrderLittleEndian(ByteBuffer buffer) {
+ if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
+ throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
+ }
+ }
+
+ /**
+ * {@link DataDigester} that updates multiple {@link MessageDigest}s whenever data is fed.
+ */
+ private static class MultipleDigestDataDigester implements DataDigester {
+ private final MessageDigest[] mMds;
+
+ MultipleDigestDataDigester(MessageDigest[] mds) {
+ mMds = mds;
+ }
+
+ @Override
+ public void consume(ByteBuffer buffer) {
+ buffer = buffer.slice();
+ for (MessageDigest md : mMds) {
+ buffer.position(0);
+ md.update(buffer);
+ }
+ }
+ }
+
+}
diff --git a/android/util/apk/ByteBufferDataSource.java b/android/util/apk/ByteBufferDataSource.java
new file mode 100644
index 0000000..3976568
--- /dev/null
+++ b/android/util/apk/ByteBufferDataSource.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.apk;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.security.DigestException;
+
+/**
+ * {@link DataSource} which provides data from a {@link ByteBuffer}.
+ */
+class ByteBufferDataSource implements DataSource {
+ /**
+ * Underlying buffer. The data is stored between position 0 and the buffer's capacity.
+ * The buffer's position is 0 and limit is equal to capacity.
+ */
+ private final ByteBuffer mBuf;
+
+ ByteBufferDataSource(ByteBuffer buf) {
+ // Defensive copy, to avoid changes to mBuf being visible in buf, and to ensure position is
+ // 0 and limit == capacity.
+ mBuf = buf.slice();
+ }
+
+ @Override
+ public long size() {
+ return mBuf.capacity();
+ }
+
+ @Override
+ public void feedIntoDataDigester(DataDigester md, long offset, int size)
+ throws IOException, DigestException {
+ // There's no way to tell MessageDigest to read data from ByteBuffer from a position
+ // other than the buffer's current position. We thus need to change the buffer's
+ // position to match the requested offset.
+ //
+ // In the future, it may be necessary to compute digests of multiple regions in
+ // parallel. Given that digest computation is a slow operation, we enable multiple
+ // such requests to be fulfilled by this instance. This is achieved by serially
+ // creating a new ByteBuffer corresponding to the requested data range and then,
+ // potentially concurrently, feeding these buffers into MessageDigest instances.
+ ByteBuffer region;
+ synchronized (mBuf) {
+ mBuf.position(0);
+ mBuf.limit((int) offset + size);
+ mBuf.position((int) offset);
+ region = mBuf.slice();
+ }
+
+ md.consume(region);
+ }
+}
diff --git a/android/util/apk/ByteBufferFactory.java b/android/util/apk/ByteBufferFactory.java
new file mode 100644
index 0000000..7a99882
--- /dev/null
+++ b/android/util/apk/ByteBufferFactory.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.apk;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Provider of {@link ByteBuffer} instances.
+ * @hide
+ */
+public interface ByteBufferFactory {
+ /** Initiates a {@link ByteBuffer} with the given size. */
+ ByteBuffer create(int capacity);
+}
diff --git a/android/util/apk/DataDigester.java b/android/util/apk/DataDigester.java
new file mode 100644
index 0000000..18d1dff
--- /dev/null
+++ b/android/util/apk/DataDigester.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.apk;
+
+import java.nio.ByteBuffer;
+import java.security.DigestException;
+
+interface DataDigester {
+ /** Consumes the {@link ByteBuffer}. */
+ void consume(ByteBuffer buffer) throws DigestException;
+}
diff --git a/android/util/apk/DataSource.java b/android/util/apk/DataSource.java
new file mode 100644
index 0000000..82f3800
--- /dev/null
+++ b/android/util/apk/DataSource.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.apk;
+
+import java.io.IOException;
+import java.security.DigestException;
+
+/** Source of data to be digested. */
+interface DataSource {
+
+ /**
+ * Returns the size (in bytes) of the data offered by this source.
+ */
+ long size();
+
+ /**
+ * Feeds the specified region of this source's data into the provided digester.
+ *
+ * @param offset offset of the region inside this data source.
+ * @param size size (in bytes) of the region.
+ */
+ void feedIntoDataDigester(DataDigester md, long offset, int size)
+ throws IOException, DigestException;
+}
diff --git a/android/util/apk/MemoryMappedFileDataSource.java b/android/util/apk/MemoryMappedFileDataSource.java
new file mode 100644
index 0000000..8d2b1e3
--- /dev/null
+++ b/android/util/apk/MemoryMappedFileDataSource.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.apk;
+
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.DirectByteBuffer;
+import java.security.DigestException;
+
+/**
+ * {@link DataSource} which provides data from a file descriptor by memory-mapping the sections
+ * of the file.
+ */
+class MemoryMappedFileDataSource implements DataSource {
+ private static final long MEMORY_PAGE_SIZE_BYTES = Os.sysconf(OsConstants._SC_PAGESIZE);
+
+ private final FileDescriptor mFd;
+ private final long mFilePosition;
+ private final long mSize;
+
+ /**
+ * Constructs a new {@code MemoryMappedFileDataSource} for the specified region of the file.
+ *
+ * @param position start position of the region in the file.
+ * @param size size (in bytes) of the region.
+ */
+ MemoryMappedFileDataSource(FileDescriptor fd, long position, long size) {
+ mFd = fd;
+ mFilePosition = position;
+ mSize = size;
+ }
+
+ @Override
+ public long size() {
+ return mSize;
+ }
+
+ @Override
+ public void feedIntoDataDigester(DataDigester md, long offset, int size)
+ throws IOException, DigestException {
+ // IMPLEMENTATION NOTE: After a lot of experimentation, the implementation of this
+ // method was settled on a straightforward mmap with prefaulting.
+ //
+ // This method is not using FileChannel.map API because that API does not offset a way
+ // to "prefault" the resulting memory pages. Without prefaulting, performance is about
+ // 10% slower on small to medium APKs, but is significantly worse for APKs in 500+ MB
+ // range. FileChannel.load (which currently uses madvise) doesn't help. Finally,
+ // invoking madvise (MADV_SEQUENTIAL) after mmap with prefaulting wastes quite a bit of
+ // time, which is not compensated for by faster reads.
+
+ // We mmap the smallest region of the file containing the requested data. mmap requires
+ // that the start offset in the file must be a multiple of memory page size. We thus may
+ // need to mmap from an offset less than the requested offset.
+ long filePosition = mFilePosition + offset;
+ long mmapFilePosition =
+ (filePosition / MEMORY_PAGE_SIZE_BYTES) * MEMORY_PAGE_SIZE_BYTES;
+ int dataStartOffsetInMmapRegion = (int) (filePosition - mmapFilePosition);
+ long mmapRegionSize = size + dataStartOffsetInMmapRegion;
+ long mmapPtr = 0;
+ try {
+ mmapPtr = Os.mmap(
+ 0, // let the OS choose the start address of the region in memory
+ mmapRegionSize,
+ OsConstants.PROT_READ,
+ OsConstants.MAP_SHARED | OsConstants.MAP_POPULATE, // "prefault" all pages
+ mFd,
+ mmapFilePosition);
+ ByteBuffer buf = new DirectByteBuffer(
+ size,
+ mmapPtr + dataStartOffsetInMmapRegion,
+ mFd, // not really needed, but just in case
+ null, // no need to clean up -- it's taken care of by the finally block
+ true // read only buffer
+ );
+ md.consume(buf);
+ } catch (ErrnoException e) {
+ throw new IOException("Failed to mmap " + mmapRegionSize + " bytes", e);
+ } finally {
+ if (mmapPtr != 0) {
+ try {
+ Os.munmap(mmapPtr, mmapRegionSize);
+ } catch (ErrnoException ignored) { }
+ }
+ }
+ }
+}
diff --git a/android/util/apk/SignatureInfo.java b/android/util/apk/SignatureInfo.java
new file mode 100644
index 0000000..8e1233a
--- /dev/null
+++ b/android/util/apk/SignatureInfo.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.apk;
+
+import java.nio.ByteBuffer;
+
+/**
+ * APK Signature Scheme v2 block and additional information relevant to verifying the signatures
+ * contained in the block against the file.
+ */
+class SignatureInfo {
+ /** Contents of APK Signature Scheme v2 block. */
+ public final ByteBuffer signatureBlock;
+
+ /** Position of the APK Signing Block in the file. */
+ public final long apkSigningBlockOffset;
+
+ /** Position of the ZIP Central Directory in the file. */
+ public final long centralDirOffset;
+
+ /** Position of the ZIP End of Central Directory (EoCD) in the file. */
+ public final long eocdOffset;
+
+ /** Contents of ZIP End of Central Directory (EoCD) of the file. */
+ public final ByteBuffer eocd;
+
+ SignatureInfo(ByteBuffer signatureBlock, long apkSigningBlockOffset, long centralDirOffset,
+ long eocdOffset, ByteBuffer eocd) {
+ this.signatureBlock = signatureBlock;
+ this.apkSigningBlockOffset = apkSigningBlockOffset;
+ this.centralDirOffset = centralDirOffset;
+ this.eocdOffset = eocdOffset;
+ this.eocd = eocd;
+ }
+}
diff --git a/android/util/apk/SignatureNotFoundException.java b/android/util/apk/SignatureNotFoundException.java
new file mode 100644
index 0000000..9c7c760
--- /dev/null
+++ b/android/util/apk/SignatureNotFoundException.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.apk;
+
+/**
+ * Indicates that the APK is missing a signature.
+ *
+ * @hide
+ */
+public class SignatureNotFoundException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public SignatureNotFoundException(String message) {
+ super(message);
+ }
+
+ public SignatureNotFoundException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/android/util/apk/SourceStampVerificationResult.java b/android/util/apk/SourceStampVerificationResult.java
new file mode 100644
index 0000000..2edaf62
--- /dev/null
+++ b/android/util/apk/SourceStampVerificationResult.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.apk;
+
+import android.annotation.Nullable;
+
+import java.security.cert.Certificate;
+
+/**
+ * A class encapsulating the result from the source stamp verifier
+ *
+ * <p>It indicates whether the source stamp is verified or not, and the source stamp certificate.
+ *
+ * @hide
+ */
+public final class SourceStampVerificationResult {
+
+ private final boolean mPresent;
+ private final boolean mVerified;
+ private final Certificate mCertificate;
+
+ private SourceStampVerificationResult(
+ boolean present, boolean verified, @Nullable Certificate certificate) {
+ this.mPresent = present;
+ this.mVerified = verified;
+ this.mCertificate = certificate;
+ }
+
+ public boolean isPresent() {
+ return mPresent;
+ }
+
+ public boolean isVerified() {
+ return mVerified;
+ }
+
+ public Certificate getCertificate() {
+ return mCertificate;
+ }
+
+ /**
+ * Create a non-present source stamp outcome.
+ *
+ * @return A non-present source stamp result.
+ */
+ public static SourceStampVerificationResult notPresent() {
+ return new SourceStampVerificationResult(
+ /* present= */ false, /* verified= */ false, /* certificate= */ null);
+ }
+
+ /**
+ * Create a verified source stamp outcome.
+ *
+ * @param certificate The source stamp certificate.
+ * @return A verified source stamp result, and the source stamp certificate.
+ */
+ public static SourceStampVerificationResult verified(Certificate certificate) {
+ return new SourceStampVerificationResult(
+ /* present= */ true, /* verified= */ true, certificate);
+ }
+
+ /**
+ * Create a non-verified source stamp outcome.
+ *
+ * @return A non-verified source stamp result.
+ */
+ public static SourceStampVerificationResult notVerified() {
+ return new SourceStampVerificationResult(
+ /* present= */ true, /* verified= */ false, /* certificate= */ null);
+ }
+}
diff --git a/android/util/apk/SourceStampVerifier.java b/android/util/apk/SourceStampVerifier.java
new file mode 100644
index 0000000..a7ae32d
--- /dev/null
+++ b/android/util/apk/SourceStampVerifier.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.apk;
+
+import static android.util.apk.ApkSigningBlockUtils.compareSignatureAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
+import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmContentDigestAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaSignatureAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.isSupportedSignatureAlgorithm;
+import static android.util.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray;
+
+import android.util.Pair;
+import android.util.Slog;
+import android.util.jar.StrictJarFile;
+
+import libcore.io.IoUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.RandomAccessFile;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.AlgorithmParameterSpec;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+
+/**
+ * Source Stamp verifier.
+ *
+ * <p>SourceStamp improves traceability of apps with respect to unauthorized distribution.
+ *
+ * <p>The stamp is part of the APK that is protected by the signing block.
+ *
+ * <p>The APK contents hash is signed using the stamp key, and is saved as part of the signing
+ * block.
+ *
+ * @hide for internal use only.
+ */
+public abstract class SourceStampVerifier {
+
+ private static final String TAG = "SourceStampVerifier";
+
+ private static final int APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a;
+ private static final int APK_SIGNATURE_SCHEME_V3_BLOCK_ID = 0xf05368c0;
+ private static final int SOURCE_STAMP_BLOCK_ID = 0x2b09189e;
+
+ /** Name of the SourceStamp certificate hash ZIP entry in APKs. */
+ private static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256";
+
+ /** Hidden constructor to prevent instantiation. */
+ private SourceStampVerifier() {}
+
+ /** Verifies SourceStamp present in a list of APKs. */
+ public static SourceStampVerificationResult verify(List<String> apkFiles) {
+ Certificate stampCertificate = null;
+ for (String apkFile : apkFiles) {
+ SourceStampVerificationResult sourceStampVerificationResult = verify(apkFile);
+ if (!sourceStampVerificationResult.isPresent()
+ || !sourceStampVerificationResult.isVerified()) {
+ return sourceStampVerificationResult;
+ }
+ if (stampCertificate != null
+ && !stampCertificate.equals(sourceStampVerificationResult.getCertificate())) {
+ return SourceStampVerificationResult.notVerified();
+ }
+ stampCertificate = sourceStampVerificationResult.getCertificate();
+ }
+ return SourceStampVerificationResult.verified(stampCertificate);
+ }
+
+ /** Verifies SourceStamp present in the provided APK. */
+ public static SourceStampVerificationResult verify(String apkFile) {
+ StrictJarFile apkJar = null;
+ try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) {
+ apkJar =
+ new StrictJarFile(
+ apkFile,
+ /* verify= */ false,
+ /* signatureSchemeRollbackProtectionsEnforced= */ false);
+ byte[] sourceStampCertificateDigest = getSourceStampCertificateDigest(apkJar);
+ if (sourceStampCertificateDigest == null) {
+ // SourceStamp certificate hash file not found, which means that there is not
+ // SourceStamp present.
+ return SourceStampVerificationResult.notPresent();
+ }
+ return verify(apk, sourceStampCertificateDigest);
+ } catch (IOException e) {
+ // Any exception in reading the APK returns a non-present SourceStamp outcome
+ // without affecting the outcome of any of the other signature schemes.
+ return SourceStampVerificationResult.notPresent();
+ } finally {
+ closeApkJar(apkJar);
+ }
+ }
+
+ private static SourceStampVerificationResult verify(
+ RandomAccessFile apk, byte[] sourceStampCertificateDigest) {
+ try {
+ SignatureInfo signatureInfo =
+ ApkSigningBlockUtils.findSignature(apk, SOURCE_STAMP_BLOCK_ID);
+ Map<Integer, byte[]> apkContentDigests = getApkContentDigests(apk);
+ return verify(signatureInfo, apkContentDigests, sourceStampCertificateDigest);
+ } catch (IOException | SignatureNotFoundException e) {
+ return SourceStampVerificationResult.notVerified();
+ }
+ }
+
+ private static SourceStampVerificationResult verify(
+ SignatureInfo signatureInfo,
+ Map<Integer, byte[]> apkContentDigests,
+ byte[] sourceStampCertificateDigest)
+ throws SecurityException, IOException {
+ CertificateFactory certFactory;
+ try {
+ certFactory = CertificateFactory.getInstance("X.509");
+ } catch (CertificateException e) {
+ throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
+ }
+
+ List<Pair<Integer, byte[]>> digests =
+ apkContentDigests.entrySet().stream()
+ .sorted(Map.Entry.comparingByKey())
+ .map(e -> Pair.create(e.getKey(), e.getValue()))
+ .collect(Collectors.toList());
+ byte[] digestBytes = encodeApkContentDigests(digests);
+
+ ByteBuffer sourceStampBlock = signatureInfo.signatureBlock;
+ ByteBuffer sourceStampBlockData =
+ ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlock);
+
+ // Parse the SourceStamp certificate.
+ byte[] sourceStampEncodedCertificate =
+ ApkSigningBlockUtils.readLengthPrefixedByteArray(sourceStampBlockData);
+ X509Certificate sourceStampCertificate;
+ try {
+ sourceStampCertificate =
+ (X509Certificate)
+ certFactory.generateCertificate(
+ new ByteArrayInputStream(sourceStampEncodedCertificate));
+ } catch (CertificateException e) {
+ throw new SecurityException("Failed to decode certificate", e);
+ }
+ sourceStampCertificate =
+ new VerbatimX509Certificate(sourceStampCertificate, sourceStampEncodedCertificate);
+
+ // Verify the SourceStamp certificate found in the signing block is the same as the
+ // SourceStamp certificate found in the APK.
+ try {
+ MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
+ messageDigest.update(sourceStampEncodedCertificate);
+ byte[] sourceStampBlockCertificateDigest = messageDigest.digest();
+ if (!Arrays.equals(sourceStampCertificateDigest, sourceStampBlockCertificateDigest)) {
+ throw new SecurityException("Certificate mismatch between APK and signature block");
+ }
+ } catch (NoSuchAlgorithmException e) {
+ throw new SecurityException("Failed to find SHA-256", e);
+ }
+
+ // Parse the signatures block and identify supported signatures
+ ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(sourceStampBlockData);
+ int signatureCount = 0;
+ int bestSigAlgorithm = -1;
+ byte[] bestSigAlgorithmSignatureBytes = null;
+ while (signatures.hasRemaining()) {
+ signatureCount++;
+ try {
+ ByteBuffer signature = getLengthPrefixedSlice(signatures);
+ if (signature.remaining() < 8) {
+ throw new SecurityException("Signature record too short");
+ }
+ int sigAlgorithm = signature.getInt();
+ if (!isSupportedSignatureAlgorithm(sigAlgorithm)) {
+ continue;
+ }
+ if ((bestSigAlgorithm == -1)
+ || (compareSignatureAlgorithm(sigAlgorithm, bestSigAlgorithm) > 0)) {
+ bestSigAlgorithm = sigAlgorithm;
+ bestSigAlgorithmSignatureBytes = readLengthPrefixedByteArray(signature);
+ }
+ } catch (IOException | BufferUnderflowException e) {
+ throw new SecurityException(
+ "Failed to parse signature record #" + signatureCount, e);
+ }
+ }
+ if (bestSigAlgorithm == -1) {
+ if (signatureCount == 0) {
+ throw new SecurityException("No signatures found");
+ } else {
+ throw new SecurityException("No supported signatures found");
+ }
+ }
+
+ // Verify signatures over digests using the SourceStamp's certificate.
+ Pair<String, ? extends AlgorithmParameterSpec> signatureAlgorithmParams =
+ getSignatureAlgorithmJcaSignatureAlgorithm(bestSigAlgorithm);
+ String jcaSignatureAlgorithm = signatureAlgorithmParams.first;
+ AlgorithmParameterSpec jcaSignatureAlgorithmParams = signatureAlgorithmParams.second;
+ PublicKey publicKey = sourceStampCertificate.getPublicKey();
+ boolean sigVerified;
+ try {
+ Signature sig = Signature.getInstance(jcaSignatureAlgorithm);
+ sig.initVerify(publicKey);
+ if (jcaSignatureAlgorithmParams != null) {
+ sig.setParameter(jcaSignatureAlgorithmParams);
+ }
+ sig.update(digestBytes);
+ sigVerified = sig.verify(bestSigAlgorithmSignatureBytes);
+ } catch (InvalidKeyException
+ | InvalidAlgorithmParameterException
+ | SignatureException
+ | NoSuchAlgorithmException e) {
+ throw new SecurityException(
+ "Failed to verify " + jcaSignatureAlgorithm + " signature", e);
+ }
+ if (!sigVerified) {
+ throw new SecurityException(jcaSignatureAlgorithm + " signature did not verify");
+ }
+
+ return SourceStampVerificationResult.verified(sourceStampCertificate);
+ }
+
+ private static Map<Integer, byte[]> getApkContentDigests(RandomAccessFile apk)
+ throws IOException, SignatureNotFoundException {
+ // Retrieve APK content digests in V3 signing block. If a V3 signature is not found, the APK
+ // content digests would be re-tried from V2 signature.
+ try {
+ SignatureInfo v3SignatureInfo =
+ ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
+ return getApkContentDigestsFromSignatureBlock(v3SignatureInfo.signatureBlock);
+ } catch (SignatureNotFoundException e) {
+ // It's fine not to find a V3 signature.
+ }
+
+ // Retrieve APK content digests in V2 signing block. If a V2 signature is not found, the
+ // process of retrieving APK content digests stops, and the stamp is considered un-verified.
+ SignatureInfo v2SignatureInfo =
+ ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
+ return getApkContentDigestsFromSignatureBlock(v2SignatureInfo.signatureBlock);
+ }
+
+ private static Map<Integer, byte[]> getApkContentDigestsFromSignatureBlock(
+ ByteBuffer signatureBlock) throws IOException {
+ Map<Integer, byte[]> apkContentDigests = new HashMap<>();
+ ByteBuffer signers = getLengthPrefixedSlice(signatureBlock);
+ while (signers.hasRemaining()) {
+ ByteBuffer signer = getLengthPrefixedSlice(signers);
+ ByteBuffer signedData = getLengthPrefixedSlice(signer);
+ ByteBuffer digests = getLengthPrefixedSlice(signedData);
+ while (digests.hasRemaining()) {
+ ByteBuffer digest = getLengthPrefixedSlice(digests);
+ int sigAlgorithm = digest.getInt();
+ byte[] contentDigest = readLengthPrefixedByteArray(digest);
+ int digestAlgorithm = getSignatureAlgorithmContentDigestAlgorithm(sigAlgorithm);
+ apkContentDigests.put(digestAlgorithm, contentDigest);
+ }
+ }
+ return apkContentDigests;
+ }
+
+ private static byte[] getSourceStampCertificateDigest(StrictJarFile apkJar) throws IOException {
+ InputStream inputStream = null;
+ try {
+ ZipEntry zipEntry = apkJar.findEntry(SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME);
+ if (zipEntry == null) {
+ // SourceStamp certificate hash file not found, which means that there is not
+ // SourceStamp present.
+ return null;
+ }
+ inputStream = apkJar.getInputStream(zipEntry);
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+
+ // Trying to read the certificate digest, which should be less than 1024 bytes.
+ byte[] buffer = new byte[1024];
+ int count = inputStream.read(buffer, 0, buffer.length);
+ byteArrayOutputStream.write(buffer, 0, count);
+
+ return byteArrayOutputStream.toByteArray();
+ } finally {
+ IoUtils.closeQuietly(inputStream);
+ }
+ }
+
+ private static byte[] encodeApkContentDigests(List<Pair<Integer, byte[]>> apkContentDigests) {
+ int resultSize = 0;
+ for (Pair<Integer, byte[]> element : apkContentDigests) {
+ resultSize += 12 + element.second.length;
+ }
+ ByteBuffer result = ByteBuffer.allocate(resultSize);
+ result.order(ByteOrder.LITTLE_ENDIAN);
+ for (Pair<Integer, byte[]> element : apkContentDigests) {
+ byte[] second = element.second;
+ result.putInt(8 + second.length);
+ result.putInt(element.first);
+ result.putInt(second.length);
+ result.put(second);
+ }
+ return result.array();
+ }
+
+ private static void closeApkJar(StrictJarFile apkJar) {
+ try {
+ if (apkJar == null) {
+ return;
+ }
+ apkJar.close();
+ } catch (IOException e) {
+ Slog.e(TAG, "Could not close APK jar", e);
+ }
+ }
+}
diff --git a/android/util/apk/VerbatimX509Certificate.java b/android/util/apk/VerbatimX509Certificate.java
new file mode 100644
index 0000000..391c5fc
--- /dev/null
+++ b/android/util/apk/VerbatimX509Certificate.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.apk;
+
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+
+/**
+ * For legacy reasons we need to return exactly the original encoded certificate bytes, instead
+ * of letting the underlying implementation have a shot at re-encoding the data.
+ */
+class VerbatimX509Certificate extends WrappedX509Certificate {
+ private final byte[] mEncodedVerbatim;
+ private int mHash = -1;
+
+ VerbatimX509Certificate(X509Certificate wrapped, byte[] encodedVerbatim) {
+ super(wrapped);
+ this.mEncodedVerbatim = encodedVerbatim;
+ }
+
+ @Override
+ public byte[] getEncoded() throws CertificateEncodingException {
+ return mEncodedVerbatim;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof VerbatimX509Certificate)) return false;
+
+ try {
+ byte[] a = this.getEncoded();
+ byte[] b = ((VerbatimX509Certificate) o).getEncoded();
+ return Arrays.equals(a, b);
+ } catch (CertificateEncodingException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ if (mHash == -1) {
+ try {
+ mHash = Arrays.hashCode(this.getEncoded());
+ } catch (CertificateEncodingException e) {
+ mHash = 0;
+ }
+ }
+ return mHash;
+ }
+}
diff --git a/android/util/apk/VerityBuilder.java b/android/util/apk/VerityBuilder.java
new file mode 100644
index 0000000..e81e3f7
--- /dev/null
+++ b/android/util/apk/VerityBuilder.java
@@ -0,0 +1,507 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.apk;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.DigestException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+
+/**
+ * VerityBuilder builds the verity Merkle tree and other metadata. The generated tree format can
+ * be stored on disk for fs-verity setup and used by kernel. The builder support standard
+ * fs-verity, and Android specific apk-verity that requires additional kernel patches.
+ *
+ * <p>Unlike a regular Merkle tree of fs-verity, the apk-verity tree does not cover the file content
+ * fully, and has to skip APK Signing Block with some special treatment for the "Central Directory
+ * offset" field of ZIP End of Central Directory.
+ *
+ * @hide
+ */
+public abstract class VerityBuilder {
+ private VerityBuilder() {}
+
+ private static final int CHUNK_SIZE_BYTES = 4096; // Typical Linux block size
+ private static final int DIGEST_SIZE_BYTES = 32; // SHA-256 size
+ private static final int FSVERITY_HEADER_SIZE_BYTES = 64;
+ private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE = 4;
+ private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16;
+ private static final String JCA_DIGEST_ALGORITHM = "SHA-256";
+ private static final byte[] DEFAULT_SALT = new byte[8];
+
+ /** Result generated by the builder. */
+ public static class VerityResult {
+ /** Raw fs-verity metadata and Merkle tree ready to be deployed on disk. */
+ public final ByteBuffer verityData;
+
+ /** Size of the Merkle tree in {@code verityData}. */
+ public final int merkleTreeSize;
+
+ /** Root hash of the Merkle tree. */
+ public final byte[] rootHash;
+
+ private VerityResult(ByteBuffer verityData, int merkleTreeSize, byte[] rootHash) {
+ this.verityData = verityData;
+ this.merkleTreeSize = merkleTreeSize;
+ this.rootHash = rootHash;
+ }
+ }
+
+ /**
+ * Generates the 4k, SHA-256 based Merkle tree for the given APK and stores in the {@link
+ * ByteBuffer} created by the {@link ByteBufferFactory}. The Merkle tree does not cover Signing
+ * Block specificed in {@code signatureInfo}. The output is suitable to be used as the on-disk
+ * format for fs-verity to use (with elide and patch extensions).
+ *
+ * @return VerityResult containing a buffer with the generated Merkle tree stored at the
+ * front, the tree size, and the calculated root hash.
+ */
+ @NonNull
+ public static VerityResult generateApkVerityTree(@NonNull RandomAccessFile apk,
+ @Nullable SignatureInfo signatureInfo, @NonNull ByteBufferFactory bufferFactory)
+ throws IOException, SecurityException, NoSuchAlgorithmException, DigestException {
+ return generateVerityTreeInternal(apk, bufferFactory, signatureInfo);
+ }
+
+ @NonNull
+ private static VerityResult generateVerityTreeInternal(@NonNull RandomAccessFile apk,
+ @NonNull ByteBufferFactory bufferFactory, @Nullable SignatureInfo signatureInfo)
+ throws IOException, SecurityException, NoSuchAlgorithmException, DigestException {
+ long signingBlockSize =
+ signatureInfo.centralDirOffset - signatureInfo.apkSigningBlockOffset;
+ long dataSize = apk.length() - signingBlockSize;
+ int[] levelOffset = calculateVerityLevelOffset(dataSize);
+ int merkleTreeSize = levelOffset[levelOffset.length - 1];
+
+ ByteBuffer output = bufferFactory.create(
+ merkleTreeSize
+ + CHUNK_SIZE_BYTES); // maximum size of apk-verity metadata
+ output.order(ByteOrder.LITTLE_ENDIAN);
+ ByteBuffer tree = slice(output, 0, merkleTreeSize);
+ byte[] apkRootHash = generateVerityTreeInternal(apk, signatureInfo, DEFAULT_SALT,
+ levelOffset, tree);
+ return new VerityResult(output, merkleTreeSize, apkRootHash);
+ }
+
+ static void generateApkVerityFooter(@NonNull RandomAccessFile apk,
+ @NonNull SignatureInfo signatureInfo, @NonNull ByteBuffer footerOutput)
+ throws IOException {
+ footerOutput.order(ByteOrder.LITTLE_ENDIAN);
+ generateApkVerityHeader(footerOutput, apk.length(), DEFAULT_SALT);
+ long signingBlockSize =
+ signatureInfo.centralDirOffset - signatureInfo.apkSigningBlockOffset;
+ generateApkVerityExtensions(footerOutput, signatureInfo.apkSigningBlockOffset,
+ signingBlockSize, signatureInfo.eocdOffset);
+ }
+
+ /**
+ * Calculates the apk-verity root hash for integrity measurement. This needs to be consistent
+ * to what kernel returns.
+ */
+ @NonNull
+ static byte[] generateApkVerityRootHash(@NonNull RandomAccessFile apk,
+ @NonNull ByteBuffer apkDigest, @NonNull SignatureInfo signatureInfo)
+ throws NoSuchAlgorithmException, DigestException, IOException {
+ assertSigningBlockAlignedAndHasFullPages(signatureInfo);
+
+ ByteBuffer footer = ByteBuffer.allocate(CHUNK_SIZE_BYTES).order(ByteOrder.LITTLE_ENDIAN);
+ generateApkVerityFooter(apk, signatureInfo, footer);
+ footer.flip();
+
+ MessageDigest md = MessageDigest.getInstance(JCA_DIGEST_ALGORITHM);
+ md.update(footer);
+ md.update(apkDigest);
+ return md.digest();
+ }
+
+ /**
+ * Generates the apk-verity header and hash tree to be used by kernel for the given apk. This
+ * method does not check whether the root hash exists in the Signing Block or not.
+ *
+ * <p>The output is stored in the {@link ByteBuffer} created by the given {@link
+ * ByteBufferFactory}.
+ *
+ * @return the root hash of the generated hash tree.
+ */
+ @NonNull
+ static byte[] generateApkVerity(@NonNull String apkPath,
+ @NonNull ByteBufferFactory bufferFactory, @NonNull SignatureInfo signatureInfo)
+ throws IOException, SignatureNotFoundException, SecurityException, DigestException,
+ NoSuchAlgorithmException {
+ try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
+ VerityResult result = generateVerityTreeInternal(apk, bufferFactory, signatureInfo);
+ ByteBuffer footer = slice(result.verityData, result.merkleTreeSize,
+ result.verityData.limit());
+ generateApkVerityFooter(apk, signatureInfo, footer);
+ // Put the reverse offset to apk-verity header at the end.
+ footer.putInt(footer.position() + 4);
+ result.verityData.limit(result.merkleTreeSize + footer.position());
+ return result.rootHash;
+ }
+ }
+
+ /**
+ * A helper class to consume and digest data by block continuously, and write into a buffer.
+ */
+ private static class BufferedDigester implements DataDigester {
+ /** Amount of the data to digest in each cycle before writting out the digest. */
+ private static final int BUFFER_SIZE = CHUNK_SIZE_BYTES;
+
+ /**
+ * Amount of data the {@link MessageDigest} has consumed since the last reset. This must be
+ * always less than BUFFER_SIZE since {@link MessageDigest} is reset whenever it has
+ * consumed BUFFER_SIZE of data.
+ */
+ private int mBytesDigestedSinceReset;
+
+ /** The final output {@link ByteBuffer} to write the digest to sequentially. */
+ private final ByteBuffer mOutput;
+
+ private final MessageDigest mMd;
+ private final byte[] mDigestBuffer = new byte[DIGEST_SIZE_BYTES];
+ private final byte[] mSalt;
+
+ private BufferedDigester(@Nullable byte[] salt, @NonNull ByteBuffer output)
+ throws NoSuchAlgorithmException {
+ mSalt = salt;
+ mOutput = output.slice();
+ mMd = MessageDigest.getInstance(JCA_DIGEST_ALGORITHM);
+ if (mSalt != null) {
+ mMd.update(mSalt);
+ }
+ mBytesDigestedSinceReset = 0;
+ }
+
+ /**
+ * Consumes and digests data up to BUFFER_SIZE (may continue from the previous remaining),
+ * then writes the final digest to the output buffer. Repeat until all data are consumed.
+ * If the last consumption is not enough for BUFFER_SIZE, the state will stay and future
+ * consumption will continuous from there.
+ */
+ @Override
+ public void consume(ByteBuffer buffer) throws DigestException {
+ int offset = buffer.position();
+ int remaining = buffer.remaining();
+ while (remaining > 0) {
+ int allowance = (int) Math.min(remaining, BUFFER_SIZE - mBytesDigestedSinceReset);
+ // Optimization: set the buffer limit to avoid allocating a new ByteBuffer object.
+ buffer.limit(buffer.position() + allowance);
+ mMd.update(buffer);
+ offset += allowance;
+ remaining -= allowance;
+ mBytesDigestedSinceReset += allowance;
+
+ if (mBytesDigestedSinceReset == BUFFER_SIZE) {
+ mMd.digest(mDigestBuffer, 0, mDigestBuffer.length);
+ mOutput.put(mDigestBuffer);
+ // After digest, MessageDigest resets automatically, so no need to reset again.
+ if (mSalt != null) {
+ mMd.update(mSalt);
+ }
+ mBytesDigestedSinceReset = 0;
+ }
+ }
+ }
+
+ public void assertEmptyBuffer() throws DigestException {
+ if (mBytesDigestedSinceReset != 0) {
+ throw new IllegalStateException("Buffer is not empty: " + mBytesDigestedSinceReset);
+ }
+ }
+
+ private void fillUpLastOutputChunk() {
+ int lastBlockSize = (int) (mOutput.position() % BUFFER_SIZE);
+ if (lastBlockSize == 0) {
+ return;
+ }
+ mOutput.put(ByteBuffer.allocate(BUFFER_SIZE - lastBlockSize));
+ }
+ }
+
+ /**
+ * Digest the source by chunk in the given range. If the last chunk is not a full chunk,
+ * digest the remaining.
+ */
+ private static void consumeByChunk(DataDigester digester, DataSource source, int chunkSize)
+ throws IOException, DigestException {
+ long inputRemaining = source.size();
+ long inputOffset = 0;
+ while (inputRemaining > 0) {
+ int size = (int) Math.min(inputRemaining, chunkSize);
+ source.feedIntoDataDigester(digester, inputOffset, size);
+ inputOffset += size;
+ inputRemaining -= size;
+ }
+ }
+
+ // Rationale: 1) 1 MB should fit in memory space on all devices. 2) It is not too granular
+ // thus the syscall overhead is not too big.
+ private static final int MMAP_REGION_SIZE_BYTES = 1024 * 1024;
+
+ private static void generateFsVerityDigestAtLeafLevel(RandomAccessFile file, ByteBuffer output)
+ throws IOException, NoSuchAlgorithmException, DigestException {
+ BufferedDigester digester = new BufferedDigester(null /* salt */, output);
+
+ // 1. Digest the whole file by chunks.
+ consumeByChunk(digester,
+ new MemoryMappedFileDataSource(file.getFD(), 0, file.length()),
+ MMAP_REGION_SIZE_BYTES);
+
+ // 2. Pad 0s up to the nearest 4096-byte block before hashing.
+ int lastIncompleteChunkSize = (int) (file.length() % CHUNK_SIZE_BYTES);
+ if (lastIncompleteChunkSize != 0) {
+ digester.consume(ByteBuffer.allocate(CHUNK_SIZE_BYTES - lastIncompleteChunkSize));
+ }
+ digester.assertEmptyBuffer();
+
+ // 3. Fill up the rest of buffer with 0s.
+ digester.fillUpLastOutputChunk();
+ }
+
+ private static void generateApkVerityDigestAtLeafLevel(RandomAccessFile apk,
+ SignatureInfo signatureInfo, byte[] salt, ByteBuffer output)
+ throws IOException, NoSuchAlgorithmException, DigestException {
+ BufferedDigester digester = new BufferedDigester(salt, output);
+
+ // 1. Digest from the beginning of the file, until APK Signing Block is reached.
+ consumeByChunk(digester,
+ new MemoryMappedFileDataSource(apk.getFD(), 0, signatureInfo.apkSigningBlockOffset),
+ MMAP_REGION_SIZE_BYTES);
+
+ // 2. Skip APK Signing Block and continue digesting, until the Central Directory offset
+ // field in EoCD is reached.
+ long eocdCdOffsetFieldPosition =
+ signatureInfo.eocdOffset + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET;
+ consumeByChunk(digester,
+ new MemoryMappedFileDataSource(apk.getFD(), signatureInfo.centralDirOffset,
+ eocdCdOffsetFieldPosition - signatureInfo.centralDirOffset),
+ MMAP_REGION_SIZE_BYTES);
+
+ // 3. Consume offset of Signing Block as an alternative EoCD.
+ ByteBuffer alternativeCentralDirOffset = ByteBuffer.allocate(
+ ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE).order(ByteOrder.LITTLE_ENDIAN);
+ alternativeCentralDirOffset.putInt(Math.toIntExact(signatureInfo.apkSigningBlockOffset));
+ alternativeCentralDirOffset.flip();
+ digester.consume(alternativeCentralDirOffset);
+
+ // 4. Read from end of the Central Directory offset field in EoCD to the end of the file.
+ long offsetAfterEocdCdOffsetField =
+ eocdCdOffsetFieldPosition + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE;
+ consumeByChunk(digester,
+ new MemoryMappedFileDataSource(apk.getFD(), offsetAfterEocdCdOffsetField,
+ apk.length() - offsetAfterEocdCdOffsetField),
+ MMAP_REGION_SIZE_BYTES);
+
+ // 5. Pad 0s up to the nearest 4096-byte block before hashing.
+ int lastIncompleteChunkSize = (int) (apk.length() % CHUNK_SIZE_BYTES);
+ if (lastIncompleteChunkSize != 0) {
+ digester.consume(ByteBuffer.allocate(CHUNK_SIZE_BYTES - lastIncompleteChunkSize));
+ }
+ digester.assertEmptyBuffer();
+
+ // 6. Fill up the rest of buffer with 0s.
+ digester.fillUpLastOutputChunk();
+ }
+
+ @NonNull
+ private static byte[] generateVerityTreeInternal(@NonNull RandomAccessFile apk,
+ @Nullable SignatureInfo signatureInfo, @Nullable byte[] salt,
+ @NonNull int[] levelOffset, @NonNull ByteBuffer output)
+ throws IOException, NoSuchAlgorithmException, DigestException {
+ // 1. Digest the apk to generate the leaf level hashes.
+ assertSigningBlockAlignedAndHasFullPages(signatureInfo);
+ generateApkVerityDigestAtLeafLevel(apk, signatureInfo, salt, slice(output,
+ levelOffset[levelOffset.length - 2], levelOffset[levelOffset.length - 1]));
+
+ // 2. Digest the lower level hashes bottom up.
+ for (int level = levelOffset.length - 3; level >= 0; level--) {
+ ByteBuffer inputBuffer = slice(output, levelOffset[level + 1], levelOffset[level + 2]);
+ ByteBuffer outputBuffer = slice(output, levelOffset[level], levelOffset[level + 1]);
+
+ DataSource source = new ByteBufferDataSource(inputBuffer);
+ BufferedDigester digester = new BufferedDigester(salt, outputBuffer);
+ consumeByChunk(digester, source, CHUNK_SIZE_BYTES);
+ digester.assertEmptyBuffer();
+ digester.fillUpLastOutputChunk();
+ }
+
+ // 3. Digest the first block (i.e. first level) to generate the root hash.
+ byte[] rootHash = new byte[DIGEST_SIZE_BYTES];
+ BufferedDigester digester = new BufferedDigester(salt, ByteBuffer.wrap(rootHash));
+ digester.consume(slice(output, 0, CHUNK_SIZE_BYTES));
+ digester.assertEmptyBuffer();
+ return rootHash;
+ }
+
+ private static ByteBuffer generateApkVerityHeader(ByteBuffer buffer, long fileSize,
+ byte[] salt) {
+ if (salt.length != 8) {
+ throw new IllegalArgumentException("salt is not 8 bytes long");
+ }
+
+ // TODO(b/30972906): update the reference when there is a better one in public.
+ buffer.put("TrueBrew".getBytes()); // magic
+
+ buffer.put((byte) 1); // major version
+ buffer.put((byte) 0); // minor version
+ buffer.put((byte) 12); // log2(block-size): log2(4096)
+ buffer.put((byte) 7); // log2(leaves-per-node): log2(4096 / 32)
+
+ buffer.putShort((short) 1); // meta algorithm, SHA256 == 1
+ buffer.putShort((short) 1); // data algorithm, SHA256 == 1
+
+ buffer.putInt(0); // flags
+ buffer.putInt(0); // reserved
+
+ buffer.putLong(fileSize); // original file size
+
+ buffer.put((byte) 2); // authenticated extension count
+ buffer.put((byte) 0); // unauthenticated extension count
+ buffer.put(salt); // salt (8 bytes)
+ skip(buffer, 22); // reserved
+
+ return buffer;
+ }
+
+ private static ByteBuffer generateApkVerityExtensions(ByteBuffer buffer,
+ long signingBlockOffset, long signingBlockSize, long eocdOffset) {
+ // Snapshot of the experimental fs-verity structs (different from upstream).
+ //
+ // struct fsverity_extension_elide {
+ // __le64 offset;
+ // __le64 length;
+ // }
+ //
+ // struct fsverity_extension_patch {
+ // __le64 offset;
+ // u8 databytes[];
+ // };
+
+ final int kSizeOfFsverityExtensionHeader = 8;
+ final int kExtensionSizeAlignment = 8;
+
+ {
+ // struct fsverity_extension #1
+ final int kSizeOfFsverityElidedExtension = 16;
+
+ // First field is total size of extension, padded to 64-bit alignment
+ buffer.putInt(kSizeOfFsverityExtensionHeader + kSizeOfFsverityElidedExtension);
+ buffer.putShort((short) 1); // ID of elide extension
+ skip(buffer, 2); // reserved
+
+ // struct fsverity_extension_elide
+ buffer.putLong(signingBlockOffset);
+ buffer.putLong(signingBlockSize);
+ }
+
+ {
+ // struct fsverity_extension #2
+ final int kTotalSize = kSizeOfFsverityExtensionHeader
+ + 8 // offset size
+ + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE;
+
+ buffer.putInt(kTotalSize); // Total size of extension, padded to 64-bit alignment
+ buffer.putShort((short) 2); // ID of patch extension
+ skip(buffer, 2); // reserved
+
+ // struct fsverity_extension_patch
+ buffer.putLong(eocdOffset + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET); // offset
+ buffer.putInt(Math.toIntExact(signingBlockOffset)); // databytes
+
+ // The extension needs to be 0-padded at the end, since the length may not be multiple
+ // of 8.
+ int kPadding = kExtensionSizeAlignment - kTotalSize % kExtensionSizeAlignment;
+ if (kPadding == kExtensionSizeAlignment) {
+ kPadding = 0;
+ }
+ skip(buffer, kPadding); // padding
+ }
+
+ return buffer;
+ }
+
+ /**
+ * Returns an array of summed area table of level size in the verity tree. In other words, the
+ * returned array is offset of each level in the verity tree file format, plus an additional
+ * offset of the next non-existing level (i.e. end of the last level + 1). Thus the array size
+ * is level + 1. Thus, the returned array is guarantee to have at least 2 elements.
+ */
+ private static int[] calculateVerityLevelOffset(long fileSize) {
+ ArrayList<Long> levelSize = new ArrayList<>();
+ while (true) {
+ long levelDigestSize = divideRoundup(fileSize, CHUNK_SIZE_BYTES) * DIGEST_SIZE_BYTES;
+ long chunksSize = CHUNK_SIZE_BYTES * divideRoundup(levelDigestSize, CHUNK_SIZE_BYTES);
+ levelSize.add(chunksSize);
+ if (levelDigestSize <= CHUNK_SIZE_BYTES) {
+ break;
+ }
+ fileSize = levelDigestSize;
+ }
+
+ // Reverse and convert to summed area table.
+ int[] levelOffset = new int[levelSize.size() + 1];
+ levelOffset[0] = 0;
+ for (int i = 0; i < levelSize.size(); i++) {
+ // We don't support verity tree if it is larger then Integer.MAX_VALUE.
+ levelOffset[i + 1] = levelOffset[i]
+ + Math.toIntExact(levelSize.get(levelSize.size() - i - 1));
+ }
+ return levelOffset;
+ }
+
+ private static void assertSigningBlockAlignedAndHasFullPages(
+ @NonNull SignatureInfo signatureInfo) {
+ if (signatureInfo.apkSigningBlockOffset % CHUNK_SIZE_BYTES != 0) {
+ throw new IllegalArgumentException(
+ "APK Signing Block does not start at the page boundary: "
+ + signatureInfo.apkSigningBlockOffset);
+ }
+
+ if ((signatureInfo.centralDirOffset - signatureInfo.apkSigningBlockOffset)
+ % CHUNK_SIZE_BYTES != 0) {
+ throw new IllegalArgumentException(
+ "Size of APK Signing Block is not a multiple of 4096: "
+ + (signatureInfo.centralDirOffset - signatureInfo.apkSigningBlockOffset));
+ }
+ }
+
+ /** Returns a slice of the buffer which shares content with the provided buffer. */
+ private static ByteBuffer slice(ByteBuffer buffer, int begin, int end) {
+ ByteBuffer b = buffer.duplicate();
+ b.position(0); // to ensure position <= limit invariant.
+ b.limit(end);
+ b.position(begin);
+ return b.slice();
+ }
+
+ /** Skip the {@code ByteBuffer} position by {@code bytes}. */
+ private static void skip(ByteBuffer buffer, int bytes) {
+ buffer.position(buffer.position() + bytes);
+ }
+
+ /** Divides a number and round up to the closest integer. */
+ private static long divideRoundup(long dividend, long divisor) {
+ return (dividend + divisor - 1) / divisor;
+ }
+}
diff --git a/android/util/apk/WrappedX509Certificate.java b/android/util/apk/WrappedX509Certificate.java
new file mode 100644
index 0000000..fdaa420
--- /dev/null
+++ b/android/util/apk/WrappedX509Certificate.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.apk;
+
+import java.math.BigInteger;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.Principal;
+import java.security.PublicKey;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+import java.util.Set;
+
+class WrappedX509Certificate extends X509Certificate {
+ private final X509Certificate mWrapped;
+
+ WrappedX509Certificate(X509Certificate wrapped) {
+ this.mWrapped = wrapped;
+ }
+
+ @Override
+ public Set<String> getCriticalExtensionOIDs() {
+ return mWrapped.getCriticalExtensionOIDs();
+ }
+
+ @Override
+ public byte[] getExtensionValue(String oid) {
+ return mWrapped.getExtensionValue(oid);
+ }
+
+ @Override
+ public Set<String> getNonCriticalExtensionOIDs() {
+ return mWrapped.getNonCriticalExtensionOIDs();
+ }
+
+ @Override
+ public boolean hasUnsupportedCriticalExtension() {
+ return mWrapped.hasUnsupportedCriticalExtension();
+ }
+
+ @Override
+ public void checkValidity()
+ throws CertificateExpiredException, CertificateNotYetValidException {
+ mWrapped.checkValidity();
+ }
+
+ @Override
+ public void checkValidity(Date date)
+ throws CertificateExpiredException, CertificateNotYetValidException {
+ mWrapped.checkValidity(date);
+ }
+
+ @Override
+ public int getVersion() {
+ return mWrapped.getVersion();
+ }
+
+ @Override
+ public BigInteger getSerialNumber() {
+ return mWrapped.getSerialNumber();
+ }
+
+ @Override
+ public Principal getIssuerDN() {
+ return mWrapped.getIssuerDN();
+ }
+
+ @Override
+ public Principal getSubjectDN() {
+ return mWrapped.getSubjectDN();
+ }
+
+ @Override
+ public Date getNotBefore() {
+ return mWrapped.getNotBefore();
+ }
+
+ @Override
+ public Date getNotAfter() {
+ return mWrapped.getNotAfter();
+ }
+
+ @Override
+ public byte[] getTBSCertificate() throws CertificateEncodingException {
+ return mWrapped.getTBSCertificate();
+ }
+
+ @Override
+ public byte[] getSignature() {
+ return mWrapped.getSignature();
+ }
+
+ @Override
+ public String getSigAlgName() {
+ return mWrapped.getSigAlgName();
+ }
+
+ @Override
+ public String getSigAlgOID() {
+ return mWrapped.getSigAlgOID();
+ }
+
+ @Override
+ public byte[] getSigAlgParams() {
+ return mWrapped.getSigAlgParams();
+ }
+
+ @Override
+ public boolean[] getIssuerUniqueID() {
+ return mWrapped.getIssuerUniqueID();
+ }
+
+ @Override
+ public boolean[] getSubjectUniqueID() {
+ return mWrapped.getSubjectUniqueID();
+ }
+
+ @Override
+ public boolean[] getKeyUsage() {
+ return mWrapped.getKeyUsage();
+ }
+
+ @Override
+ public int getBasicConstraints() {
+ return mWrapped.getBasicConstraints();
+ }
+
+ @Override
+ public byte[] getEncoded() throws CertificateEncodingException {
+ return mWrapped.getEncoded();
+ }
+
+ @Override
+ public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException,
+ InvalidKeyException, NoSuchProviderException, SignatureException {
+ mWrapped.verify(key);
+ }
+
+ @Override
+ public void verify(PublicKey key, String sigProvider)
+ throws CertificateException, NoSuchAlgorithmException, InvalidKeyException,
+ NoSuchProviderException, SignatureException {
+ mWrapped.verify(key, sigProvider);
+ }
+
+ @Override
+ public String toString() {
+ return mWrapped.toString();
+ }
+
+ @Override
+ public PublicKey getPublicKey() {
+ return mWrapped.getPublicKey();
+ }
+}
diff --git a/android/util/apk/ZipUtils.java b/android/util/apk/ZipUtils.java
new file mode 100644
index 0000000..fa5477e
--- /dev/null
+++ b/android/util/apk/ZipUtils.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.apk;
+
+import android.util.Pair;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Assorted ZIP format helpers.
+ *
+ * <p>NOTE: Most helper methods operating on {@code ByteBuffer} instances expect that the byte
+ * order of these buffers is little-endian.
+ */
+abstract class ZipUtils {
+ private ZipUtils() {}
+
+ private static final int ZIP_EOCD_REC_MIN_SIZE = 22;
+ private static final int ZIP_EOCD_REC_SIG = 0x06054b50;
+ private static final int ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET = 12;
+ private static final int ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET = 16;
+ private static final int ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET = 20;
+
+ private static final int ZIP64_EOCD_LOCATOR_SIZE = 20;
+ private static final int ZIP64_EOCD_LOCATOR_SIG_REVERSE_BYTE_ORDER = 0x504b0607;
+
+ private static final int UINT16_MAX_VALUE = 0xffff;
+
+ /**
+ * Returns the ZIP End of Central Directory record of the provided ZIP file.
+ *
+ * @return contents of the ZIP End of Central Directory record and the record's offset in the
+ * file or {@code null} if the file does not contain the record.
+ *
+ * @throws IOException if an I/O error occurs while reading the file.
+ */
+ static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(RandomAccessFile zip)
+ throws IOException {
+ // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
+ // The record can be identified by its 4-byte signature/magic which is located at the very
+ // beginning of the record. A complication is that the record is variable-length because of
+ // the comment field.
+ // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
+ // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
+ // the candidate record's comment length is such that the remainder of the record takes up
+ // exactly the remaining bytes in the buffer. The search is bounded because the maximum
+ // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
+
+ long fileSize = zip.length();
+ if (fileSize < ZIP_EOCD_REC_MIN_SIZE) {
+ return null;
+ }
+
+ // Optimization: 99.99% of APKs have a zero-length comment field in the EoCD record and thus
+ // the EoCD record offset is known in advance. Try that offset first to avoid unnecessarily
+ // reading more data.
+ Pair<ByteBuffer, Long> result = findZipEndOfCentralDirectoryRecord(zip, 0);
+ if (result != null) {
+ return result;
+ }
+
+ // EoCD does not start where we expected it to. Perhaps it contains a non-empty comment
+ // field. Expand the search. The maximum size of the comment field in EoCD is 65535 because
+ // the comment length field is an unsigned 16-bit number.
+ return findZipEndOfCentralDirectoryRecord(zip, UINT16_MAX_VALUE);
+ }
+
+ /**
+ * Returns the ZIP End of Central Directory record of the provided ZIP file.
+ *
+ * @param maxCommentSize maximum accepted size (in bytes) of EoCD comment field. The permitted
+ * value is from 0 to 65535 inclusive. The smaller the value, the faster this method
+ * locates the record, provided its comment field is no longer than this value.
+ *
+ * @return contents of the ZIP End of Central Directory record and the record's offset in the
+ * file or {@code null} if the file does not contain the record.
+ *
+ * @throws IOException if an I/O error occurs while reading the file.
+ */
+ private static Pair<ByteBuffer, Long> findZipEndOfCentralDirectoryRecord(
+ RandomAccessFile zip, int maxCommentSize) throws IOException {
+ // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
+ // The record can be identified by its 4-byte signature/magic which is located at the very
+ // beginning of the record. A complication is that the record is variable-length because of
+ // the comment field.
+ // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
+ // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
+ // the candidate record's comment length is such that the remainder of the record takes up
+ // exactly the remaining bytes in the buffer. The search is bounded because the maximum
+ // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
+
+ if ((maxCommentSize < 0) || (maxCommentSize > UINT16_MAX_VALUE)) {
+ throw new IllegalArgumentException("maxCommentSize: " + maxCommentSize);
+ }
+
+ long fileSize = zip.length();
+ if (fileSize < ZIP_EOCD_REC_MIN_SIZE) {
+ // No space for EoCD record in the file.
+ return null;
+ }
+ // Lower maxCommentSize if the file is too small.
+ maxCommentSize = (int) Math.min(maxCommentSize, fileSize - ZIP_EOCD_REC_MIN_SIZE);
+
+ ByteBuffer buf = ByteBuffer.allocate(ZIP_EOCD_REC_MIN_SIZE + maxCommentSize);
+ buf.order(ByteOrder.LITTLE_ENDIAN);
+ long bufOffsetInFile = fileSize - buf.capacity();
+ zip.seek(bufOffsetInFile);
+ zip.readFully(buf.array(), buf.arrayOffset(), buf.capacity());
+ int eocdOffsetInBuf = findZipEndOfCentralDirectoryRecord(buf);
+ if (eocdOffsetInBuf == -1) {
+ // No EoCD record found in the buffer
+ return null;
+ }
+ // EoCD found
+ buf.position(eocdOffsetInBuf);
+ ByteBuffer eocd = buf.slice();
+ eocd.order(ByteOrder.LITTLE_ENDIAN);
+ return Pair.create(eocd, bufOffsetInFile + eocdOffsetInBuf);
+ }
+
+ /**
+ * Returns the position at which ZIP End of Central Directory record starts in the provided
+ * buffer or {@code -1} if the record is not present.
+ *
+ * <p>NOTE: Byte order of {@code zipContents} must be little-endian.
+ */
+ private static int findZipEndOfCentralDirectoryRecord(ByteBuffer zipContents) {
+ assertByteOrderLittleEndian(zipContents);
+
+ // ZIP End of Central Directory (EOCD) record is located at the very end of the ZIP archive.
+ // The record can be identified by its 4-byte signature/magic which is located at the very
+ // beginning of the record. A complication is that the record is variable-length because of
+ // the comment field.
+ // The algorithm for locating the ZIP EOCD record is as follows. We search backwards from
+ // end of the buffer for the EOCD record signature. Whenever we find a signature, we check
+ // the candidate record's comment length is such that the remainder of the record takes up
+ // exactly the remaining bytes in the buffer. The search is bounded because the maximum
+ // size of the comment field is 65535 bytes because the field is an unsigned 16-bit number.
+
+ int archiveSize = zipContents.capacity();
+ if (archiveSize < ZIP_EOCD_REC_MIN_SIZE) {
+ return -1;
+ }
+ int maxCommentLength = Math.min(archiveSize - ZIP_EOCD_REC_MIN_SIZE, UINT16_MAX_VALUE);
+ int eocdWithEmptyCommentStartPosition = archiveSize - ZIP_EOCD_REC_MIN_SIZE;
+ for (int expectedCommentLength = 0; expectedCommentLength <= maxCommentLength;
+ expectedCommentLength++) {
+ int eocdStartPos = eocdWithEmptyCommentStartPosition - expectedCommentLength;
+ if (zipContents.getInt(eocdStartPos) == ZIP_EOCD_REC_SIG) {
+ int actualCommentLength =
+ getUnsignedInt16(
+ zipContents, eocdStartPos + ZIP_EOCD_COMMENT_LENGTH_FIELD_OFFSET);
+ if (actualCommentLength == expectedCommentLength) {
+ return eocdStartPos;
+ }
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Returns {@code true} if the provided file contains a ZIP64 End of Central Directory
+ * Locator.
+ *
+ * @param zipEndOfCentralDirectoryPosition offset of the ZIP End of Central Directory record
+ * in the file.
+ *
+ * @throws IOException if an I/O error occurs while reading the file.
+ */
+ public static final boolean isZip64EndOfCentralDirectoryLocatorPresent(
+ RandomAccessFile zip, long zipEndOfCentralDirectoryPosition) throws IOException {
+
+ // ZIP64 End of Central Directory Locator immediately precedes the ZIP End of Central
+ // Directory Record.
+ long locatorPosition = zipEndOfCentralDirectoryPosition - ZIP64_EOCD_LOCATOR_SIZE;
+ if (locatorPosition < 0) {
+ return false;
+ }
+
+ zip.seek(locatorPosition);
+ // RandomAccessFile.readInt assumes big-endian byte order, but ZIP format uses
+ // little-endian.
+ return zip.readInt() == ZIP64_EOCD_LOCATOR_SIG_REVERSE_BYTE_ORDER;
+ }
+
+ /**
+ * Returns the offset of the start of the ZIP Central Directory in the archive.
+ *
+ * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
+ */
+ public static long getZipEocdCentralDirectoryOffset(ByteBuffer zipEndOfCentralDirectory) {
+ assertByteOrderLittleEndian(zipEndOfCentralDirectory);
+ return getUnsignedInt32(
+ zipEndOfCentralDirectory,
+ zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET);
+ }
+
+ /**
+ * Sets the offset of the start of the ZIP Central Directory in the archive.
+ *
+ * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
+ */
+ public static void setZipEocdCentralDirectoryOffset(
+ ByteBuffer zipEndOfCentralDirectory, long offset) {
+ assertByteOrderLittleEndian(zipEndOfCentralDirectory);
+ setUnsignedInt32(
+ zipEndOfCentralDirectory,
+ zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET,
+ offset);
+ }
+
+ /**
+ * Returns the size (in bytes) of the ZIP Central Directory.
+ *
+ * <p>NOTE: Byte order of {@code zipEndOfCentralDirectory} must be little-endian.
+ */
+ public static long getZipEocdCentralDirectorySizeBytes(ByteBuffer zipEndOfCentralDirectory) {
+ assertByteOrderLittleEndian(zipEndOfCentralDirectory);
+ return getUnsignedInt32(
+ zipEndOfCentralDirectory,
+ zipEndOfCentralDirectory.position() + ZIP_EOCD_CENTRAL_DIR_SIZE_FIELD_OFFSET);
+ }
+
+ private static void assertByteOrderLittleEndian(ByteBuffer buffer) {
+ if (buffer.order() != ByteOrder.LITTLE_ENDIAN) {
+ throw new IllegalArgumentException("ByteBuffer byte order must be little endian");
+ }
+ }
+
+ private static int getUnsignedInt16(ByteBuffer buffer, int offset) {
+ return buffer.getShort(offset) & 0xffff;
+ }
+
+ private static long getUnsignedInt32(ByteBuffer buffer, int offset) {
+ return buffer.getInt(offset) & 0xffffffffL;
+ }
+
+ private static void setUnsignedInt32(ByteBuffer buffer, int offset, long value) {
+ if ((value < 0) || (value > 0xffffffffL)) {
+ throw new IllegalArgumentException("uint32 value of out range: " + value);
+ }
+ buffer.putInt(buffer.position() + offset, (int) value);
+ }
+}
diff --git a/android/util/imagepool/Bucket.java b/android/util/imagepool/Bucket.java
new file mode 100644
index 0000000..c562243
--- /dev/null
+++ b/android/util/imagepool/Bucket.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.imagepool;
+
+import com.android.tools.layoutlib.annotations.Nullable;
+import com.android.tools.layoutlib.annotations.VisibleForTesting;
+
+import android.util.imagepool.ImagePool.Image.Orientation;
+
+import java.awt.image.BufferedImage;
+import java.lang.ref.SoftReference;
+import java.util.LinkedList;
+import java.util.Queue;
+
+/**
+ * Data model for image pool. Bucket contains the list of same sized buffered image in soft ref.
+ */
+/* private package */ class Bucket {
+
+ @VisibleForTesting final Queue<SoftReference<BufferedImage>> mBufferedImageRef = new LinkedList<>();
+
+ public boolean isEmpty() {
+ return mBufferedImageRef.isEmpty();
+ }
+
+ @Nullable
+ public BufferedImage remove() {
+ if (mBufferedImageRef.isEmpty()) {
+ return null;
+ }
+
+ SoftReference<BufferedImage> reference = mBufferedImageRef.remove();
+ return reference == null ? null : reference.get();
+ }
+
+ public void offer(BufferedImage img) {
+ mBufferedImageRef.offer(new SoftReference<>(img));
+ }
+
+ public void clear() {
+ mBufferedImageRef.clear();
+ }
+
+ static class BucketCreationMetaData {
+ public final int mWidth;
+ public final int mHeight;
+ public final int mType;
+ public final int mNumberOfCopies;
+ public final Orientation mOrientation;
+ public final long mMaxCacheSize;
+
+ BucketCreationMetaData(int width, int height, int type, int numberOfCopies,
+ Orientation orientation, long maxCacheSize) {
+ mWidth = width;
+ mHeight = height;
+ mType = type;
+ mNumberOfCopies = numberOfCopies;
+ mOrientation = orientation;
+ mMaxCacheSize = maxCacheSize;
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/util/imagepool/ImageImpl.java b/android/util/imagepool/ImageImpl.java
new file mode 100644
index 0000000..42a6e73
--- /dev/null
+++ b/android/util/imagepool/ImageImpl.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.imagepool;
+
+import com.android.tools.layoutlib.annotations.NotNull;
+import com.android.tools.layoutlib.annotations.Nullable;
+import com.android.tools.layoutlib.annotations.VisibleForTesting;
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.awt.image.ImageObserver;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/**
+ * Representation of buffered image. When used with ImagePool, it provides 2 features
+ * (last one not yet impl'd):
+ *
+ * <ul>
+ * <li>Automatic recycle of BufferedImage through use of reference counter</li>
+ * <li>Able to re-use the image for similar size of buffered image</li>
+ * <li>TODO: Able to re-use the iamge for different orientation (not yet impl'd)</li>
+ * </ul>
+ */
+/* private package */ class ImageImpl implements ImagePool.Image {
+
+ private final ReadWriteLock mLock = new ReentrantReadWriteLock();
+
+ private final int mWidth;
+ private final int mHeight;
+ private final Orientation mOrientation;
+
+ @VisibleForTesting final BufferedImage mImg;
+
+ ImageImpl(
+ int width,
+ int height,
+ @NotNull BufferedImage img,
+ Orientation orientation) {
+ mImg = img;
+ mWidth = width;
+ mHeight = height;
+ mOrientation = orientation;
+ }
+
+ @Override
+ public int getWidth() {
+ return mWidth;
+ }
+
+ @Override
+ public int getHeight() {
+ return mHeight;
+ }
+
+ @Override
+ public void setRGB(int x, int y, int width, int height, int[] colors, int offset, int stride) {
+ mLock.readLock().lock();
+ try {
+ // TODO: Apply orientation.
+ mImg.setRGB(x, y, width, height, colors, offset, stride);
+ } finally {
+ mLock.readLock().unlock();
+ }
+ }
+
+ @Override
+ public void drawImage(Graphics2D graphics, int x, int y, @Nullable ImageObserver o) {
+ mLock.readLock().lock();
+ try {
+ // TODO: Apply orientation.
+ graphics.drawImage(mImg, x, y, mWidth, mHeight, o);
+ } finally {
+ mLock.readLock().unlock();
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/util/imagepool/ImagePool.java b/android/util/imagepool/ImagePool.java
new file mode 100644
index 0000000..baec65d
--- /dev/null
+++ b/android/util/imagepool/ImagePool.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.imagepool;
+
+import com.android.tools.layoutlib.annotations.NotNull;
+
+import java.awt.Graphics2D;
+import java.awt.Image;
+import java.awt.image.BufferedImage;
+import java.awt.image.ImageObserver;
+import java.util.function.Consumer;
+
+/**
+ * Simplified version of image pool that exists in Studio.
+ *
+ * Lacks:
+ * - PhantomReference and FinalizableReference to recognize the death of references automatically.
+ * (Meaning devs need to be more deligent in dispose.)
+ * Has:
+ * + Debugger that allows us to better trace where image is being leaked in stack.
+ */
+public interface ImagePool {
+
+ /**
+ * Returns a new image of width w and height h.
+ */
+ @NotNull
+ Image acquire(final int w, final int h, final int type);
+
+ /**
+ * Disposes the image pool, releasing all the references to the buffered images.
+ */
+ void dispose();
+
+ /**
+ * Interface that represents a buffered image. Using this wrapper allows us ot better track
+ * memory usages around BufferedImage. When all of it's references are removed, it will
+ * automatically be pooled back into the image pool for re-use.
+ */
+ interface Image {
+
+ /**
+ * Same as {@link BufferedImage#setRGB(int, int, int, int, int[], int, int)}
+ */
+ void setRGB(int x, int y, int width, int height, int[] colors, int offset, int stride);
+
+ /**
+ * Same as {@link Graphics2D#drawImage(java.awt.Image, int, int, ImageObserver)}
+ */
+ void drawImage(Graphics2D graphics, int x, int y, ImageObserver o);
+
+ /**
+ * Image orientation. It's not used at the moment. To be used later.
+ */
+ enum Orientation {
+ NONE,
+ CW_90
+ }
+
+ int getWidth();
+ int getHeight();
+ }
+
+ /**
+ * Policy for how to set up the memory pool.
+ */
+ class ImagePoolPolicy {
+
+ public final int[] mBucketSizes;
+ public final int[] mNumberOfCopies;
+ public final long mBucketMaxCacheSize;
+
+ /**
+ * @param bucketPixelSizes - list of pixel sizes to bucket (categorize) images. The list
+ * must be sorted (low to high).
+ * @param numberOfCopies - Allows users to create multiple copies of the bucket. It is
+ * recommended to create more copies for smaller images to avoid fragmentation in memory.
+ * It must match the size of bucketPixelSizes.
+ * @param bucketMaxCacheByteSize - Maximum cache byte sizes image pool is allowed to hold onto
+ * in memory.
+ */
+ public ImagePoolPolicy(
+ int[] bucketPixelSizes, int[] numberOfCopies, long bucketMaxCacheByteSize) {
+ assert bucketPixelSizes.length == numberOfCopies.length;
+ mBucketSizes = bucketPixelSizes;
+ mNumberOfCopies = numberOfCopies;
+ mBucketMaxCacheSize = bucketMaxCacheByteSize;
+ }
+ }
+}
diff --git a/android/util/imagepool/ImagePoolHelper.java b/android/util/imagepool/ImagePoolHelper.java
new file mode 100644
index 0000000..292fc59
--- /dev/null
+++ b/android/util/imagepool/ImagePoolHelper.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.imagepool;
+
+import com.android.tools.layoutlib.annotations.Nullable;
+
+import android.util.imagepool.Bucket.BucketCreationMetaData;
+import android.util.imagepool.ImagePool.Image.Orientation;
+import android.util.imagepool.ImagePool.ImagePoolPolicy;
+
+import java.awt.image.BufferedImage;
+import java.util.Map;
+
+/* private package */ class ImagePoolHelper {
+
+ @Nullable
+ public static BucketCreationMetaData getBucketCreationMetaData(int w, int h, int type, ImagePoolPolicy poolPolicy, ImagePoolStats stats) {
+ // Find the bucket sizes for both dimensions
+ int widthBucket = -1;
+ int heightBucket = -1;
+ int index = 0;
+
+ for (int bucketMinSize : poolPolicy.mBucketSizes) {
+ if (widthBucket == -1 && w <= bucketMinSize) {
+ widthBucket = bucketMinSize;
+
+ if (heightBucket != -1) {
+ break;
+ }
+ }
+ if (heightBucket == -1 && h <= bucketMinSize) {
+ heightBucket = bucketMinSize;
+
+ if (widthBucket != -1) {
+ break;
+ }
+ }
+ ++index;
+ }
+
+ stats.recordBucketRequest(w, h);
+
+ if (index >= poolPolicy.mNumberOfCopies.length) {
+ return null;
+ }
+
+ // TODO: Apply orientation
+// if (widthBucket < heightBucket) {
+// return new BucketCreationMetaData(heightBucket, widthBucket, type, poolPolicy.mNumberOfCopies[index],
+// Orientation.CW_90, poolPolicy.mBucketMaxCacheSize);
+// }
+ return new BucketCreationMetaData(widthBucket, heightBucket, type, poolPolicy.mNumberOfCopies[index],
+ Orientation.NONE, poolPolicy.mBucketMaxCacheSize);
+ }
+
+ @Nullable
+ public static BufferedImage getBufferedImage(
+ Bucket bucket, BucketCreationMetaData metaData, ImagePoolStats stats) {
+
+ // strongref is just for gc.
+ BufferedImage strongRef = populateBucket(bucket, metaData, stats);
+
+ // pool is too small to create the requested buffer.
+ if (bucket.isEmpty()) {
+ assert strongRef == null;
+ return null;
+ }
+
+ // Go through the bucket of soft references to find the first buffer that's not null.
+ // Even if gc is triggered here, strongref should survive.
+ BufferedImage img = bucket.remove();
+ while (img == null && !bucket.isEmpty()) {
+ img = bucket.remove();
+ }
+
+ if (img == null && bucket.isEmpty()) {
+ // Whole buffer was null. Recurse.
+ return getBufferedImage(bucket, metaData, stats);
+ }
+ return img;
+ }
+
+ /**
+ * Populate the bucket in greedy manner to avoid fragmentation.
+ * Behaviour is controlled by {@link ImagePoolPolicy}.
+ * Returns one strong referenced buffer to avoid getting results gc'd. Null if pool is not large
+ * enough.
+ */
+ @Nullable
+ private static BufferedImage populateBucket(
+ Bucket bucket, BucketCreationMetaData metaData, ImagePoolStats stats) {
+ if (!bucket.isEmpty()) {
+ // If not empty no need to populate.
+ return null;
+ }
+
+ BufferedImage strongRef = null;
+ for (int i = 0; i < metaData.mNumberOfCopies; i++) {
+ if (!stats.fitsMaxCacheSize(
+ metaData.mWidth, metaData.mHeight, metaData.mMaxCacheSize)) {
+ break;
+ }
+ strongRef =new BufferedImage(metaData.mWidth, metaData.mHeight,
+ metaData.mType);
+ bucket.offer(strongRef);
+ stats.recordBucketCreation(metaData.mWidth, metaData.mHeight);
+ }
+ return strongRef;
+ }
+
+ private static String toKey(int w, int h, int type) {
+ return new StringBuilder()
+ .append(w)
+ .append('x')
+ .append(h)
+ .append(':')
+ .append(type)
+ .toString();
+ }
+
+ public static Bucket getBucket(Map<String, Bucket> map, BucketCreationMetaData metaData, ImagePoolPolicy mPolicy) {
+ String key = toKey(metaData.mWidth, metaData.mHeight, metaData.mType);
+ Bucket bucket = map.get(key);
+ if (bucket == null) {
+ bucket = new Bucket();
+ map.put(key, bucket);
+ }
+ return bucket;
+ }
+}
\ No newline at end of file
diff --git a/android/util/imagepool/ImagePoolImpl.java b/android/util/imagepool/ImagePoolImpl.java
new file mode 100644
index 0000000..3da706e
--- /dev/null
+++ b/android/util/imagepool/ImagePoolImpl.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.imagepool;
+
+import com.android.tools.layoutlib.annotations.Nullable;
+import com.android.tools.layoutlib.annotations.VisibleForTesting;
+
+import android.util.imagepool.Bucket.BucketCreationMetaData;
+import android.util.imagepool.ImagePool.Image.Orientation;
+
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+import java.lang.ref.Reference;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Consumer;
+
+import com.google.common.base.FinalizablePhantomReference;
+import com.google.common.base.FinalizableReferenceQueue;
+
+class ImagePoolImpl implements ImagePool {
+
+ private final ReentrantReadWriteLock mReentrantLock = new ReentrantReadWriteLock();
+ private final ImagePoolPolicy mPolicy;
+ @VisibleForTesting final Map<String, Bucket> mPool = new HashMap<>();
+ @VisibleForTesting final ImagePoolStats mImagePoolStats = new ImagePoolStatsProdImpl();
+ private final FinalizableReferenceQueue mFinalizableReferenceQueue = new FinalizableReferenceQueue();
+ private final Set<Reference<?>> mReferences = new HashSet<>();
+
+ public ImagePoolImpl(ImagePoolPolicy policy) {
+ mPolicy = policy;
+ mImagePoolStats.start();
+ }
+
+ @Override
+ public Image acquire(int w, int h, int type) {
+ return acquire(w, h, type, null);
+ }
+
+ /* package private */ Image acquire(int w, int h, int type,
+ @Nullable Consumer<BufferedImage> freedCallback) {
+ mReentrantLock.writeLock().lock();
+ try {
+ BucketCreationMetaData metaData =
+ ImagePoolHelper.getBucketCreationMetaData(w, h, type, mPolicy, mImagePoolStats);
+ if (metaData == null) {
+ return defaultImageImpl(w, h, type, freedCallback);
+ }
+
+ final Bucket existingBucket = ImagePoolHelper.getBucket(mPool, metaData, mPolicy);
+ final BufferedImage img =
+ ImagePoolHelper.getBufferedImage(existingBucket, metaData, mImagePoolStats);
+ if (img == null) {
+ return defaultImageImpl(w, h, type, freedCallback);
+ }
+
+ // Clear the image. - is this necessary?
+ if (img.getRaster().getDataBuffer().getDataType() == java.awt.image.DataBuffer.TYPE_INT) {
+ Arrays.fill(((DataBufferInt)img.getRaster().getDataBuffer()).getData(), 0);
+ }
+
+ return prepareImage(
+ new ImageImpl(w, h, img, metaData.mOrientation),
+ true,
+ img,
+ existingBucket,
+ freedCallback);
+ } finally {
+ mReentrantLock.writeLock().unlock();
+ }
+ }
+
+ /**
+ * Add statistics as well as dispose behaviour before returning image.
+ */
+ private Image prepareImage(
+ Image image,
+ boolean offerBackToBucket,
+ @Nullable BufferedImage img,
+ @Nullable Bucket existingBucket,
+ @Nullable Consumer<BufferedImage> freedCallback) {
+ final Integer imageHash = image.hashCode();
+ mImagePoolStats.acquiredImage(imageHash);
+ FinalizablePhantomReference<Image> reference =
+ new FinalizablePhantomReference<ImagePool.Image>(image, mFinalizableReferenceQueue) {
+ @Override
+ public void finalizeReferent() {
+ // This method might be called twice if the user has manually called the free() method. The second call will have no effect.
+ if (mReferences.remove(this)) {
+ mImagePoolStats.disposeImage(imageHash);
+ if (offerBackToBucket) {
+ if (!mImagePoolStats.fitsMaxCacheSize(img.getWidth(), img.getHeight(),
+ mPolicy.mBucketMaxCacheSize)) {
+ mImagePoolStats.tooBigForCache();
+ // Adding this back would go over the max cache size we set for ourselves. Release it.
+ return;
+ }
+
+ // else stat does not change.
+ existingBucket.offer(img);
+ }
+ if (freedCallback != null) {
+ freedCallback.accept(img);
+ }
+ }
+ }
+ };
+ mReferences.add(reference);
+ return image;
+ }
+
+ /**
+ * Default Image Impl to be used when the pool is not big enough.
+ */
+ private Image defaultImageImpl(int w, int h, int type,
+ @Nullable Consumer<BufferedImage> freedCallback) {
+ BufferedImage bufferedImage = new BufferedImage(w, h, type);
+ mImagePoolStats.tooBigForCache();
+ mImagePoolStats.recordAllocOutsidePool(w, h);
+ return prepareImage(new ImageImpl(w, h, bufferedImage, Orientation.NONE),
+ false, null, null, freedCallback);
+ }
+
+ @Override
+ public void dispose() {
+ mReentrantLock.writeLock().lock();
+ try {
+ for (Bucket bucket : mPool.values()) {
+ bucket.clear();
+ }
+ mImagePoolStats.clear();
+ } finally {
+ mReentrantLock.writeLock().unlock();
+ }
+ }
+
+ /* package private */ void printStat() {
+ System.out.println(mImagePoolStats.getStatistic());
+ }
+}
\ No newline at end of file
diff --git a/android/util/imagepool/ImagePoolProvider.java b/android/util/imagepool/ImagePoolProvider.java
new file mode 100644
index 0000000..dbdd849
--- /dev/null
+++ b/android/util/imagepool/ImagePoolProvider.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.imagepool;
+
+import com.android.tools.layoutlib.annotations.NotNull;
+
+import android.util.imagepool.ImagePool.ImagePoolPolicy;
+
+public class ImagePoolProvider {
+
+ private static ImagePool sInstance;
+
+ @NotNull
+ public static synchronized ImagePool get() {
+ if (sInstance == null) {
+ // Idea is to create more
+ ImagePoolPolicy policy = new ImagePoolPolicy(
+ new int[]{100, 200, 400, 600, 800, 1000, 1600, 3200},
+ new int[]{ 3, 3, 2, 2, 2, 1, 1, 1},
+ 10_000_000L); // 10 MB
+
+ sInstance = new ImagePoolImpl(policy);
+ }
+ return sInstance;
+ }
+}
\ No newline at end of file
diff --git a/android/util/imagepool/ImagePoolStats.java b/android/util/imagepool/ImagePoolStats.java
new file mode 100644
index 0000000..17dab14
--- /dev/null
+++ b/android/util/imagepool/ImagePoolStats.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.imagepool;
+
+/**
+ * Keeps track of statistics. Some are purely for debugging purposes in which case it's wrapped
+ * around DEBUG check.
+ */
+interface ImagePoolStats {
+
+ void recordBucketCreation(int widthBucket, int heightBucket);
+
+ boolean fitsMaxCacheSize(int width, int height, long maxCacheSize);
+
+ void clear();
+
+ void recordBucketRequest(int w, int h);
+
+ void recordAllocOutsidePool(int width, int height);
+
+ void tooBigForCache();
+
+ void acquiredImage(Integer imageHash);
+
+ void disposeImage(Integer imageHash);
+
+ void start();
+
+ String getStatistic();
+}
diff --git a/android/util/imagepool/ImagePoolStatsDebugImpl.java b/android/util/imagepool/ImagePoolStatsDebugImpl.java
new file mode 100644
index 0000000..de0f757
--- /dev/null
+++ b/android/util/imagepool/ImagePoolStatsDebugImpl.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.imagepool;
+
+import java.lang.management.GarbageCollectorMXBean;
+import java.lang.management.ManagementFactory;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.google.common.collect.HashMultiset;
+import com.google.common.collect.Multiset;
+
+/**
+ * Useful impl for debugging reproable error.
+ */
+public class ImagePoolStatsDebugImpl extends ImagePoolStatsProdImpl {
+
+ private static String PACKAGE_NAME = ImagePoolStats.class.getPackage().getName();
+
+ // Used for deugging purposes only.
+ private final Map<Integer, String> mCallStack = new HashMap<>();
+ private long mRequestedTotalBytes = 0;
+ private long mAllocatedOutsidePoolBytes = 0;
+
+ // Used for gc-related stats.
+ private long mPreviousGcCollection = 0;
+ private long mPreviousGcTime = 0;
+
+ /** Used for policy */
+ @Override
+ public void recordBucketCreation(int widthBucket, int heightBucket) {
+ super.recordBucketCreation(widthBucket, heightBucket);
+ }
+
+ @Override
+ public boolean fitsMaxCacheSize(int width, int height, long maxCacheSize) {
+ return super.fitsMaxCacheSize(width, height, maxCacheSize);
+ }
+
+ @Override
+ public void clear() {
+ super.clear();
+
+ mRequestedTotalBytes = 0;
+ mAllocatedOutsidePoolBytes = 0;
+ mTooBigForPoolCount = 0;
+ mCallStack.clear();
+ }
+
+ @Override
+ public void tooBigForCache() {
+ super.tooBigForCache();
+ }
+
+ /** Used for Debugging only */
+ @Override
+ public void recordBucketRequest(int w, int h) {
+ mRequestedTotalBytes += (w * h * ESTIMATED_PIXEL_BYTES);
+ }
+
+ @Override
+ public void recordAllocOutsidePool(int width, int height) {
+ mAllocatedOutsidePoolBytes += (width * height * ESTIMATED_PIXEL_BYTES);
+ }
+
+ @Override
+ public void acquiredImage(Integer imageHash) {
+ for (int i = 1; i < Thread.currentThread().getStackTrace().length; i++) {
+ StackTraceElement element = Thread.currentThread().getStackTrace()[i];
+ String str = element.toString();
+
+ if (!str.contains(PACKAGE_NAME)) {
+ mCallStack.put(imageHash, str);
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void disposeImage(Integer imageHash) {
+ mCallStack.remove(imageHash);
+ }
+
+ @Override
+ public void start() {
+ long totalGarbageCollections = 0;
+ long garbageCollectionTime = 0;
+ for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
+ long count = gc.getCollectionCount();
+ if (count >= 0) {
+ totalGarbageCollections += count;
+ }
+ long time = gc.getCollectionTime();
+ if (time >= 0) {
+ garbageCollectionTime += time;
+ }
+ }
+ mPreviousGcCollection = totalGarbageCollections;
+ mPreviousGcTime = garbageCollectionTime;
+ }
+
+ private String calculateGcStatAndReturn() {
+ long totalGarbageCollections = 0;
+ long garbageCollectionTime = 0;
+ for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
+ long count = gc.getCollectionCount();
+ if (count > 0) {
+ totalGarbageCollections += count;
+ }
+ long time = gc.getCollectionTime();
+ if(time > 0) {
+ garbageCollectionTime += time;
+ }
+ }
+ totalGarbageCollections -= mPreviousGcCollection;
+ garbageCollectionTime -= mPreviousGcTime;
+
+ StringBuilder builder = new StringBuilder();
+ builder.append("Total Garbage Collections: ");
+ builder.append(totalGarbageCollections);
+ builder.append("\n");
+
+ builder.append("Total Garbage Collection Time (ms): ");
+ builder.append(garbageCollectionTime);
+ builder.append("\n");
+
+ return builder.toString();
+ }
+
+ @Override
+ public String getStatistic() {
+ StringBuilder builder = new StringBuilder();
+
+ builder.append(calculateGcStatAndReturn());
+ builder.append("Memory\n");
+ builder.append(" requested total : ");
+ builder.append(mRequestedTotalBytes / 1_000_000);
+ builder.append(" MB\n");
+ builder.append(" allocated (in pool) : ");
+ builder.append(mAllocateTotalBytes / 1_000_000);
+ builder.append(" MB\n");
+ builder.append(" allocated (out of pool) : ");
+ builder.append(mAllocatedOutsidePoolBytes / 1_000_000);
+ builder.append(" MB\n");
+
+ double percent = (1.0 - (double) mRequestedTotalBytes / (mAllocateTotalBytes +
+ mAllocatedOutsidePoolBytes));
+ if (percent < 0.0) {
+ builder.append(" saved : ");
+ builder.append(-1.0 * percent);
+ builder.append("%\n");
+ } else {
+ builder.append(" wasting : ");
+ builder.append(percent);
+ builder.append("%\n");
+ }
+
+ builder.append("Undispose images\n");
+ Multiset<String> countSet = HashMultiset.create();
+ for (String callsite : mCallStack.values()) {
+ countSet.add(callsite);
+ }
+
+ for (Multiset.Entry<String> entry : countSet.entrySet()) {
+ builder.append(" - ");
+ builder.append(entry.getElement());
+ builder.append(" - missed dispose : ");
+ builder.append(entry.getCount());
+ builder.append(" times\n");
+ }
+
+ builder.append("Number of times requested image didn't fit the pool : ");
+ builder.append(mTooBigForPoolCount);
+ builder.append("\n");
+
+ return builder.toString();
+ }
+}
diff --git a/android/util/imagepool/ImagePoolStatsProdImpl.java b/android/util/imagepool/ImagePoolStatsProdImpl.java
new file mode 100644
index 0000000..e6c6abe
--- /dev/null
+++ b/android/util/imagepool/ImagePoolStatsProdImpl.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.imagepool;
+
+import com.android.tools.layoutlib.annotations.NotNull;
+import com.android.tools.layoutlib.annotations.VisibleForTesting;
+
+class ImagePoolStatsProdImpl implements ImagePoolStats {
+
+ static int ESTIMATED_PIXEL_BYTES = 4;
+
+ // Used for determining how many buckets can be created.
+ @VisibleForTesting long mAllocateTotalBytes = 0;
+ @VisibleForTesting int mTooBigForPoolCount = 0;
+
+ /** Used for policy */
+ @Override
+ public void recordBucketCreation(int widthBucket, int heightBucket) {
+ mAllocateTotalBytes += (widthBucket * heightBucket * ESTIMATED_PIXEL_BYTES);
+ }
+
+ @Override
+ public boolean fitsMaxCacheSize(int width, int height, long maxCacheSize) {
+ long newTotal = mAllocateTotalBytes + (width * height * ESTIMATED_PIXEL_BYTES);
+ return newTotal <= maxCacheSize;
+ }
+
+ @Override
+ public void tooBigForCache() {
+ mTooBigForPoolCount++;
+ }
+
+ @Override
+ public void clear() {
+ mAllocateTotalBytes = 0;
+ }
+
+ @Override
+ public void recordBucketRequest(int w, int h) { }
+
+ @Override
+ public void recordAllocOutsidePool(int width, int height) { }
+
+ @Override
+ public void acquiredImage(@NotNull Integer imageHash) { }
+
+ @Override
+ public void disposeImage(@NotNull Integer imageHash) { }
+
+ @Override
+ public void start() { }
+
+ @Override
+ public String getStatistic() { return ""; }
+}
diff --git a/android/util/jar/StrictJarFile.java b/android/util/jar/StrictJarFile.java
new file mode 100644
index 0000000..11aee2f
--- /dev/null
+++ b/android/util/jar/StrictJarFile.java
@@ -0,0 +1,501 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package android.util.jar;
+
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+
+import dalvik.system.CloseGuard;
+import java.io.FileDescriptor;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.cert.Certificate;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.jar.JarFile;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+import java.util.zip.ZipEntry;
+import libcore.io.IoBridge;
+import libcore.io.IoUtils;
+import libcore.io.Streams;
+
+/**
+ * A subset of the JarFile API implemented as a thin wrapper over
+ * system/core/libziparchive.
+ *
+ * @hide for internal use only. Not API compatible (or as forgiving) as
+ * {@link java.util.jar.JarFile}
+ */
+public final class StrictJarFile {
+
+ private final long nativeHandle;
+
+ // NOTE: It's possible to share a file descriptor with the native
+ // code, at the cost of some additional complexity.
+ private final FileDescriptor fd;
+
+ private final StrictJarManifest manifest;
+ private final StrictJarVerifier verifier;
+
+ private final boolean isSigned;
+
+ private final CloseGuard guard = CloseGuard.get();
+ private boolean closed;
+
+ public StrictJarFile(String fileName)
+ throws IOException, SecurityException {
+ this(fileName, true, true);
+ }
+
+ public StrictJarFile(FileDescriptor fd)
+ throws IOException, SecurityException {
+ this(fd, true, true);
+ }
+
+ public StrictJarFile(FileDescriptor fd,
+ boolean verify,
+ boolean signatureSchemeRollbackProtectionsEnforced)
+ throws IOException, SecurityException {
+ this("[fd:" + fd.getInt$() + "]", fd, verify,
+ signatureSchemeRollbackProtectionsEnforced);
+ }
+
+ public StrictJarFile(String fileName,
+ boolean verify,
+ boolean signatureSchemeRollbackProtectionsEnforced)
+ throws IOException, SecurityException {
+ this(fileName, IoBridge.open(fileName, OsConstants.O_RDONLY),
+ verify, signatureSchemeRollbackProtectionsEnforced);
+ }
+
+ /**
+ * @param name of the archive (not necessarily a path).
+ * @param fd seekable file descriptor for the JAR file.
+ * @param verify whether to verify the file's JAR signatures and collect the corresponding
+ * signer certificates.
+ * @param signatureSchemeRollbackProtectionsEnforced {@code true} to enforce protections against
+ * stripping newer signature schemes (e.g., APK Signature Scheme v2) from the file, or
+ * {@code false} to ignore any such protections. This parameter is ignored when
+ * {@code verify} is {@code false}.
+ */
+ private StrictJarFile(String name,
+ FileDescriptor fd,
+ boolean verify,
+ boolean signatureSchemeRollbackProtectionsEnforced)
+ throws IOException, SecurityException {
+ this.nativeHandle = nativeOpenJarFile(name, fd.getInt$());
+ this.fd = fd;
+
+ try {
+ // Read the MANIFEST and signature files up front and try to
+ // parse them. We never want to accept a JAR File with broken signatures
+ // or manifests, so it's best to throw as early as possible.
+ if (verify) {
+ HashMap<String, byte[]> metaEntries = getMetaEntries();
+ this.manifest = new StrictJarManifest(metaEntries.get(JarFile.MANIFEST_NAME), true);
+ this.verifier =
+ new StrictJarVerifier(
+ name,
+ manifest,
+ metaEntries,
+ signatureSchemeRollbackProtectionsEnforced);
+ Set<String> files = manifest.getEntries().keySet();
+ for (String file : files) {
+ if (findEntry(file) == null) {
+ throw new SecurityException("File " + file + " in manifest does not exist");
+ }
+ }
+
+ isSigned = verifier.readCertificates() && verifier.isSignedJar();
+ } else {
+ isSigned = false;
+ this.manifest = null;
+ this.verifier = null;
+ }
+ } catch (IOException | SecurityException e) {
+ nativeClose(this.nativeHandle);
+ IoUtils.closeQuietly(fd);
+ closed = true;
+ throw e;
+ }
+
+ guard.open("close");
+ }
+
+ public StrictJarManifest getManifest() {
+ return manifest;
+ }
+
+ public Iterator<ZipEntry> iterator() throws IOException {
+ return new EntryIterator(nativeHandle, "");
+ }
+
+ public ZipEntry findEntry(String name) {
+ return nativeFindEntry(nativeHandle, name);
+ }
+
+ /**
+ * Return all certificate chains for a given {@link ZipEntry} belonging to this jar.
+ * This method MUST be called only after fully exhausting the InputStream belonging
+ * to this entry.
+ *
+ * Returns {@code null} if this jar file isn't signed or if this method is
+ * called before the stream is processed.
+ */
+ public Certificate[][] getCertificateChains(ZipEntry ze) {
+ if (isSigned) {
+ return verifier.getCertificateChains(ze.getName());
+ }
+
+ return null;
+ }
+
+ /**
+ * Return all certificates for a given {@link ZipEntry} belonging to this jar.
+ * This method MUST be called only after fully exhausting the InputStream belonging
+ * to this entry.
+ *
+ * Returns {@code null} if this jar file isn't signed or if this method is
+ * called before the stream is processed.
+ *
+ * @deprecated Switch callers to use getCertificateChains instead
+ */
+ @Deprecated
+ public Certificate[] getCertificates(ZipEntry ze) {
+ if (isSigned) {
+ Certificate[][] certChains = verifier.getCertificateChains(ze.getName());
+
+ // Measure number of certs.
+ int count = 0;
+ for (Certificate[] chain : certChains) {
+ count += chain.length;
+ }
+
+ // Create new array and copy all the certs into it.
+ Certificate[] certs = new Certificate[count];
+ int i = 0;
+ for (Certificate[] chain : certChains) {
+ System.arraycopy(chain, 0, certs, i, chain.length);
+ i += chain.length;
+ }
+
+ return certs;
+ }
+
+ return null;
+ }
+
+ public InputStream getInputStream(ZipEntry ze) {
+ final InputStream is = getZipInputStream(ze);
+
+ if (isSigned) {
+ StrictJarVerifier.VerifierEntry entry = verifier.initEntry(ze.getName());
+ if (entry == null) {
+ return is;
+ }
+
+ return new JarFileInputStream(is, ze.getSize(), entry);
+ }
+
+ return is;
+ }
+
+ public void close() throws IOException {
+ if (!closed) {
+ if (guard != null) {
+ guard.close();
+ }
+
+ nativeClose(nativeHandle);
+ IoUtils.closeQuietly(fd);
+ closed = true;
+ }
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (guard != null) {
+ guard.warnIfOpen();
+ }
+ close();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ private InputStream getZipInputStream(ZipEntry ze) {
+ if (ze.getMethod() == ZipEntry.STORED) {
+ return new FDStream(fd, ze.getDataOffset(),
+ ze.getDataOffset() + ze.getSize());
+ } else {
+ final FDStream wrapped = new FDStream(
+ fd, ze.getDataOffset(), ze.getDataOffset() + ze.getCompressedSize());
+
+ int bufSize = Math.max(1024, (int) Math.min(ze.getSize(), 65535L));
+ return new ZipInflaterInputStream(wrapped, new Inflater(true), bufSize, ze);
+ }
+ }
+
+ static final class EntryIterator implements Iterator<ZipEntry> {
+ private final long iterationHandle;
+ private ZipEntry nextEntry;
+
+ EntryIterator(long nativeHandle, String prefix) throws IOException {
+ iterationHandle = nativeStartIteration(nativeHandle, prefix);
+ }
+
+ public ZipEntry next() {
+ if (nextEntry != null) {
+ final ZipEntry ze = nextEntry;
+ nextEntry = null;
+ return ze;
+ }
+
+ return nativeNextEntry(iterationHandle);
+ }
+
+ public boolean hasNext() {
+ if (nextEntry != null) {
+ return true;
+ }
+
+ final ZipEntry ze = nativeNextEntry(iterationHandle);
+ if (ze == null) {
+ return false;
+ }
+
+ nextEntry = ze;
+ return true;
+ }
+
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ private HashMap<String, byte[]> getMetaEntries() throws IOException {
+ HashMap<String, byte[]> metaEntries = new HashMap<String, byte[]>();
+
+ Iterator<ZipEntry> entryIterator = new EntryIterator(nativeHandle, "META-INF/");
+ while (entryIterator.hasNext()) {
+ final ZipEntry entry = entryIterator.next();
+ metaEntries.put(entry.getName(), Streams.readFully(getInputStream(entry)));
+ }
+
+ return metaEntries;
+ }
+
+ static final class JarFileInputStream extends FilterInputStream {
+ private final StrictJarVerifier.VerifierEntry entry;
+
+ private long count;
+ private boolean done = false;
+
+ JarFileInputStream(InputStream is, long size, StrictJarVerifier.VerifierEntry e) {
+ super(is);
+ entry = e;
+
+ count = size;
+ }
+
+ @Override
+ public int read() throws IOException {
+ if (done) {
+ return -1;
+ }
+ if (count > 0) {
+ int r = super.read();
+ if (r != -1) {
+ entry.write(r);
+ count--;
+ } else {
+ count = 0;
+ }
+ if (count == 0) {
+ done = true;
+ entry.verify();
+ }
+ return r;
+ } else {
+ done = true;
+ entry.verify();
+ return -1;
+ }
+ }
+
+ @Override
+ public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
+ if (done) {
+ return -1;
+ }
+ if (count > 0) {
+ int r = super.read(buffer, byteOffset, byteCount);
+ if (r != -1) {
+ int size = r;
+ if (count < size) {
+ size = (int) count;
+ }
+ entry.write(buffer, byteOffset, size);
+ count -= size;
+ } else {
+ count = 0;
+ }
+ if (count == 0) {
+ done = true;
+ entry.verify();
+ }
+ return r;
+ } else {
+ done = true;
+ entry.verify();
+ return -1;
+ }
+ }
+
+ @Override
+ public int available() throws IOException {
+ if (done) {
+ return 0;
+ }
+ return super.available();
+ }
+
+ @Override
+ public long skip(long byteCount) throws IOException {
+ return Streams.skipByReading(this, byteCount);
+ }
+ }
+
+ /** @hide */
+ public static class ZipInflaterInputStream extends InflaterInputStream {
+ private final ZipEntry entry;
+ private long bytesRead = 0;
+ private boolean closed;
+
+ public ZipInflaterInputStream(InputStream is, Inflater inf, int bsize, ZipEntry entry) {
+ super(is, inf, bsize);
+ this.entry = entry;
+ }
+
+ @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
+ final int i;
+ try {
+ i = super.read(buffer, byteOffset, byteCount);
+ } catch (IOException e) {
+ throw new IOException("Error reading data for " + entry.getName() + " near offset "
+ + bytesRead, e);
+ }
+ if (i == -1) {
+ if (entry.getSize() != bytesRead) {
+ throw new IOException("Size mismatch on inflated file: " + bytesRead + " vs "
+ + entry.getSize());
+ }
+ } else {
+ bytesRead += i;
+ }
+ return i;
+ }
+
+ @Override public int available() throws IOException {
+ if (closed) {
+ // Our superclass will throw an exception, but there's a jtreg test that
+ // explicitly checks that the InputStream returned from ZipFile.getInputStream
+ // returns 0 even when closed.
+ return 0;
+ }
+ return super.available() == 0 ? 0 : (int) (entry.getSize() - bytesRead);
+ }
+
+ @Override
+ public void close() throws IOException {
+ super.close();
+ closed = true;
+ }
+ }
+
+ /**
+ * Wrap a stream around a FileDescriptor. The file descriptor is shared
+ * among all streams returned by getInputStream(), so we have to synchronize
+ * access to it. (We can optimize this by adding buffering here to reduce
+ * collisions.)
+ *
+ * <p>We could support mark/reset, but we don't currently need them.
+ *
+ * @hide
+ */
+ public static class FDStream extends InputStream {
+ private final FileDescriptor fd;
+ private long endOffset;
+ private long offset;
+
+ public FDStream(FileDescriptor fd, long initialOffset, long endOffset) {
+ this.fd = fd;
+ offset = initialOffset;
+ this.endOffset = endOffset;
+ }
+
+ @Override public int available() throws IOException {
+ return (offset < endOffset ? 1 : 0);
+ }
+
+ @Override public int read() throws IOException {
+ return Streams.readSingleByte(this);
+ }
+
+ @Override public int read(byte[] buffer, int byteOffset, int byteCount) throws IOException {
+ synchronized (this.fd) {
+ final long length = endOffset - offset;
+ if (byteCount > length) {
+ byteCount = (int) length;
+ }
+ try {
+ Os.lseek(fd, offset, OsConstants.SEEK_SET);
+ } catch (ErrnoException e) {
+ throw new IOException(e);
+ }
+ int count = IoBridge.read(fd, buffer, byteOffset, byteCount);
+ if (count > 0) {
+ offset += count;
+ return count;
+ } else {
+ return -1;
+ }
+ }
+ }
+
+ @Override public long skip(long byteCount) throws IOException {
+ if (byteCount > endOffset - offset) {
+ byteCount = endOffset - offset;
+ }
+ offset += byteCount;
+ return byteCount;
+ }
+ }
+
+ private static native long nativeOpenJarFile(String name, int fd)
+ throws IOException;
+ private static native long nativeStartIteration(long nativeHandle, String prefix);
+ private static native ZipEntry nativeNextEntry(long iterationHandle);
+ private static native ZipEntry nativeFindEntry(long nativeHandle, String entryName);
+ private static native void nativeClose(long nativeHandle);
+}
diff --git a/android/util/jar/StrictJarManifest.java b/android/util/jar/StrictJarManifest.java
new file mode 100644
index 0000000..faec099
--- /dev/null
+++ b/android/util/jar/StrictJarManifest.java
@@ -0,0 +1,318 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.jar;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CoderResult;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.jar.Attributes;
+import libcore.io.Streams;
+
+/**
+ * The {@code StrictJarManifest} class is used to obtain attribute information for a
+ * {@code StrictJarFile} and its entries.
+ *
+ * @hide
+ */
+public class StrictJarManifest implements Cloneable {
+ static final int LINE_LENGTH_LIMIT = 72;
+
+ private static final byte[] LINE_SEPARATOR = new byte[] { '\r', '\n' };
+
+ private static final byte[] VALUE_SEPARATOR = new byte[] { ':', ' ' };
+
+ /** The attribute name "Name". */
+ static final Attributes.Name ATTRIBUTE_NAME_NAME = new Attributes.Name("Name");
+
+ private final Attributes mainAttributes;
+ private final HashMap<String, Attributes> entries;
+
+ static final class Chunk {
+ final int start;
+ final int end;
+
+ Chunk(int start, int end) {
+ this.start = start;
+ this.end = end;
+ }
+ }
+
+ private HashMap<String, Chunk> chunks;
+
+ /**
+ * The end of the main attributes section in the manifest is needed in
+ * verification.
+ */
+ private int mainEnd;
+
+ /**
+ * Creates a new {@code StrictJarManifest} instance.
+ */
+ public StrictJarManifest() {
+ entries = new HashMap<String, Attributes>();
+ mainAttributes = new Attributes();
+ }
+
+ /**
+ * Creates a new {@code StrictJarManifest} instance using the attributes obtained
+ * from the input stream.
+ *
+ * @param is
+ * {@code InputStream} to parse for attributes.
+ * @throws IOException
+ * if an IO error occurs while creating this {@code StrictJarManifest}
+ */
+ public StrictJarManifest(InputStream is) throws IOException {
+ this();
+ read(Streams.readFully(is));
+ }
+
+ /**
+ * Creates a new {@code StrictJarManifest} instance. The new instance will have the
+ * same attributes as those found in the parameter {@code StrictJarManifest}.
+ *
+ * @param man
+ * {@code StrictJarManifest} instance to obtain attributes from.
+ */
+ @SuppressWarnings("unchecked")
+ public StrictJarManifest(StrictJarManifest man) {
+ mainAttributes = (Attributes) man.mainAttributes.clone();
+ entries = (HashMap<String, Attributes>) ((HashMap<String, Attributes>) man
+ .getEntries()).clone();
+ }
+
+ StrictJarManifest(byte[] manifestBytes, boolean readChunks) throws IOException {
+ this();
+ if (readChunks) {
+ chunks = new HashMap<String, Chunk>();
+ }
+ read(manifestBytes);
+ }
+
+ /**
+ * Resets the both the main attributes as well as the entry attributes
+ * associated with this {@code StrictJarManifest}.
+ */
+ public void clear() {
+ entries.clear();
+ mainAttributes.clear();
+ }
+
+ /**
+ * Returns the {@code Attributes} associated with the parameter entry
+ * {@code name}.
+ *
+ * @param name
+ * the name of the entry to obtain {@code Attributes} from.
+ * @return the Attributes for the entry or {@code null} if the entry does
+ * not exist.
+ */
+ public Attributes getAttributes(String name) {
+ return getEntries().get(name);
+ }
+
+ /**
+ * Returns a map containing the {@code Attributes} for each entry in the
+ * {@code StrictJarManifest}.
+ *
+ * @return the map of entry attributes.
+ */
+ public Map<String, Attributes> getEntries() {
+ return entries;
+ }
+
+ /**
+ * Returns the main {@code Attributes} of the {@code JarFile}.
+ *
+ * @return main {@code Attributes} associated with the source {@code
+ * JarFile}.
+ */
+ public Attributes getMainAttributes() {
+ return mainAttributes;
+ }
+
+ /**
+ * Creates a copy of this {@code StrictJarManifest}. The returned {@code StrictJarManifest}
+ * will equal the {@code StrictJarManifest} from which it was cloned.
+ *
+ * @return a copy of this instance.
+ */
+ @Override
+ public Object clone() {
+ return new StrictJarManifest(this);
+ }
+
+ /**
+ * Writes this {@code StrictJarManifest}'s name/attributes pairs to the given {@code OutputStream}.
+ * The {@code MANIFEST_VERSION} or {@code SIGNATURE_VERSION} attribute must be set before
+ * calling this method, or no attributes will be written.
+ *
+ * @throws IOException
+ * If an error occurs writing the {@code StrictJarManifest}.
+ */
+ public void write(OutputStream os) throws IOException {
+ write(this, os);
+ }
+
+ /**
+ * Merges name/attribute pairs read from the input stream {@code is} into this manifest.
+ *
+ * @param is
+ * The {@code InputStream} to read from.
+ * @throws IOException
+ * If an error occurs reading the manifest.
+ */
+ public void read(InputStream is) throws IOException {
+ read(Streams.readFullyNoClose(is));
+ }
+
+ private void read(byte[] buf) throws IOException {
+ if (buf.length == 0) {
+ return;
+ }
+
+ StrictJarManifestReader im = new StrictJarManifestReader(buf, mainAttributes);
+ mainEnd = im.getEndOfMainSection();
+ im.readEntries(entries, chunks);
+ }
+
+ /**
+ * Returns the hash code for this instance.
+ *
+ * @return this {@code StrictJarManifest}'s hashCode.
+ */
+ @Override
+ public int hashCode() {
+ return mainAttributes.hashCode() ^ getEntries().hashCode();
+ }
+
+ /**
+ * Determines if the receiver is equal to the parameter object. Two {@code
+ * StrictJarManifest}s are equal if they have identical main attributes as well as
+ * identical entry attributes.
+ *
+ * @param o
+ * the object to compare against.
+ * @return {@code true} if the manifests are equal, {@code false} otherwise
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (o == null) {
+ return false;
+ }
+ if (o.getClass() != this.getClass()) {
+ return false;
+ }
+ if (!mainAttributes.equals(((StrictJarManifest) o).mainAttributes)) {
+ return false;
+ }
+ return getEntries().equals(((StrictJarManifest) o).getEntries());
+ }
+
+ Chunk getChunk(String name) {
+ return chunks.get(name);
+ }
+
+ void removeChunks() {
+ chunks = null;
+ }
+
+ int getMainAttributesEnd() {
+ return mainEnd;
+ }
+
+ /**
+ * Writes out the attribute information of the specified manifest to the
+ * specified {@code OutputStream}
+ *
+ * @param manifest
+ * the manifest to write out.
+ * @param out
+ * The {@code OutputStream} to write to.
+ * @throws IOException
+ * If an error occurs writing the {@code StrictJarManifest}.
+ */
+ static void write(StrictJarManifest manifest, OutputStream out) throws IOException {
+ CharsetEncoder encoder = StandardCharsets.UTF_8.newEncoder();
+ ByteBuffer buffer = ByteBuffer.allocate(LINE_LENGTH_LIMIT);
+
+ Attributes.Name versionName = Attributes.Name.MANIFEST_VERSION;
+ String version = manifest.mainAttributes.getValue(versionName);
+ if (version == null) {
+ versionName = Attributes.Name.SIGNATURE_VERSION;
+ version = manifest.mainAttributes.getValue(versionName);
+ }
+ if (version != null) {
+ writeEntry(out, versionName, version, encoder, buffer);
+ Iterator<?> entries = manifest.mainAttributes.keySet().iterator();
+ while (entries.hasNext()) {
+ Attributes.Name name = (Attributes.Name) entries.next();
+ if (!name.equals(versionName)) {
+ writeEntry(out, name, manifest.mainAttributes.getValue(name), encoder, buffer);
+ }
+ }
+ }
+ out.write(LINE_SEPARATOR);
+ Iterator<String> i = manifest.getEntries().keySet().iterator();
+ while (i.hasNext()) {
+ String key = i.next();
+ writeEntry(out, ATTRIBUTE_NAME_NAME, key, encoder, buffer);
+ Attributes attributes = manifest.entries.get(key);
+ Iterator<?> entries = attributes.keySet().iterator();
+ while (entries.hasNext()) {
+ Attributes.Name name = (Attributes.Name) entries.next();
+ writeEntry(out, name, attributes.getValue(name), encoder, buffer);
+ }
+ out.write(LINE_SEPARATOR);
+ }
+ }
+
+ private static void writeEntry(OutputStream os, Attributes.Name name,
+ String value, CharsetEncoder encoder, ByteBuffer bBuf) throws IOException {
+ String nameString = name.toString();
+ os.write(nameString.getBytes(StandardCharsets.US_ASCII));
+ os.write(VALUE_SEPARATOR);
+
+ encoder.reset();
+ bBuf.clear().limit(LINE_LENGTH_LIMIT - nameString.length() - 2);
+
+ CharBuffer cBuf = CharBuffer.wrap(value);
+
+ while (true) {
+ CoderResult r = encoder.encode(cBuf, bBuf, true);
+ if (CoderResult.UNDERFLOW == r) {
+ r = encoder.flush(bBuf);
+ }
+ os.write(bBuf.array(), bBuf.arrayOffset(), bBuf.position());
+ os.write(LINE_SEPARATOR);
+ if (CoderResult.UNDERFLOW == r) {
+ break;
+ }
+ os.write(' ');
+ bBuf.clear().limit(LINE_LENGTH_LIMIT - 1);
+ }
+ }
+}
diff --git a/android/util/jar/StrictJarManifestReader.java b/android/util/jar/StrictJarManifestReader.java
new file mode 100644
index 0000000..b17abc8
--- /dev/null
+++ b/android/util/jar/StrictJarManifestReader.java
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.jar;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.jar.Attributes;
+
+/**
+ * Reads a JAR file manifest. The specification is here:
+ * http://java.sun.com/javase/6/docs/technotes/guides/jar/jar.html
+ */
+class StrictJarManifestReader {
+ // There are relatively few unique attribute names,
+ // but a manifest might have thousands of entries.
+ private final HashMap<String, Attributes.Name> attributeNameCache = new HashMap<String, Attributes.Name>();
+
+ private final ByteArrayOutputStream valueBuffer = new ByteArrayOutputStream(80);
+
+ private final byte[] buf;
+
+ private final int endOfMainSection;
+
+ private int pos;
+
+ private Attributes.Name name;
+
+ private String value;
+
+ private int consecutiveLineBreaks = 0;
+
+ public StrictJarManifestReader(byte[] buf, Attributes main) throws IOException {
+ this.buf = buf;
+ while (readHeader()) {
+ main.put(name, value);
+ }
+ this.endOfMainSection = pos;
+ }
+
+ public void readEntries(Map<String, Attributes> entries, Map<String, StrictJarManifest.Chunk> chunks) throws IOException {
+ int mark = pos;
+ while (readHeader()) {
+ if (!StrictJarManifest.ATTRIBUTE_NAME_NAME.equals(name)) {
+ throw new IOException("Entry is not named");
+ }
+ String entryNameValue = value;
+
+ Attributes entry = entries.get(entryNameValue);
+ if (entry == null) {
+ entry = new Attributes(12);
+ }
+
+ while (readHeader()) {
+ entry.put(name, value);
+ }
+
+ if (chunks != null) {
+ if (chunks.get(entryNameValue) != null) {
+ // TODO A bug: there might be several verification chunks for
+ // the same name. I believe they should be used to update
+ // signature in order of appearance; there are two ways to fix
+ // this: either use a list of chunks, or decide on used
+ // signature algorithm in advance and reread the chunks while
+ // updating the signature; for now a defensive error is thrown
+ throw new IOException("A jar verifier does not support more than one entry with the same name");
+ }
+ chunks.put(entryNameValue, new StrictJarManifest.Chunk(mark, pos));
+ mark = pos;
+ }
+
+ entries.put(entryNameValue, entry);
+ }
+ }
+
+ public int getEndOfMainSection() {
+ return endOfMainSection;
+ }
+
+ /**
+ * Read a single line from the manifest buffer.
+ */
+ private boolean readHeader() throws IOException {
+ if (consecutiveLineBreaks > 1) {
+ // break a section on an empty line
+ consecutiveLineBreaks = 0;
+ return false;
+ }
+ readName();
+ consecutiveLineBreaks = 0;
+ readValue();
+ // if the last line break is missed, the line
+ // is ignored by the reference implementation
+ return consecutiveLineBreaks > 0;
+ }
+
+ private void readName() throws IOException {
+ int mark = pos;
+
+ while (pos < buf.length) {
+ if (buf[pos++] != ':') {
+ continue;
+ }
+
+ String nameString = new String(buf, mark, pos - mark - 1, StandardCharsets.US_ASCII);
+
+ if (buf[pos++] != ' ') {
+ throw new IOException(String.format("Invalid value for attribute '%s'", nameString));
+ }
+
+ try {
+ name = attributeNameCache.get(nameString);
+ if (name == null) {
+ name = new Attributes.Name(nameString);
+ attributeNameCache.put(nameString, name);
+ }
+ } catch (IllegalArgumentException e) {
+ // new Attributes.Name() throws IllegalArgumentException but we declare IOException
+ throw new IOException(e.getMessage());
+ }
+ return;
+ }
+ }
+
+ private void readValue() throws IOException {
+ boolean lastCr = false;
+ int mark = pos;
+ int last = pos;
+ valueBuffer.reset();
+ while (pos < buf.length) {
+ byte next = buf[pos++];
+ switch (next) {
+ case 0:
+ throw new IOException("NUL character in a manifest");
+ case '\n':
+ if (lastCr) {
+ lastCr = false;
+ } else {
+ consecutiveLineBreaks++;
+ }
+ continue;
+ case '\r':
+ lastCr = true;
+ consecutiveLineBreaks++;
+ continue;
+ case ' ':
+ if (consecutiveLineBreaks == 1) {
+ valueBuffer.write(buf, mark, last - mark);
+ mark = pos;
+ consecutiveLineBreaks = 0;
+ continue;
+ }
+ }
+
+ if (consecutiveLineBreaks >= 1) {
+ pos--;
+ break;
+ }
+ last = pos;
+ }
+
+ valueBuffer.write(buf, mark, last - mark);
+ // A bit frustrating that that Charset.forName will be called
+ // again.
+ value = valueBuffer.toString(StandardCharsets.UTF_8.name());
+ }
+}
diff --git a/android/util/jar/StrictJarVerifier.java b/android/util/jar/StrictJarVerifier.java
new file mode 100644
index 0000000..4525490
--- /dev/null
+++ b/android/util/jar/StrictJarVerifier.java
@@ -0,0 +1,548 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.jar;
+
+import android.util.apk.ApkSignatureSchemeV2Verifier;
+import android.util.apk.ApkSignatureSchemeV3Verifier;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+
+import sun.security.jca.Providers;
+import sun.security.pkcs.PKCS7;
+import sun.security.pkcs.SignerInfo;
+
+/**
+ * Non-public class used by {@link JarFile} and {@link JarInputStream} to manage
+ * the verification of signed JARs. {@code JarFile} and {@code JarInputStream}
+ * objects are expected to have a {@code JarVerifier} instance member which
+ * can be used to carry out the tasks associated with verifying a signed JAR.
+ * These tasks would typically include:
+ * <ul>
+ * <li>verification of all signed signature files
+ * <li>confirmation that all signed data was signed only by the party or parties
+ * specified in the signature block data
+ * <li>verification that the contents of all signature files (i.e. {@code .SF}
+ * files) agree with the JAR entries information found in the JAR manifest.
+ * </ul>
+ */
+class StrictJarVerifier {
+ /**
+ * {@code .SF} file header section attribute indicating that the APK is signed not just with
+ * JAR signature scheme but also with APK Signature Scheme v2 or newer. This attribute
+ * facilitates v2 signature stripping detection.
+ *
+ * <p>The attribute contains a comma-separated set of signature scheme IDs.
+ */
+ private static final String SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME = "X-Android-APK-Signed";
+
+ /**
+ * List of accepted digest algorithms. This list is in order from most
+ * preferred to least preferred.
+ */
+ private static final String[] DIGEST_ALGORITHMS = new String[] {
+ "SHA-512",
+ "SHA-384",
+ "SHA-256",
+ "SHA1",
+ };
+
+ private final String jarName;
+ private final StrictJarManifest manifest;
+ private final HashMap<String, byte[]> metaEntries;
+ private final int mainAttributesEnd;
+ private final boolean signatureSchemeRollbackProtectionsEnforced;
+
+ private final Hashtable<String, HashMap<String, Attributes>> signatures =
+ new Hashtable<String, HashMap<String, Attributes>>(5);
+
+ private final Hashtable<String, Certificate[]> certificates =
+ new Hashtable<String, Certificate[]>(5);
+
+ private final Hashtable<String, Certificate[][]> verifiedEntries =
+ new Hashtable<String, Certificate[][]>();
+
+ /**
+ * Stores and a hash and a message digest and verifies that massage digest
+ * matches the hash.
+ */
+ static class VerifierEntry extends OutputStream {
+
+ private final String name;
+
+ private final MessageDigest digest;
+
+ private final byte[] hash;
+
+ private final Certificate[][] certChains;
+
+ private final Hashtable<String, Certificate[][]> verifiedEntries;
+
+ VerifierEntry(String name, MessageDigest digest, byte[] hash,
+ Certificate[][] certChains, Hashtable<String, Certificate[][]> verifedEntries) {
+ this.name = name;
+ this.digest = digest;
+ this.hash = hash;
+ this.certChains = certChains;
+ this.verifiedEntries = verifedEntries;
+ }
+
+ /**
+ * Updates a digest with one byte.
+ */
+ @Override
+ public void write(int value) {
+ digest.update((byte) value);
+ }
+
+ /**
+ * Updates a digest with byte array.
+ */
+ @Override
+ public void write(byte[] buf, int off, int nbytes) {
+ digest.update(buf, off, nbytes);
+ }
+
+ /**
+ * Verifies that the digests stored in the manifest match the decrypted
+ * digests from the .SF file. This indicates the validity of the
+ * signing, not the integrity of the file, as its digest must be
+ * calculated and verified when its contents are read.
+ *
+ * @throws SecurityException
+ * if the digest value stored in the manifest does <i>not</i>
+ * agree with the decrypted digest as recovered from the
+ * <code>.SF</code> file.
+ */
+ void verify() {
+ byte[] d = digest.digest();
+ if (!verifyMessageDigest(d, hash)) {
+ throw invalidDigest(JarFile.MANIFEST_NAME, name, name);
+ }
+ verifiedEntries.put(name, certChains);
+ }
+ }
+
+ private static SecurityException invalidDigest(String signatureFile, String name,
+ String jarName) {
+ throw new SecurityException(signatureFile + " has invalid digest for " + name +
+ " in " + jarName);
+ }
+
+ private static SecurityException failedVerification(String jarName, String signatureFile) {
+ throw new SecurityException(jarName + " failed verification of " + signatureFile);
+ }
+
+ private static SecurityException failedVerification(String jarName, String signatureFile,
+ Throwable e) {
+ throw new SecurityException(jarName + " failed verification of " + signatureFile, e);
+ }
+
+
+ /**
+ * Constructs and returns a new instance of {@code JarVerifier}.
+ *
+ * @param name
+ * the name of the JAR file being verified.
+ *
+ * @param signatureSchemeRollbackProtectionsEnforced {@code true} to enforce protections against
+ * stripping newer signature schemes (e.g., APK Signature Scheme v2) from the file, or
+ * {@code false} to ignore any such protections.
+ */
+ StrictJarVerifier(String name, StrictJarManifest manifest,
+ HashMap<String, byte[]> metaEntries, boolean signatureSchemeRollbackProtectionsEnforced) {
+ jarName = name;
+ this.manifest = manifest;
+ this.metaEntries = metaEntries;
+ this.mainAttributesEnd = manifest.getMainAttributesEnd();
+ this.signatureSchemeRollbackProtectionsEnforced =
+ signatureSchemeRollbackProtectionsEnforced;
+ }
+
+ /**
+ * Invoked for each new JAR entry read operation from the input
+ * stream. This method constructs and returns a new {@link VerifierEntry}
+ * which contains the certificates used to sign the entry and its hash value
+ * as specified in the JAR MANIFEST format.
+ *
+ * @param name
+ * the name of an entry in a JAR file which is <b>not</b> in the
+ * {@code META-INF} directory.
+ * @return a new instance of {@link VerifierEntry} which can be used by
+ * callers as an {@link OutputStream}.
+ */
+ VerifierEntry initEntry(String name) {
+ // If no manifest is present by the time an entry is found,
+ // verification cannot occur. If no signature files have
+ // been found, do not verify.
+ if (manifest == null || signatures.isEmpty()) {
+ return null;
+ }
+
+ Attributes attributes = manifest.getAttributes(name);
+ // entry has no digest
+ if (attributes == null) {
+ return null;
+ }
+
+ ArrayList<Certificate[]> certChains = new ArrayList<Certificate[]>();
+ Iterator<Map.Entry<String, HashMap<String, Attributes>>> it = signatures.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry<String, HashMap<String, Attributes>> entry = it.next();
+ HashMap<String, Attributes> hm = entry.getValue();
+ if (hm.get(name) != null) {
+ // Found an entry for entry name in .SF file
+ String signatureFile = entry.getKey();
+ Certificate[] certChain = certificates.get(signatureFile);
+ if (certChain != null) {
+ certChains.add(certChain);
+ }
+ }
+ }
+
+ // entry is not signed
+ if (certChains.isEmpty()) {
+ return null;
+ }
+ Certificate[][] certChainsArray = certChains.toArray(new Certificate[certChains.size()][]);
+
+ for (int i = 0; i < DIGEST_ALGORITHMS.length; i++) {
+ final String algorithm = DIGEST_ALGORITHMS[i];
+ final String hash = attributes.getValue(algorithm + "-Digest");
+ if (hash == null) {
+ continue;
+ }
+ byte[] hashBytes = hash.getBytes(StandardCharsets.ISO_8859_1);
+
+ try {
+ return new VerifierEntry(name, MessageDigest.getInstance(algorithm), hashBytes,
+ certChainsArray, verifiedEntries);
+ } catch (NoSuchAlgorithmException ignored) {
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Add a new meta entry to the internal collection of data held on each JAR
+ * entry in the {@code META-INF} directory including the manifest
+ * file itself. Files associated with the signing of a JAR would also be
+ * added to this collection.
+ *
+ * @param name
+ * the name of the file located in the {@code META-INF}
+ * directory.
+ * @param buf
+ * the file bytes for the file called {@code name}.
+ * @see #removeMetaEntries()
+ */
+ void addMetaEntry(String name, byte[] buf) {
+ metaEntries.put(name.toUpperCase(Locale.US), buf);
+ }
+
+ /**
+ * If the associated JAR file is signed, check on the validity of all of the
+ * known signatures.
+ *
+ * @return {@code true} if the associated JAR is signed and an internal
+ * check verifies the validity of the signature(s). {@code false} if
+ * the associated JAR file has no entries at all in its {@code
+ * META-INF} directory. This situation is indicative of an invalid
+ * JAR file.
+ * <p>
+ * Will also return {@code true} if the JAR file is <i>not</i>
+ * signed.
+ * @throws SecurityException
+ * if the JAR file is signed and it is determined that a
+ * signature block file contains an invalid signature for the
+ * corresponding signature file.
+ */
+ synchronized boolean readCertificates() {
+ if (metaEntries.isEmpty()) {
+ return false;
+ }
+
+ Iterator<String> it = metaEntries.keySet().iterator();
+ while (it.hasNext()) {
+ String key = it.next();
+ if (key.endsWith(".DSA") || key.endsWith(".RSA") || key.endsWith(".EC")) {
+ verifyCertificate(key);
+ it.remove();
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Verifies that the signature computed from {@code sfBytes} matches
+ * that specified in {@code blockBytes} (which is a PKCS7 block). Returns
+ * certificates listed in the PKCS7 block. Throws a {@code GeneralSecurityException}
+ * if something goes wrong during verification.
+ */
+ static Certificate[] verifyBytes(byte[] blockBytes, byte[] sfBytes)
+ throws GeneralSecurityException {
+
+ Object obj = null;
+ try {
+
+ obj = Providers.startJarVerification();
+ PKCS7 block = new PKCS7(blockBytes);
+ SignerInfo[] verifiedSignerInfos = block.verify(sfBytes);
+ if ((verifiedSignerInfos == null) || (verifiedSignerInfos.length == 0)) {
+ throw new GeneralSecurityException(
+ "Failed to verify signature: no verified SignerInfos");
+ }
+ // Ignore any SignerInfo other than the first one, to be compatible with older Android
+ // platforms which have been doing this for years. See
+ // libcore/luni/src/main/java/org/apache/harmony/security/utils/JarUtils.java
+ // verifySignature method of older platforms.
+ SignerInfo verifiedSignerInfo = verifiedSignerInfos[0];
+ List<X509Certificate> verifiedSignerCertChain =
+ verifiedSignerInfo.getCertificateChain(block);
+ if (verifiedSignerCertChain == null) {
+ // Should never happen
+ throw new GeneralSecurityException(
+ "Failed to find verified SignerInfo certificate chain");
+ } else if (verifiedSignerCertChain.isEmpty()) {
+ // Should never happen
+ throw new GeneralSecurityException(
+ "Verified SignerInfo certificate chain is emtpy");
+ }
+ return verifiedSignerCertChain.toArray(
+ new X509Certificate[verifiedSignerCertChain.size()]);
+ } catch (IOException e) {
+ throw new GeneralSecurityException("IO exception verifying jar cert", e);
+ } finally {
+ Providers.stopJarVerification(obj);
+ }
+ }
+
+ /**
+ * @param certFile
+ */
+ private void verifyCertificate(String certFile) {
+ // Found Digital Sig, .SF should already have been read
+ String signatureFile = certFile.substring(0, certFile.lastIndexOf('.')) + ".SF";
+ byte[] sfBytes = metaEntries.get(signatureFile);
+ if (sfBytes == null) {
+ return;
+ }
+
+ byte[] manifestBytes = metaEntries.get(JarFile.MANIFEST_NAME);
+ // Manifest entry is required for any verifications.
+ if (manifestBytes == null) {
+ return;
+ }
+
+ byte[] sBlockBytes = metaEntries.get(certFile);
+ try {
+ Certificate[] signerCertChain = verifyBytes(sBlockBytes, sfBytes);
+ if (signerCertChain != null) {
+ certificates.put(signatureFile, signerCertChain);
+ }
+ } catch (GeneralSecurityException e) {
+ throw failedVerification(jarName, signatureFile, e);
+ }
+
+ // Verify manifest hash in .sf file
+ Attributes attributes = new Attributes();
+ HashMap<String, Attributes> entries = new HashMap<String, Attributes>();
+ try {
+ StrictJarManifestReader im = new StrictJarManifestReader(sfBytes, attributes);
+ im.readEntries(entries, null);
+ } catch (IOException e) {
+ return;
+ }
+
+ // If requested, check whether a newer APK Signature Scheme signature was stripped.
+ if (signatureSchemeRollbackProtectionsEnforced) {
+ String apkSignatureSchemeIdList =
+ attributes.getValue(SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME);
+ if (apkSignatureSchemeIdList != null) {
+ // This field contains a comma-separated list of APK signature scheme IDs which
+ // were used to sign this APK. If an ID is known to us, it means signatures of that
+ // scheme were stripped from the APK because otherwise we wouldn't have fallen back
+ // to verifying the APK using the JAR signature scheme.
+ boolean v2SignatureGenerated = false;
+ boolean v3SignatureGenerated = false;
+ StringTokenizer tokenizer = new StringTokenizer(apkSignatureSchemeIdList, ",");
+ while (tokenizer.hasMoreTokens()) {
+ String idText = tokenizer.nextToken().trim();
+ if (idText.isEmpty()) {
+ continue;
+ }
+ int id;
+ try {
+ id = Integer.parseInt(idText);
+ } catch (Exception ignored) {
+ continue;
+ }
+ if (id == ApkSignatureSchemeV2Verifier.SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID) {
+ // This APK was supposed to be signed with APK Signature Scheme v2 but no
+ // such signature was found.
+ v2SignatureGenerated = true;
+ break;
+ }
+ if (id == ApkSignatureSchemeV3Verifier.SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID) {
+ // This APK was supposed to be signed with APK Signature Scheme v3 but no
+ // such signature was found.
+ v3SignatureGenerated = true;
+ break;
+ }
+ }
+
+ if (v2SignatureGenerated) {
+ throw new SecurityException(signatureFile + " indicates " + jarName
+ + " is signed using APK Signature Scheme v2, but no such signature was"
+ + " found. Signature stripped?");
+ }
+ if (v3SignatureGenerated) {
+ throw new SecurityException(signatureFile + " indicates " + jarName
+ + " is signed using APK Signature Scheme v3, but no such signature was"
+ + " found. Signature stripped?");
+ }
+ }
+ }
+
+ // Do we actually have any signatures to look at?
+ if (attributes.get(Attributes.Name.SIGNATURE_VERSION) == null) {
+ return;
+ }
+
+ boolean createdBySigntool = false;
+ String createdBy = attributes.getValue("Created-By");
+ if (createdBy != null) {
+ createdBySigntool = createdBy.indexOf("signtool") != -1;
+ }
+
+ // Use .SF to verify the mainAttributes of the manifest
+ // If there is no -Digest-Manifest-Main-Attributes entry in .SF
+ // file, such as those created before java 1.5, then we ignore
+ // such verification.
+ if (mainAttributesEnd > 0 && !createdBySigntool) {
+ String digestAttribute = "-Digest-Manifest-Main-Attributes";
+ if (!verify(attributes, digestAttribute, manifestBytes, 0, mainAttributesEnd, false, true)) {
+ throw failedVerification(jarName, signatureFile);
+ }
+ }
+
+ // Use .SF to verify the whole manifest.
+ String digestAttribute = createdBySigntool ? "-Digest" : "-Digest-Manifest";
+ if (!verify(attributes, digestAttribute, manifestBytes, 0, manifestBytes.length, false, false)) {
+ Iterator<Map.Entry<String, Attributes>> it = entries.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry<String, Attributes> entry = it.next();
+ StrictJarManifest.Chunk chunk = manifest.getChunk(entry.getKey());
+ if (chunk == null) {
+ return;
+ }
+ if (!verify(entry.getValue(), "-Digest", manifestBytes,
+ chunk.start, chunk.end, createdBySigntool, false)) {
+ throw invalidDigest(signatureFile, entry.getKey(), jarName);
+ }
+ }
+ }
+ metaEntries.put(signatureFile, null);
+ signatures.put(signatureFile, entries);
+ }
+
+ /**
+ * Returns a <code>boolean</code> indication of whether or not the
+ * associated jar file is signed.
+ *
+ * @return {@code true} if the JAR is signed, {@code false}
+ * otherwise.
+ */
+ boolean isSignedJar() {
+ return certificates.size() > 0;
+ }
+
+ private boolean verify(Attributes attributes, String entry, byte[] data,
+ int start, int end, boolean ignoreSecondEndline, boolean ignorable) {
+ for (int i = 0; i < DIGEST_ALGORITHMS.length; i++) {
+ String algorithm = DIGEST_ALGORITHMS[i];
+ String hash = attributes.getValue(algorithm + entry);
+ if (hash == null) {
+ continue;
+ }
+
+ MessageDigest md;
+ try {
+ md = MessageDigest.getInstance(algorithm);
+ } catch (NoSuchAlgorithmException e) {
+ continue;
+ }
+ if (ignoreSecondEndline && data[end - 1] == '\n' && data[end - 2] == '\n') {
+ md.update(data, start, end - 1 - start);
+ } else {
+ md.update(data, start, end - start);
+ }
+ byte[] b = md.digest();
+ byte[] encodedHashBytes = hash.getBytes(StandardCharsets.ISO_8859_1);
+ return verifyMessageDigest(b, encodedHashBytes);
+ }
+ return ignorable;
+ }
+
+ private static boolean verifyMessageDigest(byte[] expected, byte[] encodedActual) {
+ byte[] actual;
+ try {
+ actual = java.util.Base64.getDecoder().decode(encodedActual);
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ return MessageDigest.isEqual(expected, actual);
+ }
+
+ /**
+ * Returns all of the {@link java.security.cert.Certificate} chains that
+ * were used to verify the signature on the JAR entry called
+ * {@code name}. Callers must not modify the returned arrays.
+ *
+ * @param name
+ * the name of a JAR entry.
+ * @return an array of {@link java.security.cert.Certificate} chains.
+ */
+ Certificate[][] getCertificateChains(String name) {
+ return verifiedEntries.get(name);
+ }
+
+ /**
+ * Remove all entries from the internal collection of data held about each
+ * JAR entry in the {@code META-INF} directory.
+ */
+ void removeMetaEntries() {
+ metaEntries.clear();
+ }
+}
diff --git a/android/util/perftests/LogPerfTest.java b/android/util/perftests/LogPerfTest.java
new file mode 100644
index 0000000..26cec95
--- /dev/null
+++ b/android/util/perftests/LogPerfTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.perftests;
+
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.util.Log;
+
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class LogPerfTest {
+
+ private final String[] strings = new String[] {
+ "This is a test log string 1",
+ "This is a test log string 2",
+ "This is a test log string 3",
+ "This is a test log string 4",
+ "This is a test log string 5",
+ };
+
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ @Test
+ public void testLogPerf() {
+ BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ int i = 0;
+ while (state.keepRunning()) {
+ Log.d("LogPerfTest", strings[(i++) % strings.length]);
+ }
+ }
+}
diff --git a/android/util/proto/EncodedBuffer.java b/android/util/proto/EncodedBuffer.java
new file mode 100644
index 0000000..56a0bfa
--- /dev/null
+++ b/android/util/proto/EncodedBuffer.java
@@ -0,0 +1,686 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.proto;
+
+import android.annotation.TestApi;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * A stream of bytes containing a read pointer and a write pointer,
+ * backed by a set of fixed-size buffers. There are write functions for the
+ * primitive types stored by protocol buffers, but none of the logic
+ * for tags, inner objects, or any of that.
+ *
+ * Terminology:
+ * *Pos: Position in the whole data set (as if it were a single buffer).
+ * *Index: Position within a buffer.
+ * *BufIndex: Index of a buffer within the mBuffers list
+ * @hide
+ */
+@TestApi
+public final class EncodedBuffer {
+ private static final String TAG = "EncodedBuffer";
+
+ private final ArrayList<byte[]> mBuffers = new ArrayList<byte[]>();
+
+ private final int mChunkSize;
+
+ /**
+ * The number of buffers in mBuffers. Stored separately to avoid the extra
+ * function call to size() everywhere for bounds checking.
+ */
+ private int mBufferCount;
+
+ /**
+ * The buffer we are currently writing to.
+ */
+ private byte[] mWriteBuffer;
+
+ /**
+ * The index into mWriteBuffer that we will write to next.
+ * It may point to the end of the buffer, in which case,
+ * the NEXT write will allocate a new buffer.
+ */
+ private int mWriteIndex;
+
+ /**
+ * The index of mWriteBuffer in mBuffers.
+ */
+ private int mWriteBufIndex;
+
+ /**
+ * The buffer we are currently reading from.
+ */
+ private byte[] mReadBuffer;
+
+ /**
+ * The index of mReadBuffer in mBuffers.
+ */
+ private int mReadBufIndex;
+
+ /**
+ * The index into mReadBuffer that we will read from next.
+ * It may point to the end of the buffer, in which case,
+ * the NEXT read will advance to the next buffer.
+ */
+ private int mReadIndex;
+
+ /**
+ * The amount of data in the last buffer.
+ */
+ private int mReadLimit = -1;
+
+ /**
+ * How much data there is total.
+ */
+ private int mReadableSize = -1;
+
+ public EncodedBuffer() {
+ this(0);
+ }
+
+ /**
+ * Construct an EncodedBuffer object.
+ *
+ * @param chunkSize The size of the buffers to use. If chunkSize <= 0, a default
+ * size will be used instead.
+ */
+ public EncodedBuffer(int chunkSize) {
+ if (chunkSize <= 0) {
+ chunkSize = 8 * 1024;
+ }
+ mChunkSize = chunkSize;
+ mWriteBuffer = new byte[mChunkSize];
+ mBuffers.add(mWriteBuffer);
+ mBufferCount = 1;
+ }
+
+ //
+ // Buffer management.
+ //
+
+ /**
+ * Rewind the read and write pointers, and record how much data was last written.
+ */
+ public void startEditing() {
+ mReadableSize = ((mWriteBufIndex) * mChunkSize) + mWriteIndex;
+ mReadLimit = mWriteIndex;
+
+ mWriteBuffer = mBuffers.get(0);
+ mWriteIndex = 0;
+ mWriteBufIndex = 0;
+
+ mReadBuffer = mWriteBuffer;
+ mReadBufIndex = 0;
+ mReadIndex = 0;
+ }
+
+ /**
+ * Rewind the read pointer. Don't touch the write pointer.
+ */
+ public void rewindRead() {
+ mReadBuffer = mBuffers.get(0);
+ mReadBufIndex = 0;
+ mReadIndex = 0;
+ }
+
+ /**
+ * Only valid after startEditing. Returns -1 before that.
+ */
+ public int getReadableSize() {
+ return mReadableSize;
+ }
+
+ /**
+ * Returns the buffer size
+ * @return the buffer size
+ */
+ public int getSize() {
+ return ((mBufferCount - 1) * mChunkSize) + mWriteIndex;
+ }
+
+ //
+ // Reading from the read position.
+ //
+
+ /**
+ * Only valid after startEditing.
+ */
+ public int getReadPos() {
+ return ((mReadBufIndex) * mChunkSize) + mReadIndex;
+ }
+
+ /**
+ * Skip over _amount_ bytes.
+ */
+ public void skipRead(int amount) {
+ if (amount < 0) {
+ throw new RuntimeException("skipRead with negative amount=" + amount);
+ }
+ if (amount == 0) {
+ return;
+ }
+ if (amount <= mChunkSize - mReadIndex) {
+ mReadIndex += amount;
+ } else {
+ amount -= mChunkSize - mReadIndex;
+ mReadIndex = amount % mChunkSize;
+ if (mReadIndex == 0) {
+ mReadIndex = mChunkSize;
+ mReadBufIndex += (amount / mChunkSize);
+ } else {
+ mReadBufIndex += 1 + (amount / mChunkSize);
+ }
+ mReadBuffer = mBuffers.get(mReadBufIndex);
+ }
+ }
+
+ /**
+ * Read one byte from the stream and advance the read pointer.
+ *
+ * @throws IndexOutOfBoundsException if the read point is past the end of
+ * the buffer or past the read limit previously set by startEditing().
+ */
+ public byte readRawByte() {
+ if (mReadBufIndex > mBufferCount
+ || (mReadBufIndex == mBufferCount - 1 && mReadIndex >= mReadLimit)) {
+ throw new IndexOutOfBoundsException("Trying to read too much data"
+ + " mReadBufIndex=" + mReadBufIndex + " mBufferCount=" + mBufferCount
+ + " mReadIndex=" + mReadIndex + " mReadLimit=" + mReadLimit);
+ }
+ if (mReadIndex >= mChunkSize) {
+ mReadBufIndex++;
+ mReadBuffer = mBuffers.get(mReadBufIndex);
+ mReadIndex = 0;
+ }
+ return mReadBuffer[mReadIndex++];
+ }
+
+ /**
+ * Read an unsigned varint. The value will be returend in a java signed long.
+ */
+ public long readRawUnsigned() {
+ int bits = 0;
+ long result = 0;
+ while (true) {
+ final byte b = readRawByte();
+ result |= ((long)(b & 0x7F)) << bits;
+ if ((b & 0x80) == 0) {
+ return result;
+ }
+ bits += 7;
+ if (bits > 64) {
+ throw new ProtoParseException("Varint too long -- " + getDebugString());
+ }
+ }
+ }
+
+ /**
+ * Read 32 little endian bits from the stream.
+ */
+ public int readRawFixed32() {
+ return (readRawByte() & 0x0ff)
+ | ((readRawByte() & 0x0ff) << 8)
+ | ((readRawByte() & 0x0ff) << 16)
+ | ((readRawByte() & 0x0ff) << 24);
+ }
+
+ //
+ // Writing at a the end of the stream.
+ //
+
+ /**
+ * Advance to the next write buffer, allocating it if necessary.
+ *
+ * Must be called immediately <b>before</b> the next write, not after a write,
+ * so that a dangling empty buffer is not created. Doing so will interfere
+ * with the expectation that mWriteIndex will point past the end of the buffer
+ * until the next read happens.
+ */
+ private void nextWriteBuffer() {
+ mWriteBufIndex++;
+ if (mWriteBufIndex >= mBufferCount) {
+ mWriteBuffer = new byte[mChunkSize];
+ mBuffers.add(mWriteBuffer);
+ mBufferCount++;
+ } else {
+ mWriteBuffer = mBuffers.get(mWriteBufIndex);
+ }
+ mWriteIndex = 0;
+ }
+
+ /**
+ * Write a single byte to the stream.
+ */
+ public void writeRawByte(byte val) {
+ if (mWriteIndex >= mChunkSize) {
+ nextWriteBuffer();
+ }
+ mWriteBuffer[mWriteIndex++] = val;
+ }
+
+ /**
+ * Return how many bytes a 32 bit unsigned varint will take when written to the stream.
+ */
+ public static int getRawVarint32Size(int val) {
+ if ((val & (0xffffffff << 7)) == 0) return 1;
+ if ((val & (0xffffffff << 14)) == 0) return 2;
+ if ((val & (0xffffffff << 21)) == 0) return 3;
+ if ((val & (0xffffffff << 28)) == 0) return 4;
+ return 5;
+ }
+
+ /**
+ * Write an unsigned varint to the stream. A signed value would need to take 10 bytes.
+ *
+ * @param val treated as unsigned.
+ */
+ public void writeRawVarint32(int val) {
+ while (true) {
+ if ((val & ~0x7F) == 0) {
+ writeRawByte((byte)val);
+ return;
+ } else {
+ writeRawByte((byte)((val & 0x7F) | 0x80));
+ val >>>= 7;
+ }
+ }
+ }
+
+ /**
+ * Return how many bytes a 32 bit signed zig zag value will take when written to the stream.
+ */
+ public static int getRawZigZag32Size(int val) {
+ return getRawVarint32Size(zigZag32(val));
+ }
+
+ /**
+ * Write a zig-zag encoded value.
+ *
+ * @param val treated as signed
+ */
+ public void writeRawZigZag32(int val) {
+ writeRawVarint32(zigZag32(val));
+ }
+
+ /**
+ * Return how many bytes a 64 bit varint will take when written to the stream.
+ */
+ public static int getRawVarint64Size(long val) {
+ if ((val & (0xffffffffffffffffL << 7)) == 0) return 1;
+ if ((val & (0xffffffffffffffffL << 14)) == 0) return 2;
+ if ((val & (0xffffffffffffffffL << 21)) == 0) return 3;
+ if ((val & (0xffffffffffffffffL << 28)) == 0) return 4;
+ if ((val & (0xffffffffffffffffL << 35)) == 0) return 5;
+ if ((val & (0xffffffffffffffffL << 42)) == 0) return 6;
+ if ((val & (0xffffffffffffffffL << 49)) == 0) return 7;
+ if ((val & (0xffffffffffffffffL << 56)) == 0) return 8;
+ if ((val & (0xffffffffffffffffL << 63)) == 0) return 9;
+ return 10;
+ }
+
+ /**
+ * Write a 64 bit varint to the stream.
+ */
+ public void writeRawVarint64(long val) {
+ while (true) {
+ if ((val & ~0x7FL) == 0) {
+ writeRawByte((byte)val);
+ return;
+ } else {
+ writeRawByte((byte)((val & 0x7F) | 0x80));
+ val >>>= 7;
+ }
+ }
+ }
+
+ /**
+ * Return how many bytes a signed 64 bit zig zag value will take when written to the stream.
+ */
+ public static int getRawZigZag64Size(long val) {
+ return getRawVarint64Size(zigZag64(val));
+ }
+
+ /**
+ * Write a 64 bit signed zig zag value to the stream.
+ */
+ public void writeRawZigZag64(long val) {
+ writeRawVarint64(zigZag64(val));
+ }
+
+ /**
+ * Write 4 little endian bytes to the stream.
+ */
+ public void writeRawFixed32(int val) {
+ writeRawByte((byte)(val));
+ writeRawByte((byte)(val >> 8));
+ writeRawByte((byte)(val >> 16));
+ writeRawByte((byte)(val >> 24));
+ }
+
+ /**
+ * Write 8 little endian bytes to the stream.
+ */
+ public void writeRawFixed64(long val) {
+ writeRawByte((byte)(val));
+ writeRawByte((byte)(val >> 8));
+ writeRawByte((byte)(val >> 16));
+ writeRawByte((byte)(val >> 24));
+ writeRawByte((byte)(val >> 32));
+ writeRawByte((byte)(val >> 40));
+ writeRawByte((byte)(val >> 48));
+ writeRawByte((byte)(val >> 56));
+ }
+
+ /**
+ * Write a buffer to the stream. Writes nothing if val is null or zero-length.
+ */
+ public void writeRawBuffer(byte[] val) {
+ if (val != null && val.length > 0) {
+ writeRawBuffer(val, 0, val.length);
+ }
+ }
+
+ /**
+ * Write part of an array of bytes.
+ */
+ public void writeRawBuffer(byte[] val, int offset, int length) {
+ if (val == null) {
+ return;
+ }
+ // Write up to the amount left in the first chunk to write.
+ int amt = length < (mChunkSize - mWriteIndex) ? length : (mChunkSize - mWriteIndex);
+ if (amt > 0) {
+ System.arraycopy(val, offset, mWriteBuffer, mWriteIndex, amt);
+ mWriteIndex += amt;
+ length -= amt;
+ offset += amt;
+ }
+ while (length > 0) {
+ // We know we're now at the beginning of a chunk
+ nextWriteBuffer();
+ amt = length < mChunkSize ? length : mChunkSize;
+ System.arraycopy(val, offset, mWriteBuffer, mWriteIndex, amt);
+ mWriteIndex += amt;
+ length -= amt;
+ offset += amt;
+ }
+ }
+
+ /**
+ * Copies data _size_ bytes of data within this buffer from _srcOffset_
+ * to the current write position. Like memmov but handles the chunked buffer.
+ */
+ public void writeFromThisBuffer(int srcOffset, int size) {
+ if (mReadLimit < 0) {
+ throw new IllegalStateException("writeFromThisBuffer before startEditing");
+ }
+ if (srcOffset < getWritePos()) {
+ throw new IllegalArgumentException("Can only move forward in the buffer --"
+ + " srcOffset=" + srcOffset + " size=" + size + " " + getDebugString());
+ }
+ if (srcOffset + size > mReadableSize) {
+ throw new IllegalArgumentException("Trying to move more data than there is --"
+ + " srcOffset=" + srcOffset + " size=" + size + " " + getDebugString());
+ }
+ if (size == 0) {
+ return;
+ }
+ if (srcOffset == ((mWriteBufIndex) * mChunkSize) + mWriteIndex /* write pos */) {
+ // Writing to the same location. Just advance the write pointer. We already
+ // checked that size is in bounds, so we don't need to do any more range
+ // checking.
+ if (size <= mChunkSize - mWriteIndex) {
+ mWriteIndex += size;
+ } else {
+ size -= mChunkSize - mWriteIndex;
+ mWriteIndex = size % mChunkSize;
+ if (mWriteIndex == 0) {
+ // Roll it back so nextWriteBuffer can do its job
+ // on the next call (also makes mBuffers.get() not
+ // fail if we're at the end).
+ mWriteIndex = mChunkSize;
+ mWriteBufIndex += (size / mChunkSize);
+ } else {
+ mWriteBufIndex += 1 + (size / mChunkSize);
+ }
+ mWriteBuffer = mBuffers.get(mWriteBufIndex);
+ }
+ } else {
+ // Loop through the buffer, copying as much as we can each time.
+ // We already bounds checked so we don't need to do it again here,
+ // and nextWriteBuffer will never allocate.
+ int readBufIndex = srcOffset / mChunkSize;
+ byte[] readBuffer = mBuffers.get(readBufIndex);
+ int readIndex = srcOffset % mChunkSize;
+ while (size > 0) {
+ if (mWriteIndex >= mChunkSize) {
+ nextWriteBuffer();
+ }
+ if (readIndex >= mChunkSize) {
+ readBufIndex++;
+ readBuffer = mBuffers.get(readBufIndex);
+ readIndex = 0;
+ }
+ final int spaceInWriteBuffer = mChunkSize - mWriteIndex;
+ final int availableInReadBuffer = mChunkSize - readIndex;
+ final int amt = Math.min(size, Math.min(spaceInWriteBuffer, availableInReadBuffer));
+ System.arraycopy(readBuffer, readIndex, mWriteBuffer, mWriteIndex, amt);
+ mWriteIndex += amt;
+ readIndex += amt;
+ size -= amt;
+ }
+ }
+ }
+
+ //
+ // Writing at a particular location.
+ //
+
+ /**
+ * Returns the index into the virtual array of the write pointer.
+ */
+ public int getWritePos() {
+ return ((mWriteBufIndex) * mChunkSize) + mWriteIndex;
+ }
+
+ /**
+ * Resets the write pointer to a virtual location as returned by getWritePos.
+ */
+ public void rewindWriteTo(int writePos) {
+ if (writePos > getWritePos()) {
+ throw new RuntimeException("rewindWriteTo only can go backwards" + writePos);
+ }
+ mWriteBufIndex = writePos / mChunkSize;
+ mWriteIndex = writePos % mChunkSize;
+ if (mWriteIndex == 0 && mWriteBufIndex != 0) {
+ // Roll back so nextWriteBuffer can do its job on the next call
+ // but at the first write we're at 0.
+ mWriteIndex = mChunkSize;
+ mWriteBufIndex--;
+ }
+ mWriteBuffer = mBuffers.get(mWriteBufIndex);
+ }
+
+ /**
+ * Read a 32 bit value from the stream.
+ *
+ * Doesn't touch or affect mWritePos.
+ */
+ public int getRawFixed32At(int pos) {
+ return (0x00ff & (int)mBuffers.get(pos / mChunkSize)[pos % mChunkSize])
+ | ((0x0ff & (int)mBuffers.get((pos+1) / mChunkSize)[(pos+1) % mChunkSize]) << 8)
+ | ((0x0ff & (int)mBuffers.get((pos+2) / mChunkSize)[(pos+2) % mChunkSize]) << 16)
+ | ((0x0ff & (int)mBuffers.get((pos+3) / mChunkSize)[(pos+3) % mChunkSize]) << 24);
+ }
+
+ /**
+ * Overwrite a 32 bit value in the stream.
+ *
+ * Doesn't touch or affect mWritePos.
+ */
+ public void editRawFixed32(int pos, int val) {
+ mBuffers.get(pos / mChunkSize)[pos % mChunkSize] = (byte)(val);
+ mBuffers.get((pos+1) / mChunkSize)[(pos+1) % mChunkSize] = (byte)(val >> 8);
+ mBuffers.get((pos+2) / mChunkSize)[(pos+2) % mChunkSize] = (byte)(val >> 16);
+ mBuffers.get((pos+3) / mChunkSize)[(pos+3) % mChunkSize] = (byte)(val >> 24);
+ }
+
+ //
+ // Zigging and zagging
+ //
+
+ /**
+ * Zig-zag encode a 32 bit value.
+ */
+ private static int zigZag32(int val) {
+ return (val << 1) ^ (val >> 31);
+ }
+
+ /**
+ * Zig-zag encode a 64 bit value.
+ */
+ private static long zigZag64(long val) {
+ return (val << 1) ^ (val >> 63);
+ }
+
+ //
+ // Debugging / testing
+ //
+ // VisibleForTesting
+
+ /**
+ * Get a copy of the first _size_ bytes of data. This is not range
+ * checked, and if the bounds are outside what has been written you will
+ * get garbage and if it is outside the buffers that have been allocated,
+ * you will get an exception.
+ */
+ public byte[] getBytes(int size) {
+ final byte[] result = new byte[size];
+
+ final int bufCount = size / mChunkSize;
+ int bufIndex;
+ int writeIndex = 0;
+
+ for (bufIndex=0; bufIndex<bufCount; bufIndex++) {
+ System.arraycopy(mBuffers.get(bufIndex), 0, result, writeIndex, mChunkSize);
+ writeIndex += mChunkSize;
+ }
+
+ final int lastSize = size - (bufCount * mChunkSize);
+ if (lastSize > 0) {
+ System.arraycopy(mBuffers.get(bufIndex), 0, result, writeIndex, lastSize);
+ }
+
+ return result;
+ }
+
+ /**
+ * Get the number of chunks allocated.
+ */
+ // VisibleForTesting
+ public int getChunkCount() {
+ return mBuffers.size();
+ }
+
+ /**
+ * Get the write position inside the current write chunk.
+ */
+ // VisibleForTesting
+ public int getWriteIndex() {
+ return mWriteIndex;
+ }
+
+ /**
+ * Get the index of the current write chunk in the list of chunks.
+ */
+ // VisibleForTesting
+ public int getWriteBufIndex() {
+ return mWriteBufIndex;
+ }
+
+ /**
+ * Return debugging information about this EncodedBuffer object.
+ */
+ public String getDebugString() {
+ return "EncodedBuffer( mChunkSize=" + mChunkSize + " mBuffers.size=" + mBuffers.size()
+ + " mBufferCount=" + mBufferCount + " mWriteIndex=" + mWriteIndex
+ + " mWriteBufIndex=" + mWriteBufIndex + " mReadBufIndex=" + mReadBufIndex
+ + " mReadIndex=" + mReadIndex + " mReadableSize=" + mReadableSize
+ + " mReadLimit=" + mReadLimit + " )";
+ }
+
+ /**
+ * Print the internal buffer chunks.
+ */
+ public void dumpBuffers(String tag) {
+ final int N = mBuffers.size();
+ int start = 0;
+ for (int i=0; i<N; i++) {
+ start += dumpByteString(tag, "{" + i + "} ", start, mBuffers.get(i));
+ }
+ }
+
+ /**
+ * Print the internal buffer chunks.
+ */
+ public static void dumpByteString(String tag, String prefix, byte[] buf) {
+ dumpByteString(tag, prefix, 0, buf);
+ }
+
+ /**
+ * Print the internal buffer chunks.
+ */
+ private static int dumpByteString(String tag, String prefix, int start, byte[] buf) {
+ StringBuffer sb = new StringBuffer();
+ final int length = buf.length;
+ final int lineLen = 16;
+ int i;
+ for (i=0; i<length; i++) {
+ if (i % lineLen == 0) {
+ if (i != 0) {
+ Log.d(tag, sb.toString());
+ sb = new StringBuffer();
+ }
+ sb.append(prefix);
+ sb.append('[');
+ sb.append(start + i);
+ sb.append(']');
+ sb.append(' ');
+ } else {
+ sb.append(' ');
+ }
+ byte b = buf[i];
+ byte c = (byte)((b >> 4) & 0x0f);
+ if (c < 10) {
+ sb.append((char)('0' + c));
+ } else {
+ sb.append((char)('a' - 10 + c));
+ }
+ byte d = (byte)(b & 0x0f);
+ if (d < 10) {
+ sb.append((char)('0' + d));
+ } else {
+ sb.append((char)('a' - 10 + d));
+ }
+ }
+ Log.d(tag, sb.toString());
+ return length;
+ }
+}
diff --git a/android/util/proto/ProtoInputStream.java b/android/util/proto/ProtoInputStream.java
new file mode 100644
index 0000000..cbe3734
--- /dev/null
+++ b/android/util/proto/ProtoInputStream.java
@@ -0,0 +1,993 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.proto;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+
+/**
+ * Class to read to a protobuf stream.
+ *
+ * Each read method takes an ID code from the protoc generated classes
+ * and return a value of the field. To read a nested object, call #start
+ * and then #end when you are done.
+ *
+ * The ID codes have type information embedded into them, so if you call
+ * the incorrect function you will get an IllegalArgumentException.
+ *
+ * nextField will return the field number of the next field, which can be
+ * matched to the protoc generated ID code and used to determine how to
+ * read the next field.
+ *
+ * It is STRONGLY RECOMMENDED to read from the ProtoInputStream with a switch
+ * statement wrapped in a while loop. Additionally, it is worth logging or
+ * storing unexpected fields or ones that do not match the expected wire type
+ *
+ * ex:
+ * void parseFromProto(ProtoInputStream stream) {
+ * while(stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ * try {
+ * switch (stream.getFieldNumber()) {
+ * case (int) DummyProto.NAME:
+ * mName = stream.readString(DummyProto.NAME);
+ * break;
+ * case (int) DummyProto.VALUE:
+ * mValue = stream.readInt(DummyProto.VALUE);
+ * break;
+ * default:
+ * LOG(TAG, "Unhandled field in proto!\n"
+ * + ProtoUtils.currentFieldToString(stream));
+ * }
+ * } catch (WireTypeMismatchException wtme) {
+ * LOG(TAG, "Wire Type mismatch in proto!\n" + ProtoUtils.currentFieldToString(stream));
+ * }
+ * }
+ * }
+ *
+ * @hide
+ */
+public final class ProtoInputStream extends ProtoStream {
+
+ public static final int NO_MORE_FIELDS = -1;
+
+ /**
+ * Our stream. If there is one.
+ */
+ private InputStream mStream;
+
+ /**
+ * The field number of the current field. Will be equal to NO_MORE_FIELDS if end of message is
+ * reached
+ */
+ private int mFieldNumber;
+
+ /**
+ * The wire type of the current field
+ */
+ private int mWireType;
+
+ private static final byte STATE_STARTED_FIELD_READ = 1 << 0;
+ private static final byte STATE_READING_PACKED = 1 << 1;
+ private static final byte STATE_FIELD_MISS = 2 << 1;
+
+ /**
+ * Tracks some boolean states for the proto input stream
+ * bit 0: Started Field Read, true - tag has been read, ready to read field data.
+ * false - field data has been read, reading to start next field.
+ * bit 1: Reading Packed Field, true - currently reading values from a packed field
+ * false - not reading from packed field.
+ */
+ private byte mState = 0;
+
+ /**
+ * Keeps track of the currently read nested Objects, for end object sanity checking and debug
+ */
+ private ArrayList<Long> mExpectedObjectTokenStack = null;
+
+ /**
+ * Current nesting depth of start calls.
+ */
+ private int mDepth = -1;
+
+ /**
+ * Buffer for the to be read data. If mStream is not null, it will be constantly refilled from
+ * the stream.
+ */
+ private byte[] mBuffer;
+
+ private static final int DEFAULT_BUFFER_SIZE = 8192;
+
+ /**
+ * Size of the buffer if reading from a stream.
+ */
+ private final int mBufferSize;
+
+ /**
+ * The number of bytes that have been skipped or dropped from the buffer.
+ */
+ private int mDiscardedBytes = 0;
+
+ /**
+ * Current offset in the buffer
+ * mOffset + mDiscardedBytes = current offset in proto binary
+ */
+ private int mOffset = 0;
+
+ /**
+ * Note the offset of the last byte in the buffer. Usually will equal the size of the buffer.
+ * mEnd + mDiscardedBytes = the last known byte offset + 1
+ */
+ private int mEnd = 0;
+
+ /**
+ * Packed repeated fields are not read in one go. mPackedEnd keeps track of where the packed
+ * field ends in the proto binary if current field is packed.
+ */
+ private int mPackedEnd = 0;
+
+ /**
+ * Construct a ProtoInputStream on top of an InputStream to read a proto. Also specify the
+ * number of bytes the ProtoInputStream will buffer from the input stream
+ *
+ * @param stream from which the proto is read
+ */
+ public ProtoInputStream(InputStream stream, int bufferSize) {
+ mStream = stream;
+ if (bufferSize > 0) {
+ mBufferSize = bufferSize;
+ } else {
+ mBufferSize = DEFAULT_BUFFER_SIZE;
+ }
+ mBuffer = new byte[mBufferSize];
+ }
+
+ /**
+ * Construct a ProtoInputStream on top of an InputStream to read a proto
+ *
+ * @param stream from which the proto is read
+ */
+ public ProtoInputStream(InputStream stream) {
+ this(stream, DEFAULT_BUFFER_SIZE);
+ }
+
+ /**
+ * Construct a ProtoInputStream to read a proto directly from a byte array
+ *
+ * @param buffer - the byte array to be parsed
+ */
+ public ProtoInputStream(byte[] buffer) {
+ mBufferSize = buffer.length;
+ mEnd = buffer.length;
+ mBuffer = buffer;
+ mStream = null;
+ }
+
+ /**
+ * Get the field number of the current field.
+ */
+ public int getFieldNumber() {
+ return mFieldNumber;
+ }
+
+ /**
+ * Get the wire type of the current field.
+ *
+ * @return an int that matches one of the ProtoStream WIRE_TYPE_ constants
+ */
+ public int getWireType() {
+ if ((mState & STATE_READING_PACKED) == STATE_READING_PACKED) {
+ // mWireType got overwritten when STATE_READING_PACKED was set. Send length delimited
+ // constant instead
+ return WIRE_TYPE_LENGTH_DELIMITED;
+ }
+ return mWireType;
+ }
+
+ /**
+ * Get the current offset in the proto binary.
+ */
+ public int getOffset() {
+ return mOffset + mDiscardedBytes;
+ }
+
+ /**
+ * Reads the tag of the next field from the stream. If previous field value was not read, its
+ * data will be skipped over.
+ *
+ * @return the field number of the next field
+ * @throws IOException if an I/O error occurs
+ */
+ public int nextField() throws IOException {
+
+ if ((mState & STATE_FIELD_MISS) == STATE_FIELD_MISS) {
+ // Data from the last nextField was not used, reuse the info
+ mState &= ~STATE_FIELD_MISS;
+ return mFieldNumber;
+ }
+ if ((mState & STATE_STARTED_FIELD_READ) == STATE_STARTED_FIELD_READ) {
+ // Field data was not read, skip to the next field
+ skip();
+ mState &= ~STATE_STARTED_FIELD_READ;
+ }
+ if ((mState & STATE_READING_PACKED) == STATE_READING_PACKED) {
+ if (getOffset() < mPackedEnd) {
+ // In the middle of a packed field, return the same tag until last packed value
+ // has been read
+ mState |= STATE_STARTED_FIELD_READ;
+ return mFieldNumber;
+ } else if (getOffset() == mPackedEnd) {
+ // Reached the end of the packed field
+ mState &= ~STATE_READING_PACKED;
+ } else {
+ throw new ProtoParseException(
+ "Unexpectedly reached end of packed field at offset 0x"
+ + Integer.toHexString(mPackedEnd)
+ + dumpDebugData());
+ }
+ }
+
+ if ((mDepth >= 0) && (getOffset() == getOffsetFromToken(
+ mExpectedObjectTokenStack.get(mDepth)))) {
+ // reached end of a embedded message
+ mFieldNumber = NO_MORE_FIELDS;
+ } else {
+ readTag();
+ }
+ return mFieldNumber;
+ }
+
+ /**
+ * Reads the tag of the next field from the stream. If previous field value was not read, its
+ * data will be skipped over. If {@code fieldId} matches the next field ID, the field data will
+ * be ready to read. If it does not match, {@link #nextField()} or {@link #nextField(long)} will
+ * need to be called again before the field data can be read.
+ *
+ * @return true if fieldId matches the next field, false if not
+ */
+ public boolean nextField(long fieldId) throws IOException {
+ if (nextField() == (int) fieldId) {
+ return true;
+ }
+ // Note to reuse the info from the nextField call in the next call.
+ mState |= STATE_FIELD_MISS;
+ return false;
+ }
+
+ /**
+ * Read a single double.
+ * Will throw if the current wire type is not fixed64
+ *
+ * @param fieldId - must match the current field number and field type
+ */
+ public double readDouble(long fieldId) throws IOException {
+ assertFreshData();
+ assertFieldNumber(fieldId);
+ checkPacked(fieldId);
+
+ double value;
+ switch ((int) ((fieldId & FIELD_TYPE_MASK)
+ >>> FIELD_TYPE_SHIFT)) {
+ case (int) (FIELD_TYPE_DOUBLE >>> FIELD_TYPE_SHIFT):
+ assertWireType(WIRE_TYPE_FIXED64);
+ value = Double.longBitsToDouble(readFixed64());
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Requested field id (" + getFieldIdString(fieldId)
+ + ") cannot be read as a double"
+ + dumpDebugData());
+ }
+ // Successfully read the field
+ mState &= ~STATE_STARTED_FIELD_READ;
+ return value;
+ }
+
+ /**
+ * Read a single float.
+ * Will throw if the current wire type is not fixed32
+ *
+ * @param fieldId - must match the current field number and field type
+ */
+ public float readFloat(long fieldId) throws IOException {
+ assertFreshData();
+ assertFieldNumber(fieldId);
+ checkPacked(fieldId);
+
+ float value;
+ switch ((int) ((fieldId & FIELD_TYPE_MASK)
+ >>> FIELD_TYPE_SHIFT)) {
+ case (int) (FIELD_TYPE_FLOAT >>> FIELD_TYPE_SHIFT):
+ assertWireType(WIRE_TYPE_FIXED32);
+ value = Float.intBitsToFloat(readFixed32());
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Requested field id (" + getFieldIdString(fieldId) + ") is not a float"
+ + dumpDebugData());
+ }
+ // Successfully read the field
+ mState &= ~STATE_STARTED_FIELD_READ;
+ return value;
+ }
+
+ /**
+ * Read a single 32bit or varint proto type field as an int.
+ * Will throw if the current wire type is not varint or fixed32
+ *
+ * @param fieldId - must match the current field number and field type
+ */
+ public int readInt(long fieldId) throws IOException {
+ assertFreshData();
+ assertFieldNumber(fieldId);
+ checkPacked(fieldId);
+
+ int value;
+ switch ((int) ((fieldId & FIELD_TYPE_MASK)
+ >>> FIELD_TYPE_SHIFT)) {
+ case (int) (FIELD_TYPE_FIXED32 >>> FIELD_TYPE_SHIFT):
+ case (int) (FIELD_TYPE_SFIXED32 >>> FIELD_TYPE_SHIFT):
+ assertWireType(WIRE_TYPE_FIXED32);
+ value = readFixed32();
+ break;
+ case (int) (FIELD_TYPE_SINT32 >>> FIELD_TYPE_SHIFT):
+ assertWireType(WIRE_TYPE_VARINT);
+ value = decodeZigZag32((int) readVarint());
+ break;
+ case (int) (FIELD_TYPE_INT32 >>> FIELD_TYPE_SHIFT):
+ case (int) (FIELD_TYPE_UINT32 >>> FIELD_TYPE_SHIFT):
+ case (int) (FIELD_TYPE_ENUM >>> FIELD_TYPE_SHIFT):
+ assertWireType(WIRE_TYPE_VARINT);
+ value = (int) readVarint();
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Requested field id (" + getFieldIdString(fieldId) + ") is not an int"
+ + dumpDebugData());
+ }
+ // Successfully read the field
+ mState &= ~STATE_STARTED_FIELD_READ;
+ return value;
+ }
+
+ /**
+ * Read a single 64bit or varint proto type field as an long.
+ *
+ * @param fieldId - must match the current field number
+ */
+ public long readLong(long fieldId) throws IOException {
+ assertFreshData();
+ assertFieldNumber(fieldId);
+ checkPacked(fieldId);
+
+ long value;
+ switch ((int) ((fieldId & FIELD_TYPE_MASK)
+ >>> FIELD_TYPE_SHIFT)) {
+ case (int) (FIELD_TYPE_FIXED64 >>> FIELD_TYPE_SHIFT):
+ case (int) (FIELD_TYPE_SFIXED64 >>> FIELD_TYPE_SHIFT):
+ assertWireType(WIRE_TYPE_FIXED64);
+ value = readFixed64();
+ break;
+ case (int) (FIELD_TYPE_SINT64 >>> FIELD_TYPE_SHIFT):
+ assertWireType(WIRE_TYPE_VARINT);
+ value = decodeZigZag64(readVarint());
+ break;
+ case (int) (FIELD_TYPE_INT64 >>> FIELD_TYPE_SHIFT):
+ case (int) (FIELD_TYPE_UINT64 >>> FIELD_TYPE_SHIFT):
+ assertWireType(WIRE_TYPE_VARINT);
+ value = readVarint();
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Requested field id (" + getFieldIdString(fieldId) + ") is not an long"
+ + dumpDebugData());
+ }
+ // Successfully read the field
+ mState &= ~STATE_STARTED_FIELD_READ;
+ return value;
+ }
+
+ /**
+ * Read a single 32bit or varint proto type field as an boolean.
+ *
+ * @param fieldId - must match the current field number
+ */
+ public boolean readBoolean(long fieldId) throws IOException {
+ assertFreshData();
+ assertFieldNumber(fieldId);
+ checkPacked(fieldId);
+
+ boolean value;
+ switch ((int) ((fieldId & FIELD_TYPE_MASK)
+ >>> FIELD_TYPE_SHIFT)) {
+ case (int) (FIELD_TYPE_BOOL >>> FIELD_TYPE_SHIFT):
+ assertWireType(WIRE_TYPE_VARINT);
+ value = readVarint() != 0;
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Requested field id (" + getFieldIdString(fieldId) + ") is not an boolean"
+ + dumpDebugData());
+ }
+ // Successfully read the field
+ mState &= ~STATE_STARTED_FIELD_READ;
+ return value;
+ }
+
+ /**
+ * Read a string field
+ *
+ * @param fieldId - must match the current field number
+ */
+ public String readString(long fieldId) throws IOException {
+ assertFreshData();
+ assertFieldNumber(fieldId);
+
+ String value;
+ switch ((int) ((fieldId & FIELD_TYPE_MASK) >>> FIELD_TYPE_SHIFT)) {
+ case (int) (FIELD_TYPE_STRING >>> FIELD_TYPE_SHIFT):
+ assertWireType(WIRE_TYPE_LENGTH_DELIMITED);
+ int len = (int) readVarint();
+ value = readRawString(len);
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Requested field id(" + getFieldIdString(fieldId)
+ + ") is not an string"
+ + dumpDebugData());
+ }
+ // Successfully read the field
+ mState &= ~STATE_STARTED_FIELD_READ;
+ return value;
+ }
+
+ /**
+ * Read a bytes field
+ *
+ * @param fieldId - must match the current field number
+ */
+ public byte[] readBytes(long fieldId) throws IOException {
+ assertFreshData();
+ assertFieldNumber(fieldId);
+
+ byte[] value;
+ switch ((int) ((fieldId & FIELD_TYPE_MASK) >>> FIELD_TYPE_SHIFT)) {
+ case (int) (FIELD_TYPE_MESSAGE >>> FIELD_TYPE_SHIFT):
+ case (int) (FIELD_TYPE_BYTES >>> FIELD_TYPE_SHIFT):
+ assertWireType(WIRE_TYPE_LENGTH_DELIMITED);
+ int len = (int) readVarint();
+ value = readRawBytes(len);
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Requested field type (" + getFieldIdString(fieldId)
+ + ") cannot be read as raw bytes"
+ + dumpDebugData());
+ }
+ // Successfully read the field
+ mState &= ~STATE_STARTED_FIELD_READ;
+ return value;
+ }
+
+ /**
+ * Start the read of an embedded Object
+ *
+ * @param fieldId - must match the current field number
+ * @return a token. The token must be handed back when finished reading embedded Object
+ */
+ public long start(long fieldId) throws IOException {
+ assertFreshData();
+ assertFieldNumber(fieldId);
+ assertWireType(WIRE_TYPE_LENGTH_DELIMITED);
+
+ int messageSize = (int) readVarint();
+
+ if (mExpectedObjectTokenStack == null) {
+ mExpectedObjectTokenStack = new ArrayList<>();
+ }
+ if (++mDepth == mExpectedObjectTokenStack.size()) {
+ // Create a token to keep track of nested Object and extend the object stack
+ mExpectedObjectTokenStack.add(makeToken(0,
+ (fieldId & FIELD_COUNT_REPEATED) == FIELD_COUNT_REPEATED, mDepth,
+ (int) fieldId, getOffset() + messageSize));
+
+ } else {
+ // Create a token to keep track of nested Object
+ mExpectedObjectTokenStack.set(mDepth, makeToken(0,
+ (fieldId & FIELD_COUNT_REPEATED) == FIELD_COUNT_REPEATED, mDepth,
+ (int) fieldId, getOffset() + messageSize));
+ }
+
+ // Sanity check
+ if (mDepth > 0
+ && getOffsetFromToken(mExpectedObjectTokenStack.get(mDepth))
+ > getOffsetFromToken(mExpectedObjectTokenStack.get(mDepth - 1))) {
+ throw new ProtoParseException("Embedded Object ("
+ + token2String(mExpectedObjectTokenStack.get(mDepth))
+ + ") ends after of parent Objects's ("
+ + token2String(mExpectedObjectTokenStack.get(mDepth - 1))
+ + ") end"
+ + dumpDebugData());
+ }
+ mState &= ~STATE_STARTED_FIELD_READ;
+ return mExpectedObjectTokenStack.get(mDepth);
+ }
+
+ /**
+ * Note the end of a nested object. Must be called to continue streaming the rest of the proto.
+ * end can be called mid object parse. The offset will be moved to the next field outside the
+ * object.
+ *
+ * @param token - token
+ */
+ public void end(long token) {
+ // Sanity check to make sure user is keeping track of their embedded messages
+ if (mExpectedObjectTokenStack.get(mDepth) != token) {
+ throw new ProtoParseException(
+ "end token " + token + " does not match current message token "
+ + mExpectedObjectTokenStack.get(mDepth)
+ + dumpDebugData());
+ }
+ if (getOffsetFromToken(mExpectedObjectTokenStack.get(mDepth)) > getOffset()) {
+ // Did not read all of the message, skip to the end
+ incOffset(getOffsetFromToken(mExpectedObjectTokenStack.get(mDepth)) - getOffset());
+ }
+ mDepth--;
+ mState &= ~STATE_STARTED_FIELD_READ;
+ }
+
+ /**
+ * Read the tag at the start of the next field and collect field number and wire type.
+ * Will set mFieldNumber to NO_MORE_FIELDS if end of buffer/stream reached.
+ */
+ private void readTag() throws IOException {
+ fillBuffer();
+ if (mOffset >= mEnd) {
+ // reached end of the stream
+ mFieldNumber = NO_MORE_FIELDS;
+ return;
+ }
+ int tag = (int) readVarint();
+ mFieldNumber = tag >>> FIELD_ID_SHIFT;
+ mWireType = tag & WIRE_TYPE_MASK;
+ mState |= STATE_STARTED_FIELD_READ;
+ }
+
+ /**
+ * Decode a 32 bit ZigZag encoded signed int.
+ *
+ * @param n - int to decode
+ * @return the decoded signed int
+ */
+ public int decodeZigZag32(final int n) {
+ return (n >>> 1) ^ -(n & 1);
+ }
+
+ /**
+ * Decode a 64 bit ZigZag encoded signed long.
+ *
+ * @param n - long to decode
+ * @return the decoded signed long
+ */
+ public long decodeZigZag64(final long n) {
+ return (n >>> 1) ^ -(n & 1);
+ }
+
+ /**
+ * Read a varint from the buffer
+ *
+ * @return the varint as a long
+ */
+ private long readVarint() throws IOException {
+ long value = 0;
+ int shift = 0;
+ while (true) {
+ fillBuffer();
+ // Limit how much bookkeeping is done by checking how far away the end of the buffer is
+ // and directly accessing buffer up until the end.
+ final int fragment = mEnd - mOffset;
+ if (fragment < 0) {
+ throw new ProtoParseException(
+ "Incomplete varint at offset 0x"
+ + Integer.toHexString(getOffset())
+ + dumpDebugData());
+ }
+ for (int i = 0; i < fragment; i++) {
+ byte b = mBuffer[(mOffset + i)];
+ value |= (b & 0x7FL) << shift;
+ if ((b & 0x80) == 0) {
+ incOffset(i + 1);
+ return value;
+ }
+ shift += 7;
+ if (shift > 63) {
+ throw new ProtoParseException(
+ "Varint is too large at offset 0x"
+ + Integer.toHexString(getOffset() + i)
+ + dumpDebugData());
+ }
+ }
+ // Hit the end of the buffer, do some incrementing and checking, then continue
+ incOffset(fragment);
+ }
+ }
+
+ /**
+ * Read a fixed 32 bit int from the buffer
+ *
+ * @return the fixed32 as a int
+ */
+ private int readFixed32() throws IOException {
+ // check for fast path, which is likely with a reasonable buffer size
+ if (mOffset + 4 <= mEnd) {
+ // don't bother filling buffer since we know the end is plenty far away
+ incOffset(4);
+ return (mBuffer[mOffset - 4] & 0xFF)
+ | ((mBuffer[mOffset - 3] & 0xFF) << 8)
+ | ((mBuffer[mOffset - 2] & 0xFF) << 16)
+ | ((mBuffer[mOffset - 1] & 0xFF) << 24);
+ }
+
+ // the Fixed32 crosses the edge of a chunk, read the Fixed32 in multiple fragments.
+ // There will be two fragment reads except when the chunk size is 2 or less.
+ int value = 0;
+ int shift = 0;
+ int bytesLeft = 4;
+ while (bytesLeft > 0) {
+ fillBuffer();
+ // Find the number of bytes available until the end of the chunk or Fixed32
+ int fragment = (mEnd - mOffset) < bytesLeft ? (mEnd - mOffset) : bytesLeft;
+ if (fragment < 0) {
+ throw new ProtoParseException(
+ "Incomplete fixed32 at offset 0x"
+ + Integer.toHexString(getOffset())
+ + dumpDebugData());
+ }
+ incOffset(fragment);
+ bytesLeft -= fragment;
+ while (fragment > 0) {
+ value |= ((mBuffer[mOffset - fragment] & 0xFF) << shift);
+ fragment--;
+ shift += 8;
+ }
+ }
+ return value;
+ }
+
+ /**
+ * Read a fixed 64 bit long from the buffer
+ *
+ * @return the fixed64 as a long
+ */
+ private long readFixed64() throws IOException {
+ // check for fast path, which is likely with a reasonable buffer size
+ if (mOffset + 8 <= mEnd) {
+ // don't bother filling buffer since we know the end is plenty far away
+ incOffset(8);
+ return (mBuffer[mOffset - 8] & 0xFFL)
+ | ((mBuffer[mOffset - 7] & 0xFFL) << 8)
+ | ((mBuffer[mOffset - 6] & 0xFFL) << 16)
+ | ((mBuffer[mOffset - 5] & 0xFFL) << 24)
+ | ((mBuffer[mOffset - 4] & 0xFFL) << 32)
+ | ((mBuffer[mOffset - 3] & 0xFFL) << 40)
+ | ((mBuffer[mOffset - 2] & 0xFFL) << 48)
+ | ((mBuffer[mOffset - 1] & 0xFFL) << 56);
+ }
+
+ // the Fixed64 crosses the edge of a chunk, read the Fixed64 in multiple fragments.
+ // There will be two fragment reads except when the chunk size is 6 or less.
+ long value = 0;
+ int shift = 0;
+ int bytesLeft = 8;
+ while (bytesLeft > 0) {
+ fillBuffer();
+ // Find the number of bytes available until the end of the chunk or Fixed64
+ int fragment = (mEnd - mOffset) < bytesLeft ? (mEnd - mOffset) : bytesLeft;
+ if (fragment < 0) {
+ throw new ProtoParseException(
+ "Incomplete fixed64 at offset 0x"
+ + Integer.toHexString(getOffset())
+ + dumpDebugData());
+ }
+ incOffset(fragment);
+ bytesLeft -= fragment;
+ while (fragment > 0) {
+ value |= ((mBuffer[(mOffset - fragment)] & 0xFFL) << shift);
+ fragment--;
+ shift += 8;
+ }
+ }
+ return value;
+ }
+
+ /**
+ * Read raw bytes from the buffer
+ *
+ * @param n - number of bytes to read
+ * @return a byte array with raw bytes
+ */
+ private byte[] readRawBytes(int n) throws IOException {
+ byte[] buffer = new byte[n];
+ int pos = 0;
+ while (mOffset + n - pos > mEnd) {
+ int fragment = mEnd - mOffset;
+ if (fragment > 0) {
+ System.arraycopy(mBuffer, mOffset, buffer, pos, fragment);
+ incOffset(fragment);
+ pos += fragment;
+ }
+ fillBuffer();
+ if (mOffset >= mEnd) {
+ throw new ProtoParseException(
+ "Unexpectedly reached end of the InputStream at offset 0x"
+ + Integer.toHexString(mEnd)
+ + dumpDebugData());
+ }
+ }
+ System.arraycopy(mBuffer, mOffset, buffer, pos, n - pos);
+ incOffset(n - pos);
+ return buffer;
+ }
+
+ /**
+ * Read raw string from the buffer
+ *
+ * @param n - number of bytes to read
+ * @return a string
+ */
+ private String readRawString(int n) throws IOException {
+ fillBuffer();
+ if (mOffset + n <= mEnd) {
+ // fast path read. String is well within the current buffer
+ String value = new String(mBuffer, mOffset, n, StandardCharsets.UTF_8);
+ incOffset(n);
+ return value;
+ } else if (n <= mBufferSize) {
+ // String extends past buffer, but can be encapsulated in a buffer. Copy the first chunk
+ // of the string to the start of the buffer and then fill the rest of the buffer from
+ // the stream.
+ final int stringHead = mEnd - mOffset;
+ System.arraycopy(mBuffer, mOffset, mBuffer, 0, stringHead);
+ mEnd = stringHead + mStream.read(mBuffer, stringHead, n - stringHead);
+
+ mDiscardedBytes += mOffset;
+ mOffset = 0;
+
+ String value = new String(mBuffer, mOffset, n, StandardCharsets.UTF_8);
+ incOffset(n);
+ return value;
+ }
+ // Otherwise, the string is too large to use the buffer. Create the string from a
+ // separate byte array.
+ return new String(readRawBytes(n), 0, n, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * Fill the buffer with a chunk from the stream if need be.
+ * Will skip chunks until mOffset is reached
+ */
+ private void fillBuffer() throws IOException {
+ if (mOffset >= mEnd && mStream != null) {
+ mOffset -= mEnd;
+ mDiscardedBytes += mEnd;
+ if (mOffset >= mBufferSize) {
+ int skipped = (int) mStream.skip((mOffset / mBufferSize) * mBufferSize);
+ mDiscardedBytes += skipped;
+ mOffset -= skipped;
+ }
+ mEnd = mStream.read(mBuffer);
+ }
+ }
+
+ /**
+ * Skips the rest of current field and moves to the start of the next field. This should only be
+ * called while state is STATE_STARTED_FIELD_READ
+ */
+ public void skip() throws IOException {
+ if ((mState & STATE_READING_PACKED) == STATE_READING_PACKED) {
+ incOffset(mPackedEnd - getOffset());
+ } else {
+ switch (mWireType) {
+ case WIRE_TYPE_VARINT:
+ byte b;
+ do {
+ fillBuffer();
+ b = mBuffer[mOffset];
+ incOffset(1);
+ } while ((b & 0x80) != 0);
+ break;
+ case WIRE_TYPE_FIXED64:
+ incOffset(8);
+ break;
+ case WIRE_TYPE_LENGTH_DELIMITED:
+ fillBuffer();
+ int length = (int) readVarint();
+ incOffset(length);
+ break;
+ /*
+ case WIRE_TYPE_START_GROUP:
+ // Not implemented
+ break;
+ case WIRE_TYPE_END_GROUP:
+ // Not implemented
+ break;
+ */
+ case WIRE_TYPE_FIXED32:
+ incOffset(4);
+ break;
+ default:
+ throw new ProtoParseException(
+ "Unexpected wire type: " + mWireType + " at offset 0x"
+ + Integer.toHexString(mOffset)
+ + dumpDebugData());
+ }
+ }
+ mState &= ~STATE_STARTED_FIELD_READ;
+ }
+
+ /**
+ * Increment the offset and handle all the relevant bookkeeping
+ * Refilling the buffer when its end is reached will be handled elsewhere (ideally just before
+ * a read, to avoid unnecessary reads from stream)
+ *
+ * @param n - number of bytes to increment
+ */
+ private void incOffset(int n) {
+ mOffset += n;
+
+ if (mDepth >= 0 && getOffset() > getOffsetFromToken(
+ mExpectedObjectTokenStack.get(mDepth))) {
+ throw new ProtoParseException("Unexpectedly reached end of embedded object. "
+ + token2String(mExpectedObjectTokenStack.get(mDepth))
+ + dumpDebugData());
+ }
+ }
+
+ /**
+ * Check the current wire type to determine if current numeric field is packed. If it is packed,
+ * set up to deal with the field
+ * This should only be called for primitive numeric field types.
+ *
+ * @param fieldId - used to determine what the packed wire type is.
+ */
+ private void checkPacked(long fieldId) throws IOException {
+ if (mWireType == WIRE_TYPE_LENGTH_DELIMITED) {
+ // Primitive Field is length delimited, must be a packed field.
+ final int length = (int) readVarint();
+ mPackedEnd = getOffset() + length;
+ mState |= STATE_READING_PACKED;
+
+ // Fake the wire type, based on the field type
+ switch ((int) ((fieldId & FIELD_TYPE_MASK)
+ >>> FIELD_TYPE_SHIFT)) {
+ case (int) (FIELD_TYPE_FLOAT >>> FIELD_TYPE_SHIFT):
+ case (int) (FIELD_TYPE_FIXED32 >>> FIELD_TYPE_SHIFT):
+ case (int) (FIELD_TYPE_SFIXED32 >>> FIELD_TYPE_SHIFT):
+ if (length % 4 != 0) {
+ throw new IllegalArgumentException(
+ "Requested field id (" + getFieldIdString(fieldId)
+ + ") packed length " + length
+ + " is not aligned for fixed32"
+ + dumpDebugData());
+ }
+ mWireType = WIRE_TYPE_FIXED32;
+ break;
+ case (int) (FIELD_TYPE_DOUBLE >>> FIELD_TYPE_SHIFT):
+ case (int) (FIELD_TYPE_FIXED64 >>> FIELD_TYPE_SHIFT):
+ case (int) (FIELD_TYPE_SFIXED64 >>> FIELD_TYPE_SHIFT):
+ if (length % 8 != 0) {
+ throw new IllegalArgumentException(
+ "Requested field id (" + getFieldIdString(fieldId)
+ + ") packed length " + length
+ + " is not aligned for fixed64"
+ + dumpDebugData());
+ }
+ mWireType = WIRE_TYPE_FIXED64;
+ break;
+ case (int) (FIELD_TYPE_SINT32 >>> FIELD_TYPE_SHIFT):
+ case (int) (FIELD_TYPE_INT32 >>> FIELD_TYPE_SHIFT):
+ case (int) (FIELD_TYPE_UINT32 >>> FIELD_TYPE_SHIFT):
+ case (int) (FIELD_TYPE_SINT64 >>> FIELD_TYPE_SHIFT):
+ case (int) (FIELD_TYPE_INT64 >>> FIELD_TYPE_SHIFT):
+ case (int) (FIELD_TYPE_UINT64 >>> FIELD_TYPE_SHIFT):
+ case (int) (FIELD_TYPE_ENUM >>> FIELD_TYPE_SHIFT):
+ case (int) (FIELD_TYPE_BOOL >>> FIELD_TYPE_SHIFT):
+ mWireType = WIRE_TYPE_VARINT;
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Requested field id (" + getFieldIdString(fieldId)
+ + ") is not a packable field"
+ + dumpDebugData());
+ }
+ }
+ }
+
+
+ /**
+ * Check a field id constant against current field number
+ *
+ * @param fieldId - throws if fieldId does not match mFieldNumber
+ */
+ private void assertFieldNumber(long fieldId) {
+ if ((int) fieldId != mFieldNumber) {
+ throw new IllegalArgumentException("Requested field id (" + getFieldIdString(fieldId)
+ + ") does not match current field number (0x" + Integer.toHexString(
+ mFieldNumber)
+ + ") at offset 0x" + Integer.toHexString(getOffset())
+ + dumpDebugData());
+ }
+ }
+
+
+ /**
+ * Check a wire type against current wire type.
+ *
+ * @param wireType - throws if wireType does not match mWireType.
+ */
+ private void assertWireType(int wireType) {
+ if (wireType != mWireType) {
+ throw new WireTypeMismatchException(
+ "Current wire type " + getWireTypeString(mWireType)
+ + " does not match expected wire type " + getWireTypeString(wireType)
+ + " at offset 0x" + Integer.toHexString(getOffset())
+ + dumpDebugData());
+ }
+ }
+
+ /**
+ * Check if there is data ready to be read.
+ */
+ private void assertFreshData() {
+ if ((mState & STATE_STARTED_FIELD_READ) != STATE_STARTED_FIELD_READ) {
+ throw new ProtoParseException(
+ "Attempting to read already read field at offset 0x" + Integer.toHexString(
+ getOffset()) + dumpDebugData());
+ }
+ }
+
+ /**
+ * Dump debugging data about the buffer.
+ */
+ public String dumpDebugData() {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append("\nmFieldNumber : 0x" + Integer.toHexString(mFieldNumber));
+ sb.append("\nmWireType : 0x" + Integer.toHexString(mWireType));
+ sb.append("\nmState : 0x" + Integer.toHexString(mState));
+ sb.append("\nmDiscardedBytes : 0x" + Integer.toHexString(mDiscardedBytes));
+ sb.append("\nmOffset : 0x" + Integer.toHexString(mOffset));
+ sb.append("\nmExpectedObjectTokenStack : ");
+ if (mExpectedObjectTokenStack == null) {
+ sb.append("null");
+ } else {
+ sb.append(mExpectedObjectTokenStack);
+ }
+ sb.append("\nmDepth : 0x" + Integer.toHexString(mDepth));
+ sb.append("\nmBuffer : ");
+ if (mBuffer == null) {
+ sb.append("null");
+ } else {
+ sb.append(mBuffer);
+ }
+ sb.append("\nmBufferSize : 0x" + Integer.toHexString(mBufferSize));
+ sb.append("\nmEnd : 0x" + Integer.toHexString(mEnd));
+
+ return sb.toString();
+ }
+}
diff --git a/android/util/proto/ProtoOutputStream.java b/android/util/proto/ProtoOutputStream.java
new file mode 100644
index 0000000..5fcd38e
--- /dev/null
+++ b/android/util/proto/ProtoOutputStream.java
@@ -0,0 +1,2539 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.proto;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Class to write to a protobuf stream.
+ *
+ * <p>
+ * This API is not as convenient or type safe as the standard protobuf
+ * classes. If possible, the best recommended library is to use protobuf lite.
+ * However, in environments (such as the Android platform itself), a
+ * more memory efficient version is necessary.
+ *
+ * <p>Each write method takes an ID code from the protoc generated classes
+ * and the value to write. To make a nested object, call {@link #start(long)}
+ * and then {@link #end(long)} when you are done.
+ *
+ * <p>The ID codes have type information embedded into them, so if you call
+ * the incorrect function you will get an {@link IllegalArgumentException}.
+ *
+ * <p>To retrieve the encoded protobuf stream, call {@link #getBytes()}.
+ *
+ * stream as the top-level objects are finished.
+ *
+ */
+
+/* IMPLEMENTATION NOTES
+ *
+ * Because protobuf has inner values, and they are length prefixed, and
+ * those sizes themselves are stored with a variable length encoding, it
+ * is impossible to know how big an object will be in a single pass.
+ *
+ * The traditional way is to copy the in-memory representation of an object
+ * into the generated proto Message objects, do a traversal of those to
+ * cache the size, and then write the size-prefixed buffers.
+ *
+ * We are trying to avoid too much generated code here, but this class still
+ * needs to have a somewhat sane API. We can't have the multiple passes be
+ * done by the calling code. In addition, we want to avoid the memory high
+ * water mark of duplicating all of the values into the traditional in-memory
+ * Message objects. We need to find another way.
+ *
+ * So what we do here is to let the calling code write the data into a
+ * byte[] (actually a collection of them wrapped in the EncodedBuffer class),
+ * but not do the varint encoding of the sub-message sizes. Then, we do a
+ * recursive traversal of the buffer itself, calculating the sizes (which are
+ * then knowable, although still not the actual sizes in the buffer because of
+ * possible further nesting). Then we do a third pass, compacting the
+ * buffer and varint encoding the sizes.
+ *
+ * This gets us a relatively small number of fixed-size allocations,
+ * which is less likely to cause memory fragmentation or churn the GC, and
+ * the same number of data copies as we would have gotten with setting it
+ * field-by-field in generated code, and no code bloat from generated code.
+ * The final data copy is also done with System.arraycopy, which will be
+ * more efficient, in general, than doing the individual fields twice (as in
+ * the traditional way).
+ *
+ * To accomplish the multiple passes, whenever we write a
+ * WIRE_TYPE_LENGTH_DELIMITED field, we write the size occupied in our
+ * buffer as a fixed 32 bit int (called childRawSize), not a variable length
+ * one. We reserve another 32 bit slot for the computed size (called
+ * childEncodedSize). If we know the size up front, as we do for strings
+ * and byte[], then we also put that into childEncodedSize, if we don't, we
+ * write the negative of childRawSize, as a sentinel that we need to
+ * compute it during the second pass and recursively compact it during the
+ * third pass.
+ *
+ * Unsigned size varints can be up to five bytes long, but we reserve eight
+ * bytes for overhead, so we know that when we compact the buffer, there
+ * will always be space for the encoded varint.
+ *
+ * When we can figure out the size ahead of time, we do, in order
+ * to save overhead with recalculating it, and with the later arraycopy.
+ *
+ * During the period between when the caller has called #start, but
+ * not yet called #end, we maintain a linked list of the tokens
+ * returned by #start, stored in those 8 bytes of size storage space.
+ * We use that linked list of tokens to ensure that the caller has
+ * correctly matched pairs of #start and #end calls, and issue
+ * errors if they are not matched.
+ */
+public final class ProtoOutputStream extends ProtoStream {
+ /**
+ * @hide
+ */
+ public static final String TAG = "ProtoOutputStream";
+
+ /**
+ * Our buffer.
+ */
+ private EncodedBuffer mBuffer;
+
+ /**
+ * Our stream. If there is one.
+ */
+ private OutputStream mStream;
+
+ /**
+ * Current nesting depth of startObject calls.
+ */
+ private int mDepth;
+
+ /**
+ * An ID given to objects and returned in the token from startObject
+ * and stored in the buffer until endObject is called, where the two
+ * are checked.
+ *
+ * <p>Starts at -1 and becomes more negative, so the values
+ * aren't likely to alias with the size it will be overwritten with,
+ * which tend to be small, and we will be more likely to catch when
+ * the caller of endObject uses a stale token that they didn't intend
+ * to (e.g. copy and paste error).
+ */
+ private int mNextObjectId = -1;
+
+ /**
+ * The object token we are expecting in endObject.
+ *
+ * <p>If another call to startObject happens, this is written to that location, which gives
+ * us a stack, stored in the space for the as-yet unused size fields.
+ */
+ private long mExpectedObjectToken;
+
+ /**
+ * Index in mBuffer that we should start copying from on the next
+ * pass of compaction.
+ */
+ private int mCopyBegin;
+
+ /**
+ * Whether we've already compacted
+ */
+ private boolean mCompacted;
+
+ /**
+ * Construct a {@link ProtoOutputStream} with the default chunk size.
+ *
+ * <p>This is for an in-memory proto. The caller should use {@link #getBytes()} for the result.
+ */
+ public ProtoOutputStream() {
+ this(0);
+ }
+
+ /**
+ * Construct a {@link ProtoOutputStream with the given chunk size.
+ *
+ * <p>This is for an in-memory proto. The caller should use {@link #getBytes()} for the result.
+ */
+ public ProtoOutputStream(int chunkSize) {
+ mBuffer = new EncodedBuffer(chunkSize);
+ }
+
+ /**
+ * Construct a {@link ProtoOutputStream} that sits on top of an {@link OutputStream}.
+ *
+ * <p>The {@link #flush()} method must be called when done writing
+ * to flush any remaining data, although data *may* be written at intermediate
+ * points within the writing as well.
+ */
+ public ProtoOutputStream(@NonNull OutputStream stream) {
+ this();
+ mStream = stream;
+ }
+
+ /**
+ * Construct a {@link ProtoOutputStream} that sits on top of a {@link FileDescriptor}.
+ *
+ * <p>The {@link #flush()} method must be called when done writing
+ * to flush any remaining data, although data *may* be written at intermediate
+ * points within the writing as well.
+ *
+ * @hide
+ */
+ public ProtoOutputStream(@NonNull FileDescriptor fd) {
+ this(new FileOutputStream(fd));
+ }
+
+ /**
+ * Returns the total size of the data that has been written, after full
+ * protobuf encoding has occurred.
+ *
+ * @return the uncompressed buffer size
+ */
+ public int getRawSize() {
+ if (mCompacted) {
+ return getBytes().length;
+ } else {
+ return mBuffer.getSize();
+ }
+ }
+
+ /**
+ * Write a value for the given fieldId.
+ *
+ * <p>Will automatically convert for the following field types, and
+ * throw an exception for others: double, float, int32, int64, uint32, uint64,
+ * sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, enum.
+ *
+ * @param fieldId The field identifier constant from the generated class.
+ * @param val The value.
+ */
+ public void write(long fieldId, double val) {
+ assertNotCompacted();
+ final int id = (int)fieldId;
+
+ switch ((int)((fieldId & (FIELD_TYPE_MASK | FIELD_COUNT_MASK)) >> FIELD_TYPE_SHIFT)) {
+ // double
+ case (int)((FIELD_TYPE_DOUBLE | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeDoubleImpl(id, (double)val);
+ break;
+ case (int)((FIELD_TYPE_DOUBLE | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_DOUBLE | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedDoubleImpl(id, (double)val);
+ break;
+ // float
+ case (int)((FIELD_TYPE_FLOAT | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeFloatImpl(id, (float)val);
+ break;
+ case (int)((FIELD_TYPE_FLOAT | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_FLOAT | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedFloatImpl(id, (float)val);
+ break;
+ // int32
+ case (int)((FIELD_TYPE_INT32 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeInt32Impl(id, (int)val);
+ break;
+ case (int)((FIELD_TYPE_INT32 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_INT32 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedInt32Impl(id, (int)val);
+ break;
+ // int64
+ case (int)((FIELD_TYPE_INT64 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeInt64Impl(id, (long)val);
+ break;
+ case (int)((FIELD_TYPE_INT64 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_INT64 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedInt64Impl(id, (long)val);
+ break;
+ // uint32
+ case (int)((FIELD_TYPE_UINT32 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeUInt32Impl(id, (int)val);
+ break;
+ case (int)((FIELD_TYPE_UINT32 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_UINT32 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedUInt32Impl(id, (int)val);
+ break;
+ // uint64
+ case (int)((FIELD_TYPE_UINT64 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeUInt64Impl(id, (long)val);
+ break;
+ case (int)((FIELD_TYPE_UINT64 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_UINT64 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedUInt64Impl(id, (long)val);
+ break;
+ // sint32
+ case (int)((FIELD_TYPE_SINT32 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeSInt32Impl(id, (int)val);
+ break;
+ case (int)((FIELD_TYPE_SINT32 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_SINT32 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedSInt32Impl(id, (int)val);
+ break;
+ // sint64
+ case (int)((FIELD_TYPE_SINT64 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeSInt64Impl(id, (long)val);
+ break;
+ case (int)((FIELD_TYPE_SINT64 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_SINT64 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedSInt64Impl(id, (long)val);
+ break;
+ // fixed32
+ case (int)((FIELD_TYPE_FIXED32 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeFixed32Impl(id, (int)val);
+ break;
+ case (int)((FIELD_TYPE_FIXED32 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_FIXED32 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedFixed32Impl(id, (int)val);
+ break;
+ // fixed64
+ case (int)((FIELD_TYPE_FIXED64 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeFixed64Impl(id, (long)val);
+ break;
+ case (int)((FIELD_TYPE_FIXED64 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_FIXED64 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedFixed64Impl(id, (long)val);
+ break;
+ // sfixed32
+ case (int)((FIELD_TYPE_SFIXED32 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeSFixed32Impl(id, (int)val);
+ break;
+ case (int)((FIELD_TYPE_SFIXED32 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_SFIXED32 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedSFixed32Impl(id, (int)val);
+ break;
+ // sfixed64
+ case (int)((FIELD_TYPE_SFIXED64 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeSFixed64Impl(id, (long)val);
+ break;
+ case (int)((FIELD_TYPE_SFIXED64 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_SFIXED64 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedSFixed64Impl(id, (long)val);
+ break;
+ // bool
+ case (int)((FIELD_TYPE_BOOL | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeBoolImpl(id, val != 0);
+ break;
+ case (int)((FIELD_TYPE_BOOL | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_BOOL | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedBoolImpl(id, val != 0);
+ break;
+ // enum
+ case (int)((FIELD_TYPE_ENUM | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeEnumImpl(id, (int)val);
+ break;
+ case (int)((FIELD_TYPE_ENUM | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_ENUM | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedEnumImpl(id, (int)val);
+ break;
+ // string, bytes, object not allowed here.
+ default: {
+ throw new IllegalArgumentException("Attempt to call write(long, double) with "
+ + getFieldIdString(fieldId));
+ }
+ }
+ }
+
+ /**
+ * Write a value for the given fieldId.
+ *
+ * <p>Will automatically convert for the following field types, and
+ * throw an exception for others: double, float, int32, int64, uint32, uint64,
+ * sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, enum.
+ *
+ * @param fieldId The field identifier constant from the generated class.
+ * @param val The value.
+ */
+ public void write(long fieldId, float val) {
+ assertNotCompacted();
+ final int id = (int)fieldId;
+
+ switch ((int)((fieldId & (FIELD_TYPE_MASK | FIELD_COUNT_MASK)) >> FIELD_TYPE_SHIFT)) {
+ // double
+ case (int)((FIELD_TYPE_DOUBLE | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeDoubleImpl(id, (double)val);
+ break;
+ case (int)((FIELD_TYPE_DOUBLE | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_DOUBLE | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedDoubleImpl(id, (double)val);
+ break;
+ // float
+ case (int)((FIELD_TYPE_FLOAT | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeFloatImpl(id, (float)val);
+ break;
+ case (int)((FIELD_TYPE_FLOAT | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_FLOAT | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedFloatImpl(id, (float)val);
+ break;
+ // int32
+ case (int)((FIELD_TYPE_INT32 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeInt32Impl(id, (int)val);
+ break;
+ case (int)((FIELD_TYPE_INT32 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_INT32 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedInt32Impl(id, (int)val);
+ break;
+ // int64
+ case (int)((FIELD_TYPE_INT64 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeInt64Impl(id, (long)val);
+ break;
+ case (int)((FIELD_TYPE_INT64 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_INT64 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedInt64Impl(id, (long)val);
+ break;
+ // uint32
+ case (int)((FIELD_TYPE_UINT32 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeUInt32Impl(id, (int)val);
+ break;
+ case (int)((FIELD_TYPE_UINT32 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_UINT32 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedUInt32Impl(id, (int)val);
+ break;
+ // uint64
+ case (int)((FIELD_TYPE_UINT64 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeUInt64Impl(id, (long)val);
+ break;
+ case (int)((FIELD_TYPE_UINT64 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_UINT64 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedUInt64Impl(id, (long)val);
+ break;
+ // sint32
+ case (int)((FIELD_TYPE_SINT32 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeSInt32Impl(id, (int)val);
+ break;
+ case (int)((FIELD_TYPE_SINT32 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_SINT32 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedSInt32Impl(id, (int)val);
+ break;
+ // sint64
+ case (int)((FIELD_TYPE_SINT64 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeSInt64Impl(id, (long)val);
+ break;
+ case (int)((FIELD_TYPE_SINT64 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_SINT64 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedSInt64Impl(id, (long)val);
+ break;
+ // fixed32
+ case (int)((FIELD_TYPE_FIXED32 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeFixed32Impl(id, (int)val);
+ break;
+ case (int)((FIELD_TYPE_FIXED32 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_FIXED32 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedFixed32Impl(id, (int)val);
+ break;
+ // fixed64
+ case (int)((FIELD_TYPE_FIXED64 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeFixed64Impl(id, (long)val);
+ break;
+ case (int)((FIELD_TYPE_FIXED64 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_FIXED64 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedFixed64Impl(id, (long)val);
+ break;
+ // sfixed32
+ case (int)((FIELD_TYPE_SFIXED32 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeSFixed32Impl(id, (int)val);
+ break;
+ case (int)((FIELD_TYPE_SFIXED32 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_SFIXED32 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedSFixed32Impl(id, (int)val);
+ break;
+ // sfixed64
+ case (int)((FIELD_TYPE_SFIXED64 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeSFixed64Impl(id, (long)val);
+ break;
+ case (int)((FIELD_TYPE_SFIXED64 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_SFIXED64 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedSFixed64Impl(id, (long)val);
+ break;
+ // bool
+ case (int)((FIELD_TYPE_BOOL | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeBoolImpl(id, val != 0);
+ break;
+ case (int)((FIELD_TYPE_BOOL | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_BOOL | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedBoolImpl(id, val != 0);
+ break;
+ // enum
+ case (int)((FIELD_TYPE_ENUM | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeEnumImpl(id, (int)val);
+ break;
+ case (int)((FIELD_TYPE_ENUM | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_ENUM | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedEnumImpl(id, (int)val);
+ break;
+ // string, bytes, object not allowed here.
+ default: {
+ throw new IllegalArgumentException("Attempt to call write(long, float) with "
+ + getFieldIdString(fieldId));
+ }
+ }
+ }
+
+ /**
+ * Write a value for the given fieldId.
+ *
+ * <p>Will automatically convert for the following field types, and
+ * throw an exception for others: double, float, int32, int64, uint32, uint64,
+ * sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, enum.
+ *
+ * @param fieldId The field identifier constant from the generated class.
+ * @param val The value.
+ */
+ public void write(long fieldId, int val) {
+ assertNotCompacted();
+ final int id = (int)fieldId;
+
+ switch ((int)((fieldId & (FIELD_TYPE_MASK | FIELD_COUNT_MASK)) >> FIELD_TYPE_SHIFT)) {
+ // double
+ case (int)((FIELD_TYPE_DOUBLE | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeDoubleImpl(id, (double)val);
+ break;
+ case (int)((FIELD_TYPE_DOUBLE | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_DOUBLE | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedDoubleImpl(id, (double)val);
+ break;
+ // float
+ case (int)((FIELD_TYPE_FLOAT | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeFloatImpl(id, (float)val);
+ break;
+ case (int)((FIELD_TYPE_FLOAT | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_FLOAT | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedFloatImpl(id, (float)val);
+ break;
+ // int32
+ case (int)((FIELD_TYPE_INT32 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeInt32Impl(id, (int)val);
+ break;
+ case (int)((FIELD_TYPE_INT32 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_INT32 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedInt32Impl(id, (int)val);
+ break;
+ // int64
+ case (int)((FIELD_TYPE_INT64 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeInt64Impl(id, (long)val);
+ break;
+ case (int)((FIELD_TYPE_INT64 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_INT64 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedInt64Impl(id, (long)val);
+ break;
+ // uint32
+ case (int)((FIELD_TYPE_UINT32 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeUInt32Impl(id, (int)val);
+ break;
+ case (int)((FIELD_TYPE_UINT32 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_UINT32 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedUInt32Impl(id, (int)val);
+ break;
+ // uint64
+ case (int)((FIELD_TYPE_UINT64 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeUInt64Impl(id, (long)val);
+ break;
+ case (int)((FIELD_TYPE_UINT64 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_UINT64 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedUInt64Impl(id, (long)val);
+ break;
+ // sint32
+ case (int)((FIELD_TYPE_SINT32 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeSInt32Impl(id, (int)val);
+ break;
+ case (int)((FIELD_TYPE_SINT32 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_SINT32 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedSInt32Impl(id, (int)val);
+ break;
+ // sint64
+ case (int)((FIELD_TYPE_SINT64 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeSInt64Impl(id, (long)val);
+ break;
+ case (int)((FIELD_TYPE_SINT64 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_SINT64 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedSInt64Impl(id, (long)val);
+ break;
+ // fixed32
+ case (int)((FIELD_TYPE_FIXED32 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeFixed32Impl(id, (int)val);
+ break;
+ case (int)((FIELD_TYPE_FIXED32 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_FIXED32 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedFixed32Impl(id, (int)val);
+ break;
+ // fixed64
+ case (int)((FIELD_TYPE_FIXED64 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeFixed64Impl(id, (long)val);
+ break;
+ case (int)((FIELD_TYPE_FIXED64 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_FIXED64 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedFixed64Impl(id, (long)val);
+ break;
+ // sfixed32
+ case (int)((FIELD_TYPE_SFIXED32 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeSFixed32Impl(id, (int)val);
+ break;
+ case (int)((FIELD_TYPE_SFIXED32 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_SFIXED32 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedSFixed32Impl(id, (int)val);
+ break;
+ // sfixed64
+ case (int)((FIELD_TYPE_SFIXED64 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeSFixed64Impl(id, (long)val);
+ break;
+ case (int)((FIELD_TYPE_SFIXED64 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_SFIXED64 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedSFixed64Impl(id, (long)val);
+ break;
+ // bool
+ case (int)((FIELD_TYPE_BOOL | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeBoolImpl(id, val != 0);
+ break;
+ case (int)((FIELD_TYPE_BOOL | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_BOOL | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedBoolImpl(id, val != 0);
+ break;
+ // enum
+ case (int)((FIELD_TYPE_ENUM | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeEnumImpl(id, (int)val);
+ break;
+ case (int)((FIELD_TYPE_ENUM | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_ENUM | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedEnumImpl(id, (int)val);
+ break;
+ // string, bytes, object not allowed here.
+ default: {
+ throw new IllegalArgumentException("Attempt to call write(long, int) with "
+ + getFieldIdString(fieldId));
+ }
+ }
+ }
+
+ /**
+ * Write a value for the given fieldId.
+ *
+ * <p>Will automatically convert for the following field types, and
+ * throw an exception for others: double, float, int32, int64, uint32, uint64,
+ * sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, enum.
+ *
+ * @param fieldId The field identifier constant from the generated class.
+ * @param val The value.
+ */
+ public void write(long fieldId, long val) {
+ assertNotCompacted();
+ final int id = (int)fieldId;
+
+ switch ((int)((fieldId & (FIELD_TYPE_MASK | FIELD_COUNT_MASK)) >> FIELD_TYPE_SHIFT)) {
+ // double
+ case (int)((FIELD_TYPE_DOUBLE | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeDoubleImpl(id, (double)val);
+ break;
+ case (int)((FIELD_TYPE_DOUBLE | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_DOUBLE | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedDoubleImpl(id, (double)val);
+ break;
+ // float
+ case (int)((FIELD_TYPE_FLOAT | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeFloatImpl(id, (float)val);
+ break;
+ case (int)((FIELD_TYPE_FLOAT | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_FLOAT | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedFloatImpl(id, (float)val);
+ break;
+ // int32
+ case (int)((FIELD_TYPE_INT32 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeInt32Impl(id, (int)val);
+ break;
+ case (int)((FIELD_TYPE_INT32 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_INT32 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedInt32Impl(id, (int)val);
+ break;
+ // int64
+ case (int)((FIELD_TYPE_INT64 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeInt64Impl(id, (long)val);
+ break;
+ case (int)((FIELD_TYPE_INT64 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_INT64 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedInt64Impl(id, (long)val);
+ break;
+ // uint32
+ case (int)((FIELD_TYPE_UINT32 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeUInt32Impl(id, (int)val);
+ break;
+ case (int)((FIELD_TYPE_UINT32 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_UINT32 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedUInt32Impl(id, (int)val);
+ break;
+ // uint64
+ case (int)((FIELD_TYPE_UINT64 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeUInt64Impl(id, (long)val);
+ break;
+ case (int)((FIELD_TYPE_UINT64 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_UINT64 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedUInt64Impl(id, (long)val);
+ break;
+ // sint32
+ case (int)((FIELD_TYPE_SINT32 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeSInt32Impl(id, (int)val);
+ break;
+ case (int)((FIELD_TYPE_SINT32 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_SINT32 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedSInt32Impl(id, (int)val);
+ break;
+ // sint64
+ case (int)((FIELD_TYPE_SINT64 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeSInt64Impl(id, (long)val);
+ break;
+ case (int)((FIELD_TYPE_SINT64 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_SINT64 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedSInt64Impl(id, (long)val);
+ break;
+ // fixed32
+ case (int)((FIELD_TYPE_FIXED32 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeFixed32Impl(id, (int)val);
+ break;
+ case (int)((FIELD_TYPE_FIXED32 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_FIXED32 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedFixed32Impl(id, (int)val);
+ break;
+ // fixed64
+ case (int)((FIELD_TYPE_FIXED64 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeFixed64Impl(id, (long)val);
+ break;
+ case (int)((FIELD_TYPE_FIXED64 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_FIXED64 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedFixed64Impl(id, (long)val);
+ break;
+ // sfixed32
+ case (int)((FIELD_TYPE_SFIXED32 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeSFixed32Impl(id, (int)val);
+ break;
+ case (int)((FIELD_TYPE_SFIXED32 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_SFIXED32 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedSFixed32Impl(id, (int)val);
+ break;
+ // sfixed64
+ case (int)((FIELD_TYPE_SFIXED64 | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeSFixed64Impl(id, (long)val);
+ break;
+ case (int)((FIELD_TYPE_SFIXED64 | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_SFIXED64 | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedSFixed64Impl(id, (long)val);
+ break;
+ // bool
+ case (int)((FIELD_TYPE_BOOL | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeBoolImpl(id, val != 0);
+ break;
+ case (int)((FIELD_TYPE_BOOL | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_BOOL | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedBoolImpl(id, val != 0);
+ break;
+ // enum
+ case (int)((FIELD_TYPE_ENUM | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeEnumImpl(id, (int)val);
+ break;
+ case (int)((FIELD_TYPE_ENUM | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_ENUM | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedEnumImpl(id, (int)val);
+ break;
+ // string, bytes, object not allowed here.
+ default: {
+ throw new IllegalArgumentException("Attempt to call write(long, long) with "
+ + getFieldIdString(fieldId));
+ }
+ }
+ }
+
+ /**
+ * Write a boolean value for the given fieldId.
+ *
+ * <p>If the field is not a bool field, an {@link IllegalStateException} will be thrown.
+ *
+ * @param fieldId The field identifier constant from the generated class.
+ * @param val The value.
+ */
+ public void write(long fieldId, boolean val) {
+ assertNotCompacted();
+ final int id = (int)fieldId;
+
+ switch ((int)((fieldId & (FIELD_TYPE_MASK | FIELD_COUNT_MASK)) >> FIELD_TYPE_SHIFT)) {
+ // bool
+ case (int)((FIELD_TYPE_BOOL | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeBoolImpl(id, val);
+ break;
+ case (int)((FIELD_TYPE_BOOL | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_BOOL | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedBoolImpl(id, val);
+ break;
+ // nothing else allowed
+ default: {
+ throw new IllegalArgumentException("Attempt to call write(long, boolean) with "
+ + getFieldIdString(fieldId));
+ }
+ }
+ }
+
+ /**
+ * Write a string value for the given fieldId.
+ *
+ * <p>If the field is not a string field, an exception will be thrown.
+ *
+ * @param fieldId The field identifier constant from the generated class.
+ * @param val The value.
+ */
+ public void write(long fieldId, @Nullable String val) {
+ assertNotCompacted();
+ final int id = (int)fieldId;
+
+ switch ((int)((fieldId & (FIELD_TYPE_MASK | FIELD_COUNT_MASK)) >> FIELD_TYPE_SHIFT)) {
+ // string
+ case (int)((FIELD_TYPE_STRING | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeStringImpl(id, val);
+ break;
+ case (int)((FIELD_TYPE_STRING | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int)((FIELD_TYPE_STRING | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedStringImpl(id, val);
+ break;
+ // nothing else allowed
+ default: {
+ throw new IllegalArgumentException("Attempt to call write(long, String) with "
+ + getFieldIdString(fieldId));
+ }
+ }
+ }
+
+ /**
+ * Write a byte[] value for the given fieldId.
+ *
+ * <p>If the field is not a bytes or object field, an exception will be thrown.
+ *
+ * @param fieldId The field identifier constant from the generated class.
+ * @param val The value.
+ */
+ public void write(long fieldId, @Nullable byte[] val) {
+ assertNotCompacted();
+ final int id = (int)fieldId;
+
+ switch ((int) ((fieldId & (FIELD_TYPE_MASK | FIELD_COUNT_MASK)) >> FIELD_TYPE_SHIFT)) {
+ // bytes
+ case (int) ((FIELD_TYPE_BYTES | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeBytesImpl(id, val);
+ break;
+ case (int) ((FIELD_TYPE_BYTES | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int) ((FIELD_TYPE_BYTES | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedBytesImpl(id, val);
+ break;
+ // Object
+ case (int) ((FIELD_TYPE_MESSAGE | FIELD_COUNT_SINGLE) >> FIELD_TYPE_SHIFT):
+ writeObjectImpl(id, val);
+ break;
+ case (int) ((FIELD_TYPE_MESSAGE | FIELD_COUNT_REPEATED) >> FIELD_TYPE_SHIFT):
+ case (int) ((FIELD_TYPE_MESSAGE | FIELD_COUNT_PACKED) >> FIELD_TYPE_SHIFT):
+ writeRepeatedObjectImpl(id, val);
+ break;
+ // nothing else allowed
+ default: {
+ throw new IllegalArgumentException("Attempt to call write(long, byte[]) with "
+ + getFieldIdString(fieldId));
+ }
+ }
+ }
+
+ /**
+ * Start a sub object.
+ *
+ * @param fieldId The field identifier constant from the generated class.
+ * @return The token to call {@link #end(long)} with.
+ */
+ public long start(long fieldId) {
+ assertNotCompacted();
+ final int id = (int)fieldId;
+
+ if ((fieldId & FIELD_TYPE_MASK) == FIELD_TYPE_MESSAGE) {
+ final long count = fieldId & FIELD_COUNT_MASK;
+ if (count == FIELD_COUNT_SINGLE) {
+ return startObjectImpl(id, false);
+ } else if (count == FIELD_COUNT_REPEATED || count == FIELD_COUNT_PACKED) {
+ return startObjectImpl(id, true);
+ }
+ }
+ throw new IllegalArgumentException("Attempt to call start(long) with "
+ + getFieldIdString(fieldId));
+ }
+
+ /**
+ * End the object started by start() that returned token.
+ *
+ * @param token The token returned from {@link #start(long)}
+ */
+ public void end(long token) {
+ endObjectImpl(token, getRepeatedFromToken(token));
+ }
+
+ //
+ // proto3 type: double
+ // java type: double
+ // encoding: fixed64
+ // wire type: WIRE_TYPE_FIXED64
+ //
+
+ /**
+ * Write a single proto "double" type field value.
+ *
+ * @deprecated Use {@link #write(long, double)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeDouble(long fieldId, double val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_SINGLE | FIELD_TYPE_DOUBLE);
+
+ writeDoubleImpl(id, val);
+ }
+
+ private void writeDoubleImpl(int id, double val) {
+ if (val != 0) {
+ writeTag(id, WIRE_TYPE_FIXED64);
+ mBuffer.writeRawFixed64(Double.doubleToLongBits(val));
+ }
+ }
+
+ /**
+ * Write a single repeated proto "double" type field value.
+ *
+ * @deprecated Use {@link #write(long, double)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeRepeatedDouble(long fieldId, double val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_REPEATED | FIELD_TYPE_DOUBLE);
+
+ writeRepeatedDoubleImpl(id, val);
+ }
+
+ private void writeRepeatedDoubleImpl(int id, double val) {
+ writeTag(id, WIRE_TYPE_FIXED64);
+ mBuffer.writeRawFixed64(Double.doubleToLongBits(val));
+ }
+
+ /**
+ * Write a list of packed proto "double" type field values.
+ *
+ * @deprecated Use {@link #write(long, double)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writePackedDouble(long fieldId, @Nullable double[] val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_PACKED | FIELD_TYPE_DOUBLE);
+
+ final int N = val != null ? val.length : 0;
+ if (N > 0) {
+ writeKnownLengthHeader(id, N * 8);
+ for (int i=0; i<N; i++) {
+ mBuffer.writeRawFixed64(Double.doubleToLongBits(val[i]));
+ }
+ }
+ }
+
+ //
+ // proto3 type: float
+ // java type: float
+ // encoding: fixed32
+ // wire type: WIRE_TYPE_FIXED32
+ //
+
+ /**
+ * Write a single proto "float" type field value.
+ *
+ * @deprecated Use {@link #write(long, float)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeFloat(long fieldId, float val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_SINGLE | FIELD_TYPE_FLOAT);
+
+ writeFloatImpl(id, val);
+ }
+
+ private void writeFloatImpl(int id, float val) {
+ if (val != 0) {
+ writeTag(id, WIRE_TYPE_FIXED32);
+ mBuffer.writeRawFixed32(Float.floatToIntBits(val));
+ }
+ }
+
+ /**
+ * Write a single repeated proto "float" type field value.
+ *
+ * @deprecated Use {@link #write(long, float)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeRepeatedFloat(long fieldId, float val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_REPEATED | FIELD_TYPE_FLOAT);
+
+ writeRepeatedFloatImpl(id, val);
+ }
+
+ private void writeRepeatedFloatImpl(int id, float val) {
+ writeTag(id, WIRE_TYPE_FIXED32);
+ mBuffer.writeRawFixed32(Float.floatToIntBits(val));
+ }
+
+ /**
+ * Write a list of packed proto "float" type field value.
+ *
+ * @deprecated Use {@link #write(long, float)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writePackedFloat(long fieldId, @Nullable float[] val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_PACKED | FIELD_TYPE_FLOAT);
+
+ final int N = val != null ? val.length : 0;
+ if (N > 0) {
+ writeKnownLengthHeader(id, N * 4);
+ for (int i=0; i<N; i++) {
+ mBuffer.writeRawFixed32(Float.floatToIntBits(val[i]));
+ }
+ }
+ }
+
+ //
+ // proto3 type: int32
+ // java type: int
+ // signed/unsigned: signed
+ // encoding: varint
+ // wire type: WIRE_TYPE_VARINT
+ //
+
+ /**
+ * Writes a java int as an usigned varint.
+ *
+ * <p>The unadorned int32 type in protobuf is unfortunate because it
+ * is stored in memory as a signed value, but encodes as unsigned
+ * varints, which are formally always longs. So here, we encode
+ * negative values as 64 bits, which will get the sign-extension,
+ * and positive values as 32 bits, which saves a marginal amount
+ * of work in that it processes ints instead of longs.
+ */
+ private void writeUnsignedVarintFromSignedInt(int val) {
+ if (val >= 0) {
+ mBuffer.writeRawVarint32(val);
+ } else {
+ mBuffer.writeRawVarint64(val);
+ }
+ }
+
+ /**
+ * Write a single proto "int32" type field value.
+ *
+ * <p>Note that these are stored in memory as signed values and written as unsigned
+ * varints, which if negative, are 10 bytes long. If you know the data is likely
+ * to be negative, use "sint32".
+ *
+ * @deprecated Use {@link #write(long, int)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeInt32(long fieldId, int val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_SINGLE | FIELD_TYPE_INT32);
+
+ writeInt32Impl(id, val);
+ }
+
+ private void writeInt32Impl(int id, int val) {
+ if (val != 0) {
+ writeTag(id, WIRE_TYPE_VARINT);
+ writeUnsignedVarintFromSignedInt(val);
+ }
+ }
+
+ /**
+ * Write a single repeated proto "int32" type field value.
+ *
+ * <p>Note that these are stored in memory as signed values and written as unsigned
+ * varints, which if negative, are 10 bytes long. If you know the data is likely
+ * to be negative, use "sint32".
+ *
+ * @deprecated Use {@link #write(long, int)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeRepeatedInt32(long fieldId, int val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_REPEATED | FIELD_TYPE_INT32);
+
+ writeRepeatedInt32Impl(id, val);
+ }
+
+ private void writeRepeatedInt32Impl(int id, int val) {
+ writeTag(id, WIRE_TYPE_VARINT);
+ writeUnsignedVarintFromSignedInt(val);
+ }
+
+ /**
+ * Write a list of packed proto "int32" type field value.
+ *
+ * <p>Note that these are stored in memory as signed values and written as unsigned
+ * varints, which if negative, are 10 bytes long. If you know the data is likely
+ * to be negative, use "sint32".
+ *
+ * @deprecated Use {@link #write(long, int)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writePackedInt32(long fieldId, @Nullable int[] val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_PACKED | FIELD_TYPE_INT32);
+
+ final int N = val != null ? val.length : 0;
+ if (N > 0) {
+ int size = 0;
+ for (int i=0; i<N; i++) {
+ final int v = val[i];
+ size += v >= 0 ? EncodedBuffer.getRawVarint32Size(v) : 10;
+ }
+ writeKnownLengthHeader(id, size);
+ for (int i=0; i<N; i++) {
+ writeUnsignedVarintFromSignedInt(val[i]);
+ }
+ }
+ }
+
+ //
+ // proto3 type: int64
+ // java type: int
+ // signed/unsigned: signed
+ // encoding: varint
+ // wire type: WIRE_TYPE_VARINT
+ //
+
+ /**
+ * Write a single proto "int64" type field value.
+ *
+ * @deprecated Use {@link #write(long, long)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeInt64(long fieldId, long val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_SINGLE | FIELD_TYPE_INT64);
+
+ writeInt64Impl(id, val);
+ }
+
+ private void writeInt64Impl(int id, long val) {
+ if (val != 0) {
+ writeTag(id, WIRE_TYPE_VARINT);
+ mBuffer.writeRawVarint64(val);
+ }
+ }
+
+ /**
+ * Write a single repeated proto "int64" type field value.
+ *
+ * @deprecated Use {@link #write(long, long)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeRepeatedInt64(long fieldId, long val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_REPEATED | FIELD_TYPE_INT64);
+
+ writeRepeatedInt64Impl(id, val);
+ }
+
+ private void writeRepeatedInt64Impl(int id, long val) {
+ writeTag(id, WIRE_TYPE_VARINT);
+ mBuffer.writeRawVarint64(val);
+ }
+
+ /**
+ * Write a list of packed proto "int64" type field value.
+ *
+ * @deprecated Use {@link #write(long, long)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writePackedInt64(long fieldId, @Nullable long[] val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_PACKED | FIELD_TYPE_INT64);
+
+ final int N = val != null ? val.length : 0;
+ if (N > 0) {
+ int size = 0;
+ for (int i=0; i<N; i++) {
+ size += EncodedBuffer.getRawVarint64Size(val[i]);
+ }
+ writeKnownLengthHeader(id, size);
+ for (int i=0; i<N; i++) {
+ mBuffer.writeRawVarint64(val[i]);
+ }
+ }
+ }
+
+ //
+ // proto3 type: uint32
+ // java type: int
+ // signed/unsigned: unsigned
+ // encoding: varint
+ // wire type: WIRE_TYPE_VARINT
+ //
+
+ /**
+ * Write a single proto "uint32" type field value.
+ *
+ * @deprecated Use {@link #write(long, int)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeUInt32(long fieldId, int val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_SINGLE | FIELD_TYPE_UINT32);
+
+ writeUInt32Impl(id, val);
+ }
+
+ private void writeUInt32Impl(int id, int val) {
+ if (val != 0) {
+ writeTag(id, WIRE_TYPE_VARINT);
+ mBuffer.writeRawVarint32(val);
+ }
+ }
+
+ /**
+ * Write a single repeated proto "uint32" type field value.
+ *
+ * @deprecated Use {@link #write(long, int)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeRepeatedUInt32(long fieldId, int val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_REPEATED | FIELD_TYPE_UINT32);
+
+ writeRepeatedUInt32Impl(id, val);
+ }
+
+ private void writeRepeatedUInt32Impl(int id, int val) {
+ writeTag(id, WIRE_TYPE_VARINT);
+ mBuffer.writeRawVarint32(val);
+ }
+
+ /**
+ * Write a list of packed proto "uint32" type field value.
+ *
+ * @deprecated Use {@link #write(long, int)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writePackedUInt32(long fieldId, @Nullable int[] val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_PACKED | FIELD_TYPE_UINT32);
+
+ final int N = val != null ? val.length : 0;
+ if (N > 0) {
+ int size = 0;
+ for (int i=0; i<N; i++) {
+ size += EncodedBuffer.getRawVarint32Size(val[i]);
+ }
+ writeKnownLengthHeader(id, size);
+ for (int i=0; i<N; i++) {
+ mBuffer.writeRawVarint32(val[i]);
+ }
+ }
+ }
+
+ //
+ // proto3 type: uint64
+ // java type: int
+ // signed/unsigned: unsigned
+ // encoding: varint
+ // wire type: WIRE_TYPE_VARINT
+ //
+
+ /**
+ * Write a single proto "uint64" type field value.
+ *
+ * @deprecated Use {@link #write(long, long)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeUInt64(long fieldId, long val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_SINGLE | FIELD_TYPE_UINT64);
+
+ writeUInt64Impl(id, val);
+ }
+
+ private void writeUInt64Impl(int id, long val) {
+ if (val != 0) {
+ writeTag(id, WIRE_TYPE_VARINT);
+ mBuffer.writeRawVarint64(val);
+ }
+ }
+
+ /**
+ * Write a single proto "uint64" type field value.
+ *
+ * @deprecated Use {@link #write(long, long)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeRepeatedUInt64(long fieldId, long val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_REPEATED | FIELD_TYPE_UINT64);
+
+ writeRepeatedUInt64Impl(id, val);
+ }
+
+ private void writeRepeatedUInt64Impl(int id, long val) {
+ writeTag(id, WIRE_TYPE_VARINT);
+ mBuffer.writeRawVarint64(val);
+ }
+
+ /**
+ * Write a single proto "uint64" type field value.
+ *
+ * @deprecated Use {@link #write(long, long)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writePackedUInt64(long fieldId, @Nullable long[] val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_PACKED | FIELD_TYPE_UINT64);
+
+ final int N = val != null ? val.length : 0;
+ if (N > 0) {
+ int size = 0;
+ for (int i=0; i<N; i++) {
+ size += EncodedBuffer.getRawVarint64Size(val[i]);
+ }
+ writeKnownLengthHeader(id, size);
+ for (int i=0; i<N; i++) {
+ mBuffer.writeRawVarint64(val[i]);
+ }
+ }
+ }
+
+ //
+ // proto3 type: sint32
+ // java type: int
+ // signed/unsigned: signed
+ // encoding: zig-zag
+ // wire type: WIRE_TYPE_VARINT
+ //
+
+ /**
+ * Write a single proto "sint32" type field value.
+ *
+ * @deprecated Use {@link #write(long, int)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeSInt32(long fieldId, int val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_SINGLE | FIELD_TYPE_SINT32);
+
+ writeSInt32Impl(id, val);
+ }
+
+ private void writeSInt32Impl(int id, int val) {
+ if (val != 0) {
+ writeTag(id, WIRE_TYPE_VARINT);
+ mBuffer.writeRawZigZag32(val);
+ }
+ }
+
+ /**
+ * Write a single repeated proto "sint32" type field value.
+ *
+ * @deprecated Use {@link #write(long, int)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeRepeatedSInt32(long fieldId, int val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_REPEATED | FIELD_TYPE_SINT32);
+
+ writeRepeatedSInt32Impl(id, val);
+ }
+
+ private void writeRepeatedSInt32Impl(int id, int val) {
+ writeTag(id, WIRE_TYPE_VARINT);
+ mBuffer.writeRawZigZag32(val);
+ }
+
+ /**
+ * Write a list of packed proto "sint32" type field value.
+ *
+ * @deprecated Use {@link #write(long, int)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writePackedSInt32(long fieldId, @Nullable int[] val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_PACKED | FIELD_TYPE_SINT32);
+
+ final int N = val != null ? val.length : 0;
+ if (N > 0) {
+ int size = 0;
+ for (int i=0; i<N; i++) {
+ size += EncodedBuffer.getRawZigZag32Size(val[i]);
+ }
+ writeKnownLengthHeader(id, size);
+ for (int i=0; i<N; i++) {
+ mBuffer.writeRawZigZag32(val[i]);
+ }
+ }
+ }
+
+ //
+ // proto3 type: sint64
+ // java type: int
+ // signed/unsigned: signed
+ // encoding: zig-zag
+ // wire type: WIRE_TYPE_VARINT
+ //
+
+ /**
+ * Write a single proto "sint64" type field value.
+ *
+ * @deprecated Use {@link #write(long, long)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeSInt64(long fieldId, long val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_SINGLE | FIELD_TYPE_SINT64);
+
+ writeSInt64Impl(id, val);
+ }
+
+ private void writeSInt64Impl(int id, long val) {
+ if (val != 0) {
+ writeTag(id, WIRE_TYPE_VARINT);
+ mBuffer.writeRawZigZag64(val);
+ }
+ }
+
+ /**
+ * Write a single repeated proto "sint64" type field value.
+ *
+ * @deprecated Use {@link #write(long, long)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeRepeatedSInt64(long fieldId, long val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_REPEATED | FIELD_TYPE_SINT64);
+
+ writeRepeatedSInt64Impl(id, val);
+ }
+
+ private void writeRepeatedSInt64Impl(int id, long val) {
+ writeTag(id, WIRE_TYPE_VARINT);
+ mBuffer.writeRawZigZag64(val);
+ }
+
+ /**
+ * Write a list of packed proto "sint64" type field value.
+ *
+ * @deprecated Use {@link #write(long, long)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writePackedSInt64(long fieldId, @Nullable long[] val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_PACKED | FIELD_TYPE_SINT64);
+
+ final int N = val != null ? val.length : 0;
+ if (N > 0) {
+ int size = 0;
+ for (int i=0; i<N; i++) {
+ size += EncodedBuffer.getRawZigZag64Size(val[i]);
+ }
+ writeKnownLengthHeader(id, size);
+ for (int i=0; i<N; i++) {
+ mBuffer.writeRawZigZag64(val[i]);
+ }
+ }
+ }
+
+ //
+ // proto3 type: fixed32
+ // java type: int
+ // encoding: little endian
+ // wire type: WIRE_TYPE_FIXED32
+ //
+
+ /**
+ * Write a single proto "fixed32" type field value.
+ *
+ * @deprecated Use {@link #write(long, int)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeFixed32(long fieldId, int val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_SINGLE | FIELD_TYPE_FIXED32);
+
+ writeFixed32Impl(id, val);
+ }
+
+ private void writeFixed32Impl(int id, int val) {
+ if (val != 0) {
+ writeTag(id, WIRE_TYPE_FIXED32);
+ mBuffer.writeRawFixed32(val);
+ }
+ }
+
+ /**
+ * Write a single repeated proto "fixed32" type field value.
+ *
+ * @deprecated Use {@link #write(long, int)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeRepeatedFixed32(long fieldId, int val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_REPEATED | FIELD_TYPE_FIXED32);
+
+ writeRepeatedFixed32Impl(id, val);
+ }
+
+ private void writeRepeatedFixed32Impl(int id, int val) {
+ writeTag(id, WIRE_TYPE_FIXED32);
+ mBuffer.writeRawFixed32(val);
+ }
+
+ /**
+ * Write a list of packed proto "fixed32" type field value.
+ *
+ * @deprecated Use {@link #write(long, int)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writePackedFixed32(long fieldId, @Nullable int[] val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_PACKED | FIELD_TYPE_FIXED32);
+
+ final int N = val != null ? val.length : 0;
+ if (N > 0) {
+ writeKnownLengthHeader(id, N * 4);
+ for (int i=0; i<N; i++) {
+ mBuffer.writeRawFixed32(val[i]);
+ }
+ }
+ }
+
+ //
+ // proto3 type: fixed64
+ // java type: long
+ // encoding: fixed64
+ // wire type: WIRE_TYPE_FIXED64
+ //
+
+ /**
+ * Write a single proto "fixed64" type field value.
+ *
+ * @deprecated Use {@link #write(long, long)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeFixed64(long fieldId, long val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_SINGLE | FIELD_TYPE_FIXED64);
+
+ writeFixed64Impl(id, val);
+ }
+
+ private void writeFixed64Impl(int id, long val) {
+ if (val != 0) {
+ writeTag(id, WIRE_TYPE_FIXED64);
+ mBuffer.writeRawFixed64(val);
+ }
+ }
+
+ /**
+ * Write a single repeated proto "fixed64" type field value.
+ *
+ * @deprecated Use {@link #write(long, long)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeRepeatedFixed64(long fieldId, long val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_REPEATED | FIELD_TYPE_FIXED64);
+
+ writeRepeatedFixed64Impl(id, val);
+ }
+
+ private void writeRepeatedFixed64Impl(int id, long val) {
+ writeTag(id, WIRE_TYPE_FIXED64);
+ mBuffer.writeRawFixed64(val);
+ }
+
+ /**
+ * Write a list of packed proto "fixed64" type field value.
+ *
+ * @deprecated Use {@link #write(long, long)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writePackedFixed64(long fieldId, @Nullable long[] val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_PACKED | FIELD_TYPE_FIXED64);
+
+ final int N = val != null ? val.length : 0;
+ if (N > 0) {
+ writeKnownLengthHeader(id, N * 8);
+ for (int i=0; i<N; i++) {
+ mBuffer.writeRawFixed64(val[i]);
+ }
+ }
+ }
+
+ //
+ // proto3 type: sfixed32
+ // java type: int
+ // encoding: little endian
+ // wire type: WIRE_TYPE_FIXED32
+ //
+ /**
+ * Write a single proto "sfixed32" type field value.
+ *
+ * @deprecated Use {@link #write(long, int)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeSFixed32(long fieldId, int val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_SINGLE | FIELD_TYPE_SFIXED32);
+
+ writeSFixed32Impl(id, val);
+ }
+
+ private void writeSFixed32Impl(int id, int val) {
+ if (val != 0) {
+ writeTag(id, WIRE_TYPE_FIXED32);
+ mBuffer.writeRawFixed32(val);
+ }
+ }
+
+ /**
+ * Write a single repeated proto "sfixed32" type field value.
+ *
+ * @deprecated Use {@link #write(long, int)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeRepeatedSFixed32(long fieldId, int val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_REPEATED | FIELD_TYPE_SFIXED32);
+
+ writeRepeatedSFixed32Impl(id, val);
+ }
+
+ private void writeRepeatedSFixed32Impl(int id, int val) {
+ writeTag(id, WIRE_TYPE_FIXED32);
+ mBuffer.writeRawFixed32(val);
+ }
+
+ /**
+ * Write a list of packed proto "sfixed32" type field value.
+ *
+ * @deprecated Use {@link #write(long, int)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writePackedSFixed32(long fieldId, @Nullable int[] val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_PACKED | FIELD_TYPE_SFIXED32);
+
+ final int N = val != null ? val.length : 0;
+ if (N > 0) {
+ writeKnownLengthHeader(id, N * 4);
+ for (int i=0; i<N; i++) {
+ mBuffer.writeRawFixed32(val[i]);
+ }
+ }
+ }
+
+ //
+ // proto3 type: sfixed64
+ // java type: long
+ // encoding: little endian
+ // wire type: WIRE_TYPE_FIXED64
+ //
+
+ /**
+ * Write a single proto "sfixed64" type field value.
+ *
+ * @deprecated Use {@link #write(long, long)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeSFixed64(long fieldId, long val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_SINGLE | FIELD_TYPE_SFIXED64);
+
+ writeSFixed64Impl(id, val);
+ }
+
+ private void writeSFixed64Impl(int id, long val) {
+ if (val != 0) {
+ writeTag(id, WIRE_TYPE_FIXED64);
+ mBuffer.writeRawFixed64(val);
+ }
+ }
+
+ /**
+ * Write a single repeated proto "sfixed64" type field value.
+ *
+ * @deprecated Use {@link #write(long, long)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeRepeatedSFixed64(long fieldId, long val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_REPEATED | FIELD_TYPE_SFIXED64);
+
+ writeRepeatedSFixed64Impl(id, val);
+ }
+
+ private void writeRepeatedSFixed64Impl(int id, long val) {
+ writeTag(id, WIRE_TYPE_FIXED64);
+ mBuffer.writeRawFixed64(val);
+ }
+
+ /**
+ * Write a list of packed proto "sfixed64" type field value.
+ *
+ * @deprecated Use {@link #write(long, long)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writePackedSFixed64(long fieldId, @Nullable long[] val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_PACKED | FIELD_TYPE_SFIXED64);
+
+ final int N = val != null ? val.length : 0;
+ if (N > 0) {
+ writeKnownLengthHeader(id, N * 8);
+ for (int i=0; i<N; i++) {
+ mBuffer.writeRawFixed64(val[i]);
+ }
+ }
+ }
+
+ //
+ // proto3 type: bool
+ // java type: boolean
+ // encoding: varint
+ // wire type: WIRE_TYPE_VARINT
+ //
+
+ /**
+ * Write a single proto "bool" type field value.
+ *
+ * @deprecated Use {@link #write(long, boolean)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeBool(long fieldId, boolean val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_SINGLE | FIELD_TYPE_BOOL);
+
+ writeBoolImpl(id, val);
+ }
+
+ private void writeBoolImpl(int id, boolean val) {
+ if (val) {
+ writeTag(id, WIRE_TYPE_VARINT);
+ // 0 and 1 are the same as their varint counterparts
+ mBuffer.writeRawByte((byte)1);
+ }
+ }
+
+ /**
+ * Write a single repeated proto "bool" type field value.
+ *
+ * @deprecated Use {@link #write(long, boolean)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeRepeatedBool(long fieldId, boolean val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_REPEATED | FIELD_TYPE_BOOL);
+
+ writeRepeatedBoolImpl(id, val);
+ }
+
+ private void writeRepeatedBoolImpl(int id, boolean val) {
+ writeTag(id, WIRE_TYPE_VARINT);
+ mBuffer.writeRawByte((byte)(val ? 1 : 0));
+ }
+
+ /**
+ * Write a list of packed proto "bool" type field value.
+ *
+ * @deprecated Use {@link #write(long, boolean)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writePackedBool(long fieldId, @Nullable boolean[] val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_PACKED | FIELD_TYPE_BOOL);
+
+ final int N = val != null ? val.length : 0;
+ if (N > 0) {
+ // Write the header
+ writeKnownLengthHeader(id, N);
+
+ // Write the data
+ for (int i=0; i<N; i++) {
+ // 0 and 1 are the same as their varint counterparts
+ mBuffer.writeRawByte((byte)(val[i] ? 1 : 0));
+ }
+ }
+ }
+
+ //
+ // proto3 type: string
+ // java type: String
+ // encoding: utf-8
+ // wire type: WIRE_TYPE_LENGTH_DELIMITED
+ //
+
+ /**
+ * Write a single proto "string" type field value.
+ *
+ * @deprecated Use {@link #write(long, String)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeString(long fieldId, @Nullable String val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_SINGLE | FIELD_TYPE_STRING);
+
+ writeStringImpl(id, val);
+ }
+
+ private void writeStringImpl(int id, String val) {
+ if (val != null && val.length() > 0) {
+ writeUtf8String(id, val);
+ }
+ }
+
+ /**
+ * Write a single repeated proto "string" type field value.
+ *
+ * @deprecated Use {@link #write(long, String)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeRepeatedString(long fieldId, @Nullable String val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_REPEATED | FIELD_TYPE_STRING);
+
+ writeRepeatedStringImpl(id, val);
+ }
+
+ private void writeRepeatedStringImpl(int id, String val) {
+ if (val == null || val.length() == 0) {
+ writeKnownLengthHeader(id, 0);
+ } else {
+ writeUtf8String(id, val);
+ }
+ }
+
+ /**
+ * Write a list of packed proto "string" type field value.
+ */
+ private void writeUtf8String(int id, String val) {
+ // TODO: Is it worth converting by hand in order to not allocate?
+ try {
+ final byte[] buf = val.getBytes("UTF-8");
+ writeKnownLengthHeader(id, buf.length);
+ mBuffer.writeRawBuffer(buf);
+ } catch (UnsupportedEncodingException ex) {
+ throw new RuntimeException("not possible");
+ }
+ }
+
+ //
+ // proto3 type: bytes
+ // java type: byte[]
+ // encoding: varint
+ // wire type: WIRE_TYPE_VARINT
+ //
+
+ /**
+ * Write a single proto "bytes" type field value.
+ *
+ * @deprecated Use {@link #write(long, byte[])} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeBytes(long fieldId, @Nullable byte[] val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_SINGLE | FIELD_TYPE_BYTES);
+
+ writeBytesImpl(id, val);
+ }
+
+ private void writeBytesImpl(int id, byte[] val) {
+ if (val != null && val.length > 0) {
+ writeKnownLengthHeader(id, val.length);
+ mBuffer.writeRawBuffer(val);
+ }
+ }
+
+ /**
+ * Write a single repeated proto "bytes" type field value.
+ *
+ * @deprecated Use {@link #write(long, byte[])} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeRepeatedBytes(long fieldId, @Nullable byte[] val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_REPEATED | FIELD_TYPE_BYTES);
+
+ writeRepeatedBytesImpl(id, val);
+ }
+
+ private void writeRepeatedBytesImpl(int id, byte[] val) {
+ writeKnownLengthHeader(id, val == null ? 0 : val.length);
+ mBuffer.writeRawBuffer(val);
+ }
+
+ //
+ // proto3 type: enum
+ // java type: int
+ // signed/unsigned: unsigned
+ // encoding: varint
+ // wire type: WIRE_TYPE_VARINT
+ //
+
+ /**
+ * Write a single proto enum type field value.
+ *
+ * @deprecated Use {@link #write(long, int)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeEnum(long fieldId, int val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_SINGLE | FIELD_TYPE_ENUM);
+
+ writeEnumImpl(id, val);
+ }
+
+ private void writeEnumImpl(int id, int val) {
+ if (val != 0) {
+ writeTag(id, WIRE_TYPE_VARINT);
+ writeUnsignedVarintFromSignedInt(val);
+ }
+ }
+
+ /**
+ * Write a single repeated proto enum type field value.
+ *
+ * @deprecated Use {@link #write(long, int)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeRepeatedEnum(long fieldId, int val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_REPEATED | FIELD_TYPE_ENUM);
+
+ writeRepeatedEnumImpl(id, val);
+ }
+
+ private void writeRepeatedEnumImpl(int id, int val) {
+ writeTag(id, WIRE_TYPE_VARINT);
+ writeUnsignedVarintFromSignedInt(val);
+ }
+
+ /**
+ * Write a list of packed proto enum type field value.
+ *
+ * @deprecated Use {@link #write(long, int)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writePackedEnum(long fieldId, @Nullable int[] val) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_PACKED | FIELD_TYPE_ENUM);
+
+ final int N = val != null ? val.length : 0;
+ if (N > 0) {
+ int size = 0;
+ for (int i=0; i<N; i++) {
+ final int v = val[i];
+ size += v >= 0 ? EncodedBuffer.getRawVarint32Size(v) : 10;
+ }
+ writeKnownLengthHeader(id, size);
+ for (int i=0; i<N; i++) {
+ writeUnsignedVarintFromSignedInt(val[i]);
+ }
+ }
+ }
+
+
+ /**
+ * Start a child object.
+ *
+ * Returns a token which should be passed to endObject. Calls to endObject must be
+ * nested properly.
+ *
+ * @deprecated Use {@link #start(long)} instead.
+ * @hide
+ */
+ @Deprecated
+ public long startObject(long fieldId) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_SINGLE | FIELD_TYPE_MESSAGE);
+
+ return startObjectImpl(id, false);
+ }
+
+ /**
+ * End a child object. Pass in the token from the correspoinding startObject call.
+ *
+ * @deprecated Use {@link #end(long)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void endObject(long token) {
+ assertNotCompacted();
+
+ endObjectImpl(token, false);
+ }
+
+ /**
+ * Start a repeated child object.
+ *
+ * Returns a token which should be passed to endObject. Calls to endObject must be
+ * nested properly.
+ *
+ * @deprecated Use {@link #start(long)} instead.
+ * @hide
+ */
+ @Deprecated
+ public long startRepeatedObject(long fieldId) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_REPEATED | FIELD_TYPE_MESSAGE);
+
+ return startObjectImpl(id, true);
+ }
+
+ /**
+ * End a child object. Pass in the token from the correspoinding startRepeatedObject call.
+ *
+ * @deprecated Use {@link #end(long)} instead.
+ * @hide
+ */
+ @Deprecated
+ public void endRepeatedObject(long token) {
+ assertNotCompacted();
+
+ endObjectImpl(token, true);
+ }
+
+ /**
+ * Common implementation of startObject and startRepeatedObject.
+ */
+ private long startObjectImpl(final int id, boolean repeated) {
+ writeTag(id, WIRE_TYPE_LENGTH_DELIMITED);
+ final int sizePos = mBuffer.getWritePos();
+ mDepth++;
+ mNextObjectId--;
+
+ // Write the previous token, giving us a stack of expected tokens.
+ // After endObject returns, the first fixed32 becomeschildRawSize (set in endObject)
+ // and the second one becomes childEncodedSize (set in editEncodedSize).
+ mBuffer.writeRawFixed32((int)(mExpectedObjectToken >> 32));
+ mBuffer.writeRawFixed32((int)mExpectedObjectToken);
+
+ long old = mExpectedObjectToken;
+
+ mExpectedObjectToken = makeToken(getTagSize(id), repeated, mDepth, mNextObjectId, sizePos);
+ return mExpectedObjectToken;
+ }
+
+ /**
+ * Common implementation of endObject and endRepeatedObject.
+ */
+ private void endObjectImpl(long token, boolean repeated) {
+ // The upper 32 bits of the token is the depth of startObject /
+ // endObject calls. We could get aritrarily sophisticated, but
+ // that's enough to prevent the common error of missing an
+ // endObject somewhere.
+ // The lower 32 bits of the token is the offset in the buffer
+ // at which to write the size.
+ final int depth = getDepthFromToken(token);
+ final boolean expectedRepeated = getRepeatedFromToken(token);
+ final int sizePos = getOffsetFromToken(token);
+ final int childRawSize = mBuffer.getWritePos() - sizePos - 8;
+
+ if (repeated != expectedRepeated) {
+ if (repeated) {
+ throw new IllegalArgumentException("endRepeatedObject called where endObject should"
+ + " have been");
+ } else {
+ throw new IllegalArgumentException("endObject called where endRepeatedObject should"
+ + " have been");
+ }
+ }
+
+ // Check that we're getting the token and depth that we are expecting.
+ if ((mDepth & 0x01ff) != depth || mExpectedObjectToken != token) {
+ // This text of exception is united tested. That test also implicity checks
+ // that we're tracking the objectIds and depths correctly.
+ throw new IllegalArgumentException("Mismatched startObject/endObject calls."
+ + " Current depth " + mDepth
+ + " token=" + token2String(token)
+ + " expectedToken=" + token2String(mExpectedObjectToken));
+ }
+
+ // Get the next expected token that we stashed away in the buffer.
+ mExpectedObjectToken = (((long)mBuffer.getRawFixed32At(sizePos)) << 32)
+ | (0x0ffffffffL & (long)mBuffer.getRawFixed32At(sizePos+4));
+
+ mDepth--;
+ if (childRawSize > 0) {
+ mBuffer.editRawFixed32(sizePos, -childRawSize);
+ mBuffer.editRawFixed32(sizePos+4, -1);
+ } else if (repeated) {
+ mBuffer.editRawFixed32(sizePos, 0);
+ mBuffer.editRawFixed32(sizePos+4, 0);
+ } else {
+ // The object has no data. Don't include it.
+ mBuffer.rewindWriteTo(sizePos - getTagSizeFromToken(token));
+ }
+ }
+
+ /**
+ * Write an object that has already been flattened.
+ *
+ * @deprecated Use {@link #write(long, byte[])} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeObject(long fieldId, @Nullable byte[] value) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_SINGLE | FIELD_TYPE_MESSAGE);
+
+ writeObjectImpl(id, value);
+ }
+
+ void writeObjectImpl(int id, byte[] value) {
+ if (value != null && value.length != 0) {
+ writeKnownLengthHeader(id, value.length);
+ mBuffer.writeRawBuffer(value);
+ }
+ }
+
+ /**
+ * Write an object that has already been flattened.
+ *
+ * @deprecated Use {@link #write(long, byte[])} instead.
+ * @hide
+ */
+ @Deprecated
+ public void writeRepeatedObject(long fieldId, @Nullable byte[] value) {
+ assertNotCompacted();
+ final int id = checkFieldId(fieldId, FIELD_COUNT_REPEATED | FIELD_TYPE_MESSAGE);
+
+ writeRepeatedObjectImpl(id, value);
+ }
+
+ void writeRepeatedObjectImpl(int id, byte[] value) {
+ writeKnownLengthHeader(id, value == null ? 0 : value.length);
+ mBuffer.writeRawBuffer(value);
+ }
+
+ //
+ // Tags
+ //
+
+ /**
+ * Combine a fieldId (the field keys in the proto file) and the field flags.
+ * Mostly useful for testing because the generated code contains the fieldId
+ * constants.
+ */
+ public static long makeFieldId(int id, long fieldFlags) {
+ return fieldFlags | (((long)id) & 0x0ffffffffL);
+ }
+
+ /**
+ * Validates that the fieldId provided is of the type and count from expectedType.
+ *
+ * <p>The type must match exactly to pass this check.
+ *
+ * <p>The count must match according to this truth table to pass the check:
+ *
+ * expectedFlags
+ * UNKNOWN SINGLE REPEATED PACKED
+ * fieldId
+ * UNKNOWN true false false false
+ * SINGLE x true false false
+ * REPEATED x false true false
+ * PACKED x false true true
+ *
+ * @throws {@link IllegalArgumentException} if it is not.
+ *
+ * @return The raw ID of that field.
+ */
+ public static int checkFieldId(long fieldId, long expectedFlags) {
+ final long fieldCount = fieldId & FIELD_COUNT_MASK;
+ final long fieldType = fieldId & FIELD_TYPE_MASK;
+ final long expectedCount = expectedFlags & FIELD_COUNT_MASK;
+ final long expectedType = expectedFlags & FIELD_TYPE_MASK;
+ if (((int)fieldId) == 0) {
+ throw new IllegalArgumentException("Invalid proto field " + (int)fieldId
+ + " fieldId=" + Long.toHexString(fieldId));
+ }
+ if (fieldType != expectedType
+ || !((fieldCount == expectedCount)
+ || (fieldCount == FIELD_COUNT_PACKED
+ && expectedCount == FIELD_COUNT_REPEATED))) {
+ final String countString = getFieldCountString(fieldCount);
+ final String typeString = getFieldTypeString(fieldType);
+ if (typeString != null && countString != null) {
+ final StringBuilder sb = new StringBuilder();
+ if (expectedType == FIELD_TYPE_MESSAGE) {
+ sb.append("start");
+ } else {
+ sb.append("write");
+ }
+ sb.append(getFieldCountString(expectedCount));
+ sb.append(getFieldTypeString(expectedType));
+ sb.append(" called for field ");
+ sb.append((int)fieldId);
+ sb.append(" which should be used with ");
+ if (fieldType == FIELD_TYPE_MESSAGE) {
+ sb.append("start");
+ } else {
+ sb.append("write");
+ }
+ sb.append(countString);
+ sb.append(typeString);
+ if (fieldCount == FIELD_COUNT_PACKED) {
+ sb.append(" or writeRepeated");
+ sb.append(typeString);
+ }
+ sb.append('.');
+ throw new IllegalArgumentException(sb.toString());
+ } else {
+ final StringBuilder sb = new StringBuilder();
+ if (expectedType == FIELD_TYPE_MESSAGE) {
+ sb.append("start");
+ } else {
+ sb.append("write");
+ }
+ sb.append(getFieldCountString(expectedCount));
+ sb.append(getFieldTypeString(expectedType));
+ sb.append(" called with an invalid fieldId: 0x");
+ sb.append(Long.toHexString(fieldId));
+ sb.append(". The proto field ID might be ");
+ sb.append((int)fieldId);
+ sb.append('.');
+ throw new IllegalArgumentException(sb.toString());
+ }
+ }
+ return (int)fieldId;
+ }
+
+ /**
+ * Return how many bytes an encoded field tag will require.
+ */
+ private static int getTagSize(int id) {
+ return EncodedBuffer.getRawVarint32Size(id << FIELD_ID_SHIFT);
+ }
+
+ /**
+ * Write an individual field tag by hand.
+ *
+ * See <a href="https://developers.google.com/protocol-buffers/docs/encoding">Protobuf
+ * Encoding</a> for details on the structure of how tags and data are written.
+ */
+ public void writeTag(int id, @WireType int wireType) {
+ mBuffer.writeRawVarint32((id << FIELD_ID_SHIFT) | wireType);
+ }
+
+ /**
+ * Write the header of a WIRE_TYPE_LENGTH_DELIMITED field for one where
+ * we know the size in advance and do not need to compute and compact.
+ */
+ private void writeKnownLengthHeader(int id, int size) {
+ // Write the tag
+ writeTag(id, WIRE_TYPE_LENGTH_DELIMITED);
+ // Size will be compacted later, but we know the size, so write it,
+ // once for the rawSize and once for the encodedSize.
+ mBuffer.writeRawFixed32(size);
+ mBuffer.writeRawFixed32(size);
+ }
+
+ //
+ // Getting the buffer and compaction
+ //
+
+ /**
+ * Assert that the compact call has not already occured.
+ *
+ * TODO: Will change when we add the OutputStream version of ProtoOutputStream.
+ */
+ private void assertNotCompacted() {
+ if (mCompacted) {
+ throw new IllegalArgumentException("write called after compact");
+ }
+ }
+
+ /**
+ * Finish the encoding of the data, and return a byte[] with
+ * the protobuf formatted data.
+ *
+ * <p>After this call, do not call any of the write* functions. The
+ * behavior is undefined.
+ */
+ public @NonNull byte[] getBytes() {
+ compactIfNecessary();
+
+ return mBuffer.getBytes(mBuffer.getReadableSize());
+ }
+
+ /**
+ * If the buffer hasn't already had the nested object size fields compacted
+ * and turned into an actual protobuf format, then do so.
+ */
+ private void compactIfNecessary() {
+ if (!mCompacted) {
+ if (mDepth != 0) {
+ throw new IllegalArgumentException("Trying to compact with " + mDepth
+ + " missing calls to endObject");
+ }
+
+ // The buffer must be compacted.
+ mBuffer.startEditing();
+ final int readableSize = mBuffer.getReadableSize();
+
+ // Cache the sizes of the objects
+ editEncodedSize(readableSize);
+
+ // Re-write the buffer with the sizes as proper varints instead
+ // of pairs of uint32s. We know this will always fit in the same
+ // buffer because the pair of uint32s is exactly 8 bytes long, and
+ // the single varint size will be no more than 5 bytes long.
+ mBuffer.rewindRead();
+ compactSizes(readableSize);
+
+ // If there is any data left over that wasn't copied yet, copy it.
+ if (mCopyBegin < readableSize) {
+ mBuffer.writeFromThisBuffer(mCopyBegin, readableSize - mCopyBegin);
+ }
+
+ // Set the new readableSize
+ mBuffer.startEditing();
+
+ // It's not valid to write to this object anymore. The write
+ // pointers are off, and then some of the data would be compacted
+ // and some not.
+ mCompacted = true;
+ }
+ }
+
+ /**
+ * First compaction pass. Iterate through the data, and fill in the
+ * nested object sizes so the next pass can compact them.
+ */
+ private int editEncodedSize(int rawSize) {
+ int objectStart = mBuffer.getReadPos();
+ int objectEnd = objectStart + rawSize;
+ int encodedSize = 0;
+ int tagPos;
+
+ while ((tagPos = mBuffer.getReadPos()) < objectEnd) {
+ int tag = readRawTag();
+ encodedSize += EncodedBuffer.getRawVarint32Size(tag);
+
+ final int wireType = tag & WIRE_TYPE_MASK;
+ switch (wireType) {
+ case WIRE_TYPE_VARINT:
+ encodedSize++;
+ while ((mBuffer.readRawByte() & 0x80) != 0) {
+ encodedSize++;
+ }
+ break;
+ case WIRE_TYPE_FIXED64:
+ encodedSize += 8;
+ mBuffer.skipRead(8);
+ break;
+ case WIRE_TYPE_LENGTH_DELIMITED: {
+ // This object is not of a fixed-size type. So we need to figure
+ // out how big it should be.
+ final int childRawSize = mBuffer.readRawFixed32();
+ final int childEncodedSizePos = mBuffer.getReadPos();
+ int childEncodedSize = mBuffer.readRawFixed32();
+ if (childRawSize >= 0) {
+ // We know the size, just skip ahead.
+ if (childEncodedSize != childRawSize) {
+ throw new RuntimeException("Pre-computed size where the"
+ + " precomputed size and the raw size in the buffer"
+ + " don't match! childRawSize=" + childRawSize
+ + " childEncodedSize=" + childEncodedSize
+ + " childEncodedSizePos=" + childEncodedSizePos);
+ }
+ mBuffer.skipRead(childRawSize);
+ } else {
+ // We need to compute the size. Recurse.
+ childEncodedSize = editEncodedSize(-childRawSize);
+ mBuffer.editRawFixed32(childEncodedSizePos, childEncodedSize);
+ }
+ encodedSize += EncodedBuffer.getRawVarint32Size(childEncodedSize)
+ + childEncodedSize;
+ break;
+ }
+ case WIRE_TYPE_START_GROUP:
+ case WIRE_TYPE_END_GROUP:
+ throw new RuntimeException("groups not supported at index " + tagPos);
+ case WIRE_TYPE_FIXED32:
+ encodedSize += 4;
+ mBuffer.skipRead(4);
+ break;
+ default:
+ throw new ProtoParseException("editEncodedSize Bad tag tag=0x"
+ + Integer.toHexString(tag) + " wireType=" + wireType
+ + " -- " + mBuffer.getDebugString());
+ }
+ }
+
+ return encodedSize;
+ }
+
+ /**
+ * Second compaction pass. Iterate through the data, and copy the data
+ * forward in the buffer, converting the pairs of uint32s into a single
+ * unsigned varint of the size.
+ */
+ private void compactSizes(int rawSize) {
+ int objectStart = mBuffer.getReadPos();
+ int objectEnd = objectStart + rawSize;
+ int tagPos;
+ while ((tagPos = mBuffer.getReadPos()) < objectEnd) {
+ int tag = readRawTag();
+
+ // For all the non-length-delimited field types, just skip over them,
+ // and we'll just System.arraycopy it later, either in the case for
+ // WIRE_TYPE_LENGTH_DELIMITED or at the top of the stack in compactIfNecessary().
+ final int wireType = tag & WIRE_TYPE_MASK;
+ switch (wireType) {
+ case WIRE_TYPE_VARINT:
+ while ((mBuffer.readRawByte() & 0x80) != 0) { }
+ break;
+ case WIRE_TYPE_FIXED64:
+ mBuffer.skipRead(8);
+ break;
+ case WIRE_TYPE_LENGTH_DELIMITED: {
+ // Copy everything up to now, including the tag for this field.
+ mBuffer.writeFromThisBuffer(mCopyBegin, mBuffer.getReadPos() - mCopyBegin);
+ // Write the new size.
+ final int childRawSize = mBuffer.readRawFixed32();
+ final int childEncodedSize = mBuffer.readRawFixed32();
+ mBuffer.writeRawVarint32(childEncodedSize);
+ // Next time, start copying from here.
+ mCopyBegin = mBuffer.getReadPos();
+ if (childRawSize >= 0) {
+ // This is raw data, not an object. Skip ahead by the size.
+ // Recurse into the child
+ mBuffer.skipRead(childEncodedSize);
+ } else {
+ compactSizes(-childRawSize);
+ }
+ break;
+ // TODO: What does regular proto do if the object would be 0 size
+ // (e.g. if it is all default values).
+ }
+ case WIRE_TYPE_START_GROUP:
+ case WIRE_TYPE_END_GROUP:
+ throw new RuntimeException("groups not supported at index " + tagPos);
+ case WIRE_TYPE_FIXED32:
+ mBuffer.skipRead(4);
+ break;
+ default:
+ throw new ProtoParseException("compactSizes Bad tag tag=0x"
+ + Integer.toHexString(tag) + " wireType=" + wireType
+ + " -- " + mBuffer.getDebugString());
+ }
+ }
+ }
+
+ /**
+ * Write remaining data to the output stream. If there is no output stream,
+ * this function does nothing. Any currently open objects (i.e. ones that
+ * have not had {@link #end(long)} called for them will not be written). Whether this
+ * writes objects that are closed if there are remaining open objects is
+ * undefined (current implementation does not write it, future ones will).
+ * For now, can either call {@link #getBytes()} or {@link #flush()}, but not both.
+ */
+ public void flush() {
+ if (mStream == null) {
+ return;
+ }
+ if (mDepth != 0) {
+ // TODO: The compacting code isn't ready yet to compact unless we're done.
+ // TODO: Fix that.
+ return;
+ }
+ if (mCompacted) {
+ // If we're compacted, we already wrote it finished.
+ return;
+ }
+ compactIfNecessary();
+ final byte[] data = mBuffer.getBytes(mBuffer.getReadableSize());
+ try {
+ mStream.write(data);
+ mStream.flush();
+ } catch (IOException ex) {
+ throw new RuntimeException("Error flushing proto to stream", ex);
+ }
+ }
+
+ /**
+ * Read a raw tag from the buffer.
+ */
+ private int readRawTag() {
+ if (mBuffer.getReadPos() == mBuffer.getReadableSize()) {
+ return 0;
+ }
+ return (int)mBuffer.readRawUnsigned();
+ }
+
+ /**
+ * Dump debugging data about the buffers with the given log tag.
+ */
+ public void dump(@NonNull String tag) {
+ Log.d(tag, mBuffer.getDebugString());
+ mBuffer.dumpBuffers(tag);
+ }
+}
diff --git a/android/util/proto/ProtoParseException.java b/android/util/proto/ProtoParseException.java
new file mode 100644
index 0000000..5ba9bf8
--- /dev/null
+++ b/android/util/proto/ProtoParseException.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.proto;
+
+import android.annotation.TestApi;
+
+/**
+ * Thrown when there is an error parsing protobuf data.
+ *
+ * @hide
+ */
+@TestApi
+public class ProtoParseException extends RuntimeException {
+
+ /**
+ * Construct a ProtoParseException.
+ *
+ * @param msg The message.
+ */
+ public ProtoParseException(String msg) {
+ super(msg);
+ }
+}
+
diff --git a/android/util/proto/ProtoStream.java b/android/util/proto/ProtoStream.java
new file mode 100644
index 0000000..1940da9
--- /dev/null
+++ b/android/util/proto/ProtoStream.java
@@ -0,0 +1,636 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.proto;
+
+import android.annotation.IntDef;
+import android.annotation.LongDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Base utility class for protobuf streams.
+ *
+ * Contains a set of constants and methods used in generated code for
+ * {@link ProtoOutputStream}.
+ *
+ * @hide
+ */
+public class ProtoStream {
+
+ /**
+ * A protobuf wire type. All application-level types are represented using
+ * varint, fixed64, length-delimited and fixed32 wire types. The start-group
+ * and end-group types are unused in modern protobuf versions (proto2 and proto3),
+ * but are included here for completeness.
+ *
+ * @see <a href="https://developers.google.com/protocol-buffers/docs/encoding">Protobuf
+ * Encoding</a>
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ WIRE_TYPE_VARINT,
+ WIRE_TYPE_FIXED64,
+ WIRE_TYPE_LENGTH_DELIMITED,
+ WIRE_TYPE_START_GROUP,
+ WIRE_TYPE_END_GROUP,
+ WIRE_TYPE_FIXED32
+ })
+ public @interface WireType {}
+
+ /**
+ * Application-level protobuf field types, as would be used in a .proto file.
+ *
+ * @see <a href="https://developers.google.com/protocol-buffers/docs/encoding">Protobuf
+ * Encoding</a>
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @LongDef({
+ FIELD_TYPE_UNKNOWN,
+ FIELD_TYPE_DOUBLE,
+ FIELD_TYPE_FLOAT,
+ FIELD_TYPE_INT64,
+ FIELD_TYPE_UINT64,
+ FIELD_TYPE_INT32,
+ FIELD_TYPE_FIXED64,
+ FIELD_TYPE_FIXED32,
+ FIELD_TYPE_BOOL,
+ FIELD_TYPE_STRING,
+ FIELD_TYPE_MESSAGE,
+ FIELD_TYPE_BYTES,
+ FIELD_TYPE_UINT32,
+ FIELD_TYPE_ENUM,
+ FIELD_TYPE_SFIXED32,
+ FIELD_TYPE_SFIXED64,
+ FIELD_TYPE_SINT32,
+ FIELD_TYPE_SINT64,
+ })
+ public @interface FieldType {}
+
+
+ /**
+ * Represents the cardinality of a protobuf field.
+ *
+ * @see <a href="https://developers.google.com/protocol-buffers/docs/encoding">Protobuf
+ * Encoding</a>
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @LongDef({
+ FIELD_COUNT_UNKNOWN,
+ FIELD_COUNT_SINGLE,
+ FIELD_COUNT_REPEATED,
+ FIELD_COUNT_PACKED,
+ })
+ public @interface FieldCount {}
+
+ /**
+ * Number of bits to shift the field number to form a tag.
+ *
+ * <pre>
+ * // Reading a field number from a tag.
+ * int fieldNumber = tag >>> FIELD_ID_SHIFT;
+ *
+ * // Building a tag from a field number and a wire type.
+ * int tag = (fieldNumber << FIELD_ID_SHIFT) | wireType;
+ * </pre>
+ *
+ * @see <a href="https://developers.google.com/protocol-buffers/docs/encoding">Protobuf
+ * Encoding</a>
+ */
+ public static final int FIELD_ID_SHIFT = 3;
+
+ /**
+ * Mask to select the wire type from a tag.
+ *
+ * <pre>
+ * // Reading a wire type from a tag.
+ * int wireType = tag & WIRE_TYPE_MASK;
+ *
+ * // Building a tag from a field number and a wire type.
+ * int tag = (fieldNumber << FIELD_ID_SHIFT) | wireType;
+ * </pre>
+ *
+ * @see <a href="https://developers.google.com/protocol-buffers/docs/encoding">Protobuf
+ * Encoding</a>
+ */
+ public static final int WIRE_TYPE_MASK = (1 << FIELD_ID_SHIFT) - 1;
+
+ /**
+ * Mask to select the field id from a tag.
+ * @hide (not used by anything, and not actually useful, because you also want
+ * to shift when you mask the field id).
+ */
+ public static final int FIELD_ID_MASK = ~WIRE_TYPE_MASK;
+
+ /**
+ * Varint wire type code.
+ *
+ * @see <a href="https://developers.google.com/protocol-buffers/docs/encoding">Protobuf
+ * Encoding</a>
+ */
+ public static final int WIRE_TYPE_VARINT = 0;
+
+ /**
+ * Fixed64 wire type code.
+ *
+ * @see <a href="https://developers.google.com/protocol-buffers/docs/encoding">Protobuf
+ * Encoding</a>
+ */
+ public static final int WIRE_TYPE_FIXED64 = 1;
+
+ /**
+ * Length delimited wire type code.
+ *
+ * @see <a href="https://developers.google.com/protocol-buffers/docs/encoding">Protobuf
+ * Encoding</a>
+ */
+ public static final int WIRE_TYPE_LENGTH_DELIMITED = 2;
+
+ /**
+ * Start group wire type code.
+ *
+ * @see <a href="https://developers.google.com/protocol-buffers/docs/encoding">Protobuf
+ * Encoding</a>
+ */
+ public static final int WIRE_TYPE_START_GROUP = 3;
+
+ /**
+ * End group wire type code.
+ *
+ * @see <a href="https://developers.google.com/protocol-buffers/docs/encoding">Protobuf
+ * Encoding</a>
+ */
+ public static final int WIRE_TYPE_END_GROUP = 4;
+
+ /**
+ * Fixed32 wire type code.
+ *
+ * @see <a href="https://developers.google.com/protocol-buffers/docs/encoding">Protobuf
+ * Encoding</a>
+ */
+ public static final int WIRE_TYPE_FIXED32 = 5;
+
+ /**
+ * Position of the field type in a (long) fieldId.
+ */
+ public static final int FIELD_TYPE_SHIFT = 32;
+
+ /**
+ * Mask for the field types stored in a fieldId. Leaves a whole
+ * byte for future expansion, even though there are currently only 17 types.
+ */
+ public static final long FIELD_TYPE_MASK = 0x0ffL << FIELD_TYPE_SHIFT;
+
+ /**
+ * Not a real field type.
+ * @hide
+ */
+ public static final long FIELD_TYPE_UNKNOWN = 0;
+
+
+ /*
+ * The FIELD_TYPE_ constants are copied from
+ * external/protobuf/src/google/protobuf/descriptor.h directly, so no
+ * extra mapping needs to be maintained in this case.
+ */
+
+ /**
+ * Field type code for double fields. Used to build constants in generated
+ * code for use with the {@link ProtoOutputStream#write(long, double)
+ * ProtoOutputStream.write(long, double)} method.
+ */
+ public static final long FIELD_TYPE_DOUBLE = 1L << FIELD_TYPE_SHIFT;
+
+ /**
+ * Field type code for float fields. Used to build constants in generated
+ * code for use with the {@link ProtoOutputStream#write(long, float)
+ * ProtoOutputStream.write(long, float)} method.
+ */
+ public static final long FIELD_TYPE_FLOAT = 2L << FIELD_TYPE_SHIFT;
+
+ /**
+ * Field type code for int64 fields. Used to build constants in generated
+ * code for use with the {@link ProtoOutputStream#write(long, long)
+ * ProtoOutputStream.write(long, long)} method.
+ */
+ public static final long FIELD_TYPE_INT64 = 3L << FIELD_TYPE_SHIFT;
+
+ /**
+ * Field type code for uint64 fields. Used to build constants in generated
+ * code for use with the {@link ProtoOutputStream#write(long, long)
+ * ProtoOutputStream.write(long, long)} method.
+ */
+ public static final long FIELD_TYPE_UINT64 = 4L << FIELD_TYPE_SHIFT;
+
+ /**
+ * Field type code for int32 fields. Used to build constants in generated
+ * code for use with the {@link ProtoOutputStream#write(long, int)
+ * ProtoOutputStream.write(long, int)} method.
+ */
+ public static final long FIELD_TYPE_INT32 = 5L << FIELD_TYPE_SHIFT;
+
+ /**
+ * Field type code for fixed64 fields. Used to build constants in generated
+ * code for use with the {@link ProtoOutputStream#write(long, long)
+ * ProtoOutputStream.write(long, long)} method.
+ */
+ public static final long FIELD_TYPE_FIXED64 = 6L << FIELD_TYPE_SHIFT;
+
+ /**
+ * Field type code for fixed32 fields. Used to build constants in generated
+ * code for use with the {@link ProtoOutputStream#write(long, int)
+ * ProtoOutputStream.write(long, int)} method.
+ */
+
+ /**
+ * Field type code for fixed32 fields. Used to build constants in generated
+ * code for use with the {@link ProtoOutputStream#write(long, int)
+ * ProtoOutputStream.write(long, int)} method.
+ */
+ public static final long FIELD_TYPE_FIXED32 = 7L << FIELD_TYPE_SHIFT;
+
+ /**
+ * Field type code for bool fields. Used to build constants in generated
+ * code for use with the {@link ProtoOutputStream#write(long, boolean)
+ * ProtoOutputStream.write(long, boolean)} method.
+ */
+ public static final long FIELD_TYPE_BOOL = 8L << FIELD_TYPE_SHIFT;
+
+ /**
+ * Field type code for string fields. Used to build constants in generated
+ * code for use with the {@link ProtoOutputStream#write(long, String)
+ * ProtoOutputStream.write(long, String)} method.
+ */
+ public static final long FIELD_TYPE_STRING = 9L << FIELD_TYPE_SHIFT;
+
+ // public static final long FIELD_TYPE_GROUP = 10L << FIELD_TYPE_SHIFT; // Deprecated.
+
+ /**
+ * Field type code for message fields. Used to build constants in generated
+ * code for use with the {@link ProtoOutputStream#start(long)
+ * ProtoOutputStream.start(long)} method.
+ */
+ public static final long FIELD_TYPE_MESSAGE = 11L << FIELD_TYPE_SHIFT;
+
+ /**
+ * Field type code for bytes fields. Used to build constants in generated
+ * code for use with the {@link ProtoOutputStream#write(long, byte[])
+ * ProtoOutputStream.write(long, byte[])} method.
+ */
+ public static final long FIELD_TYPE_BYTES = 12L << FIELD_TYPE_SHIFT;
+
+ /**
+ * Field type code for uint32 fields. Used to build constants in generated
+ * code for use with the {@link ProtoOutputStream#write(long, int)
+ * ProtoOutputStream.write(long, int)} method.
+ */
+ public static final long FIELD_TYPE_UINT32 = 13L << FIELD_TYPE_SHIFT;
+
+ /**
+ * Field type code for enum fields. Used to build constants in generated
+ * code for use with the {@link ProtoOutputStream#write(long, int)
+ * ProtoOutputStream.write(long, int)} method.
+ */
+ public static final long FIELD_TYPE_ENUM = 14L << FIELD_TYPE_SHIFT;
+
+ /**
+ * Field type code for sfixed32 fields. Used to build constants in generated
+ * code for use with the {@link ProtoOutputStream#write(long, int)
+ * ProtoOutputStream.write(long, int)} method.
+ */
+ public static final long FIELD_TYPE_SFIXED32 = 15L << FIELD_TYPE_SHIFT;
+
+ /**
+ * Field type code for sfixed64 fields. Used to build constants in generated
+ * code for use with the {@link ProtoOutputStream#write(long, long)
+ * ProtoOutputStream.write(long, long)} method.
+ */
+ public static final long FIELD_TYPE_SFIXED64 = 16L << FIELD_TYPE_SHIFT;
+
+ /**
+ * Field type code for sint32 fields. Used to build constants in generated
+ * code for use with the {@link ProtoOutputStream#write(long, int)
+ * ProtoOutputStream.write(long, int)} method.
+ */
+ public static final long FIELD_TYPE_SINT32 = 17L << FIELD_TYPE_SHIFT;
+
+ /**
+ * Field type code for sint64 fields. Used to build constants in generated
+ * code for use with the {@link ProtoOutputStream#write(long, long)
+ * ProtoOutputStream.write(long, long)} method.
+ */
+ public static final long FIELD_TYPE_SINT64 = 18L << FIELD_TYPE_SHIFT;
+
+ private static final @NonNull String[] FIELD_TYPE_NAMES = new String[]{
+ "Double",
+ "Float",
+ "Int64",
+ "UInt64",
+ "Int32",
+ "Fixed64",
+ "Fixed32",
+ "Bool",
+ "String",
+ "Group", // This field is deprecated but reserved here for indexing.
+ "Message",
+ "Bytes",
+ "UInt32",
+ "Enum",
+ "SFixed32",
+ "SFixed64",
+ "SInt32",
+ "SInt64",
+ };
+
+ //
+ // FieldId flags for whether the field is single, repeated or packed.
+ //
+ /**
+ * Bit offset for building a field id to be used with a
+ * <code>{@link ProtoOutputStream}.write(...)</code>.
+ *
+ * @see #FIELD_COUNT_MASK
+ * @see #FIELD_COUNT_UNKNOWN
+ * @see #FIELD_COUNT_SINGLE
+ * @see #FIELD_COUNT_REPEATED
+ * @see #FIELD_COUNT_PACKED
+ */
+ public static final int FIELD_COUNT_SHIFT = 40;
+
+ /**
+ * Bit mask for selecting the field count when reading a field id that
+ * is used with a <code>{@link ProtoOutputStream}.write(...)</code> method.
+ *
+ * @see #FIELD_COUNT_SHIFT
+ * @see #FIELD_COUNT_MASK
+ * @see #FIELD_COUNT_UNKNOWN
+ * @see #FIELD_COUNT_SINGLE
+ * @see #FIELD_COUNT_REPEATED
+ * @see #FIELD_COUNT_PACKED
+ * @see <a href="https://developers.google.com/protocol-buffers/docs/encoding">Protobuf
+ * Encoding</a>
+ */
+ public static final long FIELD_COUNT_MASK = 0x0fL << FIELD_COUNT_SHIFT;
+
+ /**
+ * Unknown field count, encoded into a field id used with a
+ * <code>{@link ProtoOutputStream}.write(...)</code> method.
+ *
+ * @see #FIELD_COUNT_SHIFT
+ * @see #FIELD_COUNT_MASK
+ * @see #FIELD_COUNT_SINGLE
+ * @see #FIELD_COUNT_REPEATED
+ * @see #FIELD_COUNT_PACKED
+ * @see <a href="https://developers.google.com/protocol-buffers/docs/encoding">Protobuf
+ * Encoding</a>
+ */
+ public static final long FIELD_COUNT_UNKNOWN = 0;
+
+ /**
+ * Single field count, encoded into a field id used with a
+ * <code>{@link ProtoOutputStream}.write(...)</code> method.
+ *
+ * @see #FIELD_COUNT_SHIFT
+ * @see #FIELD_COUNT_MASK
+ * @see #FIELD_COUNT_UNKNOWN
+ * @see #FIELD_COUNT_REPEATED
+ * @see #FIELD_COUNT_PACKED
+ * @see <a href="https://developers.google.com/protocol-buffers/docs/encoding">Protobuf
+ * Encoding</a>
+ */
+ public static final long FIELD_COUNT_SINGLE = 1L << FIELD_COUNT_SHIFT;
+
+ /**
+ * Repeated field count, encoded into a field id used with a
+ * <code>{@link ProtoOutputStream}.write(...)</code> method.
+ *
+ * @see #FIELD_COUNT_SHIFT
+ * @see #FIELD_COUNT_MASK
+ * @see #FIELD_COUNT_UNKNOWN
+ * @see #FIELD_COUNT_SINGLE
+ * @see #FIELD_COUNT_PACKED
+ * @see <a href="https://developers.google.com/protocol-buffers/docs/encoding">Protobuf
+ * Encoding</a>
+ */
+ public static final long FIELD_COUNT_REPEATED = 2L << FIELD_COUNT_SHIFT;
+
+ /**
+ * Repeated packed field count, encoded into a field id used with a
+ * <code>{@link ProtoOutputStream}.write(...)</code> method.
+ *
+ * @see #FIELD_COUNT_SHIFT
+ * @see #FIELD_COUNT_MASK
+ * @see #FIELD_COUNT_UNKNOWN
+ * @see #FIELD_COUNT_SINGLE
+ * @see #FIELD_COUNT_REPEATED
+ * @see <a href="https://developers.google.com/protocol-buffers/docs/encoding">Protobuf
+ * Encoding</a>
+ */
+ public static final long FIELD_COUNT_PACKED = 5L << FIELD_COUNT_SHIFT;
+
+
+ /**
+ * Get the developer-usable name of a field type.
+ */
+ public static @Nullable String getFieldTypeString(@FieldType long fieldType) {
+ int index = ((int) ((fieldType & FIELD_TYPE_MASK) >>> FIELD_TYPE_SHIFT)) - 1;
+ if (index >= 0 && index < FIELD_TYPE_NAMES.length) {
+ return FIELD_TYPE_NAMES[index];
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Get the developer-usable name of a field count.
+ */
+ public static @Nullable String getFieldCountString(long fieldCount) {
+ if (fieldCount == FIELD_COUNT_SINGLE) {
+ return "";
+ } else if (fieldCount == FIELD_COUNT_REPEATED) {
+ return "Repeated";
+ } else if (fieldCount == FIELD_COUNT_PACKED) {
+ return "Packed";
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Get the developer-usable name of a wire type.
+ */
+ public static @Nullable String getWireTypeString(@WireType int wireType) {
+ switch (wireType) {
+ case WIRE_TYPE_VARINT:
+ return "Varint";
+ case WIRE_TYPE_FIXED64:
+ return "Fixed64";
+ case WIRE_TYPE_LENGTH_DELIMITED:
+ return "Length Delimited";
+ case WIRE_TYPE_START_GROUP:
+ return "Start Group";
+ case WIRE_TYPE_END_GROUP:
+ return "End Group";
+ case WIRE_TYPE_FIXED32:
+ return "Fixed32";
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Get a debug string for a fieldId.
+ */
+ public static @NonNull String getFieldIdString(long fieldId) {
+ final long fieldCount = fieldId & FIELD_COUNT_MASK;
+ String countString = getFieldCountString(fieldCount);
+ if (countString == null) {
+ countString = "fieldCount=" + fieldCount;
+ }
+ if (countString.length() > 0) {
+ countString += " ";
+ }
+
+ final long fieldType = fieldId & FIELD_TYPE_MASK;
+ String typeString = getFieldTypeString(fieldType);
+ if (typeString == null) {
+ typeString = "fieldType=" + fieldType;
+ }
+
+ return countString + typeString + " tag=" + ((int) fieldId)
+ + " fieldId=0x" + Long.toHexString(fieldId);
+ }
+
+ /**
+ * Combine a fieldId (the field keys in the proto file) and the field flags.
+ * Mostly useful for testing because the generated code contains the fieldId
+ * constants.
+ */
+ public static long makeFieldId(int id, long fieldFlags) {
+ return fieldFlags | (((long) id) & 0x0ffffffffL);
+ }
+
+ //
+ // Child objects
+ //
+
+ /**
+ * Make a token.
+ * Bits 61-63 - tag size (So we can go backwards later if the object had not data)
+ * - 3 bits, max value 7, max value needed 5
+ * Bit 60 - true if the object is repeated (lets us require endObject or endRepeatedObject)
+ * Bits 59-51 - depth (For error checking)
+ * - 9 bits, max value 512, when checking, value is masked (if we really
+ * are more than 512 levels deep)
+ * Bits 32-50 - objectId (For error checking)
+ * - 19 bits, max value 524,288. that's a lot of objects. IDs will wrap
+ * because of the overflow, and only the tokens are compared.
+ * Bits 0-31 - offset of interest for the object.
+ */
+ public static long makeToken(int tagSize, boolean repeated, int depth, int objectId,
+ int offset) {
+ return ((0x07L & (long) tagSize) << 61)
+ | (repeated ? (1L << 60) : 0)
+ | (0x01ffL & (long) depth) << 51
+ | (0x07ffffL & (long) objectId) << 32
+ | (0x0ffffffffL & (long) offset);
+ }
+
+ /**
+ * Get the encoded tag size from the token.
+ *
+ * @hide
+ */
+ public static int getTagSizeFromToken(long token) {
+ return (int) (0x7 & (token >> 61));
+ }
+
+ /**
+ * Get whether the token has the repeated bit set to true or false
+ *
+ * @hide
+ */
+ public static boolean getRepeatedFromToken(long token) {
+ return (0x1 & (token >> 60)) != 0;
+ }
+
+ /**
+ * Get the nesting depth from the token.
+ *
+ * @hide
+ */
+ public static int getDepthFromToken(long token) {
+ return (int) (0x01ff & (token >> 51));
+ }
+
+ /**
+ * Get the object ID from the token.
+ *
+ * <p>The object ID is a serial number for the
+ * startObject calls that have happened on this object. The values are truncated
+ * to 9 bits, but that is sufficient for error checking.
+ *
+ * @hide
+ */
+ public static int getObjectIdFromToken(long token) {
+ return (int) (0x07ffff & (token >> 32));
+ }
+
+ /**
+ * Get the location of the offset recorded in the token.
+ *
+ * @hide
+ */
+ public static int getOffsetFromToken(long token) {
+ return (int) token;
+ }
+
+ /**
+ * Convert the object ID to the ordinal value -- the n-th call to startObject.
+ *
+ * <p>The object IDs start at -1 and count backwards, so that the value is unlikely
+ * to alias with an actual size field that had been written.
+ *
+ * @hide
+ */
+ public static int convertObjectIdToOrdinal(int objectId) {
+ return (-1 & 0x07ffff) - objectId;
+ }
+
+ /**
+ * Return a debugging string of a token.
+ */
+ public static @NonNull String token2String(long token) {
+ if (token == 0L) {
+ return "Token(0)";
+ } else {
+ return "Token(val=0x" + Long.toHexString(token)
+ + " depth=" + getDepthFromToken(token)
+ + " object=" + convertObjectIdToOrdinal(getObjectIdFromToken(token))
+ + " tagSize=" + getTagSizeFromToken(token)
+ + " offset=" + getOffsetFromToken(token)
+ + ')';
+ }
+ }
+
+ /**
+ * @hide
+ */
+ protected ProtoStream() {}
+}
diff --git a/android/util/proto/ProtoUtils.java b/android/util/proto/ProtoUtils.java
new file mode 100644
index 0000000..8464d2d
--- /dev/null
+++ b/android/util/proto/ProtoUtils.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.proto;
+
+import android.util.AggStats;
+import android.util.Duration;
+
+import java.io.IOException;
+
+/**
+ * This class contains a list of helper functions to write common proto in
+ * //frameworks/base/core/proto/android/base directory
+ * @hide
+ */
+public class ProtoUtils {
+
+ /**
+ * Dump AggStats to ProtoOutputStream
+ */
+ public static void toAggStatsProto(ProtoOutputStream proto, long fieldId,
+ long min, long average, long max, int meanKb, int maxKb) {
+ final long aggStatsToken = proto.start(fieldId);
+ proto.write(AggStats.MIN, min);
+ proto.write(AggStats.AVERAGE, average);
+ proto.write(AggStats.MAX, max);
+ proto.write(AggStats.MEAN_KB, meanKb);
+ proto.write(AggStats.MAX_KB, maxKb);
+ proto.end(aggStatsToken);
+ }
+
+ /**
+ * Dump AggStats to ProtoOutputStream
+ */
+ public static void toAggStatsProto(ProtoOutputStream proto, long fieldId,
+ long min, long average, long max) {
+ toAggStatsProto(proto, fieldId, min, average, max, 0, 0);
+ }
+
+ /**
+ * Dump Duration to ProtoOutputStream
+ */
+ public static void toDuration(ProtoOutputStream proto, long fieldId, long startMs, long endMs) {
+ final long token = proto.start(fieldId);
+ proto.write(Duration.START_MS, startMs);
+ proto.write(Duration.END_MS, endMs);
+ proto.end(token);
+ }
+
+ /**
+ * Helper function to write bit-wise flags to proto as repeated enums
+ */
+ public static void writeBitWiseFlagsToProtoEnum(ProtoOutputStream proto, long fieldId,
+ int flags, int[] origEnums, int[] protoEnums) {
+ if (protoEnums.length != origEnums.length) {
+ throw new IllegalArgumentException("The length of origEnums must match protoEnums");
+ }
+ int len = origEnums.length;
+ for (int i = 0; i < len; i++) {
+ // handle zero flag case.
+ if (origEnums[i] == 0 && flags == 0) {
+ proto.write(fieldId, protoEnums[i]);
+ return;
+ }
+ if ((flags & origEnums[i]) != 0) {
+ proto.write(fieldId, protoEnums[i]);
+ }
+ }
+ }
+
+ /**
+ * Provide debug data about the current field as a string
+ */
+ public static String currentFieldToString(ProtoInputStream proto) throws IOException {
+ StringBuilder sb = new StringBuilder();
+
+ final int fieldNumber = proto.getFieldNumber();
+ final int wireType = proto.getWireType();
+ long fieldConstant;
+
+ sb.append("Offset : 0x" + Integer.toHexString(proto.getOffset()));
+ sb.append("\nField Number : 0x" + Integer.toHexString(proto.getFieldNumber()));
+ sb.append("\nWire Type : ");
+ switch (wireType) {
+ case ProtoStream.WIRE_TYPE_VARINT:
+ sb.append("varint");
+ fieldConstant = ProtoStream.makeFieldId(fieldNumber,
+ ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_INT64);
+ sb.append("\nField Value : 0x" + Long.toHexString(proto.readLong(fieldConstant)));
+ break;
+ case ProtoStream.WIRE_TYPE_FIXED64:
+ sb.append("fixed64");
+ fieldConstant = ProtoStream.makeFieldId(fieldNumber,
+ ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_FIXED64);
+ sb.append("\nField Value : 0x" + Long.toHexString(proto.readLong(fieldConstant)));
+ break;
+ case ProtoStream.WIRE_TYPE_LENGTH_DELIMITED:
+ sb.append("length delimited");
+ fieldConstant = ProtoStream.makeFieldId(fieldNumber,
+ ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_BYTES);
+ sb.append("\nField Bytes : " + proto.readBytes(fieldConstant));
+ break;
+ case ProtoStream.WIRE_TYPE_START_GROUP:
+ sb.append("start group");
+ break;
+ case ProtoStream.WIRE_TYPE_END_GROUP:
+ sb.append("end group");
+ break;
+ case ProtoStream.WIRE_TYPE_FIXED32:
+ sb.append("fixed32");
+ fieldConstant = ProtoStream.makeFieldId(fieldNumber,
+ ProtoStream.FIELD_COUNT_SINGLE | ProtoStream.FIELD_TYPE_FIXED32);
+ sb.append("\nField Value : 0x" + Integer.toHexString(proto.readInt(fieldConstant)));
+ break;
+ default:
+ sb.append("unknown(" + proto.getWireType() + ")");
+ }
+ return sb.toString();
+ }
+}
diff --git a/android/util/proto/WireTypeMismatchException.java b/android/util/proto/WireTypeMismatchException.java
new file mode 100644
index 0000000..d90b4f8
--- /dev/null
+++ b/android/util/proto/WireTypeMismatchException.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util.proto;
+
+import android.annotation.TestApi;
+
+/**
+ * Thrown when there is an error parsing protobuf data.
+ *
+ * @hide
+ */
+@TestApi
+public class WireTypeMismatchException extends ProtoParseException {
+
+ /**
+ * Construct a WireTypeMismatchException.
+ *
+ * @param msg The message.
+ */
+ public WireTypeMismatchException(String msg) {
+ super(msg);
+ }
+}
+