diff options
| author | Xin Li <delphij@google.com> | 2021-10-07 23:50:15 +0000 |
|---|---|---|
| committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2021-10-07 23:50:15 +0000 |
| commit | c03b0fa033117c03430e361d561aa910e95a0478 (patch) | |
| tree | 9c6aaee5a3023a6c237394b44e06a3fdb46f6747 /core/java/android/util | |
| parent | 8cc0f40cf250d9c66dc15d0e8bc3a41db9a7cfa1 (diff) | |
| parent | 531b8f4f2605c44cf73e8421f674a1c7a9c277ff (diff) | |
Merge "Merge Android 12"
Diffstat (limited to 'core/java/android/util')
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 |
