From 15ba5a9097facd16d778dc1c98416d38953872d3 Mon Sep 17 00:00:00 2001 From: Harshit Mahajan <harshitmahajan@google.com> Date: Fri, 5 Apr 2024 20:42:17 +0000 Subject: [PATCH] Revert^2 "Utils required for CrashRecovery module" 940582654178a9280d1c2ff68a6bcdbfc84d3321 Change-Id: I847a3f5bd85879f02ddaa1d6043762121651ca31 Bug: 333155088 --- .../com/android/server/PackageWatchdog.java | 6 +- .../java/com/android/server/RescueParty.java | 4 +- .../java/com/android/utils/ArrayUtils.java | 115 +++++++++++ .../{util => utils}/BackgroundThread.java | 2 +- .../java/com/android/utils/FileUtils.java | 128 ++++++++++++ .../{util => utils}/HandlerExecutor.java | 2 +- .../com/android/utils/LongArrayQueue.java | 188 ++++++++++++++++++ .../java/com/android/utils/XmlUtils.java | 118 +++++++++++ .../android/server/PackageWatchdogTest.java | 4 +- 9 files changed, 558 insertions(+), 9 deletions(-) create mode 100644 packages/CrashRecovery/services/java/com/android/utils/ArrayUtils.java rename packages/CrashRecovery/services/java/com/android/{util => utils}/BackgroundThread.java (99%) create mode 100644 packages/CrashRecovery/services/java/com/android/utils/FileUtils.java rename packages/CrashRecovery/services/java/com/android/{util => utils}/HandlerExecutor.java (98%) create mode 100644 packages/CrashRecovery/services/java/com/android/utils/LongArrayQueue.java create mode 100644 packages/CrashRecovery/services/java/com/android/utils/XmlUtils.java diff --git a/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java b/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java index 8891b50352d1..75a8bdfe5416 100644 --- a/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java +++ b/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java @@ -39,15 +39,15 @@ import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.AtomicFile; -import android.util.BackgroundThread; -import android.util.LongArrayQueue; import android.util.Slog; import android.util.Xml; +import android.utils.BackgroundThread; +import android.utils.LongArrayQueue; +import android.utils.XmlUtils; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.IndentingPrintWriter; -import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; diff --git a/packages/CrashRecovery/services/java/com/android/server/RescueParty.java b/packages/CrashRecovery/services/java/com/android/server/RescueParty.java index 271d552fc574..f86eb61c365f 100644 --- a/packages/CrashRecovery/services/java/com/android/server/RescueParty.java +++ b/packages/CrashRecovery/services/java/com/android/server/RescueParty.java @@ -31,7 +31,6 @@ import android.content.pm.VersionedPackage; import android.crashrecovery.flags.Flags; import android.os.Build; import android.os.Environment; -import android.os.FileUtils; import android.os.PowerManager; import android.os.RecoverySystem; import android.os.SystemClock; @@ -44,10 +43,11 @@ import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; import android.util.Slog; +import android.utils.ArrayUtils; +import android.utils.FileUtils; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.util.ArrayUtils; import com.android.server.PackageWatchdog.FailureReasons; import com.android.server.PackageWatchdog.PackageHealthObserver; import com.android.server.PackageWatchdog.PackageHealthObserverImpact; diff --git a/packages/CrashRecovery/services/java/com/android/utils/ArrayUtils.java b/packages/CrashRecovery/services/java/com/android/utils/ArrayUtils.java new file mode 100644 index 000000000000..fa4d6afc03d3 --- /dev/null +++ b/packages/CrashRecovery/services/java/com/android/utils/ArrayUtils.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2024 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.utils; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import java.io.File; +import java.util.List; +import java.util.Objects; + +/** + * Copied over from frameworks/base/core/java/com/android/internal/util/ArrayUtils.java + * + * @hide + */ +public class ArrayUtils { + private ArrayUtils() { /* cannot be instantiated */ } + public static final File[] EMPTY_FILE = new File[0]; + + + /** + * Return first index of {@code value} in {@code array}, or {@code -1} if + * not found. + */ + public static <T> int indexOf(@Nullable T[] array, T value) { + if (array == null) return -1; + for (int i = 0; i < array.length; i++) { + if (Objects.equals(array[i], value)) return i; + } + return -1; + } + + /** @hide */ + public static @NonNull File[] defeatNullable(@Nullable File[] val) { + return (val != null) ? val : EMPTY_FILE; + } + + /** + * Checks if given array is null or has zero elements. + */ + public static boolean isEmpty(@Nullable int[] array) { + return array == null || array.length == 0; + } + + /** + * True if the byte array is null or has length 0. + */ + public static boolean isEmpty(@Nullable byte[] array) { + return array == null || array.length == 0; + } + + /** + * Converts from List of bytes to byte array + * @param list + * @return byte[] + */ + public static byte[] toPrimitive(List<byte[]> list) { + if (list.size() == 0) { + return new byte[0]; + } + int byteLen = list.get(0).length; + byte[] array = new byte[list.size() * byteLen]; + for (int i = 0; i < list.size(); i++) { + for (int j = 0; j < list.get(i).length; j++) { + array[i * byteLen + j] = list.get(i)[j]; + } + } + return array; + } + + /** + * Adds value to given array if not already present, providing set-like + * behavior. + */ + public static @NonNull int[] appendInt(@Nullable int[] cur, int val) { + return appendInt(cur, val, false); + } + + /** + * Adds value to given array. + */ + public static @NonNull int[] appendInt(@Nullable int[] cur, int val, + boolean allowDuplicates) { + if (cur == null) { + return new int[] { val }; + } + final int n = cur.length; + if (!allowDuplicates) { + for (int i = 0; i < n; i++) { + if (cur[i] == val) { + return cur; + } + } + } + int[] ret = new int[n + 1]; + System.arraycopy(cur, 0, ret, 0, n); + ret[n] = val; + return ret; + } +} diff --git a/packages/CrashRecovery/services/java/com/android/util/BackgroundThread.java b/packages/CrashRecovery/services/java/com/android/utils/BackgroundThread.java similarity index 99% rename from packages/CrashRecovery/services/java/com/android/util/BackgroundThread.java rename to packages/CrashRecovery/services/java/com/android/utils/BackgroundThread.java index a6ae68f62f10..afcf6895fd0d 100644 --- a/packages/CrashRecovery/services/java/com/android/util/BackgroundThread.java +++ b/packages/CrashRecovery/services/java/com/android/utils/BackgroundThread.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package android.util; +package android.utils; import android.annotation.NonNull; import android.os.Handler; diff --git a/packages/CrashRecovery/services/java/com/android/utils/FileUtils.java b/packages/CrashRecovery/services/java/com/android/utils/FileUtils.java new file mode 100644 index 000000000000..e4923bfc4ecb --- /dev/null +++ b/packages/CrashRecovery/services/java/com/android/utils/FileUtils.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2024 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.utils; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Bits and pieces copied from hidden API of android.os.FileUtils. + * + * @hide + */ +public class FileUtils { + /** + * Read a text file into a String, optionally limiting the length. + * + * @param file to read (will not seek, so things like /proc files are OK) + * @param max length (positive for head, negative of tail, 0 for no limit) + * @param ellipsis to add of the file was truncated (can be null) + * @return the contents of the file, possibly truncated + * @throws IOException if something goes wrong reading the file + * @hide + */ + public static @Nullable String readTextFile(@Nullable File file, @Nullable int max, + @Nullable String ellipsis) throws IOException { + InputStream input = new FileInputStream(file); + // wrapping a BufferedInputStream around it because when reading /proc with unbuffered + // input stream, bytes read not equal to buffer size is not necessarily the correct + // indication for EOF; but it is true for BufferedInputStream due to its implementation. + BufferedInputStream bis = new BufferedInputStream(input); + try { + long size = file.length(); + if (max > 0 || (size > 0 && max == 0)) { // "head" mode: read the first N bytes + if (size > 0 && (max == 0 || size < max)) max = (int) size; + byte[] data = new byte[max + 1]; + int length = bis.read(data); + if (length <= 0) return ""; + if (length <= max) return new String(data, 0, length); + if (ellipsis == null) return new String(data, 0, max); + return new String(data, 0, max) + ellipsis; + } else if (max < 0) { // "tail" mode: keep the last N + int len; + boolean rolled = false; + byte[] last = null; + byte[] data = null; + do { + if (last != null) rolled = true; + byte[] tmp = last; + last = data; + data = tmp; + if (data == null) data = new byte[-max]; + len = bis.read(data); + } while (len == data.length); + + if (last == null && len <= 0) return ""; + if (last == null) return new String(data, 0, len); + if (len > 0) { + rolled = true; + System.arraycopy(last, len, last, 0, last.length - len); + System.arraycopy(data, 0, last, last.length - len, len); + } + if (ellipsis == null || !rolled) return new String(last); + return ellipsis + new String(last); + } else { // "cat" mode: size unknown, read it all in streaming fashion + ByteArrayOutputStream contents = new ByteArrayOutputStream(); + int len; + byte[] data = new byte[1024]; + do { + len = bis.read(data); + if (len > 0) contents.write(data, 0, len); + } while (len == data.length); + return contents.toString(); + } + } finally { + bis.close(); + input.close(); + } + } + + /** + * Perform an fsync on the given FileOutputStream. The stream at this + * point must be flushed but not yet closed. + * + * @hide + */ + public static boolean sync(FileOutputStream stream) { + try { + if (stream != null) { + stream.getFD().sync(); + } + return true; + } catch (IOException e) { + } + return false; + } + + /** + * List the files in the directory or return empty file. + * + * @hide + */ + public static @NonNull File[] listFilesOrEmpty(@Nullable File dir) { + return (dir != null) ? ArrayUtils.defeatNullable(dir.listFiles()) + : ArrayUtils.EMPTY_FILE; + } +} diff --git a/packages/CrashRecovery/services/java/com/android/util/HandlerExecutor.java b/packages/CrashRecovery/services/java/com/android/utils/HandlerExecutor.java similarity index 98% rename from packages/CrashRecovery/services/java/com/android/util/HandlerExecutor.java rename to packages/CrashRecovery/services/java/com/android/utils/HandlerExecutor.java index 948ebcca0263..fdb15e2333d5 100644 --- a/packages/CrashRecovery/services/java/com/android/util/HandlerExecutor.java +++ b/packages/CrashRecovery/services/java/com/android/utils/HandlerExecutor.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package android.util; +package android.utils; import android.annotation.NonNull; import android.os.Handler; diff --git a/packages/CrashRecovery/services/java/com/android/utils/LongArrayQueue.java b/packages/CrashRecovery/services/java/com/android/utils/LongArrayQueue.java new file mode 100644 index 000000000000..5cdc2536129a --- /dev/null +++ b/packages/CrashRecovery/services/java/com/android/utils/LongArrayQueue.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2024 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.utils; + +import libcore.util.EmptyArray; + +import java.util.NoSuchElementException; + +/** + * Copied from frameworks/base/core/java/android/util/LongArrayQueue.java + * + * @hide + */ +public class LongArrayQueue { + + private long[] mValues; + private int mSize; + private int mHead; + private int mTail; + + private long[] newUnpaddedLongArray(int num) { + return new long[num]; + } + /** + * Initializes a queue with the given starting capacity. + * + * @param initialCapacity the capacity. + */ + public LongArrayQueue(int initialCapacity) { + if (initialCapacity == 0) { + mValues = EmptyArray.LONG; + } else { + mValues = newUnpaddedLongArray(initialCapacity); + } + mSize = 0; + mHead = mTail = 0; + } + + /** + * Initializes a queue with default starting capacity. + */ + public LongArrayQueue() { + this(16); + } + + /** @hide */ + public static int growSize(int currentSize) { + return currentSize <= 4 ? 8 : currentSize * 2; + } + + private void grow() { + if (mSize < mValues.length) { + throw new IllegalStateException("Queue not full yet!"); + } + final int newSize = growSize(mSize); + final long[] newArray = newUnpaddedLongArray(newSize); + final int r = mValues.length - mHead; // Number of elements on and to the right of head. + System.arraycopy(mValues, mHead, newArray, 0, r); + System.arraycopy(mValues, 0, newArray, r, mHead); + mValues = newArray; + mHead = 0; + mTail = mSize; + } + + /** + * Returns the number of elements in the queue. + */ + public int size() { + return mSize; + } + + /** + * Removes all elements from this queue. + */ + public void clear() { + mSize = 0; + mHead = mTail = 0; + } + + /** + * Adds a value to the tail of the queue. + * + * @param value the value to be added. + */ + public void addLast(long value) { + if (mSize == mValues.length) { + grow(); + } + mValues[mTail] = value; + mTail = (mTail + 1) % mValues.length; + mSize++; + } + + /** + * Removes an element from the head of the queue. + * + * @return the element at the head of the queue. + * @throws NoSuchElementException if the queue is empty. + */ + public long removeFirst() { + if (mSize == 0) { + throw new NoSuchElementException("Queue is empty!"); + } + final long ret = mValues[mHead]; + mHead = (mHead + 1) % mValues.length; + mSize--; + return ret; + } + + /** + * Returns the element at the given position from the head of the queue, where 0 represents the + * head of the queue. + * + * @param position the position from the head of the queue. + * @return the element found at the given position. + * @throws IndexOutOfBoundsException if {@code position} < {@code 0} or + * {@code position} >= {@link #size()} + */ + public long get(int position) { + if (position < 0 || position >= mSize) { + throw new IndexOutOfBoundsException("Index " + position + + " not valid for a queue of size " + mSize); + } + final int index = (mHead + position) % mValues.length; + return mValues[index]; + } + + /** + * Returns the element at the head of the queue, without removing it. + * + * @return the element at the head of the queue. + * @throws NoSuchElementException if the queue is empty + */ + public long peekFirst() { + if (mSize == 0) { + throw new NoSuchElementException("Queue is empty!"); + } + return mValues[mHead]; + } + + /** + * Returns the element at the tail of the queue. + * + * @return the element at the tail of the queue. + * @throws NoSuchElementException if the queue is empty. + */ + public long peekLast() { + if (mSize == 0) { + throw new NoSuchElementException("Queue is empty!"); + } + final int index = (mTail == 0) ? mValues.length - 1 : mTail - 1; + return mValues[index]; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + if (mSize <= 0) { + return "{}"; + } + + final StringBuilder buffer = new StringBuilder(mSize * 64); + buffer.append('{'); + buffer.append(get(0)); + for (int i = 1; i < mSize; i++) { + buffer.append(", "); + buffer.append(get(i)); + } + buffer.append('}'); + return buffer.toString(); + } +} diff --git a/packages/CrashRecovery/services/java/com/android/utils/XmlUtils.java b/packages/CrashRecovery/services/java/com/android/utils/XmlUtils.java new file mode 100644 index 000000000000..dbbef61f6777 --- /dev/null +++ b/packages/CrashRecovery/services/java/com/android/utils/XmlUtils.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2024 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.utils; + +import android.annotation.NonNull; +import android.system.ErrnoException; +import android.system.Os; + +import com.android.modules.utils.TypedXmlPullParser; + +import libcore.util.XmlObjectFactory; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Copied over partly from frameworks/base/core/java/com/android/internal/util/XmlUtils.java + * + * @hide + */ +public class XmlUtils { + + private static final String STRING_ARRAY_SEPARATOR = ":"; + + /** @hide */ + public static final void beginDocument(XmlPullParser parser, String firstElementName) + throws XmlPullParserException, IOException { + int type; + while ((type = parser.next()) != parser.START_TAG + && type != parser.END_DOCUMENT) { + // Do nothing + } + + if (type != parser.START_TAG) { + throw new XmlPullParserException("No start tag found"); + } + + if (!parser.getName().equals(firstElementName)) { + throw new XmlPullParserException("Unexpected start tag: found " + parser.getName() + + ", expected " + firstElementName); + } + } + + /** @hide */ + public static boolean nextElementWithin(XmlPullParser parser, int outerDepth) + throws IOException, XmlPullParserException { + for (;;) { + int type = parser.next(); + if (type == XmlPullParser.END_DOCUMENT + || (type == XmlPullParser.END_TAG && parser.getDepth() == outerDepth)) { + return false; + } + if (type == XmlPullParser.START_TAG + && parser.getDepth() == outerDepth + 1) { + return true; + } + } + } + + private static XmlPullParser newPullParser() { + try { + XmlPullParser parser = XmlObjectFactory.newXmlPullParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_DOCDECL, true); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); + return parser; + } catch (XmlPullParserException e) { + throw new AssertionError(); + } + } + + /** @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; + xml = (TypedXmlPullParser) newPullParser(); + try { + xml.setInput(in, "UTF_8"); + } catch (XmlPullParserException e) { + throw new IOException(e); + } + return xml; + } +} diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java index 093923f3ed53..1fdf97a4c821 100644 --- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java +++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java @@ -45,13 +45,13 @@ import android.os.test.TestLooper; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.DeviceConfig; import android.util.AtomicFile; -import android.util.LongArrayQueue; import android.util.Xml; +import android.utils.LongArrayQueue; +import android.utils.XmlUtils; import androidx.test.InstrumentationRegistry; import com.android.dx.mockito.inline.extended.ExtendedMockito; -import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import com.android.server.PackageWatchdog.HealthCheckState; -- GitLab