Utils required for CrashRecovery module
Utils that are not currently exposed as API but are required for the
module.
Bug: 289203818
Test: TH
Change-Id: I7dbb8f673b54ad9487e642bb339c5a30f561bf3c
diff --git a/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java b/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java
index 5d71b7d..37b5d40 100644
--- a/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java
+++ b/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java
@@ -38,15 +38,15 @@
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 9217e70..0fcec26 100644
--- a/packages/CrashRecovery/services/java/com/android/server/RescueParty.java
+++ b/packages/CrashRecovery/services/java/com/android/server/RescueParty.java
@@ -29,7 +29,6 @@
import android.content.pm.VersionedPackage;
import android.os.Build;
import android.os.Environment;
-import android.os.FileUtils;
import android.os.PowerManager;
import android.os.RecoverySystem;
import android.os.SystemClock;
@@ -42,10 +41,11 @@
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 0000000..fa4d6af
--- /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 a6ae68f..afcf689 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 0000000..e4923bf
--- /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 948ebcca..fdb15e2 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 0000000..5cdc253
--- /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 0000000..dbbef61
--- /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 755636a..75284c7 100644
--- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
+++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java
@@ -43,13 +43,13 @@
import android.os.test.TestLooper;
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;