diff options
Diffstat (limited to 'android/util')
26 files changed, 884 insertions, 385 deletions
diff --git a/android/util/AtomicFile.java b/android/util/AtomicFile.java index 6342c8bc..cf7ed9b0 100644 --- a/android/util/AtomicFile.java +++ b/android/util/AtomicFile.java @@ -17,6 +17,7 @@ package android.util; import android.os.FileUtils; +import android.os.SystemClock; import libcore.io.IoUtils; @@ -47,14 +48,25 @@ import java.util.function.Consumer; public class AtomicFile { private final File mBaseName; private final File mBackupName; + private final String mCommitTag; + private long mStartTime; /** * Create a new AtomicFile for a file located at the given File path. * The secondary backup file will be the same file path with ".bak" appended. */ public AtomicFile(File baseName) { + this(baseName, null); + } + + /** + * @hide Internal constructor that also allows you to have the class + * automatically log commit events. + */ + public AtomicFile(File baseName, String commitTag) { mBaseName = baseName; mBackupName = new File(baseName.getPath() + ".bak"); + mCommitTag = commitTag; } /** @@ -88,6 +100,18 @@ public class AtomicFile { * access to AtomicFile. */ public FileOutputStream startWrite() throws IOException { + return startWrite(mCommitTag != null ? SystemClock.uptimeMillis() : 0); + } + + /** + * @hide Internal version of {@link #startWrite()} that allows you to specify an earlier + * start time of the operation to adjust how the commit is logged. + * @param startTime The effective start time of the operation, in the time + * base of {@link SystemClock#uptimeMillis()}. + */ + public FileOutputStream startWrite(long startTime) throws IOException { + mStartTime = startTime; + // Rename the current file so it may be used as a backup during the next read if (mBaseName.exists()) { if (!mBackupName.exists()) { @@ -135,6 +159,10 @@ public class AtomicFile { } catch (IOException e) { Log.w("AtomicFile", "finishWrite: Got exception:", e); } + if (mCommitTag != null) { + com.android.internal.logging.EventLogTags.writeCommitSysConfigFile( + mCommitTag, SystemClock.uptimeMillis() - mStartTime); + } } } diff --git a/android/util/AttributeSet.java b/android/util/AttributeSet.java index eb8c1682..7f327c7b 100644 --- a/android/util/AttributeSet.java +++ b/android/util/AttributeSet.java @@ -17,6 +17,8 @@ package android.util; +import org.xmlpull.v1.XmlPullParser; + /** * A collection of attributes, as found associated with a tag in an XML * document. Often you will not want to use this interface directly, instead @@ -54,18 +56,42 @@ package android.util; * compiled XML resource that is not available in a normal XML file, such * as {@link #getAttributeNameResource(int)} which returns the resource * identifier associated with a particular XML attribute name. + * + * @see XmlPullParser */ public interface AttributeSet { /** * Returns the number of attributes available in the set. - * + * + * <p>See also {@link XmlPullParser#getAttributeCount XmlPullParser.getAttributeCount()}, + * which this method corresponds to when parsing a compiled XML file.</p> + * * @return A positive integer, or 0 if the set is empty. */ public int getAttributeCount(); /** + * Returns the namespace of the specified attribute. + * + * <p>See also {@link XmlPullParser#getAttributeNamespace XmlPullParser.getAttributeNamespace()}, + * which this method corresponds to when parsing a compiled XML file.</p> + * + * @param index Index of the desired attribute, 0...count-1. + * + * @return A String containing the namespace of the attribute, or null if th + * attribute cannot be found. + */ + default String getAttributeNamespace (int index) { + // This is a new method since the first interface definition, so add stub impl. + return null; + } + + /** * Returns the name of the specified attribute. - * + * + * <p>See also {@link XmlPullParser#getAttributeName XmlPullParser.getAttributeName()}, + * which this method corresponds to when parsing a compiled XML file.</p> + * * @param index Index of the desired attribute, 0...count-1. * * @return A String containing the name of the attribute, or null if the diff --git a/android/util/ByteStringUtils.java b/android/util/ByteStringUtils.java index 333208db..f6460ad9 100644 --- a/android/util/ByteStringUtils.java +++ b/android/util/ByteStringUtils.java @@ -22,61 +22,63 @@ package android.util; * @hide */ public final class ByteStringUtils { - private final static char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); + private static final char[] HEX_LOWERCASE_ARRAY = "0123456789abcdef".toCharArray(); + private static final char[] HEX_UPPERCASE_ARRAY = "0123456789ABCDEF".toCharArray(); - private ByteStringUtils() { + private ByteStringUtils() { /* hide constructor */ - } - - /** - * Returns the hex encoded string representation of bytes. - * @param bytes Byte array to encode. - * @return Hex encoded string representation of bytes. - */ - public static String toHexString(byte[] bytes) { - if (bytes == null || bytes.length == 0 || bytes.length % 2 != 0) { - return null; } - final int byteLength = bytes.length; - final int charCount = 2 * byteLength; - final char[] chars = new char[charCount]; + /** + * Returns the hex encoded string representation of bytes. + * @param bytes Byte array to encode. + * @return Hex encoded string representation of bytes. + */ + public static String toHexString(byte[] bytes) { + if (bytes == null || bytes.length == 0 || bytes.length % 2 != 0) { + return null; + } - for (int i = 0; i < byteLength; i++) { - final int byteHex = bytes[i] & 0xFF; - chars[i * 2] = HEX_ARRAY[byteHex >>> 4]; - chars[i * 2 + 1] = HEX_ARRAY[byteHex & 0x0F]; - } - return new String(chars); - } + final int byteLength = bytes.length; + final int charCount = 2 * byteLength; + final char[] chars = new char[charCount]; - /** - * Returns the decoded byte array representation of str. - * @param str Hex encoded string to decode. - * @return Decoded byte array representation of str. - */ - public static byte[] fromHexToByteArray(String str) { - if (str == null || str.length() == 0 || str.length() % 2 != 0) { - return null; + for (int i = 0; i < byteLength; i++) { + final int byteHex = bytes[i] & 0xFF; + chars[i * 2] = HEX_UPPERCASE_ARRAY[byteHex >>> 4]; + chars[i * 2 + 1] = HEX_UPPERCASE_ARRAY[byteHex & 0x0F]; + } + return new String(chars); } - final char[] chars = str.toCharArray(); - final int charLength = chars.length; - final byte[] bytes = new byte[charLength / 2]; + /** + * Returns the decoded byte array representation of str. + * @param str Hex encoded string to decode. + * @return Decoded byte array representation of str. + */ + public static byte[] fromHexToByteArray(String str) { + if (str == null || str.length() == 0 || str.length() % 2 != 0) { + return null; + } + + final char[] chars = str.toCharArray(); + final int charLength = chars.length; + final byte[] bytes = new byte[charLength / 2]; - for (int i = 0; i < bytes.length; i++) { - bytes[i] = - (byte)(((getIndex(chars[i * 2]) << 4) & 0xF0) | (getIndex(chars[i * 2 + 1]) & 0x0F)); + for (int i = 0; i < bytes.length; i++) { + bytes[i] = + (byte) (((getIndex(chars[i * 2]) << 4) & 0xF0) + | (getIndex(chars[i * 2 + 1]) & 0x0F)); + } + return bytes; } - return bytes; - } - private static int getIndex(char c) { - for (int i = 0; i < HEX_ARRAY.length; i++) { - if (HEX_ARRAY[i] == c) { - return i; - } + private static int getIndex(char c) { + for (int i = 0; i < HEX_UPPERCASE_ARRAY.length; i++) { + if (HEX_UPPERCASE_ARRAY[i] == c || HEX_LOWERCASE_ARRAY[i] == c) { + return i; + } + } + return -1; } - return -1; - } } diff --git a/android/util/DataUnit.java b/android/util/DataUnit.java index ea4266ec..cf045b8a 100644 --- a/android/util/DataUnit.java +++ b/android/util/DataUnit.java @@ -29,6 +29,8 @@ import java.util.concurrent.TimeUnit; * "kibibyte" as an IEC unit of 1024 bytes. * <p> * This design is mirrored after {@link TimeUnit} and {@link ChronoUnit}. + * + * @hide */ public enum DataUnit { KILOBYTES { @Override public long toBytes(long v) { return v * 1_000; } }, diff --git a/android/util/DebugUtils.java b/android/util/DebugUtils.java index c44f42b1..46e31693 100644 --- a/android/util/DebugUtils.java +++ b/android/util/DebugUtils.java @@ -219,7 +219,7 @@ public class DebugUtils { && field.getType().equals(int.class) && field.getName().startsWith(prefix)) { try { if (value == field.getInt(null)) { - return field.getName().substring(prefix.length()); + return constNameWithoutPrefix(prefix, field); } } catch (IllegalAccessException ignored) { } @@ -236,6 +236,7 @@ public class DebugUtils { */ public static String flagsToString(Class<?> clazz, String prefix, int flags) { final StringBuilder res = new StringBuilder(); + boolean flagsWasZero = flags == 0; for (Field field : clazz.getDeclaredFields()) { final int modifiers = field.getModifiers(); @@ -243,9 +244,12 @@ public class DebugUtils { && field.getType().equals(int.class) && field.getName().startsWith(prefix)) { try { final int value = field.getInt(null); + if (value == 0 && flagsWasZero) { + return constNameWithoutPrefix(prefix, field); + } if ((flags & value) != 0) { flags &= ~value; - res.append(field.getName().substring(prefix.length())).append('|'); + res.append(constNameWithoutPrefix(prefix, field)).append('|'); } } catch (IllegalAccessException ignored) { } @@ -258,4 +262,8 @@ public class DebugUtils { } return res.toString(); } + + private static String constNameWithoutPrefix(String prefix, Field field) { + return field.getName().substring(prefix.length()); + } } diff --git a/android/util/DisplayMetrics.java b/android/util/DisplayMetrics.java index b7099b64..13de172c 100644 --- a/android/util/DisplayMetrics.java +++ b/android/util/DisplayMetrics.java @@ -120,6 +120,14 @@ public class DisplayMetrics { public static final int DENSITY_420 = 420; /** + * Intermediate density for screens that sit somewhere between + * {@link #DENSITY_XHIGH} (320 dpi) and {@link #DENSITY_XXHIGH} (480 dpi). + * This is not a density that applications should target, instead relying + * on the system to scale their {@link #DENSITY_XXHIGH} assets for them. + */ + public static final int DENSITY_440 = 440; + + /** * Standard quantized DPI for extra-extra-high-density screens. */ public static final int DENSITY_XXHIGH = 480; diff --git a/android/util/ExceptionUtils.java b/android/util/ExceptionUtils.java index da7387fc..1a397b39 100644 --- a/android/util/ExceptionUtils.java +++ b/android/util/ExceptionUtils.java @@ -86,4 +86,16 @@ public class ExceptionUtils { while (t.getCause() != null) t = t.getCause(); return t; } -} + + /** + * Appends {@code cause} at the end of the causal chain of {@code t} + * + * @return {@code t} for convenience + */ + public static @NonNull Throwable appendCause(@NonNull Throwable t, @Nullable Throwable cause) { + if (cause != null) { + getRootCause(t).initCause(cause); + } + return t; + } +}
\ No newline at end of file diff --git a/android/util/FeatureFlagUtils.java b/android/util/FeatureFlagUtils.java index 25a177ed..eecdb74f 100644 --- a/android/util/FeatureFlagUtils.java +++ b/android/util/FeatureFlagUtils.java @@ -37,16 +37,12 @@ public class FeatureFlagUtils { private static final Map<String, String> DEFAULT_FLAGS; static { DEFAULT_FLAGS = new HashMap<>(); - DEFAULT_FLAGS.put("device_info_v2", "true"); - DEFAULT_FLAGS.put("settings_app_info_v2", "true"); - DEFAULT_FLAGS.put("settings_connected_device_v2", "true"); - DEFAULT_FLAGS.put("settings_battery_v2", "true"); DEFAULT_FLAGS.put("settings_battery_display_app_list", "false"); - DEFAULT_FLAGS.put("settings_security_settings_v2", "true"); DEFAULT_FLAGS.put("settings_zone_picker_v2", "true"); - DEFAULT_FLAGS.put("settings_suggestion_ui_v2", "false"); - DEFAULT_FLAGS.put("settings_about_phone_v2", "false"); + DEFAULT_FLAGS.put("settings_about_phone_v2", "true"); DEFAULT_FLAGS.put("settings_bluetooth_while_driving", "false"); + DEFAULT_FLAGS.put("settings_data_usage_v2", "true"); + DEFAULT_FLAGS.put("settings_audio_switcher", "false"); } /** diff --git a/android/util/KeyValueSettingObserver.java b/android/util/KeyValueSettingObserver.java new file mode 100644 index 00000000..9fca8b2c --- /dev/null +++ b/android/util/KeyValueSettingObserver.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util; + +import android.content.ContentResolver; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; + +/** + * Abstract class for observing changes to a specified setting stored as a comma-separated key value + * list of parameters. Registers and unregisters a {@link ContentObserver} and handles updates when + * the setting changes. + * + * <p>Subclasses should pass in the relevant setting's {@link Uri} in the constructor and implement + * {@link #update(KeyValueListParser)} to receive updates when the value changes. + * Calls to {@link #update(KeyValueListParser)} only trigger after calling {@link + * #start()}. + * + * <p>To get the most up-to-date parameter values, first call {@link #start()} before accessing the + * values to start observing changes, and then call {@link #stop()} once finished. + * + * @hide + */ +public abstract class KeyValueSettingObserver { + private static final String TAG = "KeyValueSettingObserver"; + + private final KeyValueListParser mParser = new KeyValueListParser(','); + + private final ContentObserver mObserver; + private final ContentResolver mResolver; + private final Uri mSettingUri; + + public KeyValueSettingObserver(Handler handler, ContentResolver resolver, + Uri uri) { + mObserver = new SettingObserver(handler); + mResolver = resolver; + mSettingUri = uri; + } + + /** Starts observing changes for the setting. Pair with {@link #stop()}. */ + public void start() { + mResolver.registerContentObserver(mSettingUri, false, mObserver); + setParserValue(); + update(mParser); + } + + /** Stops observing changes for the setting. */ + public void stop() { + mResolver.unregisterContentObserver(mObserver); + } + + /** + * Returns the {@link String} representation of the setting. Subclasses should implement this + * for their setting. + */ + public abstract String getSettingValue(ContentResolver resolver); + + /** Updates the parser with the current setting value. */ + private void setParserValue() { + String setting = getSettingValue(mResolver); + try { + mParser.setString(setting); + } catch (IllegalArgumentException e) { + Slog.e(TAG, "Malformed setting: " + setting); + } + } + + /** Subclasses should implement this to update references to their parameters. */ + public abstract void update(KeyValueListParser parser); + + private class SettingObserver extends ContentObserver { + private SettingObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange) { + setParserValue(); + update(mParser); + } + } +} diff --git a/android/util/LauncherIcons.java b/android/util/LauncherIcons.java index 402bef91..cc9991a9 100644 --- a/android/util/LauncherIcons.java +++ b/android/util/LauncherIcons.java @@ -110,9 +110,9 @@ public final class LauncherIcons { Drawable badgeColor = sysRes.getDrawable( com.android.internal.R.drawable.ic_corp_icon_badge_color) .getConstantState().newDrawable().mutate(); - badgeColor.setTint(backgroundColor); Drawable badgeForeground = sysRes.getDrawable(foregroundRes); + badgeForeground.setTint(backgroundColor); Drawable[] drawables = base == null ? new Drawable[] {badgeShadow, badgeColor, badgeForeground } diff --git a/android/util/Log.java b/android/util/Log.java index b94e48b3..02998653 100644 --- a/android/util/Log.java +++ b/android/util/Log.java @@ -16,12 +16,45 @@ package android.util; +import android.os.DeadSystemException; + +import com.android.internal.os.RuntimeInit; +import com.android.internal.util.FastPrintWriter; +import com.android.internal.util.LineBreakBufferedWriter; + import java.io.PrintWriter; import java.io.StringWriter; +import java.io.Writer; import java.net.UnknownHostException; /** - * Mock Log implementation for testing on non android host. + * API for sending log output. + * + * <p>Generally, you should use the {@link #v Log.v()}, {@link #d Log.d()}, + * {@link #i Log.i()}, {@link #w Log.w()}, and {@link #e Log.e()} methods to write logs. + * 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. + * + * <p><b>Tip:</b> A good convention is to declare a <code>TAG</code> constant + * in your class: + * + * <pre>private static final String TAG = "MyActivity";</pre> + * + * and use that in subsequent calls to the log methods. + * </p> + * + * <p><b>Tip:</b> Don't forget that when you make a call like + * <pre>Log.v(TAG, "index=" + i);</pre> + * that when you're building the string to pass into Log.d, the compiler uses a + * StringBuilder and at least three allocations occur: the StringBuilder + * itself, the buffer, and the String object. Realistically, there is also + * another buffer allocation and copy, and even more pressure on the gc. + * That means that if your log message is filtered out, you might be doing + * significant work and incurring significant overhead. */ public final class Log { @@ -55,6 +88,29 @@ public final class Log { */ public static final int ASSERT = 7; + /** + * Exception class used to capture a stack trace in {@link #wtf}. + * @hide + */ + public static class TerribleFailure extends Exception { + TerribleFailure(String msg, Throwable cause) { super(msg, cause); } + } + + /** + * Interface to handle terrible failures from {@link #wtf}. + * + * @hide + */ + public interface TerribleFailureHandler { + void onTerribleFailure(String tag, TerribleFailure what, boolean system); + } + + private static TerribleFailureHandler sWtfHandler = new TerribleFailureHandler() { + public void onTerribleFailure(String tag, TerribleFailure what, boolean system) { + RuntimeInit.wtf(tag, what, system); + } + }; + private Log() { } @@ -65,7 +121,7 @@ public final class Log { * @param msg The message you would like logged. */ public static int v(String tag, String msg) { - return println(LOG_ID_MAIN, VERBOSE, tag, msg); + return println_native(LOG_ID_MAIN, VERBOSE, tag, msg); } /** @@ -76,7 +132,7 @@ public final class Log { * @param tr An exception to log */ public static int v(String tag, String msg, Throwable tr) { - return println(LOG_ID_MAIN, VERBOSE, tag, msg + '\n' + getStackTraceString(tr)); + return printlns(LOG_ID_MAIN, VERBOSE, tag, msg, tr); } /** @@ -86,7 +142,7 @@ public final class Log { * @param msg The message you would like logged. */ public static int d(String tag, String msg) { - return println(LOG_ID_MAIN, DEBUG, tag, msg); + return println_native(LOG_ID_MAIN, DEBUG, tag, msg); } /** @@ -97,7 +153,7 @@ public final class Log { * @param tr An exception to log */ public static int d(String tag, String msg, Throwable tr) { - return println(LOG_ID_MAIN, DEBUG, tag, msg + '\n' + getStackTraceString(tr)); + return printlns(LOG_ID_MAIN, DEBUG, tag, msg, tr); } /** @@ -107,7 +163,7 @@ public final class Log { * @param msg The message you would like logged. */ public static int i(String tag, String msg) { - return println(LOG_ID_MAIN, INFO, tag, msg); + return println_native(LOG_ID_MAIN, INFO, tag, msg); } /** @@ -118,7 +174,7 @@ public final class Log { * @param tr An exception to log */ public static int i(String tag, String msg, Throwable tr) { - return println(LOG_ID_MAIN, INFO, tag, msg + '\n' + getStackTraceString(tr)); + return printlns(LOG_ID_MAIN, INFO, tag, msg, tr); } /** @@ -128,7 +184,7 @@ public final class Log { * @param msg The message you would like logged. */ public static int w(String tag, String msg) { - return println(LOG_ID_MAIN, WARN, tag, msg); + return println_native(LOG_ID_MAIN, WARN, tag, msg); } /** @@ -139,9 +195,31 @@ public final class Log { * @param tr An exception to log */ public static int w(String tag, String msg, Throwable tr) { - return println(LOG_ID_MAIN, WARN, tag, msg + '\n' + getStackTraceString(tr)); + return printlns(LOG_ID_MAIN, WARN, tag, msg, tr); } + /** + * Checks to see whether or not a log for the specified tag is loggable at the specified level. + * + * The default level of any tag is set to INFO. This means that any level above and including + * INFO will be logged. Before you make any calls to a logging method you should check to see + * if your tag should be logged. You can change the default level by setting a system property: + * 'setprop log.tag.<YOUR_LOG_TAG> <LEVEL>' + * Where level is either VERBOSE, DEBUG, INFO, WARN, ERROR, ASSERT, or SUPPRESS. SUPPRESS will + * turn off all logging for your tag. You can also create a local.prop file that with the + * following in it: + * 'log.tag.<YOUR_LOG_TAG>=<LEVEL>' + * and place that in /data/local.prop. + * + * @param tag The tag to check. + * @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 + * tag limit of concern after this API level. + */ + public static native boolean isLoggable(String tag, int level); + /* * Send a {@link #WARN} log message and log the exception. * @param tag Used to identify the source of a log message. It usually identifies @@ -149,7 +227,7 @@ public final class Log { * @param tr An exception to log */ public static int w(String tag, Throwable tr) { - return println(LOG_ID_MAIN, WARN, tag, getStackTraceString(tr)); + return printlns(LOG_ID_MAIN, WARN, tag, "", tr); } /** @@ -159,7 +237,7 @@ public final class Log { * @param msg The message you would like logged. */ public static int e(String tag, String msg) { - return println(LOG_ID_MAIN, ERROR, tag, msg); + return println_native(LOG_ID_MAIN, ERROR, tag, msg); } /** @@ -170,7 +248,82 @@ public final class Log { * @param tr An exception to log */ public static int e(String tag, String msg, Throwable tr) { - return println(LOG_ID_MAIN, ERROR, tag, msg + '\n' + getStackTraceString(tr)); + return printlns(LOG_ID_MAIN, ERROR, tag, msg, tr); + } + + /** + * What a Terrible Failure: Report a condition that should never happen. + * The error will always be logged at level ASSERT with the call stack. + * Depending on system configuration, a report may be added to the + * {@link android.os.DropBoxManager} and/or the process may be terminated + * immediately with an error dialog. + * @param tag Used to identify the source of a log message. + * @param msg The message you would like logged. + */ + public static int wtf(String tag, String msg) { + return wtf(LOG_ID_MAIN, tag, msg, null, false, false); + } + + /** + * Like {@link #wtf(String, String)}, but also writes to the log the full + * call stack. + * @hide + */ + public static int wtfStack(String tag, String msg) { + return wtf(LOG_ID_MAIN, tag, msg, null, true, false); + } + + /** + * What a Terrible Failure: Report an exception that should never happen. + * Similar to {@link #wtf(String, String)}, with an exception to log. + * @param tag Used to identify the source of a log message. + * @param tr An exception to log. + */ + public static int wtf(String tag, Throwable tr) { + return wtf(LOG_ID_MAIN, tag, tr.getMessage(), tr, false, false); + } + + /** + * What a Terrible Failure: Report an exception that should never happen. + * Similar to {@link #wtf(String, Throwable)}, with a message as well. + * @param tag Used to identify the source of a log message. + * @param msg The message you would like logged. + * @param tr An exception to log. May be null. + */ + public static int wtf(String tag, String msg, Throwable tr) { + return wtf(LOG_ID_MAIN, tag, msg, tr, false, false); + } + + static int wtf(int logId, String tag, String msg, Throwable tr, boolean localStack, + boolean system) { + TerribleFailure what = new TerribleFailure(msg, tr); + // Only mark this as ERROR, do not use ASSERT since that should be + // reserved for cases where the system is guaranteed to abort. + // The onTerribleFailure call does not always cause a crash. + int bytes = printlns(logId, ERROR, tag, msg, localStack ? what : tr); + sWtfHandler.onTerribleFailure(tag, what, system); + return bytes; + } + + static void wtfQuiet(int logId, String tag, String msg, boolean system) { + TerribleFailure what = new TerribleFailure(msg, null); + sWtfHandler.onTerribleFailure(tag, what, system); + } + + /** + * Sets the terrible failure handler, for testing. + * + * @return the old handler + * + * @hide + */ + public static TerribleFailureHandler setWtfHandler(TerribleFailureHandler handler) { + if (handler == null) { + throw new NullPointerException("handler == null"); + } + TerribleFailureHandler oldHandler = sWtfHandler; + sWtfHandler = handler; + return oldHandler; } /** @@ -193,7 +346,7 @@ public final class Log { } StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); + PrintWriter pw = new FastPrintWriter(sw, false, 256); tr.printStackTrace(pw); pw.flush(); return sw.toString(); @@ -208,7 +361,7 @@ public final class Log { * @return The number of bytes written. */ public static int println(int priority, String tag, String msg) { - return println(LOG_ID_MAIN, priority, tag, msg); + return println_native(LOG_ID_MAIN, priority, tag, msg); } /** @hide */ public static final int LOG_ID_MAIN = 0; @@ -217,9 +370,115 @@ public final class Log { /** @hide */ public static final int LOG_ID_SYSTEM = 3; /** @hide */ public static final int LOG_ID_CRASH = 4; - /** @hide */ @SuppressWarnings("unused") - public static int println(int bufID, - int priority, String tag, String msg) { - return 0; + /** @hide */ public static native int println_native(int bufID, + int priority, String tag, String msg); + + /** + * Return the maximum payload the log daemon accepts without truncation. + * @return LOGGER_ENTRY_MAX_PAYLOAD. + */ + private static native int logger_entry_max_payload_native(); + + /** + * Helper function for long messages. Uses the LineBreakBufferedWriter to break + * up long messages and stacktraces along newlines, but tries to write in large + * chunks. This is to avoid truncation. + * @hide + */ + public static int printlns(int bufID, int priority, String tag, String msg, + Throwable tr) { + ImmediateLogWriter logWriter = new ImmediateLogWriter(bufID, priority, tag); + // Acceptable buffer size. Get the native buffer size, subtract two zero terminators, + // and the length of the tag. + // Note: we implicitly accept possible truncation for Modified-UTF8 differences. It + // is too expensive to compute that ahead of time. + int bufferSize = PreloadHolder.LOGGER_ENTRY_MAX_PAYLOAD // Base. + - 2 // Two terminators. + - (tag != null ? tag.length() : 0) // Tag length. + - 32; // Some slack. + // At least assume you can print *some* characters (tag is not too large). + bufferSize = Math.max(bufferSize, 100); + + LineBreakBufferedWriter lbbw = new LineBreakBufferedWriter(logWriter, bufferSize); + + lbbw.println(msg); + + if (tr != null) { + // This is to reduce the amount of log spew that apps do in the non-error + // condition of the network being unavailable. + Throwable t = tr; + while (t != null) { + if (t instanceof UnknownHostException) { + break; + } + if (t instanceof DeadSystemException) { + lbbw.println("DeadSystemException: The system died; " + + "earlier logs will point to the root cause"); + break; + } + t = t.getCause(); + } + if (t == null) { + tr.printStackTrace(lbbw); + } + } + + lbbw.flush(); + + return logWriter.getWritten(); + } + + /** + * PreloadHelper class. Caches the LOGGER_ENTRY_MAX_PAYLOAD value to avoid + * a JNI call during logging. + */ + static class PreloadHolder { + public final static int LOGGER_ENTRY_MAX_PAYLOAD = + logger_entry_max_payload_native(); + } + + /** + * Helper class to write to the logcat. Different from LogWriter, this writes + * the whole given buffer and does not break along newlines. + */ + private static class ImmediateLogWriter extends Writer { + + private int bufID; + private int priority; + private String tag; + + private int written = 0; + + /** + * Create a writer that immediately writes to the log, using the given + * parameters. + */ + public ImmediateLogWriter(int bufID, int priority, String tag) { + this.bufID = bufID; + this.priority = priority; + this.tag = tag; + } + + public int getWritten() { + return written; + } + + @Override + public void write(char[] cbuf, int off, int len) { + // Note: using String here has a bit of overhead as a Java object is created, + // but using the char[] directly is not easier, as it needs to be translated + // to a C char[] for logging. + written += println_native(bufID, priority, tag, new String(cbuf, off, len)); + } + + @Override + public void flush() { + // Ignored. + } + + @Override + public void close() { + // Ignored. + } } } diff --git a/android/util/LongArray.java b/android/util/LongArray.java index 9b0489ca..fa980966 100644 --- a/android/util/LongArray.java +++ b/android/util/LongArray.java @@ -16,11 +16,15 @@ package android.util; +import android.annotation.Nullable; + import com.android.internal.util.ArrayUtils; import com.android.internal.util.Preconditions; -import java.util.Arrays; + import libcore.util.EmptyArray; +import java.util.Arrays; + /** * Implements a growing array of long primitives. * @@ -216,4 +220,18 @@ public class LongArray implements Cloneable { throw new ArrayIndexOutOfBoundsException(mSize, index); } } + + /** + * Test if each element of {@code a} equals corresponding element from {@code b} + */ + public static boolean elementsEqual(@Nullable LongArray a, @Nullable LongArray b) { + if (a == null || b == null) return a == b; + if (a.mSize != b.mSize) return false; + for (int i = 0; i < a.mSize; i++) { + if (a.get(i) != b.get(i)) { + return false; + } + } + return true; + } } diff --git a/android/util/MapCollections.java b/android/util/MapCollections.java index 80ab23c8..a5212688 100644 --- a/android/util/MapCollections.java +++ b/android/util/MapCollections.java @@ -16,13 +16,12 @@ package android.util; -import libcore.util.Objects; - import java.lang.reflect.Array; import java.util.Collection; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Objects; import java.util.Set; /** @@ -143,8 +142,8 @@ abstract class MapCollections<K, V> { return false; } Map.Entry<?, ?> e = (Map.Entry<?, ?>) o; - return Objects.equal(e.getKey(), colGetEntry(mIndex, 0)) - && Objects.equal(e.getValue(), colGetEntry(mIndex, 1)); + return Objects.equals(e.getKey(), colGetEntry(mIndex, 0)) + && Objects.equals(e.getValue(), colGetEntry(mIndex, 1)); } @Override @@ -195,7 +194,7 @@ abstract class MapCollections<K, V> { return false; } Object foundVal = colGetEntry(index, 1); - return Objects.equal(foundVal, e.getValue()); + return Objects.equals(foundVal, e.getValue()); } @Override diff --git a/android/util/NtpTrustedTime.java b/android/util/NtpTrustedTime.java index ed2d3c60..30d7b6c0 100644 --- a/android/util/NtpTrustedTime.java +++ b/android/util/NtpTrustedTime.java @@ -20,6 +20,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources; import android.net.ConnectivityManager; +import android.net.Network; import android.net.NetworkInfo; import android.net.SntpClient; import android.os.SystemClock; @@ -80,6 +81,18 @@ public class NtpTrustedTime implements TrustedTime { @Override public boolean forceRefresh() { + // We can't do this at initialization time: ConnectivityService might not be running yet. + synchronized (this) { + if (mCM == null) { + mCM = sContext.getSystemService(ConnectivityManager.class); + } + } + + final Network network = mCM == null ? null : mCM.getActiveNetwork(); + return forceRefresh(network); + } + + public boolean forceRefresh(Network network) { if (TextUtils.isEmpty(mServer)) { // missing server, so no trusted time available return false; @@ -88,11 +101,11 @@ public class NtpTrustedTime implements TrustedTime { // We can't do this at initialization time: ConnectivityService might not be running yet. synchronized (this) { if (mCM == null) { - mCM = (ConnectivityManager) sContext.getSystemService(Context.CONNECTIVITY_SERVICE); + mCM = sContext.getSystemService(ConnectivityManager.class); } } - final NetworkInfo ni = mCM == null ? null : mCM.getActiveNetworkInfo(); + final NetworkInfo ni = mCM == null ? null : mCM.getNetworkInfo(network); if (ni == null || !ni.isConnected()) { if (LOGD) Log.d(TAG, "forceRefresh: no connectivity"); return false; @@ -101,7 +114,7 @@ public class NtpTrustedTime implements TrustedTime { if (LOGD) Log.d(TAG, "forceRefresh() from cache miss"); final SntpClient client = new SntpClient(); - if (client.requestTime(mServer, (int) mTimeout)) { + if (client.requestTime(mServer, (int) mTimeout, network)) { mHasCache = true; mCachedNtpTime = client.getNtpTime(); mCachedNtpElapsedRealtime = client.getNtpTimeReference(); diff --git a/android/util/RecurrenceRule.java b/android/util/RecurrenceRule.java index 1fe638d6..9f115eba 100644 --- a/android/util/RecurrenceRule.java +++ b/android/util/RecurrenceRule.java @@ -41,7 +41,7 @@ import java.util.Objects; */ public class RecurrenceRule implements Parcelable { private static final String TAG = "RecurrenceRule"; - private static final boolean DEBUG = true; + private static final boolean LOGD = Log.isLoggable(TAG, Log.DEBUG); private static final int VERSION_INIT = 0; @@ -99,6 +99,7 @@ public class RecurrenceRule implements Parcelable { start = convertZonedDateTime(BackupUtils.readString(in)); end = convertZonedDateTime(BackupUtils.readString(in)); period = convertPeriod(BackupUtils.readString(in)); + break; default: throw new ProtocolException("Unknown version " + version); } @@ -192,7 +193,7 @@ public class RecurrenceRule implements Parcelable { public RecurringIterator() { final ZonedDateTime anchor = (end != null) ? end : ZonedDateTime.now(sClock).withZoneSameInstant(start.getZone()); - if (DEBUG) Log.d(TAG, "Resolving using anchor " + anchor); + if (LOGD) Log.d(TAG, "Resolving using anchor " + anchor); updateCycle(); @@ -231,7 +232,7 @@ public class RecurrenceRule implements Parcelable { @Override public Pair<ZonedDateTime, ZonedDateTime> next() { - if (DEBUG) Log.d(TAG, "Cycle " + i + " from " + cycleStart + " to " + cycleEnd); + if (LOGD) Log.d(TAG, "Cycle " + i + " from " + cycleStart + " to " + cycleEnd); Pair<ZonedDateTime, ZonedDateTime> p = new Pair<>(cycleStart, cycleEnd); i--; updateCycle(); diff --git a/android/util/SparseSetArray.java b/android/util/SparseSetArray.java new file mode 100644 index 00000000..d100f12e --- /dev/null +++ b/android/util/SparseSetArray.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.util; + +/** + * A sparse array of ArraySets, which is suitable to hold userid->packages association. + * + * @hide + */ +public class SparseSetArray<T> { + private final SparseArray<ArraySet<T>> mData = new SparseArray<>(); + + public SparseSetArray() { + } + + /** + * Add a value at index n. + * @return FALSE when the value already existed at the given index, TRUE otherwise. + */ + public boolean add(int n, T value) { + ArraySet<T> set = mData.get(n); + if (set == null) { + set = new ArraySet<>(); + mData.put(n, set); + } + if (set.contains(value)) { + return true; + } + set.add(value); + return false; + } + + /** + * @return whether a value exists at index n. + */ + public boolean contains(int n, T value) { + final ArraySet<T> set = mData.get(n); + if (set == null) { + return false; + } + return set.contains(value); + } + + /** + * Remove a value from index n. + * @return TRUE when the value existed at the given index and removed, FALSE otherwise. + */ + public boolean remove(int n, T value) { + final ArraySet<T> set = mData.get(n); + if (set == null) { + return false; + } + final boolean ret = set.remove(value); + if (set.size() == 0) { + mData.remove(n); + } + return ret; + } + + /** + * Remove all values from index n. + */ + public void remove(int n) { + mData.remove(n); + } + public int size() { + return mData.size(); + } + + public int keyAt(int index) { + return mData.keyAt(index); + } + + public int sizeAt(int index) { + final ArraySet<T> set = mData.valueAt(index); + if (set == null) { + return 0; + } + return set.size(); + } + + public T valueAt(int intIndex, int valueIndex) { + return mData.valueAt(intIndex).valueAt(valueIndex); + } +} diff --git a/android/util/StatsLog.java b/android/util/StatsLog.java index 48053183..e8b41972 100644 --- a/android/util/StatsLog.java +++ b/android/util/StatsLog.java @@ -16,10 +16,11 @@ package android.util; +import android.os.Process; + /** * StatsLog provides an API for developers to send events to statsd. The events can be used to - * define custom metrics inside statsd. We will rate-limit how often the calls can be made inside - * statsd. + * define custom metrics inside statsd. */ public final class StatsLog extends StatsLogInternal { private static final String TAG = "StatsManager"; @@ -34,7 +35,8 @@ public final class StatsLog extends StatsLogInternal { */ public static boolean logStart(int label) { if (label >= 0 && label < 16) { - StatsLog.write(APP_HOOK, label, APP_HOOK__STATE__START); + StatsLog.write(APP_BREADCRUMB_REPORTED, Process.myUid(), + label, APP_BREADCRUMB_REPORTED__STATE__START); return true; } return false; @@ -48,7 +50,8 @@ public final class StatsLog extends StatsLogInternal { */ public static boolean logStop(int label) { if (label >= 0 && label < 16) { - StatsLog.write(APP_HOOK, label, APP_HOOK__STATE__STOP); + StatsLog.write(APP_BREADCRUMB_REPORTED, Process.myUid(), + label, APP_BREADCRUMB_REPORTED__STATE__STOP); return true; } return false; @@ -62,7 +65,8 @@ public final class StatsLog extends StatsLogInternal { */ public static boolean logEvent(int label) { if (label >= 0 && label < 16) { - StatsLog.write(APP_HOOK, label, APP_HOOK__STATE__UNSPECIFIED); + StatsLog.write(APP_BREADCRUMB_REPORTED, Process.myUid(), label, + APP_BREADCRUMB_REPORTED__STATE__UNSPECIFIED); return true; } return false; diff --git a/android/util/StatsManager.java b/android/util/StatsManager.java deleted file mode 100644 index 687aa837..00000000 --- a/android/util/StatsManager.java +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package android.util; - -import android.Manifest; -import android.annotation.RequiresPermission; -import android.os.IBinder; -import android.os.IStatsManager; -import android.os.RemoteException; -import android.os.ServiceManager; - - -/* - * - * - * - * - * THIS ENTIRE FILE IS ONLY TEMPORARY TO PREVENT BREAKAGES OF DEPENDENCIES ON OLD APIS. - * The new StatsManager is to be found in android.app.StatsManager. - * TODO: Delete this file! - * - * - * - * - */ - - -/** - * API for StatsD clients to send configurations and retrieve data. - * - * @hide - */ -public class StatsManager { - IStatsManager mService; - private static final String TAG = "StatsManager"; - - /** - * Constructor for StatsManagerClient. - * - * @hide - */ - public StatsManager() { - } - - /** - * Temporary to prevent build failures. Will be deleted. - */ - @RequiresPermission(Manifest.permission.DUMP) - public boolean addConfiguration(String configKey, byte[] config, String pkg, String cls) { - // To prevent breakages of dependencies on old API. - - return false; - } - - /** - * Clients can send a configuration and simultaneously registers the name of a broadcast - * receiver that listens for when it should request data. - * - * @param configKey An arbitrary integer that allows clients to track the configuration. - * @param config Wire-encoded StatsDConfig proto that specifies metrics (and all - * dependencies eg, conditions and matchers). - * @param pkg The package name to receive the broadcast. - * @param cls The name of the class that receives the broadcast. - * @return true if successful - */ - @RequiresPermission(Manifest.permission.DUMP) - public boolean addConfiguration(long configKey, byte[] config, String pkg, String cls) { - synchronized (this) { - try { - IStatsManager service = getIStatsManagerLocked(); - if (service == null) { - Slog.d(TAG, "Failed to find statsd when adding configuration"); - return false; - } - return service.addConfiguration(configKey, config, pkg, cls); - } catch (RemoteException e) { - Slog.d(TAG, "Failed to connect to statsd when adding configuration"); - return false; - } - } - } - - /** - * Temporary to prevent build failures. Will be deleted. - */ - @RequiresPermission(Manifest.permission.DUMP) - public boolean removeConfiguration(String configKey) { - // To prevent breakages of old dependencies. - return false; - } - - /** - * Remove a configuration from logging. - * - * @param configKey Configuration key to remove. - * @return true if successful - */ - @RequiresPermission(Manifest.permission.DUMP) - public boolean removeConfiguration(long configKey) { - synchronized (this) { - try { - IStatsManager service = getIStatsManagerLocked(); - if (service == null) { - Slog.d(TAG, "Failed to find statsd when removing configuration"); - return false; - } - return service.removeConfiguration(configKey); - } catch (RemoteException e) { - Slog.d(TAG, "Failed to connect to statsd when removing configuration"); - return false; - } - } - } - - /** - * Temporary to prevent build failures. Will be deleted. - */ - @RequiresPermission(Manifest.permission.DUMP) - public byte[] getData(String configKey) { - // TODO: remove this and all other methods with String-based config keys. - // To prevent build breakages of dependencies. - return null; - } - - /** - * Clients can request data with a binder call. This getter is destructive and also clears - * the retrieved metrics from statsd memory. - * - * @param configKey Configuration key to retrieve data from. - * @return Serialized ConfigMetricsReportList proto. Returns null on failure. - */ - @RequiresPermission(Manifest.permission.DUMP) - public byte[] getData(long configKey) { - synchronized (this) { - try { - IStatsManager service = getIStatsManagerLocked(); - if (service == null) { - Slog.d(TAG, "Failed to find statsd when getting data"); - return null; - } - return service.getData(configKey); - } catch (RemoteException e) { - Slog.d(TAG, "Failed to connecto statsd when getting data"); - return null; - } - } - } - - /** - * Clients can request metadata for statsd. Will contain stats across all configurations but not - * the actual metrics themselves (metrics must be collected via {@link #getData(String)}. - * This getter is not destructive and will not reset any metrics/counters. - * - * @return Serialized StatsdStatsReport proto. Returns null on failure. - */ - @RequiresPermission(Manifest.permission.DUMP) - public byte[] getMetadata() { - synchronized (this) { - try { - IStatsManager service = getIStatsManagerLocked(); - if (service == null) { - Slog.d(TAG, "Failed to find statsd when getting metadata"); - return null; - } - return service.getMetadata(); - } catch (RemoteException e) { - Slog.d(TAG, "Failed to connecto statsd when getting metadata"); - return null; - } - } - } - - private class StatsdDeathRecipient implements IBinder.DeathRecipient { - @Override - public void binderDied() { - synchronized (this) { - mService = null; - } - } - } - - private IStatsManager getIStatsManagerLocked() throws RemoteException { - if (mService != null) { - return mService; - } - mService = IStatsManager.Stub.asInterface(ServiceManager.getService("stats")); - if (mService != null) { - mService.asBinder().linkToDeath(new StatsdDeathRecipient(), 0); - } - return mService; - } -} diff --git a/android/util/XmlPullAttributes.java b/android/util/XmlPullAttributes.java index 6c8bb397..cb35eb5c 100644 --- a/android/util/XmlPullAttributes.java +++ b/android/util/XmlPullAttributes.java @@ -34,6 +34,10 @@ class XmlPullAttributes implements AttributeSet { return mParser.getAttributeCount(); } + public String getAttributeNamespace (int index) { + return mParser.getAttributeNamespace(index); + } + public String getAttributeName(int index) { return mParser.getAttributeName(index); } diff --git a/android/util/apk/ApkSignatureSchemeV2Verifier.java b/android/util/apk/ApkSignatureSchemeV2Verifier.java index 5a09dab5..533d7259 100644 --- a/android/util/apk/ApkSignatureSchemeV2Verifier.java +++ b/android/util/apk/ApkSignatureSchemeV2Verifier.java @@ -213,7 +213,9 @@ public class ApkSignatureSchemeV2Verifier { byte[] verityRootHash = null; if (contentDigests.containsKey(CONTENT_DIGEST_VERITY_CHUNKED_SHA256)) { - verityRootHash = contentDigests.get(CONTENT_DIGEST_VERITY_CHUNKED_SHA256); + byte[] verityDigest = contentDigests.get(CONTENT_DIGEST_VERITY_CHUNKED_SHA256); + verityRootHash = ApkSigningBlockUtils.parseVerityDigestAndVerifySourceLength( + verityDigest, apk.length(), signatureInfo); } return new VerifiedSigner( @@ -412,6 +414,20 @@ public class ApkSignatureSchemeV2Verifier { } } + static byte[] generateFsverityRootHash(String apkPath) + throws IOException, SignatureNotFoundException, DigestException, + NoSuchAlgorithmException { + try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) { + SignatureInfo signatureInfo = findSignature(apk); + VerifiedSigner vSigner = verify(apk, false); + if (vSigner.verityRootHash == null) { + return null; + } + return ApkVerityBuilder.generateFsverityRootHash( + apk, ByteBuffer.wrap(vSigner.verityRootHash), signatureInfo); + } + } + private static boolean isSupportedSignatureAlgorithm(int sigAlgorithm) { switch (sigAlgorithm) { case SIGNATURE_RSA_PSS_WITH_SHA256: diff --git a/android/util/apk/ApkSignatureSchemeV3Verifier.java b/android/util/apk/ApkSignatureSchemeV3Verifier.java index 1b04eb2f..758cd2b8 100644 --- a/android/util/apk/ApkSignatureSchemeV3Verifier.java +++ b/android/util/apk/ApkSignatureSchemeV3Verifier.java @@ -62,6 +62,7 @@ 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; @@ -165,7 +166,7 @@ public class ApkSignatureSchemeV3Verifier { private static VerifiedSigner verify( RandomAccessFile apk, SignatureInfo signatureInfo, - boolean doVerifyIntegrity) throws SecurityException { + boolean doVerifyIntegrity) throws SecurityException, IOException { int signerCount = 0; Map<Integer, byte[]> contentDigests = new ArrayMap<>(); VerifiedSigner result = null; @@ -214,7 +215,9 @@ public class ApkSignatureSchemeV3Verifier { } if (contentDigests.containsKey(CONTENT_DIGEST_VERITY_CHUNKED_SHA256)) { - result.verityRootHash = contentDigests.get(CONTENT_DIGEST_VERITY_CHUNKED_SHA256); + byte[] verityDigest = contentDigests.get(CONTENT_DIGEST_VERITY_CHUNKED_SHA256); + result.verityRootHash = ApkSigningBlockUtils.parseVerityDigestAndVerifySourceLength( + verityDigest, apk.length(), signatureInfo); } return result; @@ -438,8 +441,8 @@ public class ApkSignatureSchemeV3Verifier { List<Integer> flagsList = new ArrayList<>(); // Proof-of-rotation struct: - // is basically a singly linked list of nodes, called levels here, each of which have the - // following structure: + // 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 @@ -450,9 +453,14 @@ public class ApkSignatureSchemeV3Verifier { // - 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. - while (porBuf.hasRemaining()) { - levelCount++; - try { + + 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(); @@ -477,6 +485,7 @@ public class ApkSignatureSchemeV3Verifier { } } + signedData.rewind(); byte[] encodedCert = readLengthPrefixedByteArray(signedData); int signedSigAlgorithm = signedData.getInt(); if (lastCert != null && lastSigAlgorithm != signedSigAlgorithm) { @@ -488,19 +497,25 @@ public class ApkSignatureSchemeV3Verifier { 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); } + } 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); } @@ -523,6 +538,20 @@ public class ApkSignatureSchemeV3Verifier { } } + static byte[] generateFsverityRootHash(String apkPath) + throws NoSuchAlgorithmException, DigestException, IOException, + SignatureNotFoundException { + try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) { + SignatureInfo signatureInfo = findSignature(apk); + VerifiedSigner vSigner = verify(apk, false); + if (vSigner.verityRootHash == null) { + return null; + } + return ApkVerityBuilder.generateFsverityRootHash( + apk, ByteBuffer.wrap(vSigner.verityRootHash), signatureInfo); + } + } + private static boolean isSupportedSignatureAlgorithm(int sigAlgorithm) { switch (sigAlgorithm) { case SIGNATURE_RSA_PSS_WITH_SHA256: diff --git a/android/util/apk/ApkSignatureVerifier.java b/android/util/apk/ApkSignatureVerifier.java index 87943725..de9f55b0 100644 --- a/android/util/apk/ApkSignatureVerifier.java +++ b/android/util/apk/ApkSignatureVerifier.java @@ -427,6 +427,27 @@ public class ApkSignatureVerifier { } /** + * Generates the FSVerity root hash from FSVerity header, extensions and Merkle tree root hash + * in Signing Block. + * + * @return FSverity root hash + */ + public static byte[] generateFsverityRootHash(String apkPath) + throws NoSuchAlgorithmException, DigestException, IOException { + // first try v3 + try { + return ApkSignatureSchemeV3Verifier.generateFsverityRootHash(apkPath); + } catch (SignatureNotFoundException e) { + // try older version + } + try { + return ApkSignatureSchemeV2Verifier.generateFsverityRootHash(apkPath); + } catch (SignatureNotFoundException e) { + return null; + } + } + + /** * Result of a successful APK verification operation. */ public static class Result { diff --git a/android/util/apk/ApkSigningBlockUtils.java b/android/util/apk/ApkSigningBlockUtils.java index 4146f6fa..1c67434b 100644 --- a/android/util/apk/ApkSigningBlockUtils.java +++ b/android/util/apk/ApkSigningBlockUtils.java @@ -285,11 +285,46 @@ final class ApkSigningBlockUtils { return result; } + /** + * Return the verity digest only if the length of digest content looks correct. + * When verity digest is generated, the last incomplete 4k chunk is padded with 0s before + * hashing. This means two almost identical APKs with different number of 0 at the end will have + * the same verity digest. To avoid this problem, the length of the source content (excluding + * Signing Block) is appended to the verity digest, and the digest is returned only if the + * length is consistent to the current APK. + */ + static byte[] parseVerityDigestAndVerifySourceLength( + byte[] data, long fileSize, SignatureInfo signatureInfo) throws SecurityException { + // FORMAT: + // OFFSET DATA TYPE DESCRIPTION + // * @+0 bytes uint8[32] Merkle tree root hash of SHA-256 + // * @+32 bytes int64 Length of source data + int kRootHashSize = 32; + int kSourceLengthSize = 8; + + if (data.length != kRootHashSize + kSourceLengthSize) { + throw new SecurityException("Verity digest size is wrong: " + data.length); + } + ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN); + buffer.position(kRootHashSize); + long expectedSourceLength = buffer.getLong(); + + long signingBlockSize = signatureInfo.centralDirOffset + - signatureInfo.apkSigningBlockOffset; + if (expectedSourceLength != fileSize - signingBlockSize) { + throw new SecurityException("APK content size did not verify"); + } + + return Arrays.copyOfRange(data, 0, kRootHashSize); + } + private static void verifyIntegrityForVerityBasedAlgorithm( - byte[] expectedRootHash, + byte[] expectedDigest, RandomAccessFile apk, SignatureInfo signatureInfo) throws SecurityException { try { + byte[] expectedRootHash = parseVerityDigestAndVerifySourceLength(expectedDigest, + apk.length(), signatureInfo); ApkVerityBuilder.ApkVerityResult verity = ApkVerityBuilder.generateApkVerity(apk, signatureInfo, new ByteBufferFactory() { @Override @@ -373,9 +408,9 @@ final class ApkSigningBlockUtils { static final int SIGNATURE_ECDSA_WITH_SHA256 = 0x0201; static final int SIGNATURE_ECDSA_WITH_SHA512 = 0x0202; static final int SIGNATURE_DSA_WITH_SHA256 = 0x0301; - static final int SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256 = 0x0401; - static final int SIGNATURE_VERITY_ECDSA_WITH_SHA256 = 0x0403; - static final int SIGNATURE_VERITY_DSA_WITH_SHA256 = 0x0405; + static final int SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256 = 0x0421; + static final int SIGNATURE_VERITY_ECDSA_WITH_SHA256 = 0x0423; + static final int SIGNATURE_VERITY_DSA_WITH_SHA256 = 0x0425; static final int CONTENT_DIGEST_CHUNKED_SHA256 = 1; static final int CONTENT_DIGEST_CHUNKED_SHA512 = 2; @@ -754,9 +789,6 @@ final class ApkSigningBlockUtils { md.update(buffer); } } - - @Override - public void finish() {} } } diff --git a/android/util/apk/ApkVerityBuilder.java b/android/util/apk/ApkVerityBuilder.java index ba21ccb8..f15e1a1a 100644 --- a/android/util/apk/ApkVerityBuilder.java +++ b/android/util/apk/ApkVerityBuilder.java @@ -72,22 +72,31 @@ abstract class ApkVerityBuilder { signatureInfo.centralDirOffset - signatureInfo.apkSigningBlockOffset; long dataSize = apk.length() - signingBlockSize; int[] levelOffset = calculateVerityLevelOffset(dataSize); + int merkleTreeSize = levelOffset[levelOffset.length - 1]; ByteBuffer output = bufferFactory.create( - CHUNK_SIZE_BYTES + // fsverity header + extensions + padding - levelOffset[levelOffset.length - 1]); // Merkle tree size + merkleTreeSize + + CHUNK_SIZE_BYTES); // maximum size of fsverity metadata output.order(ByteOrder.LITTLE_ENDIAN); - ByteBuffer header = slice(output, 0, FSVERITY_HEADER_SIZE_BYTES); - ByteBuffer extensions = slice(output, FSVERITY_HEADER_SIZE_BYTES, CHUNK_SIZE_BYTES); - ByteBuffer tree = slice(output, CHUNK_SIZE_BYTES, output.limit()); + ByteBuffer tree = slice(output, 0, merkleTreeSize); + ByteBuffer header = slice(output, merkleTreeSize, + merkleTreeSize + FSVERITY_HEADER_SIZE_BYTES); + ByteBuffer extensions = slice(output, merkleTreeSize + FSVERITY_HEADER_SIZE_BYTES, + merkleTreeSize + CHUNK_SIZE_BYTES); byte[] apkDigestBytes = new byte[DIGEST_SIZE_BYTES]; ByteBuffer apkDigest = ByteBuffer.wrap(apkDigestBytes); apkDigest.order(ByteOrder.LITTLE_ENDIAN); + // NB: Buffer limit is set inside once finished. calculateFsveritySignatureInternal(apk, signatureInfo, tree, apkDigest, header, extensions); - output.rewind(); + // Put the reverse offset to fs-verity header at the end. + output.position(merkleTreeSize + FSVERITY_HEADER_SIZE_BYTES + extensions.limit()); + output.putInt(FSVERITY_HEADER_SIZE_BYTES + extensions.limit() + + 4); // size of this integer right before EOF + output.flip(); + return new ApkVerityResult(output, apkDigestBytes); } @@ -101,23 +110,28 @@ abstract class ApkVerityBuilder { ByteBuffer verityBlock = ByteBuffer.allocate(CHUNK_SIZE_BYTES) .order(ByteOrder.LITTLE_ENDIAN); ByteBuffer header = slice(verityBlock, 0, FSVERITY_HEADER_SIZE_BYTES); - ByteBuffer extensions = slice(verityBlock, FSVERITY_HEADER_SIZE_BYTES, CHUNK_SIZE_BYTES); + ByteBuffer extensions = slice(verityBlock, FSVERITY_HEADER_SIZE_BYTES, + CHUNK_SIZE_BYTES - FSVERITY_HEADER_SIZE_BYTES); calculateFsveritySignatureInternal(apk, signatureInfo, null, null, header, extensions); MessageDigest md = MessageDigest.getInstance(JCA_DIGEST_ALGORITHM); - md.update(DEFAULT_SALT); - md.update(verityBlock); + md.update(header); + md.update(extensions); md.update(apkDigest); return md.digest(); } + /** + * Internal method to generate various parts of FSVerity constructs, including the header, + * extensions, Merkle tree, and the tree's root hash. The output buffer is flipped to the + * generated data size and is readey for consuming. + */ private static void calculateFsveritySignatureInternal( RandomAccessFile apk, SignatureInfo signatureInfo, ByteBuffer treeOutput, ByteBuffer rootHashOutput, ByteBuffer headerOutput, ByteBuffer extensionsOutput) throws IOException, NoSuchAlgorithmException, DigestException { assertSigningBlockAlignedAndHasFullPages(signatureInfo); - long signingBlockSize = signatureInfo.centralDirOffset - signatureInfo.apkSigningBlockOffset; long dataSize = apk.length() - signingBlockSize - ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE; @@ -128,6 +142,7 @@ abstract class ApkVerityBuilder { levelOffset, treeOutput); if (rootHashOutput != null) { rootHashOutput.put(apkRootHash); + rootHashOutput.flip(); } } @@ -202,14 +217,10 @@ abstract class ApkVerityBuilder { } } - /** Finish the current digestion if any. */ - @Override - public void finish() throws DigestException { - if (mBytesDigestedSinceReset == 0) { - return; + public void assertEmptyBuffer() throws DigestException { + if (mBytesDigestedSinceReset != 0) { + throw new IllegalStateException("Buffer is not empty: " + mBytesDigestedSinceReset); } - mMd.digest(mDigestBuffer, 0, mDigestBuffer.length); - mOutput.put(mDigestBuffer); } private void fillUpLastOutputChunk() { @@ -274,9 +285,15 @@ abstract class ApkVerityBuilder { new MemoryMappedFileDataSource(apk.getFD(), offsetAfterEocdCdOffsetField, apk.length() - offsetAfterEocdCdOffsetField), MMAP_REGION_SIZE_BYTES); - digester.finish(); - // 5. Fill up the rest of buffer with 0s. + // 5. Pad 0s up to the nearest 4096-byte block before hashing. + int lastIncompleteChunkSize = (int) (apk.length() % CHUNK_SIZE_BYTES); + if (lastIncompleteChunkSize != 0) { + digester.consume(ByteBuffer.allocate(CHUNK_SIZE_BYTES - lastIncompleteChunkSize)); + } + digester.assertEmptyBuffer(); + + // 6. Fill up the rest of buffer with 0s. digester.fillUpLastOutputChunk(); } @@ -295,8 +312,7 @@ abstract class ApkVerityBuilder { DataSource source = new ByteBufferDataSource(inputBuffer); BufferedDigester digester = new BufferedDigester(salt, outputBuffer); consumeByChunk(digester, source, CHUNK_SIZE_BYTES); - digester.finish(); - + digester.assertEmptyBuffer(); digester.fillUpLastOutputChunk(); } @@ -304,18 +320,10 @@ abstract class ApkVerityBuilder { byte[] rootHash = new byte[DIGEST_SIZE_BYTES]; BufferedDigester digester = new BufferedDigester(salt, ByteBuffer.wrap(rootHash)); digester.consume(slice(output, 0, CHUNK_SIZE_BYTES)); - digester.finish(); + digester.assertEmptyBuffer(); return rootHash; } - private static void bufferPut(ByteBuffer buffer, byte value) { - // FIXME(b/72459251): buffer.put(value) does NOT work surprisingly. The position() after put - // does NOT even change. This hack workaround the problem, but the root cause remains - // unkonwn yet. This seems only happen when it goes through the apk install flow on my - // setup. - buffer.put(new byte[] { value }); - } - private static ByteBuffer generateFsverityHeader(ByteBuffer buffer, long fileSize, int depth, byte[] salt) { if (salt.length != 8) { @@ -325,25 +333,25 @@ abstract class ApkVerityBuilder { // TODO(b/30972906): update the reference when there is a better one in public. buffer.put("TrueBrew".getBytes()); // magic - bufferPut(buffer, (byte) 1); // major version - bufferPut(buffer, (byte) 0); // minor version - bufferPut(buffer, (byte) 12); // log2(block-size): log2(4096) - bufferPut(buffer, (byte) 7); // log2(leaves-per-node): log2(4096 / 32) + buffer.put((byte) 1); // major version + buffer.put((byte) 0); // minor version + buffer.put((byte) 12); // log2(block-size): log2(4096) + buffer.put((byte) 7); // log2(leaves-per-node): log2(4096 / 32) - buffer.putShort((short) 1); // meta algorithm, SHA256_MODE == 1 - buffer.putShort((short) 1); // data algorithm, SHA256_MODE == 1 + buffer.putShort((short) 1); // meta algorithm, SHA256 == 1 + buffer.putShort((short) 1); // data algorithm, SHA256 == 1 - buffer.putInt(0x1); // flags, 0x1: has extension + buffer.putInt(0); // flags buffer.putInt(0); // reserved buffer.putLong(fileSize); // original file size - bufferPut(buffer, (byte) 0); // auth block offset, disabled here - bufferPut(buffer, (byte) 2); // extension count + buffer.put((byte) 0); // auth block offset, disabled here + buffer.put((byte) 2); // extension count buffer.put(salt); // salt (8 bytes) - // skip(buffer, 22); // reserved + skip(buffer, 22); // reserved - buffer.rewind(); + buffer.flip(); return buffer; } @@ -364,12 +372,11 @@ abstract class ApkVerityBuilder { // // struct fsverity_extension_patch { // __le64 offset; - // u8 length; - // u8 reserved[7]; // u8 databytes[]; // }; final int kSizeOfFsverityExtensionHeader = 8; + final int kExtensionSizeAlignment = 8; { // struct fsverity_extension #1 @@ -387,29 +394,28 @@ abstract class ApkVerityBuilder { { // struct fsverity_extension #2 - final int kSizeOfFsverityPatchExtension = - 8 + // offset size - 1 + // size of length from offset (up to 255) - 7 + // reserved - ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE; - final int kPadding = (int) divideRoundup(kSizeOfFsverityPatchExtension % 8, 8); + final int kTotalSize = kSizeOfFsverityExtensionHeader + + 8 // offset size + + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE; - buffer.putShort((short) // total size of extension, padded to 64-bit alignment - (kSizeOfFsverityExtensionHeader + kSizeOfFsverityPatchExtension + kPadding)); + buffer.putShort((short) kTotalSize); buffer.put((byte) 1); // ID of patch extension skip(buffer, 5); // reserved // struct fsverity_extension_patch - buffer.putLong(eocdOffset); // offset - buffer.put((byte) ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE); // length - skip(buffer, 7); // reserved - buffer.putInt(Math.toIntExact(signingBlockOffset)); // databytes - - // There are extra kPadding bytes of 0s here, included in the total size field of the - // extension header. The output ByteBuffer is assumed to be initialized to 0. + buffer.putLong(eocdOffset + ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_OFFSET); // offset + buffer.putInt(Math.toIntExact(signingBlockOffset)); // databytes + + // The extension needs to be 0-padded at the end, since the length may not be multiple + // of 8. + int kPadding = kExtensionSizeAlignment - kTotalSize % kExtensionSizeAlignment; + if (kPadding == kExtensionSizeAlignment) { + kPadding = 0; + } + skip(buffer, kPadding); // padding } - buffer.rewind(); + buffer.flip(); return buffer; } diff --git a/android/util/apk/DataDigester.java b/android/util/apk/DataDigester.java index 278be803..18d1dff7 100644 --- a/android/util/apk/DataDigester.java +++ b/android/util/apk/DataDigester.java @@ -22,7 +22,4 @@ import java.security.DigestException; interface DataDigester { /** Consumes the {@link ByteBuffer}. */ void consume(ByteBuffer buffer) throws DigestException; - - /** Finishes the digestion. Must be called after the last {@link #consume(ByteBuffer)}. */ - void finish() throws DigestException; } diff --git a/android/util/apk/VerbatimX509Certificate.java b/android/util/apk/VerbatimX509Certificate.java index 9984c6d2..391c5fc3 100644 --- a/android/util/apk/VerbatimX509Certificate.java +++ b/android/util/apk/VerbatimX509Certificate.java @@ -18,6 +18,7 @@ package android.util.apk; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; +import java.util.Arrays; /** * For legacy reasons we need to return exactly the original encoded certificate bytes, instead @@ -25,6 +26,7 @@ import java.security.cert.X509Certificate; */ class VerbatimX509Certificate extends WrappedX509Certificate { private final byte[] mEncodedVerbatim; + private int mHash = -1; VerbatimX509Certificate(X509Certificate wrapped, byte[] encodedVerbatim) { super(wrapped); @@ -35,4 +37,30 @@ class VerbatimX509Certificate extends WrappedX509Certificate { public byte[] getEncoded() throws CertificateEncodingException { return mEncodedVerbatim; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof VerbatimX509Certificate)) return false; + + try { + byte[] a = this.getEncoded(); + byte[] b = ((VerbatimX509Certificate) o).getEncoded(); + return Arrays.equals(a, b); + } catch (CertificateEncodingException e) { + return false; + } + } + + @Override + public int hashCode() { + if (mHash == -1) { + try { + mHash = Arrays.hashCode(this.getEncoded()); + } catch (CertificateEncodingException e) { + mHash = 0; + } + } + return mHash; + } } |