summaryrefslogtreecommitdiff
path: root/core/java/android/util
diff options
context:
space:
mode:
authorXin Li <delphij@google.com>2021-10-07 23:50:15 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2021-10-07 23:50:15 +0000
commitc03b0fa033117c03430e361d561aa910e95a0478 (patch)
tree9c6aaee5a3023a6c237394b44e06a3fdb46f6747 /core/java/android/util
parent8cc0f40cf250d9c66dc15d0e8bc3a41db9a7cfa1 (diff)
parent531b8f4f2605c44cf73e8421f674a1c7a9c277ff (diff)
Merge "Merge Android 12"
Diffstat (limited to 'core/java/android/util')
-rw-r--r--core/java/android/util/ArrayMap.java3
-rw-r--r--core/java/android/util/ArraySet.java2
-rw-r--r--core/java/android/util/AtomicFile.java46
-rw-r--r--core/java/android/util/CharsetUtils.java78
-rw-r--r--core/java/android/util/DebugUtils.java20
-rwxr-xr-xcore/java/android/util/DisplayMetrics.java4
-rw-r--r--core/java/android/util/EventLog.java2
-rw-r--r--core/java/android/util/ExceptionUtils.java2
-rw-r--r--core/java/android/util/FeatureFlagUtils.java37
-rw-r--r--core/java/android/util/IconDrawableFactory.java2
-rw-r--r--core/java/android/util/IndentingPrintWriter.java252
-rw-r--r--core/java/android/util/IntArray.java11
-rw-r--r--core/java/android/util/LocalLog.java5
-rw-r--r--core/java/android/util/Log.java6
-rw-r--r--core/java/android/util/MapCollections.java6
-rw-r--r--core/java/android/util/MemoryIntArray.java6
-rw-r--r--core/java/android/util/MergedConfiguration.java21
-rw-r--r--core/java/android/util/PackageUtils.java43
-rw-r--r--core/java/android/util/Pair.java4
-rw-r--r--core/java/android/util/Range.java3
-rw-r--r--core/java/android/util/Rational.java3
-rw-r--r--core/java/android/util/RecurrenceRule.java3
-rw-r--r--core/java/android/util/RotationUtils.java87
-rw-r--r--core/java/android/util/SizeF.java47
-rw-r--r--core/java/android/util/Slog.java5
-rw-r--r--core/java/android/util/SparseArray.java56
-rw-r--r--core/java/android/util/SparseArrayMap.java49
-rw-r--r--core/java/android/util/SparseBooleanArray.java3
-rw-r--r--core/java/android/util/SparseDoubleArray.java166
-rw-r--r--core/java/android/util/SparseLongArray.java4
-rw-r--r--core/java/android/util/SystemConfigFileCommitEventLogger.java73
-rw-r--r--core/java/android/util/TypedValue.java144
-rw-r--r--core/java/android/util/TypedXmlPullParser.java330
-rw-r--r--core/java/android/util/TypedXmlSerializer.java103
-rw-r--r--core/java/android/util/Xml.java225
-rw-r--r--core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java19
-rw-r--r--core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java191
-rw-r--r--core/java/android/util/apk/ApkSignatureSchemeV4Verifier.java37
-rw-r--r--core/java/android/util/apk/ApkSignatureVerifier.java111
-rw-r--r--core/java/android/util/apk/ApkSigningBlockUtils.java220
-rw-r--r--core/java/android/util/apk/DataSource.java22
-rw-r--r--core/java/android/util/apk/MemoryMappedFileDataSource.java1
-rw-r--r--core/java/android/util/apk/ReadFileDataSource.java73
-rw-r--r--core/java/android/util/apk/SignatureInfo.java13
-rw-r--r--core/java/android/util/apk/SourceStampVerificationResult.java25
-rw-r--r--core/java/android/util/apk/SourceStampVerifier.java104
-rw-r--r--core/java/android/util/apk/TEST_MAPPING11
-rw-r--r--core/java/android/util/apk/VerbatimX509Certificate.java4
-rw-r--r--core/java/android/util/apk/VerityBuilder.java70
-rw-r--r--core/java/android/util/imetracing/ImeTracing.java187
-rw-r--r--core/java/android/util/imetracing/ImeTracingClientImpl.java103
-rw-r--r--core/java/android/util/imetracing/ImeTracingServerImpl.java239
-rw-r--r--core/java/android/util/imetracing/InputConnectionHelper.java231
-rw-r--r--core/java/android/util/imetracing/OWNERS3
-rw-r--r--core/java/android/util/jar/StrictJarManifest.java7
-rw-r--r--core/java/android/util/proto/EncodedBuffer.java4
-rw-r--r--core/java/android/util/proto/ProtoInputStream.java7
57 files changed, 3159 insertions, 374 deletions
diff --git a/core/java/android/util/ArrayMap.java b/core/java/android/util/ArrayMap.java
index 418d92c44290..0b50192bfa48 100644
--- a/core/java/android/util/ArrayMap.java
+++ b/core/java/android/util/ArrayMap.java
@@ -16,6 +16,7 @@
package android.util;
+import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import com.android.internal.util.ArrayUtils;
@@ -826,7 +827,7 @@ public final class ArrayMap<K, V> implements Map<K, V> {
* equal, the method returns false, otherwise it returns true.
*/
@Override
- public boolean equals(Object object) {
+ public boolean equals(@Nullable Object object) {
if (this == object) {
return true;
}
diff --git a/core/java/android/util/ArraySet.java b/core/java/android/util/ArraySet.java
index 7f652ba2cd3a..f53548a41177 100644
--- a/core/java/android/util/ArraySet.java
+++ b/core/java/android/util/ArraySet.java
@@ -778,7 +778,7 @@ public final class ArraySet<E> implements Collection<E>, Set<E> {
* returns true.
*/
@Override
- public boolean equals(Object object) {
+ public boolean equals(@Nullable Object object) {
if (this == object) {
return true;
}
diff --git a/core/java/android/util/AtomicFile.java b/core/java/android/util/AtomicFile.java
index e0d857af597c..e2e94799a8d7 100644
--- a/core/java/android/util/AtomicFile.java
+++ b/core/java/android/util/AtomicFile.java
@@ -16,6 +16,11 @@
package android.util;
+import android.annotation.CurrentTimeMillisLong;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
import android.os.FileUtils;
import android.os.SystemClock;
@@ -49,15 +54,14 @@ public class AtomicFile {
private final File mBaseName;
private final File mNewName;
private final File mLegacyBackupName;
- private final String mCommitTag;
- private long mStartTime;
+ private SystemConfigFileCommitEventLogger mCommitEventLogger;
/**
* Create a new AtomicFile for a file located at the given File path.
* The new file created when writing will be the same file path with ".new" appended.
*/
public AtomicFile(File baseName) {
- this(baseName, null);
+ this(baseName, (SystemConfigFileCommitEventLogger) null);
}
/**
@@ -65,10 +69,23 @@ public class AtomicFile {
* automatically log commit events.
*/
public AtomicFile(File baseName, String commitTag) {
+ this(baseName, new SystemConfigFileCommitEventLogger(commitTag));
+ }
+
+ /**
+ * Internal constructor that also allows you to have the class
+ * automatically log commit events.
+ *
+ * @hide
+ */
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+ @SuppressLint("StreamFiles")
+ public AtomicFile(@NonNull File baseName,
+ @Nullable SystemConfigFileCommitEventLogger commitEventLogger) {
mBaseName = baseName;
mNewName = new File(baseName.getPath() + ".new");
mLegacyBackupName = new File(baseName.getPath() + ".bak");
- mCommitTag = commitTag;
+ mCommitEventLogger = commitEventLogger;
}
/**
@@ -103,7 +120,7 @@ public class AtomicFile {
* access to AtomicFile.
*/
public FileOutputStream startWrite() throws IOException {
- return startWrite(mCommitTag != null ? SystemClock.uptimeMillis() : 0);
+ return startWrite(0);
}
/**
@@ -111,9 +128,19 @@ public class AtomicFile {
* 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()}.
+ *
+ * @deprecated Use {@link SystemConfigFileCommitEventLogger#setStartTime} followed
+ * by {@link #startWrite()}
*/
+ @Deprecated
public FileOutputStream startWrite(long startTime) throws IOException {
- mStartTime = startTime;
+ if (mCommitEventLogger != null) {
+ if (startTime != 0) {
+ mCommitEventLogger.setStartTime(startTime);
+ }
+
+ mCommitEventLogger.onStartWrite();
+ }
if (mLegacyBackupName.exists()) {
rename(mLegacyBackupName, mBaseName);
@@ -155,9 +182,8 @@ public class AtomicFile {
Log.e(LOG_TAG, "Failed to close file output stream", e);
}
rename(mNewName, mBaseName);
- if (mCommitTag != null) {
- com.android.internal.logging.EventLogTags.writeCommitSysConfigFile(
- mCommitTag, SystemClock.uptimeMillis() - mStartTime);
+ if (mCommitEventLogger != null) {
+ mCommitEventLogger.onFinishWrite();
}
}
@@ -245,11 +271,11 @@ public class AtomicFile {
/**
* 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.
*/
+ @CurrentTimeMillisLong
public long getLastModifiedTime() {
if (mLegacyBackupName.exists()) {
return mLegacyBackupName.lastModified();
diff --git a/core/java/android/util/CharsetUtils.java b/core/java/android/util/CharsetUtils.java
new file mode 100644
index 000000000000..fa146675b8d1
--- /dev/null
+++ b/core/java/android/util/CharsetUtils.java
@@ -0,0 +1,78 @@
+/*
+ * 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 android.annotation.NonNull;
+
+import dalvik.annotation.optimization.FastNative;
+
+/**
+ * Specializations of {@code libcore.util.CharsetUtils} which enable efficient
+ * in-place encoding without making any new allocations.
+ * <p>
+ * These methods purposefully accept only non-movable byte array addresses to
+ * avoid extra JNI overhead.
+ *
+ * @hide
+ */
+public class CharsetUtils {
+ /**
+ * Attempt to encode the given string as modified UTF-8 into the destination
+ * byte array without making any new allocations.
+ *
+ * @param src string value to be encoded
+ * @param dest destination byte array to encode into
+ * @param destOff offset into destination where encoding should begin
+ * @param destLen length of destination
+ * @return positive value when encoding succeeded, or negative value when
+ * failed; the magnitude of the value is the number of bytes
+ * required to encode the string.
+ */
+ public static int toModifiedUtf8Bytes(@NonNull String src,
+ long dest, int destOff, int destLen) {
+ return toModifiedUtf8Bytes(src, src.length(), dest, destOff, destLen);
+ }
+
+ /**
+ * Attempt to encode the given string as modified UTF-8 into the destination
+ * byte array without making any new allocations.
+ *
+ * @param src string value to be encoded
+ * @param srcLen exact length of string to be encoded
+ * @param dest destination byte array to encode into
+ * @param destOff offset into destination where encoding should begin
+ * @param destLen length of destination
+ * @return positive value when encoding succeeded, or negative value when
+ * failed; the magnitude of the value is the number of bytes
+ * required to encode the string.
+ */
+ @FastNative
+ private static native int toModifiedUtf8Bytes(@NonNull String src, int srcLen,
+ long dest, int destOff, int destLen);
+
+ /**
+ * Attempt to decode a modified UTF-8 string from the source byte array.
+ *
+ * @param src source byte array to decode from
+ * @param srcOff offset into source where decoding should begin
+ * @param srcLen length of source that should be decoded
+ * @return the successfully decoded string
+ */
+ @FastNative
+ public static native @NonNull String fromModifiedUtf8Bytes(
+ long src, int srcOff, int srcLen);
+}
diff --git a/core/java/android/util/DebugUtils.java b/core/java/android/util/DebugUtils.java
index 6d5e830346cd..ece6b3516f7a 100644
--- a/core/java/android/util/DebugUtils.java
+++ b/core/java/android/util/DebugUtils.java
@@ -271,6 +271,26 @@ public class DebugUtils {
return res.toString();
}
+ /**
+ * Gets human-readable representation of constants (static final values).
+ *
+ * @hide
+ */
+ public static String constantToString(Class<?> clazz, String prefix, int value) {
+ for (Field field : clazz.getDeclaredFields()) {
+ final int modifiers = field.getModifiers();
+ try {
+ if (Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers)
+ && field.getType().equals(int.class) && field.getName().startsWith(prefix)
+ && field.getInt(null) == value) {
+ return constNameWithoutPrefix(prefix, field);
+ }
+ } catch (IllegalAccessException ignored) {
+ }
+ }
+ return prefix + Integer.toString(value);
+ }
+
private static String constNameWithoutPrefix(String prefix, Field field) {
return field.getName().substring(prefix.length());
}
diff --git a/core/java/android/util/DisplayMetrics.java b/core/java/android/util/DisplayMetrics.java
index 9f6065e695c5..0a3e6b1cff38 100755
--- a/core/java/android/util/DisplayMetrics.java
+++ b/core/java/android/util/DisplayMetrics.java
@@ -16,10 +16,10 @@
package android.util;
+import android.annotation.Nullable;
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.
@@ -362,7 +362,7 @@ public class DisplayMetrics {
}
@Override
- public boolean equals(Object o) {
+ public boolean equals(@Nullable Object o) {
return o instanceof DisplayMetrics && equals((DisplayMetrics)o);
}
diff --git a/core/java/android/util/EventLog.java b/core/java/android/util/EventLog.java
index 1f580af1caae..4654dbfa9531 100644
--- a/core/java/android/util/EventLog.java
+++ b/core/java/android/util/EventLog.java
@@ -308,7 +308,7 @@ public class EventLog {
* @hide
*/
@Override
- public boolean equals(Object o) {
+ public boolean equals(@Nullable 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;
diff --git a/core/java/android/util/ExceptionUtils.java b/core/java/android/util/ExceptionUtils.java
index 1a397b39ef3c..4b511acc280f 100644
--- a/core/java/android/util/ExceptionUtils.java
+++ b/core/java/android/util/ExceptionUtils.java
@@ -98,4 +98,6 @@ public class ExceptionUtils {
}
return t;
}
+
+
} \ No newline at end of file
diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java
index 9d0ae30f1420..6c3c38359957 100644
--- a/core/java/android/util/FeatureFlagUtils.java
+++ b/core/java/android/util/FeatureFlagUtils.java
@@ -23,7 +23,9 @@ import android.provider.Settings;
import android.text.TextUtils;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Map;
+import java.util.Set;
/**
* Util class to get feature flag information.
@@ -36,14 +38,18 @@ 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 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_PROVIDER_MODEL = "settings_provider_model";
+ /** @hide */
+ public static final String SETTINGS_USE_NEW_BACKUP_ELIGIBILITY_RULES
+ = "settings_use_new_backup_eligibility_rules";
+ /** @hide */
+ public static final String SETTINGS_ENABLE_SECURITY_HUB = "settings_enable_security_hub";
private static final Map<String, String> DEFAULT_FLAGS;
@@ -51,10 +57,7 @@ public class FeatureFlagUtils {
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(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");
@@ -65,6 +68,16 @@ public class FeatureFlagUtils {
DEFAULT_FLAGS.put(SETTINGS_DO_NOT_RESTORE_PRESERVED, "true");
DEFAULT_FLAGS.put("settings_tether_all_in_one", "false");
+ DEFAULT_FLAGS.put("settings_contextual_home", "false");
+ DEFAULT_FLAGS.put(SETTINGS_PROVIDER_MODEL, "true");
+ DEFAULT_FLAGS.put(SETTINGS_USE_NEW_BACKUP_ELIGIBILITY_RULES, "true");
+ DEFAULT_FLAGS.put(SETTINGS_ENABLE_SECURITY_HUB, "true");
+ }
+
+ private static final Set<String> PERSISTENT_FLAGS;
+ static {
+ PERSISTENT_FLAGS = new HashSet<>();
+ PERSISTENT_FLAGS.add(SETTINGS_PROVIDER_MODEL);
}
/**
@@ -86,8 +99,9 @@ public class FeatureFlagUtils {
}
}
- // Step 2: check if feature flag has any override. Flag name: sys.fflag.override.<feature>
- value = SystemProperties.get(FFLAG_OVERRIDE_PREFIX + feature);
+ // Step 2: check if feature flag has any override.
+ // Flag name: [persist.]sys.fflag.override.<feature>
+ value = SystemProperties.get(getSystemPropertyPrefix(feature) + feature);
if (!TextUtils.isEmpty(value)) {
return Boolean.parseBoolean(value);
}
@@ -100,7 +114,8 @@ public class FeatureFlagUtils {
* 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");
+ SystemProperties.set(getSystemPropertyPrefix(feature) + feature,
+ enabled ? "true" : "false");
}
/**
@@ -109,4 +124,8 @@ public class FeatureFlagUtils {
public static Map<String, String> getAllFeatureFlags() {
return DEFAULT_FLAGS;
}
+
+ private static String getSystemPropertyPrefix(String feature) {
+ return PERSISTENT_FLAGS.contains(feature) ? PERSIST_PREFIX : FFLAG_OVERRIDE_PREFIX;
+ }
}
diff --git a/core/java/android/util/IconDrawableFactory.java b/core/java/android/util/IconDrawableFactory.java
index 9b1e6cf5be87..b5e8dd7ed0af 100644
--- a/core/java/android/util/IconDrawableFactory.java
+++ b/core/java/android/util/IconDrawableFactory.java
@@ -49,7 +49,7 @@ public class IconDrawableFactory {
}
protected boolean needsBadging(ApplicationInfo appInfo, @UserIdInt int userId) {
- return appInfo.isInstantApp() || mUm.isManagedProfile(userId);
+ return appInfo.isInstantApp() || mUm.hasBadge(userId);
}
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
diff --git a/core/java/android/util/IndentingPrintWriter.java b/core/java/android/util/IndentingPrintWriter.java
new file mode 100644
index 000000000000..9d2ebe85a1cb
--- /dev/null
+++ b/core/java/android/util/IndentingPrintWriter.java
@@ -0,0 +1,252 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.util.Arrays;
+
+/**
+ * Lightweight wrapper around {@link PrintWriter} that automatically indents
+ * newlines based on internal state. It also automatically wraps long lines
+ * based on given line length.
+ * <p>
+ * Delays writing indent until first actual write on a newline, enabling indent
+ * modification after newline.
+ *
+ * @hide
+ */
+public class IndentingPrintWriter extends PrintWriter {
+ private final String mSingleIndent;
+ private final int mWrapLength;
+
+ /** Mutable version of current indent */
+ private StringBuilder mIndentBuilder = new StringBuilder();
+ /** Cache of current {@link #mIndentBuilder} value */
+ private char[] mCurrentIndent;
+ /** Length of current line being built, excluding any indent */
+ private int mCurrentLength;
+
+ /**
+ * Flag indicating if we're currently sitting on an empty line, and that
+ * next write should be prefixed with the current indent.
+ */
+ private boolean mEmptyLine = true;
+
+ private char[] mSingleChar = new char[1];
+
+ public IndentingPrintWriter(@NonNull Writer writer) {
+ this(writer, " ", -1);
+ }
+
+ public IndentingPrintWriter(@NonNull Writer writer, @NonNull String singleIndent) {
+ this(writer, singleIndent, null, -1);
+ }
+
+ public IndentingPrintWriter(@NonNull Writer writer, @NonNull String singleIndent,
+ String prefix) {
+ this(writer, singleIndent, prefix, -1);
+ }
+
+ public IndentingPrintWriter(@NonNull Writer writer, @NonNull String singleIndent,
+ int wrapLength) {
+ this(writer, singleIndent, null, wrapLength);
+ }
+
+ public IndentingPrintWriter(@NonNull Writer writer, @NonNull String singleIndent,
+ @Nullable String prefix, int wrapLength) {
+ super(writer);
+ mSingleIndent = singleIndent;
+ mWrapLength = wrapLength;
+ if (prefix != null) {
+ mIndentBuilder.append(prefix);
+ }
+ }
+
+ /**
+ * Overrides the indent set in the constructor for the next printed line.
+ *
+ * @deprecated Use the "prefix" constructor parameter
+ * @hide
+ */
+ @NonNull
+ @Deprecated
+ public IndentingPrintWriter setIndent(@NonNull String indent) {
+ mIndentBuilder.setLength(0);
+ mIndentBuilder.append(indent);
+ mCurrentIndent = null;
+ return this;
+ }
+
+ /**
+ * Overrides the indent set in the constructor with {@code singleIndent} repeated {@code indent}
+ * times.
+ *
+ * @deprecated Use the "prefix" constructor parameter
+ * @hide
+ */
+ @NonNull
+ @Deprecated
+ public IndentingPrintWriter setIndent(int indent) {
+ mIndentBuilder.setLength(0);
+ for (int i = 0; i < indent; i++) {
+ increaseIndent();
+ }
+ return this;
+ }
+
+ /**
+ * Increases the indent starting with the next printed line.
+ */
+ @NonNull
+ public IndentingPrintWriter increaseIndent() {
+ mIndentBuilder.append(mSingleIndent);
+ mCurrentIndent = null;
+ return this;
+ }
+
+ /**
+ * Decreases the indent starting with the next printed line.
+ */
+ @NonNull
+ public IndentingPrintWriter decreaseIndent() {
+ mIndentBuilder.delete(0, mSingleIndent.length());
+ mCurrentIndent = null;
+ return this;
+ }
+
+ /**
+ * Prints a key-value pair.
+ */
+ @NonNull
+ public IndentingPrintWriter print(@NonNull String key, @Nullable Object value) {
+ String string;
+ if (value == null) {
+ string = "null";
+ } else if (value.getClass().isArray()) {
+ if (value.getClass() == boolean[].class) {
+ string = Arrays.toString((boolean[]) value);
+ } else if (value.getClass() == byte[].class) {
+ string = Arrays.toString((byte[]) value);
+ } else if (value.getClass() == char[].class) {
+ string = Arrays.toString((char[]) value);
+ } else if (value.getClass() == double[].class) {
+ string = Arrays.toString((double[]) value);
+ } else if (value.getClass() == float[].class) {
+ string = Arrays.toString((float[]) value);
+ } else if (value.getClass() == int[].class) {
+ string = Arrays.toString((int[]) value);
+ } else if (value.getClass() == long[].class) {
+ string = Arrays.toString((long[]) value);
+ } else if (value.getClass() == short[].class) {
+ string = Arrays.toString((short[]) value);
+ } else {
+ string = Arrays.toString((Object[]) value);
+ }
+ } else {
+ string = String.valueOf(value);
+ }
+ print(key + "=" + string + " ");
+ return this;
+ }
+
+ /**
+ * Prints a key-value pair, using hexadecimal format for the value.
+ */
+ @NonNull
+ public IndentingPrintWriter printHexInt(@NonNull String key, int value) {
+ print(key + "=0x" + Integer.toHexString(value) + " ");
+ return this;
+ }
+
+ @Override
+ public void println() {
+ write('\n');
+ }
+
+ @Override
+ public void write(int c) {
+ mSingleChar[0] = (char) c;
+ write(mSingleChar, 0, 1);
+ }
+
+ @Override
+ public void write(@NonNull String s, int off, int len) {
+ final char[] buf = new char[len];
+ s.getChars(off, len - off, buf, 0);
+ write(buf, 0, len);
+ }
+
+ @Override
+ public void write(@NonNull char[] buf, int offset, int count) {
+ final int indentLength = mIndentBuilder.length();
+ final int bufferEnd = offset + count;
+ int lineStart = offset;
+ int lineEnd = offset;
+
+ // March through incoming buffer looking for newlines
+ while (lineEnd < bufferEnd) {
+ char ch = buf[lineEnd++];
+ mCurrentLength++;
+ if (ch == '\n') {
+ maybeWriteIndent();
+ super.write(buf, lineStart, lineEnd - lineStart);
+ lineStart = lineEnd;
+ mEmptyLine = true;
+ mCurrentLength = 0;
+ }
+
+ // Wrap if we've pushed beyond line length
+ if (mWrapLength > 0 && mCurrentLength >= mWrapLength - indentLength) {
+ if (!mEmptyLine) {
+ // Give ourselves a fresh line to work with
+ super.write('\n');
+ mEmptyLine = true;
+ mCurrentLength = lineEnd - lineStart;
+ } else {
+ // We need more than a dedicated line, slice it hard
+ maybeWriteIndent();
+ super.write(buf, lineStart, lineEnd - lineStart);
+ super.write('\n');
+ mEmptyLine = true;
+ lineStart = lineEnd;
+ mCurrentLength = 0;
+ }
+ }
+ }
+
+ if (lineStart != lineEnd) {
+ maybeWriteIndent();
+ super.write(buf, lineStart, lineEnd - lineStart);
+ }
+ }
+
+ private void maybeWriteIndent() {
+ if (mEmptyLine) {
+ mEmptyLine = false;
+ if (mIndentBuilder.length() != 0) {
+ if (mCurrentIndent == null) {
+ mCurrentIndent = mIndentBuilder.toString().toCharArray();
+ }
+ super.write(mCurrentIndent, 0, mCurrentIndent.length);
+ }
+ }
+ }
+}
diff --git a/core/java/android/util/IntArray.java b/core/java/android/util/IntArray.java
index 5a74ec0e52c0..b77265b0ebf6 100644
--- a/core/java/android/util/IntArray.java
+++ b/core/java/android/util/IntArray.java
@@ -144,6 +144,17 @@ public class IntArray implements Cloneable {
}
/**
+ * Adds the values in the specified array to this array.
+ */
+ public void addAll(int[] values) {
+ final int count = values.length;
+ ensureCapacity(count);
+
+ System.arraycopy(values, 0, mValues, mSize, count);
+ mSize += count;
+ }
+
+ /**
* Ensures capacity to append at least <code>count</code> values.
*/
private void ensureCapacity(int count) {
diff --git a/core/java/android/util/LocalLog.java b/core/java/android/util/LocalLog.java
index 17fb9e469606..8c4dcb3b28e2 100644
--- a/core/java/android/util/LocalLog.java
+++ b/core/java/android/util/LocalLog.java
@@ -61,10 +61,9 @@ public final class LocalLog {
}
final String logLine;
if (mUseLocalTimestamps) {
- logLine = String.format("%s - %s", LocalDateTime.now(), msg);
+ logLine = LocalDateTime.now() + " - " + msg;
} else {
- logLine = String.format(
- "%s / %s - %s", SystemClock.elapsedRealtime(), Instant.now(), msg);
+ logLine = SystemClock.elapsedRealtime() + " / " + Instant.now() + " - " + msg;
}
append(logLine);
}
diff --git a/core/java/android/util/Log.java b/core/java/android/util/Log.java
index 2ded473930f7..12bcd8b0aa97 100644
--- a/core/java/android/util/Log.java
+++ b/core/java/android/util/Log.java
@@ -44,9 +44,7 @@ import java.net.UnknownHostException;
* You can then <a href="{@docRoot}studio/debug/am-logcat.html">view the logs in logcat</a>.
*
* <p>The order in terms of verbosity, from least to most is
- * ERROR, WARN, INFO, DEBUG, VERBOSE. Verbose should never be compiled
- * into an application except during development. Debug logs are compiled
- * in but stripped at runtime. Error, warning and info logs are always kept.
+ * ERROR, WARN, INFO, DEBUG, VERBOSE.
*
* <p><b>Tip:</b> A good convention is to declare a <code>TAG</code> constant
* in your class:
@@ -227,7 +225,7 @@ public final class Log {
* @param level The level to check.
* @return Whether or not that this is allowed to be logged.
* @throws IllegalArgumentException is thrown if the tag.length() > 23
- * for Nougat (7.0) releases (API <= 23) and prior, there is no
+ * for Nougat (7.0) and prior releases (API <= 25), there is no
* tag limit of concern after this API level.
*/
@FastNative
diff --git a/core/java/android/util/MapCollections.java b/core/java/android/util/MapCollections.java
index a5212688c6c6..7ab3fcaeedc4 100644
--- a/core/java/android/util/MapCollections.java
+++ b/core/java/android/util/MapCollections.java
@@ -16,6 +16,8 @@
package android.util;
+import android.annotation.Nullable;
+
import java.lang.reflect.Array;
import java.util.Collection;
import java.util.Iterator;
@@ -249,7 +251,7 @@ abstract class MapCollections<K, V> {
}
@Override
- public boolean equals(Object object) {
+ public boolean equals(@Nullable Object object) {
return equalsSetHelper(this, object);
}
@@ -339,7 +341,7 @@ abstract class MapCollections<K, V> {
}
@Override
- public boolean equals(Object object) {
+ public boolean equals(@Nullable Object object) {
return equalsSetHelper(this, object);
}
diff --git a/core/java/android/util/MemoryIntArray.java b/core/java/android/util/MemoryIntArray.java
index 7d287e38ba86..9073d3ae91ca 100644
--- a/core/java/android/util/MemoryIntArray.java
+++ b/core/java/android/util/MemoryIntArray.java
@@ -16,13 +16,15 @@
package android.util;
+import android.annotation.Nullable;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
-import libcore.io.IoUtils;
import dalvik.system.CloseGuard;
+import libcore.io.IoUtils;
+
import java.io.Closeable;
import java.io.IOException;
import java.util.UUID;
@@ -183,7 +185,7 @@ public final class MemoryIntArray implements Parcelable, Closeable {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (obj == null) {
return false;
}
diff --git a/core/java/android/util/MergedConfiguration.java b/core/java/android/util/MergedConfiguration.java
index 2399adac3cba..4328e0826bd0 100644
--- a/core/java/android/util/MergedConfiguration.java
+++ b/core/java/android/util/MergedConfiguration.java
@@ -17,6 +17,7 @@
package android.util;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.res.Configuration;
import android.os.Parcel;
import android.os.Parcelable;
@@ -32,9 +33,9 @@ import java.io.PrintWriter;
*/
public class MergedConfiguration implements Parcelable {
- private Configuration mGlobalConfig = new Configuration();
- private Configuration mOverrideConfig = new Configuration();
- private Configuration mMergedConfig = new Configuration();
+ private final Configuration mGlobalConfig = new Configuration();
+ private final Configuration mOverrideConfig = new Configuration();
+ private final Configuration mMergedConfig = new Configuration();
public MergedConfiguration() {
}
@@ -58,15 +59,15 @@ public class MergedConfiguration implements Parcelable {
@Override
public void writeToParcel(Parcel dest, int flags) {
- dest.writeParcelable(mGlobalConfig, flags);
- dest.writeParcelable(mOverrideConfig, flags);
- dest.writeParcelable(mMergedConfig, flags);
+ mGlobalConfig.writeToParcel(dest, flags);
+ mOverrideConfig.writeToParcel(dest, flags);
+ mMergedConfig.writeToParcel(dest, flags);
}
public void readFromParcel(Parcel source) {
- mGlobalConfig = source.readParcelable(Configuration.class.getClassLoader());
- mOverrideConfig = source.readParcelable(Configuration.class.getClassLoader());
- mMergedConfig = source.readParcelable(Configuration.class.getClassLoader());
+ mGlobalConfig.readFromParcel(source);
+ mOverrideConfig.readFromParcel(source);
+ mMergedConfig.readFromParcel(source);
}
@Override
@@ -167,7 +168,7 @@ public class MergedConfiguration implements Parcelable {
}
@Override
- public boolean equals(Object that) {
+ public boolean equals(@Nullable Object that) {
if (!(that instanceof MergedConfiguration)) {
return false;
}
diff --git a/core/java/android/util/PackageUtils.java b/core/java/android/util/PackageUtils.java
index 8061bf36fa6a..ff04825f788f 100644
--- a/core/java/android/util/PackageUtils.java
+++ b/core/java/android/util/PackageUtils.java
@@ -19,6 +19,7 @@ package android.util;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.pm.Signature;
+import android.text.TextUtils;
import libcore.util.HexEncoding;
@@ -39,18 +40,27 @@ public final class PackageUtils {
}
/**
+ * @see #computeSignaturesSha256Digests(Signature[], String)
+ */
+ public static @NonNull String[] computeSignaturesSha256Digests(
+ @NonNull Signature[] signatures) {
+ return computeSignaturesSha256Digests(signatures, null);
+ }
+
+ /**
* 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.
+ * @param separator Separator between each pair of characters, such as a colon, or null to omit.
* @return The digest array.
*/
public static @NonNull String[] computeSignaturesSha256Digests(
- @NonNull Signature[] signatures) {
+ @NonNull Signature[] signatures, @Nullable String separator) {
final int signatureCount = signatures.length;
final String[] digests = new String[signatureCount];
for (int i = 0; i < signatureCount; i++) {
- digests[i] = computeSha256Digest(signatures[i].toByteArray());
+ digests[i] = computeSha256Digest(signatures[i].toByteArray(), separator);
}
return digests;
}
@@ -66,11 +76,11 @@ public final class PackageUtils {
@NonNull Signature[] signatures) {
// Shortcut for optimization - most apps singed by a single cert
if (signatures.length == 1) {
- return computeSha256Digest(signatures[0].toByteArray());
+ return computeSha256Digest(signatures[0].toByteArray(), null);
}
// Make sure these are sorted to handle reversed certificates
- final String[] sha256Digests = computeSignaturesSha256Digests(signatures);
+ final String[] sha256Digests = computeSignaturesSha256Digests(signatures, null);
return computeSignaturesSha256Digest(sha256Digests);
}
@@ -99,7 +109,7 @@ public final class PackageUtils {
/* ignore - can't happen */
}
}
- return computeSha256Digest(bytes.toByteArray());
+ return computeSha256Digest(bytes.toByteArray(), null);
}
/**
@@ -122,15 +132,34 @@ public final class PackageUtils {
}
/**
+ * @see #computeSha256Digest(byte[], String)
+ */
+ public static @Nullable String computeSha256Digest(@NonNull byte[] data) {
+ return computeSha256Digest(data, null);
+ }
+ /**
* Computes the SHA256 digest of some data.
* @param data The data.
+ * @param separator Separator between each pair of characters, such as a colon, or null to omit.
* @return The digest or null if an error occurs.
*/
- public static @Nullable String computeSha256Digest(@NonNull byte[] data) {
+ public static @Nullable String computeSha256Digest(@NonNull byte[] data,
+ @Nullable String separator) {
byte[] sha256DigestBytes = computeSha256DigestBytes(data);
if (sha256DigestBytes == null) {
return null;
}
- return HexEncoding.encodeToString(sha256DigestBytes, true /* uppercase */);
+
+ if (separator == null) {
+ return HexEncoding.encodeToString(sha256DigestBytes, true /* uppercase */);
+ }
+
+ int length = sha256DigestBytes.length;
+ String[] pieces = new String[length];
+ for (int index = 0; index < length; index++) {
+ pieces[index] = HexEncoding.encodeToString(sha256DigestBytes[index], true);
+ }
+
+ return TextUtils.join(separator, pieces);
}
}
diff --git a/core/java/android/util/Pair.java b/core/java/android/util/Pair.java
index f96da729b831..b0866b440be5 100644
--- a/core/java/android/util/Pair.java
+++ b/core/java/android/util/Pair.java
@@ -16,6 +16,8 @@
package android.util;
+import android.annotation.Nullable;
+
import java.util.Objects;
/**
@@ -47,7 +49,7 @@ public class Pair<F, S> {
* equal
*/
@Override
- public boolean equals(Object o) {
+ public boolean equals(@Nullable Object o) {
if (!(o instanceof Pair)) {
return false;
}
diff --git a/core/java/android/util/Range.java b/core/java/android/util/Range.java
index f31ddd9b0307..9fd0ab99f01b 100644
--- a/core/java/android/util/Range.java
+++ b/core/java/android/util/Range.java
@@ -18,6 +18,7 @@ package android.util;
import static com.android.internal.util.Preconditions.*;
+import android.annotation.Nullable;
import android.hardware.camera2.utils.HashCodeHelpers;
/**
@@ -146,7 +147,7 @@ public final class Range<T extends Comparable<? super T>> {
* @return {@code true} if the ranges are equal, {@code false} otherwise
*/
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (obj == null) {
return false;
} else if (this == obj) {
diff --git a/core/java/android/util/Rational.java b/core/java/android/util/Rational.java
index c89dd16ed22f..d7730f2b0b3f 100644
--- a/core/java/android/util/Rational.java
+++ b/core/java/android/util/Rational.java
@@ -17,6 +17,7 @@ package android.util;
import static com.android.internal.util.Preconditions.checkNotNull;
+import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.os.Build;
@@ -241,7 +242,7 @@ public final class Rational extends Number implements Comparable<Rational> {
* @return A boolean that determines whether or not the two Rational objects are equal.
*/
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
return obj instanceof Rational && equals((Rational) obj);
}
diff --git a/core/java/android/util/RecurrenceRule.java b/core/java/android/util/RecurrenceRule.java
index 89a06d2fd21d..39d1f2cbef19 100644
--- a/core/java/android/util/RecurrenceRule.java
+++ b/core/java/android/util/RecurrenceRule.java
@@ -16,6 +16,7 @@
package android.util;
+import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.os.Build;
import android.os.Parcel;
@@ -131,7 +132,7 @@ public class RecurrenceRule implements Parcelable {
}
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (obj instanceof RecurrenceRule) {
final RecurrenceRule other = (RecurrenceRule) obj;
return Objects.equals(start, other.start)
diff --git a/core/java/android/util/RotationUtils.java b/core/java/android/util/RotationUtils.java
index a44ed59c14d4..0ac2c9c25ad1 100644
--- a/core/java/android/util/RotationUtils.java
+++ b/core/java/android/util/RotationUtils.java
@@ -21,7 +21,10 @@ import static android.view.Surface.ROTATION_180;
import static android.view.Surface.ROTATION_270;
import static android.view.Surface.ROTATION_90;
+import android.annotation.Dimension;
import android.graphics.Insets;
+import android.graphics.Matrix;
+import android.graphics.Rect;
import android.view.Surface.Rotation;
/**
@@ -69,4 +72,88 @@ public class RotationUtils {
}
return rotated;
}
+
+ /**
+ * Rotates bounds as if parentBounds and bounds are a group. The group is rotated from
+ * oldRotation to newRotation. This assumes that parentBounds is at 0,0 and remains at 0,0 after
+ * rotation. The bounds will be at the same physical position in parentBounds.
+ *
+ * Only 'inOutBounds' is mutated.
+ */
+ public static void rotateBounds(Rect inOutBounds, Rect parentBounds, @Rotation int oldRotation,
+ @Rotation int newRotation) {
+ rotateBounds(inOutBounds, parentBounds, deltaRotation(oldRotation, newRotation));
+ }
+
+ /**
+ * Rotates bounds as if parentBounds and bounds are a group. The group is rotated by `delta`
+ * 90-degree counter-clockwise increments. This assumes that parentBounds is at 0,0 and
+ * remains at 0,0 after rotation. The bounds will be at the same physical position in
+ * parentBounds.
+ *
+ * Only 'inOutBounds' is mutated.
+ */
+ public static void rotateBounds(Rect inOutBounds, Rect parentBounds, @Rotation int rotation) {
+ final int origLeft = inOutBounds.left;
+ final int origTop = inOutBounds.top;
+ switch (rotation) {
+ case ROTATION_0:
+ return;
+ case ROTATION_90:
+ inOutBounds.left = inOutBounds.top;
+ inOutBounds.top = parentBounds.right - inOutBounds.right;
+ inOutBounds.right = inOutBounds.bottom;
+ inOutBounds.bottom = parentBounds.right - origLeft;
+ return;
+ case ROTATION_180:
+ inOutBounds.left = parentBounds.right - inOutBounds.right;
+ inOutBounds.right = parentBounds.right - origLeft;
+ inOutBounds.top = parentBounds.bottom - inOutBounds.bottom;
+ inOutBounds.bottom = parentBounds.bottom - origTop;
+ return;
+ case ROTATION_270:
+ inOutBounds.left = parentBounds.bottom - inOutBounds.bottom;
+ inOutBounds.bottom = inOutBounds.right;
+ inOutBounds.right = parentBounds.bottom - inOutBounds.top;
+ inOutBounds.top = origLeft;
+ }
+ }
+
+ /** @return the rotation needed to rotate from oldRotation to newRotation. */
+ @Rotation
+ public static int deltaRotation(int oldRotation, int newRotation) {
+ int delta = newRotation - oldRotation;
+ if (delta < 0) delta += 4;
+ return delta;
+ }
+
+ /**
+ * Sets a matrix such that given a rotation, it transforms physical display
+ * coordinates to that rotation's logical coordinates.
+ *
+ * @param rotation the rotation to which the matrix should transform
+ * @param out the matrix to be set
+ */
+ public static void transformPhysicalToLogicalCoordinates(@Rotation int rotation,
+ @Dimension int physicalWidth, @Dimension int physicalHeight, Matrix out) {
+ switch (rotation) {
+ case ROTATION_0:
+ out.reset();
+ break;
+ case ROTATION_90:
+ out.setRotate(270);
+ out.postTranslate(0, physicalWidth);
+ break;
+ case ROTATION_180:
+ out.setRotate(180);
+ out.postTranslate(physicalWidth, physicalHeight);
+ break;
+ case ROTATION_270:
+ out.setRotate(90);
+ out.postTranslate(physicalHeight, 0);
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown rotation: " + rotation);
+ }
+ }
}
diff --git a/core/java/android/util/SizeF.java b/core/java/android/util/SizeF.java
index 2edc4a7ff588..c77a02434941 100644
--- a/core/java/android/util/SizeF.java
+++ b/core/java/android/util/SizeF.java
@@ -16,8 +16,12 @@
package android.util;
-import static com.android.internal.util.Preconditions.checkNotNull;
import static com.android.internal.util.Preconditions.checkArgumentFinite;
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
/**
* Immutable class for describing width and height dimensions in some arbitrary
@@ -26,7 +30,7 @@ import static com.android.internal.util.Preconditions.checkArgumentFinite;
* Width and height are finite values stored as a floating point representation.
* </p>
*/
-public final class SizeF {
+public final class SizeF implements Parcelable {
/**
* Create a new immutable SizeF instance.
*
@@ -161,4 +165,43 @@ public final class SizeF {
private final float mWidth;
private final float mHeight;
+
+ /**
+ * Parcelable interface methods
+ */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Write this size to the specified parcel. To restore a size from a parcel, use the
+ * {@link #CREATOR}.
+ * @param out The parcel to write the point's coordinates into
+ */
+ @Override
+ public void writeToParcel(@NonNull Parcel out, int flags) {
+ out.writeFloat(mWidth);
+ out.writeFloat(mHeight);
+ }
+
+ public static final @NonNull Creator<SizeF> CREATOR = new Creator<SizeF>() {
+ /**
+ * Return a new size from the data in the specified parcel.
+ */
+ @Override
+ public @NonNull SizeF createFromParcel(@NonNull Parcel in) {
+ float width = in.readFloat();
+ float height = in.readFloat();
+ return new SizeF(width, height);
+ }
+
+ /**
+ * Return an array of sizes of the specified size.
+ */
+ @Override
+ public @NonNull SizeF[] newArray(int size) {
+ return new SizeF[size];
+ }
+ };
}
diff --git a/core/java/android/util/Slog.java b/core/java/android/util/Slog.java
index 3880131324fc..117d75e0981c 100644
--- a/core/java/android/util/Slog.java
+++ b/core/java/android/util/Slog.java
@@ -20,6 +20,10 @@ import android.compat.annotation.UnsupportedAppUsage;
import android.os.Build;
/**
+ * API for sending log output to the {@link Log#LOG_ID_SYSTEM} buffer.
+ *
+ * <p>Should be used by system components. Use {@code adb logcat --buffer=system} to fetch the logs.
+ *
* @hide
*/
public final class Slog {
@@ -135,4 +139,3 @@ public final class Slog {
return Log.println_native(Log.LOG_ID_SYSTEM, priority, tag, msg);
}
}
-
diff --git a/core/java/android/util/SparseArray.java b/core/java/android/util/SparseArray.java
index dae760f989f6..05c8617294da 100644
--- a/core/java/android/util/SparseArray.java
+++ b/core/java/android/util/SparseArray.java
@@ -16,6 +16,7 @@
package android.util;
+import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import com.android.internal.util.ArrayUtils;
@@ -23,6 +24,8 @@ import com.android.internal.util.GrowingArrayUtils;
import libcore.util.EmptyArray;
+import java.util.Objects;
+
/**
* <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
@@ -241,6 +244,14 @@ public class SparseArray<E> implements Cloneable {
}
/**
+ * Alias for {@link #put(int, Object)} to support Kotlin [index]= operator.
+ * @see #put(int, Object)
+ */
+ public void set(int key, E value) {
+ put(key, value);
+ }
+
+ /**
* Adds a mapping from the specified key to the specified value,
* replacing the previous mapping from the specified key if there
* was one.
@@ -497,4 +508,49 @@ public class SparseArray<E> implements Cloneable {
buffer.append('}');
return buffer.toString();
}
+
+ /**
+ * Compares the contents of this {@link SparseArray} to the specified {@link SparseArray}.
+ *
+ * For backwards compatibility reasons, {@link Object#equals(Object)} cannot be implemented,
+ * so this serves as a manually invoked alternative.
+ */
+ public boolean contentEquals(@Nullable SparseArray<?> other) {
+ if (other == null) {
+ return false;
+ }
+
+ int size = size();
+ if (size != other.size()) {
+ return false;
+ }
+
+ for (int index = 0; index < size; index++) {
+ int key = keyAt(index);
+ if (!Objects.equals(valueAt(index), other.get(key))) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns a hash code value for the contents of this {@link SparseArray}, combining the
+ * {@link Objects#hashCode(Object)} result of all its keys and values.
+ *
+ * For backwards compatibility, {@link Object#hashCode()} cannot be implemented, so this serves
+ * as a manually invoked alternative.
+ */
+ public int contentHashCode() {
+ int hash = 0;
+ int size = size();
+ for (int index = 0; index < size; index++) {
+ int key = keyAt(index);
+ E value = valueAt(index);
+ hash = 31 * hash + Objects.hashCode(key);
+ hash = 31 * hash + Objects.hashCode(value);
+ }
+ return hash;
+ }
}
diff --git a/core/java/android/util/SparseArrayMap.java b/core/java/android/util/SparseArrayMap.java
index 3ec6b810fda8..3287c279c87f 100644
--- a/core/java/android/util/SparseArrayMap.java
+++ b/core/java/android/util/SparseArrayMap.java
@@ -26,16 +26,17 @@ import java.util.function.Consumer;
* A sparse array of ArrayMaps, which is suitable for holding (userId, packageName)->object
* associations.
*
- * @param <T> Any class
+ * @param <K> Any class
+ * @param <V> Any class
* @hide
*/
@TestApi
-public class SparseArrayMap<T> {
- private final SparseArray<ArrayMap<String, T>> mData = new SparseArray<>();
+public class SparseArrayMap<K, V> {
+ private final SparseArray<ArrayMap<K, V>> 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);
+ /** Add an entry associating obj with the int-K pair. */
+ public void add(int key, @NonNull K mapKey, @Nullable V obj) {
+ ArrayMap<K, V> data = mData.get(key);
if (data == null) {
data = new ArrayMap<>();
mData.put(key, data);
@@ -50,8 +51,8 @@ public class SparseArrayMap<T> {
}
}
- /** Return true if the structure contains an explicit entry for the int-String pair. */
- public boolean contains(int key, @NonNull String mapKey) {
+ /** Return true if the structure contains an explicit entry for the int-K pair. */
+ public boolean contains(int key, @NonNull K mapKey) {
return mData.contains(key) && mData.get(key).containsKey(mapKey);
}
@@ -66,8 +67,8 @@ public class SparseArrayMap<T> {
* @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);
+ public V delete(int key, @NonNull K mapKey) {
+ ArrayMap<K, V> data = mData.get(key);
if (data != null) {
return data.remove(mapKey);
}
@@ -75,11 +76,11 @@ public class SparseArrayMap<T> {
}
/**
- * Get the value associated with the int-String pair.
+ * Get the value associated with the int-K pair.
*/
@Nullable
- public T get(int key, @NonNull String mapKey) {
- ArrayMap<String, T> data = mData.get(key);
+ public V get(int key, @NonNull K mapKey) {
+ ArrayMap<K, V> data = mData.get(key);
if (data != null) {
return data.get(mapKey);
}
@@ -91,9 +92,9 @@ public class SparseArrayMap<T> {
* map contains no mapping for them.
*/
@Nullable
- public T getOrDefault(int key, @NonNull String mapKey, T defaultValue) {
+ public V getOrDefault(int key, @NonNull K mapKey, V defaultValue) {
if (mData.contains(key)) {
- ArrayMap<String, T> data = mData.get(key);
+ ArrayMap<K, V> data = mData.get(key);
if (data != null && data.containsKey(mapKey)) {
return data.get(mapKey);
}
@@ -111,8 +112,8 @@ public class SparseArrayMap<T> {
*
* @see SparseArray#indexOfKey
*/
- public int indexOfKey(int key, @NonNull String mapKey) {
- ArrayMap<String, T> data = mData.get(key);
+ public int indexOfKey(int key, @NonNull K mapKey) {
+ ArrayMap<K, V> data = mData.get(key);
if (data != null) {
return data.indexOfKey(mapKey);
}
@@ -126,7 +127,7 @@ public class SparseArrayMap<T> {
/** Returns the map's key at the given mapIndex for the given keyIndex. */
@NonNull
- public String keyAt(int keyIndex, int mapIndex) {
+ public K keyAt(int keyIndex, int mapIndex) {
return mData.valueAt(keyIndex).keyAt(mapIndex);
}
@@ -137,20 +138,20 @@ public class SparseArrayMap<T> {
/** Returns the number of elements in the map of the given key. */
public int numElementsForKey(int key) {
- ArrayMap<String, T> data = mData.get(key);
+ ArrayMap<K, V> data = mData.get(key);
return data == null ? 0 : data.size();
}
- /** Returns the value T at the given key and map index. */
+ /** Returns the value V at the given key and map index. */
@Nullable
- public T valueAt(int keyIndex, int mapIndex) {
+ public V 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) {
+ /** Iterate through all int-K pairs and operate on all of the values. */
+ public void forEach(@NonNull Consumer<V> consumer) {
for (int i = numMaps() - 1; i >= 0; --i) {
- ArrayMap<String, T> data = mData.valueAt(i);
+ ArrayMap<K, V> data = mData.valueAt(i);
for (int j = data.size() - 1; j >= 0; --j) {
consumer.accept(data.valueAt(j));
}
diff --git a/core/java/android/util/SparseBooleanArray.java b/core/java/android/util/SparseBooleanArray.java
index 846df397aa6d..c145b20e6f6c 100644
--- a/core/java/android/util/SparseBooleanArray.java
+++ b/core/java/android/util/SparseBooleanArray.java
@@ -16,6 +16,7 @@
package android.util;
+import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import com.android.internal.util.ArrayUtils;
@@ -289,7 +290,7 @@ public class SparseBooleanArray implements Cloneable {
}
@Override
- public boolean equals(Object that) {
+ public boolean equals(@Nullable Object that) {
if (this == that) {
return true;
}
diff --git a/core/java/android/util/SparseDoubleArray.java b/core/java/android/util/SparseDoubleArray.java
new file mode 100644
index 000000000000..dc93a473fe44
--- /dev/null
+++ b/core/java/android/util/SparseDoubleArray.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2021 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;
+
+/**
+ * SparseDoubleArrays map integers to doubles. Unlike a normal array of doubles,
+ * there can be gaps in the indices. It is intended to be more memory efficient
+ * than using a HashMap to map Integers to Doubles, 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>
+ *
+ * @see SparseLongArray
+ *
+ * @hide
+ */
+public class SparseDoubleArray implements Cloneable {
+ /**
+ * The int->double map, but storing the doubles as longs using
+ * {@link Double#doubleToRawLongBits(double)}.
+ */
+ private SparseLongArray mValues;
+
+ /** Creates a new SparseDoubleArray containing no mappings. */
+ public SparseDoubleArray() {
+ this(10);
+ }
+
+ /**
+ * Creates a new SparseDoubleArray, 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 SparseDoubleArray(int initialCapacity) {
+ mValues = new SparseLongArray(initialCapacity);
+ }
+
+ @Override
+ public SparseDoubleArray clone() {
+ SparseDoubleArray clone = null;
+ try {
+ clone = (SparseDoubleArray) super.clone();
+ clone.mValues = mValues.clone();
+ } catch (CloneNotSupportedException cnse) {
+ /* ignore */
+ }
+ return clone;
+ }
+
+ /**
+ * Gets the double mapped from the specified key, or <code>0</code>
+ * if no such mapping has been made.
+ */
+ public double get(int key) {
+ final int index = mValues.indexOfKey(key);
+ if (index < 0) {
+ return 0.0d;
+ }
+ return valueAt(index);
+ }
+
+ /**
+ * 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, double value) {
+ mValues.put(key, Double.doubleToRawLongBits(value));
+ }
+
+ /**
+ * Adds a mapping from the specified key to the specified value,
+ * <b>adding</b> its value to the previous mapping from the specified key if there
+ * was one.
+ *
+ * <p>This differs from {@link #put} because instead of replacing any previous value, it adds
+ * (in the numerical sense) to it.
+ */
+ public void add(int key, double summand) {
+ final double oldValue = get(key);
+ put(key, oldValue + summand);
+ }
+
+ /** Returns the number of key-value mappings that this SparseDoubleArray currently stores. */
+ public int size() {
+ return mValues.size();
+ }
+
+ /**
+ * 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
+ * SparseDoubleArray stores.
+ *
+ * @see SparseLongArray#keyAt(int)
+ */
+ public int keyAt(int index) {
+ return mValues.keyAt(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
+ * SparseDoubleArray stores.
+ *
+ * @see SparseLongArray#valueAt(int)
+ */
+ public double valueAt(int index) {
+ return Double.longBitsToDouble(mValues.valueAt(index));
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>This implementation composes a string by iterating over its mappings.
+ */
+ @Override
+ public String toString() {
+ if (size() <= 0) {
+ return "{}";
+ }
+
+ StringBuilder buffer = new StringBuilder(size() * 34);
+ buffer.append('{');
+ for (int i = 0; i < size(); i++) {
+ if (i > 0) {
+ buffer.append(", ");
+ }
+ int key = keyAt(i);
+ buffer.append(key);
+ buffer.append('=');
+ double value = valueAt(i);
+ buffer.append(value);
+ }
+ buffer.append('}');
+ return buffer.toString();
+ }
+}
diff --git a/core/java/android/util/SparseLongArray.java b/core/java/android/util/SparseLongArray.java
index b0916d3d0cca..f2bc0c5a34d6 100644
--- a/core/java/android/util/SparseLongArray.java
+++ b/core/java/android/util/SparseLongArray.java
@@ -164,7 +164,7 @@ public class SparseLongArray implements Cloneable {
}
/**
- * Returns the number of key-value mappings that this SparseIntArray
+ * Returns the number of key-value mappings that this SparseLongArray
* currently stores.
*/
public int size() {
@@ -246,7 +246,7 @@ public class SparseLongArray implements Cloneable {
}
/**
- * Removes all key-value mappings from this SparseIntArray.
+ * Removes all key-value mappings from this SparseLongArray.
*/
public void clear() {
mSize = 0;
diff --git a/core/java/android/util/SystemConfigFileCommitEventLogger.java b/core/java/android/util/SystemConfigFileCommitEventLogger.java
new file mode 100644
index 000000000000..04d72fb7a255
--- /dev/null
+++ b/core/java/android/util/SystemConfigFileCommitEventLogger.java
@@ -0,0 +1,73 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.UptimeMillisLong;
+import android.os.SystemClock;
+
+/**
+ * Writes an EventLog event capturing the performance of system config file writes.
+ * The event log entry is formatted like this:
+ * <code>525000 commit_sys_config_file (name|3),(time|2|3)</code>, where <code>name</code> is
+ * a short unique name representing the type of configuration file and <code>time</code> is
+ * duration in the {@link SystemClock#uptimeMillis()} time base.
+ *
+ * @hide
+ */
+@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+public class SystemConfigFileCommitEventLogger {
+ private final String mName;
+ private long mStartTime;
+
+ /**
+ * @param name The short name of the config file that is included in the event log event,
+ * e.g. "jobs", "appops", "uri-grants" etc.
+ */
+ public SystemConfigFileCommitEventLogger(@NonNull String name) {
+ mName = name;
+ }
+
+ /**
+ * Override the start timestamp. Use this method when it's desired to include the time
+ * taken by the preparation of the configuration data in the overall duration of the
+ * "commitSysConfigFile" event.
+ *
+ * @param startTime Overridden start time, in system uptime milliseconds
+ */
+ public void setStartTime(@UptimeMillisLong long startTime) {
+ mStartTime = startTime;
+ }
+
+ /**
+ * Invoked just before the configuration file writing begins.
+ */
+ void onStartWrite() {
+ if (mStartTime == 0) {
+ mStartTime = SystemClock.uptimeMillis();
+ }
+ }
+
+ /**
+ * Invoked just after the configuration file writing ends.
+ */
+ void onFinishWrite() {
+ com.android.internal.logging.EventLogTags.writeCommitSysConfigFile(mName,
+ SystemClock.uptimeMillis() - mStartTime);
+ }
+}
diff --git a/core/java/android/util/TypedValue.java b/core/java/android/util/TypedValue.java
index 7f1ee302903b..19de396c4a4a 100644
--- a/core/java/android/util/TypedValue.java
+++ b/core/java/android/util/TypedValue.java
@@ -17,8 +17,14 @@
package android.util;
import android.annotation.AnyRes;
+import android.annotation.FloatRange;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
import android.content.pm.ActivityInfo.Config;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
/**
* Container for a dynamically typed data value. Primarily used with
* {@link android.content.res.Resources} for holding resource values.
@@ -95,6 +101,18 @@ public class TypedValue {
* defined below. */
public static final int COMPLEX_UNIT_MASK = 0xf;
+ /** @hide **/
+ @IntDef(prefix = "COMPLEX_UNIT_", value = {
+ COMPLEX_UNIT_PX,
+ COMPLEX_UNIT_DIP,
+ COMPLEX_UNIT_SP,
+ COMPLEX_UNIT_PT,
+ COMPLEX_UNIT_IN,
+ COMPLEX_UNIT_MM,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ComplexDimensionUnit {}
+
/** {@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
@@ -381,7 +399,7 @@ public class TypedValue {
* @return The complex floating point value multiplied by the appropriate
* metrics depending on its unit.
*/
- public static float applyDimension(int unit, float value,
+ public static float applyDimension(@ComplexDimensionUnit int unit, float value,
DisplayMetrics metrics)
{
switch (unit) {
@@ -417,6 +435,130 @@ public class TypedValue {
}
/**
+ * Construct a complex data integer. This validates the radix and the magnitude of the
+ * mantissa, and sets the {@link TypedValue#COMPLEX_MANTISSA_MASK} and
+ * {@link TypedValue#COMPLEX_RADIX_MASK} components as provided. The units are not set.
+ **
+ * @param mantissa an integer representing the mantissa.
+ * @param radix a radix option, e.g. {@link TypedValue#COMPLEX_RADIX_23p0}.
+ * @return A complex data integer representing the value.
+ * @hide
+ */
+ private static int createComplex(@IntRange(from = -0x800000, to = 0x7FFFFF) int mantissa,
+ int radix) {
+ if (mantissa < -0x800000 || mantissa >= 0x800000) {
+ throw new IllegalArgumentException("Magnitude of mantissa is too large: " + mantissa);
+ }
+ if (radix < TypedValue.COMPLEX_RADIX_23p0 || radix > TypedValue.COMPLEX_RADIX_0p23) {
+ throw new IllegalArgumentException("Invalid radix: " + radix);
+ }
+ return ((mantissa & TypedValue.COMPLEX_MANTISSA_MASK) << TypedValue.COMPLEX_MANTISSA_SHIFT)
+ | (radix << TypedValue.COMPLEX_RADIX_SHIFT);
+ }
+
+ /**
+ * Convert a base value to a complex data integer. This sets the {@link
+ * TypedValue#COMPLEX_MANTISSA_MASK} and {@link TypedValue#COMPLEX_RADIX_MASK} fields of the
+ * data to create a floating point representation of the given value. The units are not set.
+ *
+ * <p>This is the inverse of {@link TypedValue#complexToFloat(int)}.
+ *
+ * @param value An integer value.
+ * @return A complex data integer representing the value.
+ * @hide
+ */
+ public static int intToComplex(int value) {
+ if (value < -0x800000 || value >= 0x800000) {
+ throw new IllegalArgumentException("Magnitude of the value is too large: " + value);
+ }
+ return createComplex(value, TypedValue.COMPLEX_RADIX_23p0);
+ }
+
+ /**
+ * Convert a base value to a complex data integer. This sets the {@link
+ * TypedValue#COMPLEX_MANTISSA_MASK} and {@link TypedValue#COMPLEX_RADIX_MASK} fields of the
+ * data to create a floating point representation of the given value. The units are not set.
+ *
+ * <p>This is the inverse of {@link TypedValue#complexToFloat(int)}.
+ *
+ * @param value A floating point value.
+ * @return A complex data integer representing the value.
+ * @hide
+ */
+ public static int floatToComplex(@FloatRange(from = -0x800000, to = 0x7FFFFF) float value) {
+ // validate that the magnitude fits in this representation
+ if (value < (float) -0x800000 - .5f || value >= (float) 0x800000 - .5f) {
+ throw new IllegalArgumentException("Magnitude of the value is too large: " + value);
+ }
+ try {
+ // If there's no fraction, use integer representation, as that's clearer
+ if (value == (float) (int) value) {
+ return createComplex((int) value, TypedValue.COMPLEX_RADIX_23p0);
+ }
+ float absValue = Math.abs(value);
+ // If the magnitude is 0, we don't need any magnitude digits
+ if (absValue < 1f) {
+ return createComplex(Math.round(value * (1 << 23)), TypedValue.COMPLEX_RADIX_0p23);
+ }
+ // If the magnitude is less than 2^8, use 8 magnitude digits
+ if (absValue < (float) (1 << 8)) {
+ return createComplex(Math.round(value * (1 << 15)), TypedValue.COMPLEX_RADIX_8p15);
+ }
+ // If the magnitude is less than 2^16, use 16 magnitude digits
+ if (absValue < (float) (1 << 16)) {
+ return createComplex(Math.round(value * (1 << 7)), TypedValue.COMPLEX_RADIX_16p7);
+ }
+ // The magnitude requires all 23 digits
+ return createComplex(Math.round(value), TypedValue.COMPLEX_RADIX_23p0);
+ } catch (IllegalArgumentException ex) {
+ // Wrap exception so as to include the value argument in the message.
+ throw new IllegalArgumentException("Unable to convert value to complex: " + value, ex);
+ }
+ }
+
+ /**
+ * <p>Creates a complex data integer that stores a dimension value and units.
+ *
+ * <p>The resulting value can be passed to e.g.
+ * {@link TypedValue#complexToDimensionPixelOffset(int, DisplayMetrics)} to calculate the pixel
+ * value for the dimension.
+ *
+ * @param value the value of the dimension
+ * @param units the units of the dimension, e.g. {@link TypedValue#COMPLEX_UNIT_DIP}
+ * @return A complex data integer representing the value and units of the dimension.
+ * @hide
+ */
+ public static int createComplexDimension(
+ @IntRange(from = -0x800000, to = 0x7FFFFF) int value,
+ @ComplexDimensionUnit int units) {
+ if (units < TypedValue.COMPLEX_UNIT_PX || units > TypedValue.COMPLEX_UNIT_MM) {
+ throw new IllegalArgumentException("Must be a valid COMPLEX_UNIT_*: " + units);
+ }
+ return intToComplex(value) | units;
+ }
+
+ /**
+ * <p>Creates a complex data integer that stores a dimension value and units.
+ *
+ * <p>The resulting value can be passed to e.g.
+ * {@link TypedValue#complexToDimensionPixelOffset(int, DisplayMetrics)} to calculate the pixel
+ * value for the dimension.
+ *
+ * @param value the value of the dimension
+ * @param units the units of the dimension, e.g. {@link TypedValue#COMPLEX_UNIT_DIP}
+ * @return A complex data integer representing the value and units of the dimension.
+ * @hide
+ */
+ public static int createComplexDimension(
+ @FloatRange(from = -0x800000, to = 0x7FFFFF) float value,
+ @ComplexDimensionUnit int units) {
+ if (units < TypedValue.COMPLEX_UNIT_PX || units > TypedValue.COMPLEX_UNIT_MM) {
+ throw new IllegalArgumentException("Must be a valid COMPLEX_UNIT_*: " + units);
+ }
+ return floatToComplex(value) | units;
+ }
+
+ /**
* 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}.
diff --git a/core/java/android/util/TypedXmlPullParser.java b/core/java/android/util/TypedXmlPullParser.java
new file mode 100644
index 000000000000..aa68bf42fe52
--- /dev/null
+++ b/core/java/android/util/TypedXmlPullParser.java
@@ -0,0 +1,330 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+/**
+ * Specialization of {@link XmlPullParser} which adds explicit methods to
+ * support consistent and efficient conversion of primitive data types.
+ *
+ * @hide
+ */
+public interface TypedXmlPullParser extends XmlPullParser {
+ /**
+ * @return index of requested attribute, otherwise {@code -1} if undefined
+ */
+ default int getAttributeIndex(@Nullable String namespace, @NonNull String name) {
+ final boolean namespaceNull = (namespace == null);
+ final int count = getAttributeCount();
+ for (int i = 0; i < count; i++) {
+ if ((namespaceNull || namespace.equals(getAttributeNamespace(i)))
+ && name.equals(getAttributeName(i))) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * @return index of requested attribute
+ * @throws XmlPullParserException if the value is undefined
+ */
+ default int getAttributeIndexOrThrow(@Nullable String namespace, @NonNull String name)
+ throws XmlPullParserException {
+ final int index = getAttributeIndex(namespace, name);
+ if (index == -1) {
+ throw new XmlPullParserException("Missing attribute " + name);
+ } else {
+ return index;
+ }
+ }
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}
+ * @throws XmlPullParserException if the value is malformed
+ */
+ @NonNull byte[] getAttributeBytesHex(int index) throws XmlPullParserException;
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}
+ * @throws XmlPullParserException if the value is malformed
+ */
+ @NonNull byte[] getAttributeBytesBase64(int index) throws XmlPullParserException;
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}
+ * @throws XmlPullParserException if the value is malformed
+ */
+ int getAttributeInt(int index) throws XmlPullParserException;
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}
+ * @throws XmlPullParserException if the value is malformed
+ */
+ int getAttributeIntHex(int index) throws XmlPullParserException;
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}
+ * @throws XmlPullParserException if the value is malformed
+ */
+ long getAttributeLong(int index) throws XmlPullParserException;
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}
+ * @throws XmlPullParserException if the value is malformed
+ */
+ long getAttributeLongHex(int index) throws XmlPullParserException;
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}
+ * @throws XmlPullParserException if the value is malformed
+ */
+ float getAttributeFloat(int index) throws XmlPullParserException;
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}
+ * @throws XmlPullParserException if the value is malformed
+ */
+ double getAttributeDouble(int index) throws XmlPullParserException;
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}
+ * @throws XmlPullParserException if the value is malformed
+ */
+ boolean getAttributeBoolean(int index) throws XmlPullParserException;
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}
+ * @throws XmlPullParserException if the value is malformed or undefined
+ */
+ default @NonNull byte[] getAttributeBytesHex(@Nullable String namespace,
+ @NonNull String name) throws XmlPullParserException {
+ return getAttributeBytesHex(getAttributeIndexOrThrow(namespace, name));
+ }
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}
+ * @throws XmlPullParserException if the value is malformed or undefined
+ */
+ default @NonNull byte[] getAttributeBytesBase64(@Nullable String namespace,
+ @NonNull String name) throws XmlPullParserException {
+ return getAttributeBytesBase64(getAttributeIndexOrThrow(namespace, name));
+ }
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}
+ * @throws XmlPullParserException if the value is malformed or undefined
+ */
+ default int getAttributeInt(@Nullable String namespace, @NonNull String name)
+ throws XmlPullParserException {
+ return getAttributeInt(getAttributeIndexOrThrow(namespace, name));
+ }
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}
+ * @throws XmlPullParserException if the value is malformed or undefined
+ */
+ default int getAttributeIntHex(@Nullable String namespace, @NonNull String name)
+ throws XmlPullParserException {
+ return getAttributeIntHex(getAttributeIndexOrThrow(namespace, name));
+ }
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}
+ * @throws XmlPullParserException if the value is malformed or undefined
+ */
+ default long getAttributeLong(@Nullable String namespace, @NonNull String name)
+ throws XmlPullParserException {
+ return getAttributeLong(getAttributeIndexOrThrow(namespace, name));
+ }
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}
+ * @throws XmlPullParserException if the value is malformed or undefined
+ */
+ default long getAttributeLongHex(@Nullable String namespace, @NonNull String name)
+ throws XmlPullParserException {
+ return getAttributeLongHex(getAttributeIndexOrThrow(namespace, name));
+ }
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}
+ * @throws XmlPullParserException if the value is malformed or undefined
+ */
+ default float getAttributeFloat(@Nullable String namespace, @NonNull String name)
+ throws XmlPullParserException {
+ return getAttributeFloat(getAttributeIndexOrThrow(namespace, name));
+ }
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}
+ * @throws XmlPullParserException if the value is malformed or undefined
+ */
+ default double getAttributeDouble(@Nullable String namespace, @NonNull String name)
+ throws XmlPullParserException {
+ return getAttributeDouble(getAttributeIndexOrThrow(namespace, name));
+ }
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}
+ * @throws XmlPullParserException if the value is malformed or undefined
+ */
+ default boolean getAttributeBoolean(@Nullable String namespace, @NonNull String name)
+ throws XmlPullParserException {
+ return getAttributeBoolean(getAttributeIndexOrThrow(namespace, name));
+ }
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
+ * default value if the value is malformed or undefined
+ */
+ default @Nullable byte[] getAttributeBytesHex(@Nullable String namespace,
+ @NonNull String name, @Nullable byte[] defaultValue) {
+ final int index = getAttributeIndex(namespace, name);
+ if (index == -1) return defaultValue;
+ try {
+ return getAttributeBytesHex(index);
+ } catch (Exception ignored) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
+ * default value if the value is malformed or undefined
+ */
+ default @Nullable byte[] getAttributeBytesBase64(@Nullable String namespace,
+ @NonNull String name, @Nullable byte[] defaultValue) {
+ final int index = getAttributeIndex(namespace, name);
+ if (index == -1) return defaultValue;
+ try {
+ return getAttributeBytesBase64(index);
+ } catch (Exception ignored) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
+ * default value if the value is malformed or undefined
+ */
+ default int getAttributeInt(@Nullable String namespace, @NonNull String name,
+ int defaultValue) {
+ final int index = getAttributeIndex(namespace, name);
+ if (index == -1) return defaultValue;
+ try {
+ return getAttributeInt(index);
+ } catch (Exception ignored) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
+ * default value if the value is malformed or undefined
+ */
+ default int getAttributeIntHex(@Nullable String namespace, @NonNull String name,
+ int defaultValue) {
+ final int index = getAttributeIndex(namespace, name);
+ if (index == -1) return defaultValue;
+ try {
+ return getAttributeIntHex(index);
+ } catch (Exception ignored) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
+ * default value if the value is malformed or undefined
+ */
+ default long getAttributeLong(@Nullable String namespace, @NonNull String name,
+ long defaultValue) {
+ final int index = getAttributeIndex(namespace, name);
+ if (index == -1) return defaultValue;
+ try {
+ return getAttributeLong(index);
+ } catch (Exception ignored) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
+ * default value if the value is malformed or undefined
+ */
+ default long getAttributeLongHex(@Nullable String namespace, @NonNull String name,
+ long defaultValue) {
+ final int index = getAttributeIndex(namespace, name);
+ if (index == -1) return defaultValue;
+ try {
+ return getAttributeLongHex(index);
+ } catch (Exception ignored) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
+ * default value if the value is malformed or undefined
+ */
+ default float getAttributeFloat(@Nullable String namespace, @NonNull String name,
+ float defaultValue) {
+ final int index = getAttributeIndex(namespace, name);
+ if (index == -1) return defaultValue;
+ try {
+ return getAttributeFloat(index);
+ } catch (Exception ignored) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
+ * default value if the value is malformed or undefined
+ */
+ default double getAttributeDouble(@Nullable String namespace, @NonNull String name,
+ double defaultValue) {
+ final int index = getAttributeIndex(namespace, name);
+ if (index == -1) return defaultValue;
+ try {
+ return getAttributeDouble(index);
+ } catch (Exception ignored) {
+ return defaultValue;
+ }
+ }
+
+ /**
+ * @return decoded strongly-typed {@link #getAttributeValue}, otherwise
+ * default value if the value is malformed or undefined
+ */
+ default boolean getAttributeBoolean(@Nullable String namespace, @NonNull String name,
+ boolean defaultValue) {
+ final int index = getAttributeIndex(namespace, name);
+ if (index == -1) return defaultValue;
+ try {
+ return getAttributeBoolean(index);
+ } catch (Exception ignored) {
+ return defaultValue;
+ }
+ }
+}
diff --git a/core/java/android/util/TypedXmlSerializer.java b/core/java/android/util/TypedXmlSerializer.java
new file mode 100644
index 000000000000..3f9eaa89a58e
--- /dev/null
+++ b/core/java/android/util/TypedXmlSerializer.java
@@ -0,0 +1,103 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * Specialization of {@link XmlSerializer} which adds explicit methods to
+ * support consistent and efficient conversion of primitive data types.
+ *
+ * @hide
+ */
+public interface TypedXmlSerializer extends XmlSerializer {
+ /**
+ * Functionally equivalent to {@link #attribute(String, String, String)} but
+ * with the additional signal that the given value is a candidate for being
+ * canonicalized, similar to {@link String#intern()}.
+ */
+ @NonNull XmlSerializer attributeInterned(@Nullable String namespace, @NonNull String name,
+ @NonNull String value) throws IOException;
+
+ /**
+ * Encode the given strongly-typed value and serialize using
+ * {@link #attribute(String, String, String)}.
+ */
+ @NonNull XmlSerializer attributeBytesHex(@Nullable String namespace, @NonNull String name,
+ @NonNull byte[] value) throws IOException;
+
+ /**
+ * Encode the given strongly-typed value and serialize using
+ * {@link #attribute(String, String, String)}.
+ */
+ @NonNull XmlSerializer attributeBytesBase64(@Nullable String namespace, @NonNull String name,
+ @NonNull byte[] value) throws IOException;
+
+ /**
+ * Encode the given strongly-typed value and serialize using
+ * {@link #attribute(String, String, String)}.
+ */
+ @NonNull XmlSerializer attributeInt(@Nullable String namespace, @NonNull String name,
+ int value) throws IOException;
+
+ /**
+ * Encode the given strongly-typed value and serialize using
+ * {@link #attribute(String, String, String)}.
+ */
+ @NonNull XmlSerializer attributeIntHex(@Nullable String namespace, @NonNull String name,
+ int value) throws IOException;
+
+ /**
+ * Encode the given strongly-typed value and serialize using
+ * {@link #attribute(String, String, String)}.
+ */
+ @NonNull XmlSerializer attributeLong(@Nullable String namespace, @NonNull String name,
+ long value) throws IOException;
+
+ /**
+ * Encode the given strongly-typed value and serialize using
+ * {@link #attribute(String, String, String)}.
+ */
+ @NonNull XmlSerializer attributeLongHex(@Nullable String namespace, @NonNull String name,
+ long value) throws IOException;
+
+ /**
+ * Encode the given strongly-typed value and serialize using
+ * {@link #attribute(String, String, String)}.
+ */
+ @NonNull XmlSerializer attributeFloat(@Nullable String namespace, @NonNull String name,
+ float value) throws IOException;
+
+ /**
+ * Encode the given strongly-typed value and serialize using
+ * {@link #attribute(String, String, String)}.
+ */
+ @NonNull XmlSerializer attributeDouble(@Nullable String namespace, @NonNull String name,
+ double value) throws IOException;
+
+ /**
+ * Encode the given strongly-typed value and serialize using
+ * {@link #attribute(String, String, String)}.
+ */
+ @NonNull XmlSerializer attributeBoolean(@Nullable String namespace, @NonNull String name,
+ boolean value) throws IOException;
+}
diff --git a/core/java/android/util/Xml.java b/core/java/android/util/Xml.java
index e3b8fec3559e..38decf9951a7 100644
--- a/core/java/android/util/Xml.java
+++ b/core/java/android/util/Xml.java
@@ -16,6 +16,17 @@
package android.util;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.SystemProperties;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import com.android.internal.util.BinaryXmlPullParser;
+import com.android.internal.util.BinaryXmlSerializer;
+import com.android.internal.util.FastXmlSerializer;
+import com.android.internal.util.XmlUtils;
+
import libcore.util.XmlObjectFactory;
import org.xml.sax.ContentHandler;
@@ -26,11 +37,16 @@ import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
+import java.io.BufferedInputStream;
+import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
/**
* XML utility methods.
@@ -47,6 +63,15 @@ public class Xml {
public static String FEATURE_RELAXED = "http://xmlpull.org/v1/doc/features.html#relaxed";
/**
+ * Feature flag: when set, {@link #resolveSerializer(OutputStream)} will
+ * emit binary XML by default.
+ *
+ * @hide
+ */
+ public static final boolean ENABLE_BINARY_DEFAULT = SystemProperties
+ .getBoolean("persist.sys.binary_xml", true);
+
+ /**
* Parses the given xml string and fires events on the given SAX handler.
*/
public static void parse(String xml, ContentHandler contentHandler)
@@ -99,6 +124,78 @@ public class Xml {
}
/**
+ * Creates a new {@link TypedXmlPullParser} which is optimized for use
+ * inside the system, typically by supporting only a basic set of features.
+ * <p>
+ * In particular, the returned parser does not support namespaces, prefixes,
+ * properties, or options.
+ *
+ * @hide
+ */
+ @SuppressWarnings("AndroidFrameworkEfficientXml")
+ public static @NonNull TypedXmlPullParser newFastPullParser() {
+ return XmlUtils.makeTyped(newPullParser());
+ }
+
+ /**
+ * Creates a new {@link XmlPullParser} that reads XML documents using a
+ * custom binary wire protocol which benchmarking has shown to be 8.5x
+ * faster than {@code Xml.newFastPullParser()} for a typical
+ * {@code packages.xml}.
+ *
+ * @hide
+ */
+ public static @NonNull TypedXmlPullParser newBinaryPullParser() {
+ return new BinaryXmlPullParser();
+ }
+
+ /**
+ * Creates a new {@link XmlPullParser} which is optimized for use inside the
+ * system, typically by supporting only a basic set of features.
+ * <p>
+ * This returned instance may be configured to read using an efficient
+ * binary format instead of a human-readable text format, depending on
+ * device feature flags.
+ * <p>
+ * To ensure that both formats are detected and transparently handled
+ * correctly, you must shift to using both {@link #resolveSerializer} and
+ * {@link #resolvePullParser}.
+ *
+ * @hide
+ */
+ public static @NonNull TypedXmlPullParser resolvePullParser(@NonNull InputStream in)
+ throws IOException {
+ final byte[] magic = new byte[4];
+ if (in instanceof FileInputStream) {
+ try {
+ Os.pread(((FileInputStream) in).getFD(), magic, 0, magic.length, 0);
+ } catch (ErrnoException e) {
+ throw e.rethrowAsIOException();
+ }
+ } else {
+ if (!in.markSupported()) {
+ in = new BufferedInputStream(in);
+ }
+ in.mark(8);
+ in.read(magic);
+ in.reset();
+ }
+
+ final TypedXmlPullParser xml;
+ if (Arrays.equals(magic, BinaryXmlSerializer.PROTOCOL_MAGIC_VERSION_0)) {
+ xml = newBinaryPullParser();
+ } else {
+ xml = newFastPullParser();
+ }
+ try {
+ xml.setInput(in, StandardCharsets.UTF_8.name());
+ } catch (XmlPullParserException e) {
+ throw new IOException(e);
+ }
+ return xml;
+ }
+
+ /**
* Creates a new xml serializer.
*/
public static XmlSerializer newSerializer() {
@@ -106,6 +203,134 @@ public class Xml {
}
/**
+ * Creates a new {@link XmlSerializer} which is optimized for use inside the
+ * system, typically by supporting only a basic set of features.
+ * <p>
+ * In particular, the returned parser does not support namespaces, prefixes,
+ * properties, or options.
+ *
+ * @hide
+ */
+ @SuppressWarnings("AndroidFrameworkEfficientXml")
+ public static @NonNull TypedXmlSerializer newFastSerializer() {
+ return XmlUtils.makeTyped(new FastXmlSerializer());
+ }
+
+ /**
+ * Creates a new {@link XmlSerializer} that writes XML documents using a
+ * custom binary wire protocol which benchmarking has shown to be 4.4x
+ * faster and use 2.8x less disk space than {@code Xml.newFastSerializer()}
+ * for a typical {@code packages.xml}.
+ *
+ * @hide
+ */
+ public static @NonNull TypedXmlSerializer newBinarySerializer() {
+ return new BinaryXmlSerializer();
+ }
+
+ /**
+ * Creates a new {@link XmlSerializer} which is optimized for use inside the
+ * system, typically by supporting only a basic set of features.
+ * <p>
+ * This returned instance may be configured to write using an efficient
+ * binary format instead of a human-readable text format, depending on
+ * device feature flags.
+ * <p>
+ * To ensure that both formats are detected and transparently handled
+ * correctly, you must shift to using both {@link #resolveSerializer} and
+ * {@link #resolvePullParser}.
+ *
+ * @hide
+ */
+ public static @NonNull TypedXmlSerializer resolveSerializer(@NonNull OutputStream out)
+ throws IOException {
+ final TypedXmlSerializer xml;
+ if (ENABLE_BINARY_DEFAULT) {
+ xml = newBinarySerializer();
+ } else {
+ xml = newFastSerializer();
+ }
+ xml.setOutput(out, StandardCharsets.UTF_8.name());
+ return xml;
+ }
+
+ /**
+ * Copy the first XML document into the second document.
+ * <p>
+ * Implemented by reading all events from the given {@link XmlPullParser}
+ * and writing them directly to the given {@link XmlSerializer}. This can be
+ * useful for transparently converting between underlying wire protocols.
+ *
+ * @hide
+ */
+ public static void copy(@NonNull XmlPullParser in, @NonNull XmlSerializer out)
+ throws XmlPullParserException, IOException {
+ // Some parsers may have already consumed the event that starts the
+ // document, so we manually emit that event here for consistency
+ if (in.getEventType() == XmlPullParser.START_DOCUMENT) {
+ out.startDocument(in.getInputEncoding(), true);
+ }
+
+ while (true) {
+ final int token = in.nextToken();
+ switch (token) {
+ case XmlPullParser.START_DOCUMENT:
+ out.startDocument(in.getInputEncoding(), true);
+ break;
+ case XmlPullParser.END_DOCUMENT:
+ out.endDocument();
+ return;
+ case XmlPullParser.START_TAG:
+ out.startTag(normalizeNamespace(in.getNamespace()), in.getName());
+ for (int i = 0; i < in.getAttributeCount(); i++) {
+ out.attribute(normalizeNamespace(in.getAttributeNamespace(i)),
+ in.getAttributeName(i), in.getAttributeValue(i));
+ }
+ break;
+ case XmlPullParser.END_TAG:
+ out.endTag(normalizeNamespace(in.getNamespace()), in.getName());
+ break;
+ case XmlPullParser.TEXT:
+ out.text(in.getText());
+ break;
+ case XmlPullParser.CDSECT:
+ out.cdsect(in.getText());
+ break;
+ case XmlPullParser.ENTITY_REF:
+ out.entityRef(in.getName());
+ break;
+ case XmlPullParser.IGNORABLE_WHITESPACE:
+ out.ignorableWhitespace(in.getText());
+ break;
+ case XmlPullParser.PROCESSING_INSTRUCTION:
+ out.processingInstruction(in.getText());
+ break;
+ case XmlPullParser.COMMENT:
+ out.comment(in.getText());
+ break;
+ case XmlPullParser.DOCDECL:
+ out.docdecl(in.getText());
+ break;
+ default:
+ throw new IllegalStateException("Unknown token " + token);
+ }
+ }
+ }
+
+ /**
+ * Some parsers may return an empty string {@code ""} when a namespace in
+ * unsupported, which can confuse serializers. This method normalizes empty
+ * strings to be {@code null}.
+ */
+ private static @Nullable String normalizeNamespace(@Nullable String namespace) {
+ if (namespace == null || namespace.isEmpty()) {
+ return null;
+ } else {
+ return namespace;
+ }
+ }
+
+ /**
* Supported character encodings.
*/
public enum Encoding {
diff --git a/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java b/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java
index 077115769374..c7d9b9c4ab3e 100644
--- a/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java
+++ b/core/java/android/util/apk/ApkSignatureSchemeV2Verifier.java
@@ -24,7 +24,6 @@ import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmContent
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;
@@ -150,7 +149,7 @@ public class ApkSignatureSchemeV2Verifier {
* @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)
+ public static SignatureInfo findSignature(RandomAccessFile apk)
throws IOException, SignatureNotFoundException {
return ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V2_BLOCK_ID);
}
@@ -213,11 +212,9 @@ public class ApkSignatureSchemeV2Verifier {
verityDigest, apk.getChannel().size(), signatureInfo);
}
- byte[] digest = pickBestDigestForV4(contentDigests);
-
return new VerifiedSigner(
signerCerts.toArray(new X509Certificate[signerCerts.size()][]),
- verityRootHash, digest);
+ verityRootHash, contentDigests);
}
private static X509Certificate[] verifySigner(
@@ -339,8 +336,7 @@ public class ApkSignatureSchemeV2Verifier {
} catch (CertificateException e) {
throw new SecurityException("Failed to decode certificate #" + certificateCount, e);
}
- certificate = new VerbatimX509Certificate(
- certificate, encodedCert);
+ certificate = new VerbatimX509Certificate(certificate, encodedCert);
certs.add(certificate);
}
@@ -434,12 +430,15 @@ public class ApkSignatureSchemeV2Verifier {
public final X509Certificate[][] certs;
public final byte[] verityRootHash;
- public final byte[] digest;
+ // Algorithm -> digest map of signed digests in the signature.
+ // All these are verified if requested.
+ public final Map<Integer, byte[]> contentDigests;
- public VerifiedSigner(X509Certificate[][] certs, byte[] verityRootHash, byte[] digest) {
+ public VerifiedSigner(X509Certificate[][] certs, byte[] verityRootHash,
+ Map<Integer, byte[]> contentDigests) {
this.certs = certs;
this.verityRootHash = verityRootHash;
- this.digest = digest;
+ this.contentDigests = contentDigests;
}
}
diff --git a/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java b/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java
index 44f01a4f9759..b07b5223d296 100644
--- a/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java
+++ b/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java
@@ -24,8 +24,8 @@ import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmContent
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 static android.util.apk.ApkSigningBlockUtils.verifyProofOfRotationStruct;
import android.os.Build;
import android.util.ArrayMap;
@@ -54,7 +54,6 @@ 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;
@@ -91,9 +90,10 @@ public class ApkSignatureSchemeV3Verifier {
* 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.
+ * @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 {
@@ -107,7 +107,7 @@ public class ApkSignatureSchemeV3Verifier {
* 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.
+ * @throws IOException if an I/O error occurs while reading the APK file.
*/
public static VerifiedSigner unsafeGetCertsWithoutVerification(String apkFile)
throws SignatureNotFoundException, SecurityException, IOException {
@@ -126,9 +126,10 @@ public class ApkSignatureSchemeV3Verifier {
* 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.
+ * @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 {
@@ -141,9 +142,9 @@ public class ApkSignatureSchemeV3Verifier {
* 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.
+ * @throws IOException if an I/O error occurs while reading the APK file.
*/
- private static SignatureInfo findSignature(RandomAccessFile apk)
+ public static SignatureInfo findSignature(RandomAccessFile apk)
throws IOException, SignatureNotFoundException {
return ApkSigningBlockUtils.findSignature(apk, APK_SIGNATURE_SCHEME_V3_BLOCK_ID);
}
@@ -153,7 +154,7 @@ public class ApkSignatureSchemeV3Verifier {
* Block.
*
* @param signatureInfo APK Signature Scheme v3 Block and information relevant for verifying it
- * against the APK file.
+ * against the APK file.
*/
private static VerifiedSigner verify(
RandomAccessFile apk,
@@ -161,7 +162,7 @@ public class ApkSignatureSchemeV3Verifier {
boolean doVerifyIntegrity) throws SecurityException, IOException {
int signerCount = 0;
Map<Integer, byte[]> contentDigests = new ArrayMap<>();
- VerifiedSigner result = null;
+ Pair<X509Certificate[], ApkSigningBlockUtils.VerifiedProofOfRotation> result = null;
CertificateFactory certFactory;
try {
certFactory = CertificateFactory.getInstance("X.509");
@@ -206,21 +207,21 @@ public class ApkSignatureSchemeV3Verifier {
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);
- result.verityRootHash = ApkSigningBlockUtils.parseVerityDigestAndVerifySourceLength(
+ verityRootHash = ApkSigningBlockUtils.parseVerityDigestAndVerifySourceLength(
verityDigest, apk.getChannel().size(), signatureInfo);
}
- result.digest = pickBestDigestForV4(contentDigests);
-
- return result;
+ return new VerifiedSigner(result.first, result.second, verityRootHash, contentDigests);
}
- private static VerifiedSigner verifySigner(
- ByteBuffer signerBlock,
- Map<Integer, byte[]> contentDigests,
- CertificateFactory certFactory)
+ private static Pair<X509Certificate[], ApkSigningBlockUtils.VerifiedProofOfRotation>
+ verifySigner(
+ ByteBuffer signerBlock,
+ Map<Integer, byte[]> contentDigests,
+ CertificateFactory certFactory)
throws SecurityException, IOException, PlatformNotSupportedException {
ByteBuffer signedData = getLengthPrefixedSlice(signerBlock);
int minSdkVersion = signerBlock.getInt();
@@ -230,9 +231,9 @@ public class ApkSignatureSchemeV3Verifier {
// 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);
+ + "version. This platform: " + Build.VERSION.SDK_INT
+ + ", signer minSdkVersion: " + minSdkVersion
+ + ", maxSdkVersion: " + maxSdkVersion);
}
ByteBuffer signatures = getLengthPrefixedSlice(signerBlock);
@@ -333,7 +334,8 @@ public class ApkSignatureSchemeV3Verifier {
&& (!MessageDigest.isEqual(previousSignerDigest, contentDigest))) {
throw new SecurityException(
getContentDigestAlgorithmJcaDigestAlgorithm(digestAlgorithm)
- + " contents digest does not match the digest specified by a preceding signer");
+ + " contents digest does not match the digest specified by a "
+ + "preceding signer");
}
ByteBuffer certificates = getLengthPrefixedSlice(signedData);
@@ -349,8 +351,7 @@ public class ApkSignatureSchemeV3Verifier {
} catch (CertificateException e) {
throw new SecurityException("Failed to decode certificate #" + certificateCount, e);
}
- certificate = new VerbatimX509Certificate(
- certificate, encodedCert);
+ certificate = new VerbatimX509Certificate(certificate, encodedCert);
certs.add(certificate);
}
@@ -382,10 +383,11 @@ public class ApkSignatureSchemeV3Verifier {
private static final int PROOF_OF_ROTATION_ATTR_ID = 0x3ba06f8c;
- private static VerifiedSigner verifyAdditionalAttributes(ByteBuffer attrs,
- List<X509Certificate> certs, CertificateFactory certFactory) throws IOException {
+ private static Pair<X509Certificate[], ApkSigningBlockUtils.VerifiedProofOfRotation>
+ verifyAdditionalAttributes(ByteBuffer attrs, List<X509Certificate> certs,
+ CertificateFactory certFactory) throws IOException {
X509Certificate[] certChain = certs.toArray(new X509Certificate[certs.size()]);
- VerifiedProofOfRotation por = null;
+ ApkSigningBlockUtils.VerifiedProofOfRotation por = null;
while (attrs.hasRemaining()) {
ByteBuffer attr = getLengthPrefixedSlice(attrs);
@@ -394,7 +396,7 @@ public class ApkSignatureSchemeV3Verifier {
+ "ID. Remaining: " + attr.remaining());
}
int id = attr.getInt();
- switch(id) {
+ switch (id) {
case PROOF_OF_ROTATION_ATTR_ID:
if (por != null) {
throw new SecurityException("Encountered multiple Proof-of-rotation records"
@@ -406,7 +408,7 @@ public class ApkSignatureSchemeV3Verifier {
try {
if (por.certs.size() > 0
&& !Arrays.equals(por.certs.get(por.certs.size() - 1).getEncoded(),
- certChain[0].getEncoded())) {
+ certChain[0].getEncoded())) {
throw new SecurityException("Terminal certificate in Proof-of-rotation"
+ " record does not match APK signing certificate");
}
@@ -421,97 +423,7 @@ public class ApkSignatureSchemeV3Verifier {
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);
+ return Pair.create(certChain, por);
}
static byte[] getVerityRootHash(String apkPath)
@@ -525,7 +437,7 @@ public class ApkSignatureSchemeV3Verifier {
static byte[] generateApkVerity(String apkPath, ByteBufferFactory bufferFactory)
throws IOException, SignatureNotFoundException, SecurityException, DigestException,
- NoSuchAlgorithmException {
+ NoSuchAlgorithmException {
try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
SignatureInfo signatureInfo = findSignature(apk);
return VerityBuilder.generateApkVerity(apkPath, bufferFactory, signatureInfo);
@@ -534,7 +446,7 @@ public class ApkSignatureSchemeV3Verifier {
static byte[] generateApkVerityRootHash(String apkPath)
throws NoSuchAlgorithmException, DigestException, IOException,
- SignatureNotFoundException {
+ SignatureNotFoundException {
try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
SignatureInfo signatureInfo = findSignature(apk);
VerifiedSigner vSigner = verify(apk, false);
@@ -547,35 +459,26 @@ public class ApkSignatureSchemeV3Verifier {
}
/**
- * 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 final ApkSigningBlockUtils.VerifiedProofOfRotation por;
- public byte[] verityRootHash;
- public byte[] digest;
+ public final byte[] verityRootHash;
+ // Algorithm -> digest map of signed digests in the signature.
+ // All these are verified if requested.
+ public final Map<Integer, byte[]> contentDigests;
- public VerifiedSigner(X509Certificate[] certs, VerifiedProofOfRotation por) {
+ public VerifiedSigner(X509Certificate[] certs,
+ ApkSigningBlockUtils.VerifiedProofOfRotation por,
+ byte[] verityRootHash, Map<Integer, byte[]> contentDigests) {
this.certs = certs;
this.por = por;
+ this.verityRootHash = verityRootHash;
+ this.contentDigests = contentDigests;
}
}
diff --git a/core/java/android/util/apk/ApkSignatureSchemeV4Verifier.java b/core/java/android/util/apk/ApkSignatureSchemeV4Verifier.java
index d40efce0b3b3..844816c0d903 100644
--- a/core/java/android/util/apk/ApkSignatureSchemeV4Verifier.java
+++ b/core/java/android/util/apk/ApkSignatureSchemeV4Verifier.java
@@ -16,12 +16,14 @@
package android.util.apk;
+import static android.util.apk.ApkSigningBlockUtils.CONTENT_DIGEST_VERITY_CHUNKED_SHA256;
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.ArrayMap;
import android.util.Pair;
import java.io.ByteArrayInputStream;
@@ -42,6 +44,7 @@ import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
+import java.util.Map;
/**
* APK Signature Scheme v4 verifier.
@@ -79,13 +82,20 @@ public class ApkSignatureSchemeV4Verifier {
throw new SignatureNotFoundException("Failed to read V4 signature.", e);
}
- final byte[] signedData = V4Signature.getSigningData(apk.length(), hashingInfo,
+ // Verify signed data and extract certificates and apk digest.
+ final byte[] signedData = V4Signature.getSignedData(apk.length(), hashingInfo,
signingInfo);
+ final Pair<Certificate, byte[]> result = verifySigner(signingInfo, signedData);
- return verifySigner(signingInfo, signedData);
+ // Populate digests enforced by IncFS driver.
+ Map<Integer, byte[]> contentDigests = new ArrayMap<>();
+ contentDigests.put(convertToContentDigestType(hashingInfo.hashAlgorithm),
+ hashingInfo.rawRootHash);
+
+ return new VerifiedSigner(new Certificate[]{result.first}, result.second, contentDigests);
}
- private static VerifiedSigner verifySigner(V4Signature.SigningInfo signingInfo,
+ private static Pair<Certificate, byte[]> verifySigner(V4Signature.SigningInfo signingInfo,
final byte[] signedData) throws SecurityException {
if (!isSupportedSignatureAlgorithm(signingInfo.signatureAlgorithmId)) {
throw new SecurityException("No supported signatures found");
@@ -145,21 +155,34 @@ public class ApkSignatureSchemeV4Verifier {
"Public key mismatch between certificate and signature record");
}
- return new VerifiedSigner(new Certificate[]{certificate}, signingInfo.apkDigest);
+ return Pair.create(certificate, signingInfo.apkDigest);
+ }
+
+ private static int convertToContentDigestType(int hashAlgorithm) throws SecurityException {
+ if (hashAlgorithm == V4Signature.HASHING_ALGORITHM_SHA256) {
+ return CONTENT_DIGEST_VERITY_CHUNKED_SHA256;
+ }
+ throw new SecurityException("Unsupported hashAlgorithm: " + hashAlgorithm);
}
/**
- * Verified APK Signature Scheme v4 signer, including V3 digest.
+ * Verified APK Signature Scheme v4 signer, including V2/V3 digest.
*
* @hide for internal use only.
*/
public static class VerifiedSigner {
public final Certificate[] certs;
- public byte[] apkDigest;
+ public final byte[] apkDigest;
+
+ // Algorithm -> digest map of signed digests in the signature.
+ // These are continuously enforced by the IncFS driver.
+ public final Map<Integer, byte[]> contentDigests;
- public VerifiedSigner(Certificate[] certs, byte[] apkDigest) {
+ public VerifiedSigner(Certificate[] certs, byte[] apkDigest,
+ Map<Integer, byte[]> contentDigests) {
this.certs = certs;
this.apkDigest = apkDigest;
+ this.contentDigests = contentDigests;
}
}
diff --git a/core/java/android/util/apk/ApkSignatureVerifier.java b/core/java/android/util/apk/ApkSignatureVerifier.java
index ab8f80d3d1a5..73f7543ba819 100644
--- a/core/java/android/util/apk/ApkSignatureVerifier.java
+++ b/core/java/android/util/apk/ApkSignatureVerifier.java
@@ -27,6 +27,7 @@ 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.content.pm.parsing.ParsingPackageUtils;
import android.os.Build;
import android.os.Trace;
import android.util.jar.StrictJarFile;
@@ -45,6 +46,7 @@ import java.security.cert.CertificateEncodingException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.zip.ZipEntry;
@@ -91,6 +93,20 @@ public class ApkSignatureVerifier {
private static PackageParser.SigningDetails verifySignatures(String apkPath,
@SignatureSchemeVersion int minSignatureSchemeVersion, boolean verifyFull)
throws PackageParserException {
+ return verifySignaturesInternal(apkPath, minSignatureSchemeVersion,
+ verifyFull).signingDetails;
+ }
+
+ /**
+ * Verifies the provided APK using all allowed signing schemas.
+ * @return the certificates associated with each signer and content digests.
+ * @param verifyFull whether to verify all contents of this APK or just collect certificates.
+ * @throws PackageParserException if there was a problem collecting certificates
+ * @hide
+ */
+ public static SigningDetailsWithDigests verifySignaturesInternal(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
@@ -120,7 +136,7 @@ public class ApkSignatureVerifier {
return verifyV3AndBelowSignatures(apkPath, minSignatureSchemeVersion, verifyFull);
}
- private static PackageParser.SigningDetails verifyV3AndBelowSignatures(String apkPath,
+ private static SigningDetailsWithDigests verifyV3AndBelowSignatures(String apkPath,
@SignatureSchemeVersion int minSignatureSchemeVersion, boolean verifyFull)
throws PackageParserException {
// try v3
@@ -173,7 +189,7 @@ public class ApkSignatureVerifier {
* @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,
+ private static SigningDetailsWithDigests verifyV4Signature(String apkPath,
@SignatureSchemeVersion int minSignatureSchemeVersion, boolean verifyFull)
throws SignatureNotFoundException, PackageParserException {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, verifyFull ? "verifyV4" : "certsOnlyV4");
@@ -182,23 +198,33 @@ public class ApkSignatureVerifier {
ApkSignatureSchemeV4Verifier.extractCertificates(apkPath);
Certificate[][] signerCerts = new Certificate[][]{vSigner.certs};
Signature[] signerSigs = convertToSignatures(signerCerts);
+ Signature[] pastSignerSigs = null;
if (verifyFull) {
- byte[] nonstreamingDigest = null;
- Certificate[][] nonstreamingCerts = null;
+ Map<Integer, byte[]> nonstreamingDigests;
+ Certificate[][] nonstreamingCerts;
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;
+ nonstreamingDigests = v3Signer.contentDigests;
nonstreamingCerts = new Certificate[][]{v3Signer.certs};
+ if (v3Signer.por != null) {
+ // populate proof-of-rotation information
+ pastSignerSigs = new Signature[v3Signer.por.certs.size()];
+ for (int i = 0; i < pastSignerSigs.length; i++) {
+ pastSignerSigs[i] = new Signature(
+ v3Signer.por.certs.get(i).getEncoded());
+ pastSignerSigs[i].setFlags(v3Signer.por.flagsList.get(i));
+ }
+ }
} catch (SignatureNotFoundException e) {
try {
ApkSignatureSchemeV2Verifier.VerifiedSigner v2Signer =
ApkSignatureSchemeV2Verifier.verify(apkPath, false);
- nonstreamingDigest = v2Signer.digest;
+ nonstreamingDigests = v2Signer.contentDigests;
nonstreamingCerts = v2Signer.certs;
} catch (SignatureNotFoundException ee) {
throw new SecurityException(
@@ -220,14 +246,22 @@ public class ApkSignatureVerifier {
}
}
- if (!ArrayUtils.equals(vSigner.apkDigest, nonstreamingDigest,
- vSigner.apkDigest.length)) {
+ boolean found = false;
+ for (byte[] nonstreamingDigest : nonstreamingDigests.values()) {
+ if (ArrayUtils.equals(vSigner.apkDigest, nonstreamingDigest,
+ vSigner.apkDigest.length)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
throw new SecurityException("APK digest in V4 signature does not match V2/V3");
}
}
- return new PackageParser.SigningDetails(signerSigs,
- SignatureSchemeVersion.SIGNING_BLOCK_V4);
+ return new SigningDetailsWithDigests(new PackageParser.SigningDetails(signerSigs,
+ SignatureSchemeVersion.SIGNING_BLOCK_V4, pastSignerSigs),
+ vSigner.contentDigests);
} catch (SignatureNotFoundException e) {
throw e;
} catch (Exception e) {
@@ -248,8 +282,8 @@ public class ApkSignatureVerifier {
* @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 {
+ private static SigningDetailsWithDigests verifyV3Signature(String apkPath, boolean verifyFull)
+ throws SignatureNotFoundException, PackageParserException {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, verifyFull ? "verifyV3" : "certsOnlyV3");
try {
ApkSignatureSchemeV3Verifier.VerifiedSigner vSigner =
@@ -267,8 +301,9 @@ public class ApkSignatureVerifier {
pastSignerSigs[i].setFlags(vSigner.por.flagsList.get(i));
}
}
- return new PackageParser.SigningDetails(signerSigs,
- SignatureSchemeVersion.SIGNING_BLOCK_V3, pastSignerSigs);
+ return new SigningDetailsWithDigests(new PackageParser.SigningDetails(signerSigs,
+ SignatureSchemeVersion.SIGNING_BLOCK_V3, pastSignerSigs),
+ vSigner.contentDigests);
} catch (SignatureNotFoundException e) {
throw e;
} catch (Exception e) {
@@ -289,15 +324,16 @@ public class ApkSignatureVerifier {
* @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 {
+ private static SigningDetailsWithDigests 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);
+ ApkSignatureSchemeV2Verifier.VerifiedSigner vSigner =
+ ApkSignatureSchemeV2Verifier.verify(apkPath, verifyFull);
+ Certificate[][] signerCerts = vSigner.certs;
Signature[] signerSigs = convertToSignatures(signerCerts);
- return new PackageParser.SigningDetails(signerSigs,
- SignatureSchemeVersion.SIGNING_BLOCK_V2);
+ return new SigningDetailsWithDigests(new PackageParser.SigningDetails(signerSigs,
+ SignatureSchemeVersion.SIGNING_BLOCK_V2), vSigner.contentDigests);
} catch (SignatureNotFoundException e) {
throw e;
} catch (Exception e) {
@@ -316,8 +352,7 @@ public class ApkSignatureVerifier {
* @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)
+ private static SigningDetailsWithDigests verifyV1Signature(String apkPath, boolean verifyFull)
throws PackageParserException {
StrictJarFile jarFile = null;
@@ -338,7 +373,7 @@ public class ApkSignatureVerifier {
// 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);
+ ParsingPackageUtils.ANDROID_MANIFEST_FILENAME);
if (manifestEntry == null) {
throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST,
"Package " + apkPath + " has no manifest");
@@ -347,7 +382,7 @@ public class ApkSignatureVerifier {
if (ArrayUtils.isEmpty(lastCerts)) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES, "Package "
+ apkPath + " has no certificates at entry "
- + PackageParser.ANDROID_MANIFEST_FILENAME);
+ + ParsingPackageUtils.ANDROID_MANIFEST_FILENAME);
}
lastSigs = convertToSignatures(lastCerts);
@@ -360,7 +395,7 @@ public class ApkSignatureVerifier {
final String entryName = entry.getName();
if (entryName.startsWith("META-INF/")) continue;
- if (entryName.equals(PackageParser.ANDROID_MANIFEST_FILENAME)) continue;
+ if (entryName.equals(ParsingPackageUtils.ANDROID_MANIFEST_FILENAME)) continue;
toVerify.add(entry);
}
@@ -383,7 +418,8 @@ public class ApkSignatureVerifier {
}
}
}
- return new PackageParser.SigningDetails(lastSigs, SignatureSchemeVersion.JAR);
+ return new SigningDetailsWithDigests(
+ new PackageParser.SigningDetails(lastSigs, SignatureSchemeVersion.JAR), null);
} catch (GeneralSecurityException e) {
throw new PackageParserException(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING,
"Failed to collect certificates from " + apkPath, e);
@@ -534,4 +570,27 @@ public class ApkSignatureVerifier {
return null;
}
}
+
+ /**
+ * Extended signing details.
+ * @hide for internal use only.
+ */
+ public static class SigningDetailsWithDigests {
+ public final PackageParser.SigningDetails signingDetails;
+
+ /**
+ * APK Signature Schemes v2/v3/v4 might contain multiple content digests.
+ * SignatureVerifier usually chooses one of them to verify.
+ * For certain signature schemes, e.g. v4, this digest is verified continuously.
+ * For others, e.g. v2, the caller has to specify if they want to verify.
+ * Please refer to documentation for more details.
+ */
+ public final Map<Integer, byte[]> contentDigests;
+
+ SigningDetailsWithDigests(PackageParser.SigningDetails signingDetails,
+ Map<Integer, byte[]> contentDigests) {
+ this.signingDetails = signingDetails;
+ this.contentDigests = contentDigests;
+ }
+ }
}
diff --git a/core/java/android/util/apk/ApkSigningBlockUtils.java b/core/java/android/util/apk/ApkSigningBlockUtils.java
index f79b5b99b7e6..6a24de25a08c 100644
--- a/core/java/android/util/apk/ApkSigningBlockUtils.java
+++ b/core/java/android/util/apk/ApkSigningBlockUtils.java
@@ -19,6 +19,7 @@ package android.util.apk;
import android.util.ArrayMap;
import android.util.Pair;
+import java.io.ByteArrayInputStream;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.RandomAccessFile;
@@ -26,12 +27,23 @@ import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.DigestException;
+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.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PSSParameterSpec;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
import java.util.Map;
/**
@@ -39,7 +51,7 @@ import java.util.Map;
*
* @hide for internal use only.
*/
-final class ApkSigningBlockUtils {
+public final class ApkSigningBlockUtils {
private ApkSigningBlockUtils() {
}
@@ -51,9 +63,8 @@ final class ApkSigningBlockUtils {
* @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.
+ * @throws IOException if an I/O error occurs while reading the APK file.
*/
static SignatureInfo findSignature(RandomAccessFile apk, int blockId)
throws IOException, SignatureNotFoundException {
@@ -146,6 +157,37 @@ final class ApkSigningBlockUtils {
Map<Integer, byte[]> expectedDigests,
FileDescriptor apkFileDescriptor,
SignatureInfo signatureInfo) throws SecurityException {
+ int[] digestAlgorithms = new int[expectedDigests.size()];
+ int digestAlgorithmCount = 0;
+ for (int digestAlgorithm : expectedDigests.keySet()) {
+ digestAlgorithms[digestAlgorithmCount] = digestAlgorithm;
+ digestAlgorithmCount++;
+ }
+ byte[][] actualDigests;
+ try {
+ actualDigests = computeContentDigestsPer1MbChunk(digestAlgorithms, apkFileDescriptor,
+ signatureInfo);
+ } 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");
+ }
+ }
+ }
+
+ /**
+ * Calculate digests using digestAlgorithms for apkFileDescriptor.
+ * This will skip signature block described by signatureInfo.
+ */
+ public static byte[][] computeContentDigestsPer1MbChunk(int[] digestAlgorithms,
+ FileDescriptor apkFileDescriptor, SignatureInfo signatureInfo) throws DigestException {
// 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.
@@ -156,11 +198,11 @@ final class ApkSigningBlockUtils {
// 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.create(apkFileDescriptor, 0, signatureInfo.apkSigningBlockOffset);
DataSource centralDir =
- new MemoryMappedFileDataSource(
+ DataSource.create(
apkFileDescriptor, signatureInfo.centralDirOffset,
signatureInfo.eocdOffset - signatureInfo.centralDirOffset);
@@ -171,31 +213,8 @@ final class ApkSigningBlockUtils {
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");
- }
- }
+ return computeContentDigestsPer1MbChunk(digestAlgorithms,
+ new DataSource[]{beforeApkSigningBlock, centralDir, eocd});
}
private static byte[][] computeContentDigestsPer1MbChunk(
@@ -368,7 +387,7 @@ final class ApkSigningBlockUtils {
/**
* 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 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)
@@ -389,13 +408,13 @@ final class ApkSigningBlockUtils {
if (centralDirOffset > eocdOffset) {
throw new SignatureNotFoundException(
"ZIP Central Directory offset out of range: " + centralDirOffset
- + ". ZIP End of Central Directory offset: " + eocdOffset);
+ + ". 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");
+ + " Directory");
}
return centralDirOffset;
}
@@ -417,14 +436,10 @@ final class ApkSigningBlockUtils {
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;
- static final int CONTENT_DIGEST_SHA256 = 4;
-
- private static final int[] V4_CONTENT_DIGEST_ALGORITHMS =
- {CONTENT_DIGEST_CHUNKED_SHA512, CONTENT_DIGEST_VERITY_CHUNKED_SHA256,
- CONTENT_DIGEST_CHUNKED_SHA256};
+ public static final int CONTENT_DIGEST_CHUNKED_SHA256 = 1;
+ public static final int CONTENT_DIGEST_CHUNKED_SHA512 = 2;
+ public static final int CONTENT_DIGEST_VERITY_CHUNKED_SHA256 = 3;
+ public static final int CONTENT_DIGEST_SHA256 = 4;
static int compareSignatureAlgorithm(int sigAlgorithm1, int sigAlgorithm2) {
int digestAlgorithm1 = getSignatureAlgorithmContentDigestAlgorithm(sigAlgorithm1);
@@ -577,21 +592,6 @@ final class ApkSigningBlockUtils {
}
/**
- * 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
@@ -697,7 +697,7 @@ final class ApkSigningBlockUtils {
static Pair<ByteBuffer, Long> findApkSigningBlock(
RandomAccessFile apk, long centralDirOffset)
- throws IOException, SignatureNotFoundException {
+ throws IOException, SignatureNotFoundException {
// FORMAT:
// OFFSET DATA TYPE DESCRIPTION
// * @+0 bytes uint64: size in bytes (excluding this field)
@@ -816,4 +816,108 @@ final class ApkSigningBlockUtils {
}
}
+ 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);
+ }
+
+ /**
+ * 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;
+ }
+ }
}
diff --git a/core/java/android/util/apk/DataSource.java b/core/java/android/util/apk/DataSource.java
index 82f3800aa6d3..dd6389d012bc 100644
--- a/core/java/android/util/apk/DataSource.java
+++ b/core/java/android/util/apk/DataSource.java
@@ -16,6 +16,10 @@
package android.util.apk;
+import android.annotation.NonNull;
+import android.os.incremental.IncrementalManager;
+
+import java.io.FileDescriptor;
import java.io.IOException;
import java.security.DigestException;
@@ -35,4 +39,22 @@ interface DataSource {
*/
void feedIntoDataDigester(DataDigester md, long offset, int size)
throws IOException, DigestException;
+
+ /**
+ * Creates a DataSource that can handle the passed fd in the most efficient and safe manner.
+ * @param fd file descriptor to read from
+ * @param pos starting offset
+ * @param size size of the region
+ * @return created DataSource object
+ */
+ static @NonNull DataSource create(@NonNull FileDescriptor fd, long pos, long size) {
+ if (IncrementalManager.isIncrementalFileFd(fd)) {
+ // IncFS-based files may have missing pages, and reading those via mmap() results
+ // in a SIGBUS signal. Java doesn't have a good way of catching it, ending up killing
+ // the process by default. Going back to read() is the safest option for these files.
+ return new ReadFileDataSource(fd, pos, size);
+ } else {
+ return new MemoryMappedFileDataSource(fd, pos, size);
+ }
+ }
}
diff --git a/core/java/android/util/apk/MemoryMappedFileDataSource.java b/core/java/android/util/apk/MemoryMappedFileDataSource.java
index 8d2b1e328862..69a526d09ad9 100644
--- a/core/java/android/util/apk/MemoryMappedFileDataSource.java
+++ b/core/java/android/util/apk/MemoryMappedFileDataSource.java
@@ -40,6 +40,7 @@ class MemoryMappedFileDataSource implements DataSource {
/**
* Constructs a new {@code MemoryMappedFileDataSource} for the specified region of the file.
*
+ * @param fd file descriptor to read from.
* @param position start position of the region in the file.
* @param size size (in bytes) of the region.
*/
diff --git a/core/java/android/util/apk/ReadFileDataSource.java b/core/java/android/util/apk/ReadFileDataSource.java
new file mode 100644
index 000000000000..d0e1140c0eb4
--- /dev/null
+++ b/core/java/android/util/apk/ReadFileDataSource.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2021 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 java.io.FileDescriptor;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.security.DigestException;
+
+/**
+ * {@link DataSource} which provides data from a file descriptor by reading the sections
+ * of the file via raw read() syscall. This is slower than memory-mapping but safer.
+ */
+class ReadFileDataSource implements DataSource {
+ private final FileDescriptor mFd;
+ private final long mFilePosition;
+ private final long mSize;
+
+ private static final int CHUNK_SIZE = 1024 * 1024;
+
+ /**
+ * Constructs a new {@code ReadFileDataSource} for the specified region of the file.
+ *
+ * @param fd file descriptor to read from.
+ * @param position start position of the region in the file.
+ * @param size size (in bytes) of the region.
+ */
+ ReadFileDataSource(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 {
+ try {
+ final byte[] buffer = new byte[Math.min(size, CHUNK_SIZE)];
+ final long start = mFilePosition + offset;
+ final long end = start + size;
+ for (long pos = start, curSize = Math.min(size, CHUNK_SIZE);
+ pos < end; curSize = Math.min(end - pos, CHUNK_SIZE)) {
+ final int readSize = Os.pread(mFd, buffer, 0, (int) curSize, pos);
+ md.consume(ByteBuffer.wrap(buffer, 0, readSize));
+ pos += readSize;
+ }
+ } catch (ErrnoException e) {
+ throw new IOException(e);
+ }
+ }
+}
diff --git a/core/java/android/util/apk/SignatureInfo.java b/core/java/android/util/apk/SignatureInfo.java
index 8e1233af34a1..7638293618ba 100644
--- a/core/java/android/util/apk/SignatureInfo.java
+++ b/core/java/android/util/apk/SignatureInfo.java
@@ -16,15 +16,18 @@
package android.util.apk;
+import android.annotation.NonNull;
+
import java.nio.ByteBuffer;
/**
* APK Signature Scheme v2 block and additional information relevant to verifying the signatures
* contained in the block against the file.
+ * @hide
*/
-class SignatureInfo {
+public class SignatureInfo {
/** Contents of APK Signature Scheme v2 block. */
- public final ByteBuffer signatureBlock;
+ public final @NonNull ByteBuffer signatureBlock;
/** Position of the APK Signing Block in the file. */
public final long apkSigningBlockOffset;
@@ -36,10 +39,10 @@ class SignatureInfo {
public final long eocdOffset;
/** Contents of ZIP End of Central Directory (EoCD) of the file. */
- public final ByteBuffer eocd;
+ public final @NonNull ByteBuffer eocd;
- SignatureInfo(ByteBuffer signatureBlock, long apkSigningBlockOffset, long centralDirOffset,
- long eocdOffset, ByteBuffer eocd) {
+ SignatureInfo(@NonNull ByteBuffer signatureBlock, long apkSigningBlockOffset,
+ long centralDirOffset, long eocdOffset, @NonNull ByteBuffer eocd) {
this.signatureBlock = signatureBlock;
this.apkSigningBlockOffset = apkSigningBlockOffset;
this.centralDirOffset = centralDirOffset;
diff --git a/core/java/android/util/apk/SourceStampVerificationResult.java b/core/java/android/util/apk/SourceStampVerificationResult.java
index 2edaf623fb94..8b9eee2f796e 100644
--- a/core/java/android/util/apk/SourceStampVerificationResult.java
+++ b/core/java/android/util/apk/SourceStampVerificationResult.java
@@ -19,6 +19,8 @@ package android.util.apk;
import android.annotation.Nullable;
import java.security.cert.Certificate;
+import java.util.Collections;
+import java.util.List;
/**
* A class encapsulating the result from the source stamp verifier
@@ -32,12 +34,15 @@ public final class SourceStampVerificationResult {
private final boolean mPresent;
private final boolean mVerified;
private final Certificate mCertificate;
+ private final List<? extends Certificate> mCertificateLineage;
private SourceStampVerificationResult(
- boolean present, boolean verified, @Nullable Certificate certificate) {
+ boolean present, boolean verified, @Nullable Certificate certificate,
+ List<? extends Certificate> certificateLineage) {
this.mPresent = present;
this.mVerified = verified;
this.mCertificate = certificate;
+ this.mCertificateLineage = certificateLineage;
}
public boolean isPresent() {
@@ -52,6 +57,10 @@ public final class SourceStampVerificationResult {
return mCertificate;
}
+ public List<? extends Certificate> getCertificateLineage() {
+ return mCertificateLineage;
+ }
+
/**
* Create a non-present source stamp outcome.
*
@@ -59,18 +68,21 @@ public final class SourceStampVerificationResult {
*/
public static SourceStampVerificationResult notPresent() {
return new SourceStampVerificationResult(
- /* present= */ false, /* verified= */ false, /* certificate= */ null);
+ /* present= */ false, /* verified= */ false, /* certificate= */
+ null, /* certificateLineage= */ Collections.emptyList());
}
/**
* Create a verified source stamp outcome.
*
- * @param certificate The source stamp certificate.
+ * @param certificate The source stamp certificate.
+ * @param certificateLineage The proof-of-rotation lineage for the source stamp.
* @return A verified source stamp result, and the source stamp certificate.
*/
- public static SourceStampVerificationResult verified(Certificate certificate) {
+ public static SourceStampVerificationResult verified(Certificate certificate,
+ List<? extends Certificate> certificateLineage) {
return new SourceStampVerificationResult(
- /* present= */ true, /* verified= */ true, certificate);
+ /* present= */ true, /* verified= */ true, certificate, certificateLineage);
}
/**
@@ -80,6 +92,7 @@ public final class SourceStampVerificationResult {
*/
public static SourceStampVerificationResult notVerified() {
return new SourceStampVerificationResult(
- /* present= */ true, /* verified= */ false, /* certificate= */ null);
+ /* present= */ true, /* verified= */ false, /* certificate= */
+ null, /* certificateLineage= */ Collections.emptyList());
}
}
diff --git a/core/java/android/util/apk/SourceStampVerifier.java b/core/java/android/util/apk/SourceStampVerifier.java
index 5fc242353d51..f9e312146ccf 100644
--- a/core/java/android/util/apk/SourceStampVerifier.java
+++ b/core/java/android/util/apk/SourceStampVerifier.java
@@ -23,6 +23,7 @@ import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmContent
import static android.util.apk.ApkSigningBlockUtils.getSignatureAlgorithmJcaSignatureAlgorithm;
import static android.util.apk.ApkSigningBlockUtils.isSupportedSignatureAlgorithm;
import static android.util.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray;
+import static android.util.apk.ApkSigningBlockUtils.verifyProofOfRotationStruct;
import android.util.Pair;
import android.util.Slog;
@@ -44,12 +45,14 @@ import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.Certificate;
+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.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
@@ -76,6 +79,7 @@ public abstract class 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 = 0x6dff800d;
+ private static final int PROOF_OF_ROTATION_ATTR_ID = 0x9d6303f7;
private static final int VERSION_JAR_SIGNATURE_SCHEME = 1;
private static final int VERSION_APK_SIGNATURE_SCHEME_V2 = 2;
@@ -85,11 +89,13 @@ public abstract class SourceStampVerifier {
private static final String SOURCE_STAMP_CERTIFICATE_HASH_ZIP_ENTRY_NAME = "stamp-cert-sha256";
/** Hidden constructor to prevent instantiation. */
- private SourceStampVerifier() {}
+ private SourceStampVerifier() {
+ }
- /** Verifies SourceStamp present in a list of APKs. */
+ /** Verifies SourceStamp present in a list of (split) APKs for the same app. */
public static SourceStampVerificationResult verify(List<String> apkFiles) {
Certificate stampCertificate = null;
+ List<? extends Certificate> stampCertificateLineage = Collections.emptyList();
for (String apkFile : apkFiles) {
SourceStampVerificationResult sourceStampVerificationResult = verify(apkFile);
if (!sourceStampVerificationResult.isPresent()
@@ -97,12 +103,15 @@ public abstract class SourceStampVerifier {
return sourceStampVerificationResult;
}
if (stampCertificate != null
- && !stampCertificate.equals(sourceStampVerificationResult.getCertificate())) {
+ && (!stampCertificate.equals(sourceStampVerificationResult.getCertificate())
+ || !stampCertificateLineage.equals(
+ sourceStampVerificationResult.getCertificateLineage()))) {
return SourceStampVerificationResult.notVerified();
}
stampCertificate = sourceStampVerificationResult.getCertificate();
+ stampCertificateLineage = sourceStampVerificationResult.getCertificateLineage();
}
- return SourceStampVerificationResult.verified(stampCertificate);
+ return SourceStampVerificationResult.verified(stampCertificate, stampCertificateLineage);
}
/** Verifies SourceStamp present in the provided APK. */
@@ -177,21 +186,44 @@ public abstract class SourceStampVerifier {
"No signatures found for signature scheme %d",
signatureSchemeDigest.getKey()));
}
+ ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(
+ signedSignatureSchemeData.get(signatureSchemeDigest.getKey()));
verifySourceStampSignature(
- signedSignatureSchemeData.get(signatureSchemeDigest.getKey()),
+ signatureSchemeDigest.getValue(),
sourceStampCertificate,
- signatureSchemeDigest.getValue());
+ signatures);
}
- return SourceStampVerificationResult.verified(sourceStampCertificate);
+ List<? extends Certificate> sourceStampCertificateLineage = Collections.emptyList();
+ if (sourceStampBlockData.hasRemaining()) {
+ // The stamp block contains some additional attributes.
+ ByteBuffer stampAttributeData = getLengthPrefixedSlice(sourceStampBlockData);
+ ByteBuffer stampAttributeDataSignatures = getLengthPrefixedSlice(sourceStampBlockData);
+
+ byte[] stampAttributeBytes = new byte[stampAttributeData.remaining()];
+ stampAttributeData.get(stampAttributeBytes);
+ stampAttributeData.flip();
+
+ verifySourceStampSignature(stampAttributeBytes, sourceStampCertificate,
+ stampAttributeDataSignatures);
+ ApkSigningBlockUtils.VerifiedProofOfRotation verifiedProofOfRotation =
+ verifySourceStampAttributes(stampAttributeData, sourceStampCertificate);
+ if (verifiedProofOfRotation != null) {
+ sourceStampCertificateLineage = verifiedProofOfRotation.certs;
+ }
+ }
+
+ return SourceStampVerificationResult.verified(sourceStampCertificate,
+ sourceStampCertificateLineage);
}
/**
* Verify the SourceStamp certificate found in the signing block is the same as the SourceStamp
* certificate found in the APK. It returns the verified certificate.
*
- * @param sourceStampBlockData the source stamp block in the APK signing block which contains
- * the certificate used to sign the stamp digests.
+ * @param sourceStampBlockData the source stamp block in the APK signing block which
+ * contains
+ * the certificate used to sign the stamp digests.
* @param sourceStampCertificateDigest the source stamp certificate digest found in the APK.
*/
private static X509Certificate verifySourceStampCertificate(
@@ -230,16 +262,16 @@ public abstract class SourceStampVerifier {
* Verify the SourceStamp signature found in the signing block is signed by the SourceStamp
* certificate found in the APK.
*
- * @param signedBlockData the source stamp block in the APK signing block which contains the
- * stamp signed digests.
+ * @param data the digest to be verified being signed by the source stamp
+ * certificate.
* @param sourceStampCertificate the source stamp certificate used to sign the stamp digests.
- * @param digest the digest to be verified being signed by the source stamp certificate.
+ * @param signatures the source stamp block in the APK signing block which contains
+ * the stamp signed digests.
*/
- private static void verifySourceStampSignature(
- ByteBuffer signedBlockData, X509Certificate sourceStampCertificate, byte[] digest)
+ private static void verifySourceStampSignature(byte[] data,
+ X509Certificate sourceStampCertificate, ByteBuffer signatures)
throws IOException {
// Parse the signatures block and identify supported signatures
- ByteBuffer signatures = ApkSigningBlockUtils.getLengthPrefixedSlice(signedBlockData);
int signatureCount = 0;
int bestSigAlgorithm = -1;
byte[] bestSigAlgorithmSignatureBytes = null;
@@ -285,7 +317,7 @@ public abstract class SourceStampVerifier {
if (jcaSignatureAlgorithmParams != null) {
sig.setParameter(jcaSignatureAlgorithmParams);
}
- sig.update(digest);
+ sig.update(data);
sigVerified = sig.verify(bestSigAlgorithmSignatureBytes);
} catch (InvalidKeyException
| InvalidAlgorithmParameterException
@@ -414,6 +446,46 @@ public abstract class SourceStampVerifier {
return result.array();
}
+ private static ApkSigningBlockUtils.VerifiedProofOfRotation verifySourceStampAttributes(
+ ByteBuffer stampAttributeData,
+ X509Certificate sourceStampCertificate)
+ throws IOException {
+ CertificateFactory certFactory;
+ try {
+ certFactory = CertificateFactory.getInstance("X.509");
+ } catch (CertificateException e) {
+ throw new RuntimeException("Failed to obtain X.509 CertificateFactory", e);
+ }
+ ByteBuffer stampAttributes = getLengthPrefixedSlice(stampAttributeData);
+ ApkSigningBlockUtils.VerifiedProofOfRotation verifiedProofOfRotation = null;
+ while (stampAttributes.hasRemaining()) {
+ ByteBuffer attribute = getLengthPrefixedSlice(stampAttributes);
+ int id = attribute.getInt();
+ if (id == PROOF_OF_ROTATION_ATTR_ID) {
+ if (verifiedProofOfRotation != null) {
+ throw new SecurityException("Encountered multiple Proof-of-rotation records"
+ + " when verifying source stamp signature");
+ }
+ verifiedProofOfRotation = verifyProofOfRotationStruct(attribute, certFactory);
+ // Make sure that the last certificate in the Proof-of-rotation record matches
+ // the one used to sign this APK.
+ try {
+ if (verifiedProofOfRotation.certs.size() > 0
+ && !Arrays.equals(verifiedProofOfRotation.certs.get(
+ verifiedProofOfRotation.certs.size() - 1).getEncoded(),
+ sourceStampCertificate.getEncoded())) {
+ throw new SecurityException("Terminal certificate in Proof-of-rotation"
+ + " record does not match source stamp certificate");
+ }
+ } catch (CertificateEncodingException e) {
+ throw new SecurityException("Failed to encode certificate when comparing"
+ + " Proof-of-rotation record and source stamp certificate", e);
+ }
+ }
+ }
+ return verifiedProofOfRotation;
+ }
+
private static byte[] computeSha256Digest(byte[] input) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
diff --git a/core/java/android/util/apk/TEST_MAPPING b/core/java/android/util/apk/TEST_MAPPING
index 8544e82e04e0..4598b4ffe4f6 100644
--- a/core/java/android/util/apk/TEST_MAPPING
+++ b/core/java/android/util/apk/TEST_MAPPING
@@ -1,6 +1,17 @@
{
"presubmit": [
{
+ "name": "CtsContentTestCases",
+ "options": [
+ {
+ "include-filter": "android.content.pm.cts.PackageManagerShellCommandIncrementalTest"
+ },
+ {
+ "include-filter": "android.content.pm.cts.PackageManagerShellCommandTest"
+ }
+ ]
+ },
+ {
"name": "FrameworksCoreTests",
"options": [
{
diff --git a/core/java/android/util/apk/VerbatimX509Certificate.java b/core/java/android/util/apk/VerbatimX509Certificate.java
index 391c5fc39416..d9a00b232484 100644
--- a/core/java/android/util/apk/VerbatimX509Certificate.java
+++ b/core/java/android/util/apk/VerbatimX509Certificate.java
@@ -16,6 +16,8 @@
package android.util.apk;
+import android.annotation.Nullable;
+
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
@@ -39,7 +41,7 @@ class VerbatimX509Certificate extends WrappedX509Certificate {
}
@Override
- public boolean equals(Object o) {
+ public boolean equals(@Nullable Object o) {
if (this == o) return true;
if (!(o instanceof VerbatimX509Certificate)) return false;
diff --git a/core/java/android/util/apk/VerityBuilder.java b/core/java/android/util/apk/VerityBuilder.java
index 3dfa4cd1a0d2..c7c465d30dad 100644
--- a/core/java/android/util/apk/VerityBuilder.java
+++ b/core/java/android/util/apk/VerityBuilder.java
@@ -116,6 +116,34 @@ public abstract class VerityBuilder {
}
/**
+ * Generates the fs-verity hash tree. It is the actual verity tree format on disk, as is
+ * re-generated on device.
+ *
+ * The tree is built bottom up. The bottom level has 256-bit digest for each 4 KB block in the
+ * input file. If the total size is larger than 4 KB, take this level as input and repeat the
+ * same procedure, until the level is within 4 KB. If salt is given, it will apply to each
+ * digestion before the actual data.
+ *
+ * The returned root hash is calculated from the last level of 4 KB chunk, similarly with salt.
+ *
+ * @return the root hash of the generated hash tree.
+ */
+ public static byte[] generateFsVerityRootHash(@NonNull String apkPath, byte[] salt,
+ @NonNull ByteBufferFactory bufferFactory)
+ throws IOException, NoSuchAlgorithmException, DigestException {
+ try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
+ int[] levelOffset = calculateVerityLevelOffset(apk.length());
+ 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);
+ return generateFsVerityTreeInternal(apk, salt, levelOffset, tree);
+ }
+ }
+ /**
* Calculates the apk-verity root hash for integrity measurement. This needs to be consistent
* to what kernel returns.
*/
@@ -259,13 +287,14 @@ public abstract class VerityBuilder {
// 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)
+ private static void generateFsVerityDigestAtLeafLevel(RandomAccessFile file,
+ @Nullable byte[] salt, ByteBuffer output)
throws IOException, NoSuchAlgorithmException, DigestException {
- BufferedDigester digester = new BufferedDigester(null /* salt */, output);
+ BufferedDigester digester = new BufferedDigester(salt, output);
// 1. Digest the whole file by chunks.
consumeByChunk(digester,
- new MemoryMappedFileDataSource(file.getFD(), 0, file.length()),
+ DataSource.create(file.getFD(), 0, file.length()),
MMAP_REGION_SIZE_BYTES);
// 2. Pad 0s up to the nearest 4096-byte block before hashing.
@@ -286,7 +315,7 @@ public abstract class VerityBuilder {
// 1. Digest from the beginning of the file, until APK Signing Block is reached.
consumeByChunk(digester,
- new MemoryMappedFileDataSource(apk.getFD(), 0, signatureInfo.apkSigningBlockOffset),
+ DataSource.create(apk.getFD(), 0, signatureInfo.apkSigningBlockOffset),
MMAP_REGION_SIZE_BYTES);
// 2. Skip APK Signing Block and continue digesting, until the Central Directory offset
@@ -294,7 +323,7 @@ public abstract class VerityBuilder {
long eocdCdOffsetFieldPosition =
signatureInfo.eocdOffset + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET;
consumeByChunk(digester,
- new MemoryMappedFileDataSource(apk.getFD(), signatureInfo.centralDirOffset,
+ DataSource.create(apk.getFD(), signatureInfo.centralDirOffset,
eocdCdOffsetFieldPosition - signatureInfo.centralDirOffset),
MMAP_REGION_SIZE_BYTES);
@@ -309,7 +338,7 @@ public abstract class VerityBuilder {
long offsetAfterEocdCdOffsetField =
eocdCdOffsetFieldPosition + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE;
consumeByChunk(digester,
- new MemoryMappedFileDataSource(apk.getFD(), offsetAfterEocdCdOffsetField,
+ DataSource.create(apk.getFD(), offsetAfterEocdCdOffsetField,
apk.getChannel().size() - offsetAfterEocdCdOffsetField),
MMAP_REGION_SIZE_BYTES);
@@ -325,6 +354,35 @@ public abstract class VerityBuilder {
}
@NonNull
+ private static byte[] generateFsVerityTreeInternal(@NonNull RandomAccessFile apk,
+ @Nullable byte[] salt, @NonNull int[] levelOffset, @NonNull ByteBuffer output)
+ throws IOException, NoSuchAlgorithmException, DigestException {
+ // 1. Digest the apk to generate the leaf level hashes.
+ generateFsVerityDigestAtLeafLevel(apk, 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;
+ }
+
+ @NonNull
private static byte[] generateVerityTreeInternal(@NonNull RandomAccessFile apk,
@Nullable SignatureInfo signatureInfo, @Nullable byte[] salt,
@NonNull int[] levelOffset, @NonNull ByteBuffer output)
diff --git a/core/java/android/util/imetracing/ImeTracing.java b/core/java/android/util/imetracing/ImeTracing.java
new file mode 100644
index 000000000000..4696ae325e7b
--- /dev/null
+++ b/core/java/android/util/imetracing/ImeTracing.java
@@ -0,0 +1,187 @@
+/*
+ * 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.imetracing;
+
+import android.annotation.Nullable;
+import android.app.ActivityThread;
+import android.content.Context;
+import android.inputmethodservice.AbstractInputMethodService;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.ServiceManager.ServiceNotFoundException;
+import android.util.Log;
+import android.util.proto.ProtoOutputStream;
+import android.view.inputmethod.InputMethodManager;
+
+import com.android.internal.view.IInputMethodManager;
+
+import java.io.PrintWriter;
+
+/**
+ *
+ * An abstract class that declares the methods for ime trace related operations - enable trace,
+ * schedule trace and add new trace to buffer. Both the client and server side classes can use
+ * it by getting an implementation through {@link ImeTracing#getInstance()}.
+ *
+ * @hide
+ */
+public abstract class ImeTracing {
+
+ static final String TAG = "imeTracing";
+ public static final String PROTO_ARG = "--proto-com-android-imetracing";
+
+ /* Constants describing the component type that triggered a dump. */
+ public static final int IME_TRACING_FROM_CLIENT = 0;
+ public static final int IME_TRACING_FROM_IMS = 1;
+ public static final int IME_TRACING_FROM_IMMS = 2;
+
+ private static ImeTracing sInstance;
+ static boolean sEnabled = false;
+ IInputMethodManager mService;
+
+ protected boolean mDumpInProgress;
+ protected final Object mDumpInProgressLock = new Object();
+
+ ImeTracing() throws ServiceNotFoundException {
+ mService = IInputMethodManager.Stub.asInterface(
+ ServiceManager.getServiceOrThrow(Context.INPUT_METHOD_SERVICE));
+ }
+
+ /**
+ * Returns an instance of {@link ImeTracingServerImpl} when called from a server side class
+ * and an instance of {@link ImeTracingClientImpl} when called from a client side class.
+ * Useful to schedule a dump for next frame or save a dump when certain methods are called.
+ *
+ * @return Instance of one of the children classes of {@link ImeTracing}
+ */
+ public static ImeTracing getInstance() {
+ if (sInstance == null) {
+ try {
+ sInstance = isSystemProcess()
+ ? new ImeTracingServerImpl() : new ImeTracingClientImpl();
+ } catch (RemoteException | ServiceNotFoundException e) {
+ Log.e(TAG, "Exception while creating ImeTracing instance", e);
+ }
+ }
+ return sInstance;
+ }
+
+ /**
+ * Transmits the information from client or InputMethodService side to the server, in order to
+ * be stored persistently to the current IME tracing dump.
+ *
+ * @param protoDump client or service side information to be stored by the server
+ * @param source where the information is coming from, refer to {@see #IME_TRACING_FROM_CLIENT}
+ * and {@see #IME_TRACING_FROM_IMS}
+ * @param where
+ */
+ public void sendToService(byte[] protoDump, int source, String where) throws RemoteException {
+ mService.startProtoDump(protoDump, source, where);
+ }
+
+ /**
+ * @param proto dump to be added to the buffer
+ */
+ public abstract void addToBuffer(ProtoOutputStream proto, int source);
+
+ /**
+ * Starts a proto dump of the client side information.
+ *
+ * @param where Place where the trace was triggered.
+ * @param immInstance The {@link InputMethodManager} instance to dump.
+ * @param icProto {@link android.view.inputmethod.InputConnection} call data in proto format.
+ */
+ public abstract void triggerClientDump(String where, InputMethodManager immInstance,
+ ProtoOutputStream icProto);
+
+ /**
+ * Starts a proto dump of the currently connected InputMethodService information.
+ *
+ * @param where Place where the trace was triggered.
+ * @param service The {@link android.inputmethodservice.InputMethodService} to be dumped.
+ * @param icProto {@link android.view.inputmethod.InputConnection} call data in proto format.
+ */
+ public abstract void triggerServiceDump(String where, AbstractInputMethodService service,
+ ProtoOutputStream icProto);
+
+ /**
+ * Starts a proto dump of the InputMethodManagerService information.
+ *
+ * @param where Place where the trace was triggered.
+ */
+ public abstract void triggerManagerServiceDump(String where);
+
+ /**
+ * Being called while taking a bugreport so that tracing files can be included in the bugreport
+ * when the IME tracing is running. Does nothing otherwise.
+ *
+ * @param pw Print writer
+ */
+ public void saveForBugreport(@Nullable PrintWriter pw) {
+ // does nothing by default.
+ }
+
+ /**
+ * Sets whether ime tracing is enabled.
+ *
+ * @param enabled Tells whether ime tracing should be enabled or disabled.
+ */
+ public void setEnabled(boolean enabled) {
+ sEnabled = enabled;
+ }
+
+ /**
+ * @return {@code true} if dumping is enabled, {@code false} otherwise.
+ */
+ public boolean isEnabled() {
+ return sEnabled;
+ }
+
+ /**
+ * @return {@code true} if tracing is available, {@code false} otherwise.
+ */
+ public boolean isAvailable() {
+ return mService != null;
+ }
+
+ /**
+ * Starts a new IME trace if one is not already started.
+ *
+ * @param pw Print writer
+ */
+ public abstract void startTrace(@Nullable PrintWriter pw);
+
+ /**
+ * Stops the IME trace if one was previously started and writes the current buffers to disk.
+ *
+ * @param pw Print writer
+ */
+ public abstract void stopTrace(@Nullable PrintWriter pw);
+
+ private static boolean isSystemProcess() {
+ return ActivityThread.isSystem();
+ }
+
+ protected void logAndPrintln(@Nullable PrintWriter pw, String msg) {
+ Log.i(TAG, msg);
+ if (pw != null) {
+ pw.println(msg);
+ pw.flush();
+ }
+ }
+
+}
diff --git a/core/java/android/util/imetracing/ImeTracingClientImpl.java b/core/java/android/util/imetracing/ImeTracingClientImpl.java
new file mode 100644
index 000000000000..5a57a6ade98b
--- /dev/null
+++ b/core/java/android/util/imetracing/ImeTracingClientImpl.java
@@ -0,0 +1,103 @@
+/*
+ * 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.imetracing;
+
+import android.annotation.NonNull;
+import android.inputmethodservice.AbstractInputMethodService;
+import android.os.RemoteException;
+import android.os.ServiceManager.ServiceNotFoundException;
+import android.util.Log;
+import android.util.proto.ProtoOutputStream;
+import android.view.inputmethod.InputMethodManager;
+
+import java.io.PrintWriter;
+
+/**
+ * @hide
+ */
+class ImeTracingClientImpl extends ImeTracing {
+ ImeTracingClientImpl() throws ServiceNotFoundException, RemoteException {
+ sEnabled = mService.isImeTraceEnabled();
+ }
+
+ @Override
+ public void addToBuffer(ProtoOutputStream proto, int source) {
+ }
+
+ @Override
+ public void triggerClientDump(String where, @NonNull InputMethodManager immInstance,
+ ProtoOutputStream icProto) {
+ if (!isEnabled() || !isAvailable()) {
+ return;
+ }
+
+ synchronized (mDumpInProgressLock) {
+ if (mDumpInProgress) {
+ return;
+ }
+ mDumpInProgress = true;
+ }
+
+ try {
+ ProtoOutputStream proto = new ProtoOutputStream();
+ immInstance.dumpDebug(proto, icProto);
+ sendToService(proto.getBytes(), IME_TRACING_FROM_CLIENT, where);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Exception while sending ime-related client dump to server", e);
+ } finally {
+ mDumpInProgress = false;
+ }
+ }
+
+ @Override
+ public void triggerServiceDump(String where, @NonNull AbstractInputMethodService service,
+ ProtoOutputStream icProto) {
+ if (!isEnabled() || !isAvailable()) {
+ return;
+ }
+
+ synchronized (mDumpInProgressLock) {
+ if (mDumpInProgress) {
+ return;
+ }
+ mDumpInProgress = true;
+ }
+
+ try {
+ ProtoOutputStream proto = new ProtoOutputStream();
+ service.dumpProtoInternal(proto, icProto);
+ sendToService(proto.getBytes(), IME_TRACING_FROM_IMS, where);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Exception while sending ime-related service dump to server", e);
+ } finally {
+ mDumpInProgress = false;
+ }
+ }
+
+ @Override
+ public void triggerManagerServiceDump(String where) {
+ // Intentionally left empty, this is implemented in ImeTracingServerImpl
+ }
+
+ @Override
+ public void startTrace(PrintWriter pw) {
+ }
+
+ @Override
+ public void stopTrace(PrintWriter pw) {
+ }
+}
diff --git a/core/java/android/util/imetracing/ImeTracingServerImpl.java b/core/java/android/util/imetracing/ImeTracingServerImpl.java
new file mode 100644
index 000000000000..06e4c5002776
--- /dev/null
+++ b/core/java/android/util/imetracing/ImeTracingServerImpl.java
@@ -0,0 +1,239 @@
+/*
+ * 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.imetracing;
+
+import static android.os.Build.IS_USER;
+
+import android.annotation.Nullable;
+import android.inputmethodservice.AbstractInputMethodService;
+import android.os.RemoteException;
+import android.os.ServiceManager.ServiceNotFoundException;
+import android.util.Log;
+import android.util.proto.ProtoOutputStream;
+import android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceFileProto;
+import android.view.inputmethod.InputMethodEditorTraceProto.InputMethodManagerServiceTraceFileProto;
+import android.view.inputmethod.InputMethodEditorTraceProto.InputMethodServiceTraceFileProto;
+import android.view.inputmethod.InputMethodManager;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.TraceBuffer;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+/**
+ * @hide
+ */
+class ImeTracingServerImpl extends ImeTracing {
+ private static final String TRACE_DIRNAME = "/data/misc/wmtrace/";
+ private static final String TRACE_FILENAME_CLIENTS = "ime_trace_clients.pb";
+ private static final String TRACE_FILENAME_IMS = "ime_trace_service.pb";
+ private static final String TRACE_FILENAME_IMMS = "ime_trace_managerservice.pb";
+ private static final int BUFFER_CAPACITY = 4096 * 1024;
+
+ // Needed for winscope to auto-detect the dump type. Explained further in
+ // core.proto.android.view.inputmethod.inputmethodeditortrace.proto.
+ // This magic number corresponds to InputMethodClientsTraceFileProto.
+ private static final long MAGIC_NUMBER_CLIENTS_VALUE =
+ ((long) InputMethodClientsTraceFileProto.MAGIC_NUMBER_H << 32)
+ | InputMethodClientsTraceFileProto.MAGIC_NUMBER_L;
+ // This magic number corresponds to InputMethodServiceTraceFileProto.
+ private static final long MAGIC_NUMBER_IMS_VALUE =
+ ((long) InputMethodServiceTraceFileProto.MAGIC_NUMBER_H << 32)
+ | InputMethodServiceTraceFileProto.MAGIC_NUMBER_L;
+ // This magic number corresponds to InputMethodManagerServiceTraceFileProto.
+ private static final long MAGIC_NUMBER_IMMS_VALUE =
+ ((long) InputMethodManagerServiceTraceFileProto.MAGIC_NUMBER_H << 32)
+ | InputMethodManagerServiceTraceFileProto.MAGIC_NUMBER_L;
+
+ private final TraceBuffer mBufferClients;
+ private final File mTraceFileClients;
+ private final TraceBuffer mBufferIms;
+ private final File mTraceFileIms;
+ private final TraceBuffer mBufferImms;
+ private final File mTraceFileImms;
+
+ private final Object mEnabledLock = new Object();
+
+ ImeTracingServerImpl() throws ServiceNotFoundException {
+ mBufferClients = new TraceBuffer<>(BUFFER_CAPACITY);
+ mTraceFileClients = new File(TRACE_DIRNAME + TRACE_FILENAME_CLIENTS);
+ mBufferIms = new TraceBuffer<>(BUFFER_CAPACITY);
+ mTraceFileIms = new File(TRACE_DIRNAME + TRACE_FILENAME_IMS);
+ mBufferImms = new TraceBuffer<>(BUFFER_CAPACITY);
+ mTraceFileImms = new File(TRACE_DIRNAME + TRACE_FILENAME_IMMS);
+ }
+
+ /**
+ * The provided dump is added to the corresponding dump buffer:
+ * {@link ImeTracingServerImpl#mBufferClients} or {@link ImeTracingServerImpl#mBufferIms}.
+ *
+ * @param proto dump to be added to the buffer
+ */
+ @Override
+ public void addToBuffer(ProtoOutputStream proto, int source) {
+ if (isAvailable() && isEnabled()) {
+ switch (source) {
+ case IME_TRACING_FROM_CLIENT:
+ mBufferClients.add(proto);
+ return;
+ case IME_TRACING_FROM_IMS:
+ mBufferIms.add(proto);
+ return;
+ case IME_TRACING_FROM_IMMS:
+ mBufferImms.add(proto);
+ return;
+ default:
+ // Source not recognised.
+ Log.w(TAG, "Request to add to buffer, but source not recognised.");
+ }
+ }
+ }
+
+ @Override
+ public void triggerClientDump(String where, InputMethodManager immInstance,
+ ProtoOutputStream icProto) {
+ // Intentionally left empty, this is implemented in ImeTracingClientImpl
+ }
+
+ @Override
+ public void triggerServiceDump(String where, AbstractInputMethodService service,
+ ProtoOutputStream icProto) {
+ // Intentionally left empty, this is implemented in ImeTracingClientImpl
+ }
+
+ @Override
+ public void triggerManagerServiceDump(String where) {
+ if (!isEnabled() || !isAvailable()) {
+ return;
+ }
+
+ synchronized (mDumpInProgressLock) {
+ if (mDumpInProgress) {
+ return;
+ }
+ mDumpInProgress = true;
+ }
+
+ try {
+ sendToService(null, IME_TRACING_FROM_IMMS, where);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Exception while sending ime-related manager service dump to server", e);
+ } finally {
+ mDumpInProgress = false;
+ }
+ }
+
+ private void writeTracesToFilesLocked() {
+ try {
+ ProtoOutputStream clientsProto = new ProtoOutputStream();
+ clientsProto.write(InputMethodClientsTraceFileProto.MAGIC_NUMBER,
+ MAGIC_NUMBER_CLIENTS_VALUE);
+ mBufferClients.writeTraceToFile(mTraceFileClients, clientsProto);
+
+ ProtoOutputStream imsProto = new ProtoOutputStream();
+ imsProto.write(InputMethodServiceTraceFileProto.MAGIC_NUMBER, MAGIC_NUMBER_IMS_VALUE);
+ mBufferIms.writeTraceToFile(mTraceFileIms, imsProto);
+
+ ProtoOutputStream immsProto = new ProtoOutputStream();
+ immsProto.write(InputMethodManagerServiceTraceFileProto.MAGIC_NUMBER,
+ MAGIC_NUMBER_IMMS_VALUE);
+ mBufferImms.writeTraceToFile(mTraceFileImms, immsProto);
+
+ resetBuffers();
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to write buffer to file", e);
+ }
+ }
+
+ @GuardedBy("mEnabledLock")
+ @Override
+ public void startTrace(@Nullable PrintWriter pw) {
+ if (IS_USER) {
+ Log.w(TAG, "Warn: Tracing is not supported on user builds.");
+ return;
+ }
+
+ synchronized (mEnabledLock) {
+ if (isAvailable() && isEnabled()) {
+ Log.w(TAG, "Warn: Tracing is already started.");
+ return;
+ }
+
+ logAndPrintln(pw, "Starting tracing in " + TRACE_DIRNAME + ": " + TRACE_FILENAME_CLIENTS
+ + ", " + TRACE_FILENAME_IMS + ", " + TRACE_FILENAME_IMMS);
+ sEnabled = true;
+ resetBuffers();
+ }
+ }
+
+ @Override
+ public void stopTrace(@Nullable PrintWriter pw) {
+ if (IS_USER) {
+ Log.w(TAG, "Warn: Tracing is not supported on user builds.");
+ return;
+ }
+
+ synchronized (mEnabledLock) {
+ if (!isAvailable() || !isEnabled()) {
+ Log.w(TAG, "Warn: Tracing is not available or not started.");
+ return;
+ }
+
+ logAndPrintln(pw, "Stopping tracing and writing traces in " + TRACE_DIRNAME + ": "
+ + TRACE_FILENAME_CLIENTS + ", " + TRACE_FILENAME_IMS + ", "
+ + TRACE_FILENAME_IMMS);
+ sEnabled = false;
+ writeTracesToFilesLocked();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void saveForBugreport(@Nullable PrintWriter pw) {
+ if (IS_USER) {
+ return;
+ }
+ synchronized (mEnabledLock) {
+ if (!isAvailable() || !isEnabled()) {
+ return;
+ }
+ // Temporarily stop accepting logs from trace event providers. There is a small chance
+ // that we may drop some trace events while writing the file, but we currently need to
+ // live with that. Note that addToBuffer() also has a bug that it doesn't do
+ // read-acquire so flipping sEnabled here doesn't even guarantee that addToBuffer() will
+ // temporarily stop accepting incoming events...
+ // TODO(b/175761228): Implement atomic snapshot to avoid downtime.
+ // TODO(b/175761228): Fix synchronization around sEnabled.
+ sEnabled = false;
+ logAndPrintln(pw, "Writing traces in " + TRACE_DIRNAME + ": "
+ + TRACE_FILENAME_CLIENTS + ", " + TRACE_FILENAME_IMS + ", "
+ + TRACE_FILENAME_IMMS);
+ writeTracesToFilesLocked();
+ sEnabled = true;
+ }
+ }
+
+ private void resetBuffers() {
+ mBufferClients.resetBuffer();
+ mBufferIms.resetBuffer();
+ mBufferImms.resetBuffer();
+ }
+}
diff --git a/core/java/android/util/imetracing/InputConnectionHelper.java b/core/java/android/util/imetracing/InputConnectionHelper.java
new file mode 100644
index 000000000000..39f1e01eb4a9
--- /dev/null
+++ b/core/java/android/util/imetracing/InputConnectionHelper.java
@@ -0,0 +1,231 @@
+/*
+ * 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.imetracing;
+
+import static android.view.inputmethod.InputConnectionCallProto.GET_CURSOR_CAPS_MODE;
+import static android.view.inputmethod.InputConnectionCallProto.GET_EXTRACTED_TEXT;
+import static android.view.inputmethod.InputConnectionCallProto.GET_SELECTED_TEXT;
+import static android.view.inputmethod.InputConnectionCallProto.GET_SURROUNDING_TEXT;
+import static android.view.inputmethod.InputConnectionCallProto.GET_TEXT_AFTER_CURSOR;
+import static android.view.inputmethod.InputConnectionCallProto.GET_TEXT_BEFORE_CURSOR;
+import static android.view.inputmethod.InputConnectionCallProto.GetExtractedText.REQUEST;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.proto.ProtoOutputStream;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnectionCallProto.GetCursorCapsMode;
+import android.view.inputmethod.InputConnectionCallProto.GetExtractedText;
+import android.view.inputmethod.InputConnectionCallProto.GetSelectedText;
+import android.view.inputmethod.InputConnectionCallProto.GetSurroundingText;
+import android.view.inputmethod.InputConnectionCallProto.GetTextAfterCursor;
+import android.view.inputmethod.InputConnectionCallProto.GetTextBeforeCursor;
+import android.view.inputmethod.SurroundingText;
+
+/**
+ * Helper class for constructing {@link android.view.inputmethod.InputConnection} dumps, which are
+ * integrated into {@link ImeTracing}.
+ * @hide
+ */
+public class InputConnectionHelper {
+ static final String TAG = "InputConnectionHelper";
+ public static final boolean DUMP_TEXT = false;
+
+ private InputConnectionHelper() {}
+
+ /**
+ * Builder for InputConnectionCallProto to hold
+ * {@link android.view.inputmethod.InputConnection#getTextAfterCursor(int, int)} data.
+ *
+ * @param length The expected length of the text. This must be non-negative.
+ * @param flags Supplies additional options controlling how the text is
+ * returned. May be either {@code 0} or
+ * {@link android.view.inputmethod.InputConnection#GET_TEXT_WITH_STYLES}.
+ * @param result The text after the cursor position; the length of the
+ * returned text might be less than <var>length</var>.
+ * @return ProtoOutputStream holding the InputConnectionCallProto data.
+ */
+ public static ProtoOutputStream buildGetTextAfterCursorProto(@IntRange(from = 0) int length,
+ int flags, @Nullable CharSequence result) {
+ ProtoOutputStream proto = new ProtoOutputStream();
+ final long token = proto.start(GET_TEXT_AFTER_CURSOR);
+ proto.write(GetTextAfterCursor.LENGTH, length);
+ proto.write(GetTextAfterCursor.FLAGS, flags);
+ if (result == null) {
+ proto.write(GetTextAfterCursor.RESULT, "null result");
+ } else if (DUMP_TEXT) {
+ proto.write(GetTextAfterCursor.RESULT, result.toString());
+ }
+ proto.end(token);
+ return proto;
+ }
+
+ /**
+ * Builder for InputConnectionCallProto to hold
+ * {@link android.view.inputmethod.InputConnection#getTextBeforeCursor(int, int)} data.
+ *
+ * @param length The expected length of the text. This must be non-negative.
+ * @param flags Supplies additional options controlling how the text is
+ * returned. May be either {@code 0} or
+ * {@link android.view.inputmethod.InputConnection#GET_TEXT_WITH_STYLES}.
+ * @param result The text before the cursor position; the length of the
+ * returned text might be less than <var>length</var>.
+ * @return ProtoOutputStream holding the InputConnectionCallProto data.
+ */
+ public static ProtoOutputStream buildGetTextBeforeCursorProto(@IntRange(from = 0) int length,
+ int flags, @Nullable CharSequence result) {
+ ProtoOutputStream proto = new ProtoOutputStream();
+ final long token = proto.start(GET_TEXT_BEFORE_CURSOR);
+ proto.write(GetTextBeforeCursor.LENGTH, length);
+ proto.write(GetTextBeforeCursor.FLAGS, flags);
+ if (result == null) {
+ proto.write(GetTextBeforeCursor.RESULT, "null result");
+ } else if (DUMP_TEXT) {
+ proto.write(GetTextBeforeCursor.RESULT, result.toString());
+ }
+ proto.end(token);
+ return proto;
+ }
+
+ /**
+ * Builder for InputConnectionCallProto to hold
+ * {@link android.view.inputmethod.InputConnection#getSelectedText(int)} data.
+ *
+ * @param flags Supplies additional options controlling how the text is
+ * returned. May be either {@code 0} or
+ * {@link android.view.inputmethod.InputConnection#GET_TEXT_WITH_STYLES}.
+ * @param result the text that is currently selected, if any, or null if
+ * no text is selected. In {@link android.os.Build.VERSION_CODES#N} and
+ * later, returns false when the target application does not implement
+ * this method.
+ * @return ProtoOutputStream holding the InputConnectionCallProto data.
+ */
+ public static ProtoOutputStream buildGetSelectedTextProto(int flags,
+ @Nullable CharSequence result) {
+ ProtoOutputStream proto = new ProtoOutputStream();
+ final long token = proto.start(GET_SELECTED_TEXT);
+ proto.write(GetSelectedText.FLAGS, flags);
+ if (result == null) {
+ proto.write(GetSelectedText.RESULT, "null result");
+ } else if (DUMP_TEXT) {
+ proto.write(GetSelectedText.RESULT, result.toString());
+ }
+ proto.end(token);
+ return proto;
+ }
+
+ /**
+ * Builder for InputConnectionCallProto to hold
+ * {@link android.view.inputmethod.InputConnection#getSurroundingText(int, int, int)} data.
+ *
+ * @param beforeLength The expected length of the text before the cursor.
+ * @param afterLength The expected length of the text after the cursor.
+ * @param flags Supplies additional options controlling how the text is
+ * returned. May be either {@code 0} or
+ * {@link android.view.inputmethod.InputConnection#GET_TEXT_WITH_STYLES}.
+ * @param result an {@link android.view.inputmethod.SurroundingText} object describing the
+ * surrounding text and state of selection, or null if the input connection is no longer valid,
+ * or the editor can't comply with the request for some reason, or the application does not
+ * implement this method. The length of the returned text might be less than the sum of
+ * <var>beforeLength</var> and <var>afterLength</var> .
+ * @return ProtoOutputStream holding the InputConnectionCallProto data.
+ */
+ public static ProtoOutputStream buildGetSurroundingTextProto(@IntRange(from = 0)
+ int beforeLength, @IntRange(from = 0) int afterLength, int flags,
+ @Nullable SurroundingText result) {
+ ProtoOutputStream proto = new ProtoOutputStream();
+ final long token = proto.start(GET_SURROUNDING_TEXT);
+ proto.write(GetSurroundingText.BEFORE_LENGTH, beforeLength);
+ proto.write(GetSurroundingText.AFTER_LENGTH, afterLength);
+ proto.write(GetSurroundingText.FLAGS, flags);
+ if (result == null) {
+ final long token_result = proto.start(GetSurroundingText.RESULT);
+ proto.write(GetSurroundingText.SurroundingText.TEXT, "null result");
+ proto.end(token_result);
+ } else if (DUMP_TEXT) {
+ final long token_result = proto.start(GetSurroundingText.RESULT);
+ proto.write(GetSurroundingText.SurroundingText.TEXT, result.getText().toString());
+ proto.write(GetSurroundingText.SurroundingText.SELECTION_START,
+ result.getSelectionStart());
+ proto.write(GetSurroundingText.SurroundingText.SELECTION_END,
+ result.getSelectionEnd());
+ proto.write(GetSurroundingText.SurroundingText.OFFSET, result.getOffset());
+ proto.end(token_result);
+ }
+ proto.end(token);
+ return proto;
+ }
+
+ /**
+ * Builder for InputConnectionCallProto to hold
+ * {@link android.view.inputmethod.InputConnection#getCursorCapsMode(int)} data.
+ *
+ * @param reqModes The desired modes to retrieve, as defined by
+ * {@link android.text.TextUtils#getCapsMode TextUtils.getCapsMode}.
+ * @param result the caps mode flags that are in effect at the current
+ * cursor position. See TYPE_TEXT_FLAG_CAPS_* in {@link android.text.InputType}.
+ * @return ProtoOutputStream holding the InputConnectionCallProto data.
+ */
+ public static ProtoOutputStream buildGetCursorCapsModeProto(int reqModes, int result) {
+ ProtoOutputStream proto = new ProtoOutputStream();
+ final long token = proto.start(GET_CURSOR_CAPS_MODE);
+ proto.write(GetCursorCapsMode.REQ_MODES, reqModes);
+ if (DUMP_TEXT) {
+ proto.write(GetCursorCapsMode.RESULT, result);
+ }
+ proto.end(token);
+ return proto;
+ }
+
+ /**
+ * Builder for InputConnectionCallProto to hold
+ * {@link android.view.inputmethod.InputConnection#getExtractedText(ExtractedTextRequest, int)}
+ * data.
+ *
+ * @param request Description of how the text should be returned.
+ * {@link android.view.inputmethod.ExtractedTextRequest}
+ * @param flags Additional options to control the client, either {@code 0} or
+ * {@link android.view.inputmethod.InputConnection#GET_EXTRACTED_TEXT_MONITOR}.
+ * @param result an {@link android.view.inputmethod.ExtractedText}
+ * object describing the state of the text view and containing the
+ * extracted text itself, or null if the input connection is no
+ * longer valid of the editor can't comply with the request for
+ * some reason.
+ * @return ProtoOutputStream holding the InputConnectionCallProto data.
+ */
+ public static ProtoOutputStream buildGetExtractedTextProto(@NonNull ExtractedTextRequest
+ request, int flags, @Nullable ExtractedText result) {
+ ProtoOutputStream proto = new ProtoOutputStream();
+ final long token = proto.start(GET_EXTRACTED_TEXT);
+ final long token_request = proto.start(REQUEST);
+ proto.write(GetExtractedText.ExtractedTextRequest.TOKEN, request.token);
+ proto.write(GetExtractedText.ExtractedTextRequest.FLAGS, request.flags);
+ proto.write(GetExtractedText.ExtractedTextRequest.HINT_MAX_LINES, request.hintMaxLines);
+ proto.write(GetExtractedText.ExtractedTextRequest.HINT_MAX_CHARS, request.hintMaxChars);
+ proto.end(token_request);
+ proto.write(GetExtractedText.FLAGS, flags);
+ if (result == null) {
+ proto.write(GetExtractedText.RESULT, "null result");
+ } else if (DUMP_TEXT) {
+ proto.write(GetExtractedText.RESULT, result.text.toString());
+ }
+ proto.end(token);
+ return proto;
+ }
+}
diff --git a/core/java/android/util/imetracing/OWNERS b/core/java/android/util/imetracing/OWNERS
new file mode 100644
index 000000000000..885fd0ab9a45
--- /dev/null
+++ b/core/java/android/util/imetracing/OWNERS
@@ -0,0 +1,3 @@
+set noparent
+
+include /services/core/java/com/android/server/inputmethod/OWNERS
diff --git a/core/java/android/util/jar/StrictJarManifest.java b/core/java/android/util/jar/StrictJarManifest.java
index faec099b001f..5c2fd9e41b9b 100644
--- a/core/java/android/util/jar/StrictJarManifest.java
+++ b/core/java/android/util/jar/StrictJarManifest.java
@@ -17,6 +17,10 @@
package android.util.jar;
+import android.annotation.Nullable;
+
+import libcore.io.Streams;
+
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -29,7 +33,6 @@ 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
@@ -219,7 +222,7 @@ public class StrictJarManifest implements Cloneable {
* @return {@code true} if the manifests are equal, {@code false} otherwise
*/
@Override
- public boolean equals(Object o) {
+ public boolean equals(@Nullable Object o) {
if (o == null) {
return false;
}
diff --git a/core/java/android/util/proto/EncodedBuffer.java b/core/java/android/util/proto/EncodedBuffer.java
index 56a0bfa2adb1..2a8f405ffd7f 100644
--- a/core/java/android/util/proto/EncodedBuffer.java
+++ b/core/java/android/util/proto/EncodedBuffer.java
@@ -648,7 +648,7 @@ public final class EncodedBuffer {
* Print the internal buffer chunks.
*/
private static int dumpByteString(String tag, String prefix, int start, byte[] buf) {
- StringBuffer sb = new StringBuffer();
+ StringBuilder sb = new StringBuilder();
final int length = buf.length;
final int lineLen = 16;
int i;
@@ -656,7 +656,7 @@ public final class EncodedBuffer {
if (i % lineLen == 0) {
if (i != 0) {
Log.d(tag, sb.toString());
- sb = new StringBuffer();
+ sb = new StringBuilder();
}
sb.append(prefix);
sb.append('[');
diff --git a/core/java/android/util/proto/ProtoInputStream.java b/core/java/android/util/proto/ProtoInputStream.java
index aa70d07ff787..9789b10a0a61 100644
--- a/core/java/android/util/proto/ProtoInputStream.java
+++ b/core/java/android/util/proto/ProtoInputStream.java
@@ -16,10 +16,11 @@
package android.util.proto;
+import android.util.LongArray;
+
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
/**
* Class to read to a protobuf stream.
@@ -98,7 +99,7 @@ public final class ProtoInputStream extends ProtoStream {
/**
* Keeps track of the currently read nested Objects, for end object checking and debug
*/
- private ArrayList<Long> mExpectedObjectTokenStack = null;
+ private LongArray mExpectedObjectTokenStack = null;
/**
* Current nesting depth of start calls.
@@ -498,7 +499,7 @@ public final class ProtoInputStream extends ProtoStream {
int messageSize = (int) readVarint();
if (mExpectedObjectTokenStack == null) {
- mExpectedObjectTokenStack = new ArrayList<>();
+ mExpectedObjectTokenStack = new LongArray();
}
if (++mDepth == mExpectedObjectTokenStack.size()) {
// Create a token to keep track of nested Object and extend the object stack