Add settings for KernelCpuThreadReader

Settings include:
- Number of buckets
- UID predicate for which threads to collect data for

Test: to test UID selection: `atest KernelCpuThreadReaderTest`
Test: to test settings application: `adb shell cmd settings put ...` and
manual inspection of `pull-source`
Bug: 123562450
Change-Id: I9d97dfc1c120fcf1b04dadb6ce24863afeff053c
diff --git a/apct-tests/perftests/core/src/android/os/KernelCpuThreadReaderPerfTest.java b/apct-tests/perftests/core/src/android/os/KernelCpuThreadReaderPerfTest.java
index 11c7599..9234849 100644
--- a/apct-tests/perftests/core/src/android/os/KernelCpuThreadReaderPerfTest.java
+++ b/apct-tests/perftests/core/src/android/os/KernelCpuThreadReaderPerfTest.java
@@ -39,7 +39,8 @@
     @Rule
     public final PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
 
-    private final KernelCpuThreadReader mKernelCpuThreadReader = KernelCpuThreadReader.create();
+    private final KernelCpuThreadReader mKernelCpuThreadReader =
+            KernelCpuThreadReader.create(8, uid -> 1000 <= uid && uid < 2000);
 
     @Test
     public void timeReadCurrentProcessCpuUsage() {
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 43e68e5..4dd18c9 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -14212,6 +14212,19 @@
         public static final String LOOPER_STATS = "looper_stats";
 
         /**
+         * Settings for collecting statistics on CPU usage per thread
+         *
+         * The following strings are supported as keys:
+         * <pre>
+         *     num_buckets          (int)
+         *     collected_uids       (string)
+         * </pre>
+         *
+         * @hide
+         */
+        public static final String KERNEL_CPU_THREAD_READER = "kernel_cpu_thread_reader";
+
+        /**
          * Default user id to boot into. They map to user ids, for example, 10, 11, 12.
          *
          * @hide
diff --git a/core/java/com/android/internal/os/KernelCpuThreadReader.java b/core/java/com/android/internal/os/KernelCpuThreadReader.java
index 2742b7c..b1328e8 100644
--- a/core/java/com/android/internal/os/KernelCpuThreadReader.java
+++ b/core/java/com/android/internal/os/KernelCpuThreadReader.java
@@ -37,9 +37,10 @@
  * frequency band.
  *
  * <p>Frequencies are bucketed together to reduce the amount of data created. This means that we
- * return {@link #NUM_BUCKETS} frequencies instead of the full number. Frequencies are reported as
- * the lowest frequency in that range. Frequencies are spread as evenly as possible across the
- * buckets. The buckets do not cross over the little/big frequencies reported.
+ * return less frequencies than provided by {@link ProcTimeInStateReader}. The number of
+ * frequencies is configurable by {@link #setNumBuckets}. Frequencies are reported as the lowest
+ * frequency in that range. Frequencies are spread as evenly as possible across the buckets. The
+ * buckets do not cross over the little/big frequencies reported.
  *
  * <p>N.B.: In order to bucket across little/big frequencies correctly, we assume that the {@code
  * time_in_state} file contains every little core frequency in ascending order, followed by every
@@ -97,22 +98,17 @@
             DEFAULT_PROC_PATH.resolve("self/time_in_state");
 
     /**
-     * Number of frequency buckets
-     */
-    private static final int NUM_BUCKETS = 8;
-
-    /**
-     * Default predicate for what UIDs to check for when getting processes. This filters to only
-     * select UID 1000 (the {@code system} user)
-     */
-    private static final Predicate<Integer> DEFAULT_UID_PREDICATE = uid -> uid == 1000;
-
-    /**
      * Value returned when there was an error getting an integer ID value (e.g. PID, UID)
      */
     private static final int ID_ERROR = -1;
 
     /**
+     * When checking whether to report data for a thread, we check the UID of the thread's owner
+     * against this predicate
+     */
+    private Predicate<Integer> mUidPredicate;
+
+    /**
      * Where the proc filesystem is mounted
      */
     private final Path mProcPath;
@@ -121,7 +117,7 @@
      * Frequencies read from the {@code time_in_state} file. Read from {@link
      * #mProcTimeInStateReader#getCpuFrequenciesKhz()} and cast to {@code int[]}
      */
-    private final int[] mFrequenciesKhz;
+    private int[] mFrequenciesKhz;
 
     /**
      * Used to read and parse {@code time_in_state} files
@@ -131,17 +127,10 @@
     /**
      * Used to sort frequencies and usage times into buckets
      */
-    private final FrequencyBucketCreator mFrequencyBucketCreator;
+    private FrequencyBucketCreator mFrequencyBucketCreator;
 
     private final Injector mInjector;
 
-    private KernelCpuThreadReader() throws IOException {
-        this(
-                DEFAULT_PROC_PATH,
-                DEFAULT_INITIAL_TIME_IN_STATE_PATH,
-                new Injector());
-    }
-
     /**
      * Create with a path where `proc` is mounted. Used primarily for testing
      *
@@ -151,17 +140,16 @@
      */
     @VisibleForTesting
     public KernelCpuThreadReader(
+            int numBuckets,
+            Predicate<Integer> uidPredicate,
             Path procPath,
             Path initialTimeInStatePath,
             Injector injector) throws IOException {
+        mUidPredicate = uidPredicate;
         mProcPath = procPath;
         mProcTimeInStateReader = new ProcTimeInStateReader(initialTimeInStatePath);
         mInjector = injector;
-
-        // Copy mProcTimeInState's frequencies and initialize bucketing
-        final long[] frequenciesKhz = mProcTimeInStateReader.getFrequenciesKhz();
-        mFrequencyBucketCreator = new FrequencyBucketCreator(frequenciesKhz, NUM_BUCKETS);
-        mFrequenciesKhz = mFrequencyBucketCreator.getBucketMinFrequencies(frequenciesKhz);
+        setNumBuckets(numBuckets);
     }
 
     /**
@@ -170,9 +158,14 @@
      * @return the reader, null if an exception was thrown during creation
      */
     @Nullable
-    public static KernelCpuThreadReader create() {
+    public static KernelCpuThreadReader create(int numBuckets, Predicate<Integer> uidPredicate) {
         try {
-            return new KernelCpuThreadReader();
+            return new KernelCpuThreadReader(
+                    numBuckets,
+                    uidPredicate,
+                    DEFAULT_PROC_PATH,
+                    DEFAULT_INITIAL_TIME_IN_STATE_PATH,
+                    new Injector());
         } catch (IOException e) {
             Slog.e(TAG, "Failed to initialize KernelCpuThreadReader", e);
             return null;
@@ -180,14 +173,6 @@
     }
 
     /**
-     * Get the per-thread CPU usage of all processes belonging to UIDs between {@code [1000, 2000)}
-     */
-    @Nullable
-    public ArrayList<ProcessCpuUsage> getProcessCpuUsageByUids() {
-        return getProcessCpuUsageByUids(DEFAULT_UID_PREDICATE);
-    }
-
-    /**
      * Get the per-thread CPU usage of all processes belonging to a set of UIDs
      *
      * <p>This function will crawl through all process {@code proc} directories found by the pattern
@@ -195,10 +180,11 @@
      * approximately 500ms on a Pixel 2. Therefore, this method can be computationally expensive,
      * and should not be called more than once an hour.
      *
-     * @param uidPredicate only get usage from processes owned by UIDs that match this predicate
+     * <p>Data is only collected for UIDs passing the predicate supplied in {@link
+     * #setUidPredicate}.
      */
     @Nullable
-    public ArrayList<ProcessCpuUsage> getProcessCpuUsageByUids(Predicate<Integer> uidPredicate) {
+    public ArrayList<ProcessCpuUsage> getProcessCpuUsageByUids() {
         if (DEBUG) {
             Slog.d(TAG, "Reading CPU thread usages for processes owned by UIDs");
         }
@@ -213,7 +199,7 @@
                 if (uid == ID_ERROR || processId == ID_ERROR) {
                     continue;
                 }
-                if (!uidPredicate.test(uid)) {
+                if (!mUidPredicate.test(uid)) {
                     continue;
                 }
 
@@ -247,10 +233,7 @@
      */
     @Nullable
     public ProcessCpuUsage getCurrentProcessCpuUsage() {
-        return getProcessCpuUsage(
-                mProcPath.resolve("self"),
-                mInjector.myPid(),
-                mInjector.myUid());
+        return getProcessCpuUsage(mProcPath.resolve("self"), mInjector.myPid(), mInjector.myUid());
     }
 
     /**
@@ -300,6 +283,31 @@
     }
 
     /**
+     * Set the number of frequency buckets to use
+     */
+    void setNumBuckets(int numBuckets) {
+        if (numBuckets < 1) {
+            Slog.w(TAG, "Number of buckets must be at least 1, but was " + numBuckets);
+            return;
+        }
+        // If `numBuckets` hasn't changed since the last set, do nothing
+        if (mFrequenciesKhz != null && mFrequenciesKhz.length == numBuckets) {
+            return;
+        }
+        mFrequencyBucketCreator = new FrequencyBucketCreator(
+                mProcTimeInStateReader.getFrequenciesKhz(), numBuckets);
+        mFrequenciesKhz = mFrequencyBucketCreator.getBucketMinFrequencies(
+                mProcTimeInStateReader.getFrequenciesKhz());
+    }
+
+    /**
+     * Set the UID predicate for {@link #getProcessCpuUsageByUids}
+     */
+    void setUidPredicate(Predicate<Integer> uidPredicate) {
+        mUidPredicate = uidPredicate;
+    }
+
+    /**
      * Get the CPU frequencies that correspond to the times reported in
      * {@link ThreadCpuUsage#usageTimesMillis}
      */
diff --git a/core/java/com/android/internal/os/KernelCpuThreadReaderSettingsObserver.java b/core/java/com/android/internal/os/KernelCpuThreadReaderSettingsObserver.java
new file mode 100644
index 0000000..77f6a17
--- /dev/null
+++ b/core/java/com/android/internal/os/KernelCpuThreadReaderSettingsObserver.java
@@ -0,0 +1,182 @@
+/*
+ * 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 com.android.internal.os;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.KeyValueListParser;
+import android.util.Range;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Service that handles settings for {@link KernelCpuThreadReader}
+ *
+ * <p>N.B.: The `collected_uids` setting takes a string representation of what UIDs to collect data
+ * for. A string representation is used as we will want to express UID ranges, therefore an integer
+ * array could not be used. The format of the string representation is detailed here: {@link
+ * UidPredicate#fromString}.
+ *
+ * @hide Only for use within the system server
+ */
+public class KernelCpuThreadReaderSettingsObserver extends ContentObserver {
+    private static final String TAG = "KernelCpuThreadReaderSettingsObserver";
+
+    /**
+     * The number of frequency buckets to report
+     */
+    private static final String NUM_BUCKETS_SETTINGS_KEY = "num_buckets";
+    private static final int NUM_BUCKETS_DEFAULT = 8;
+
+    /**
+     * List of UIDs to report data for
+     */
+    private static final String COLLECTED_UIDS_SETTINGS_KEY = "collected_uids";
+    private static final String COLLECTED_UIDS_DEFAULT = "1000-1000";
+
+    private final Context mContext;
+
+    @Nullable
+    private final KernelCpuThreadReader mKernelCpuThreadReader;
+
+    /**
+     * @return returns a created {@link KernelCpuThreadReader} that will be modified by any
+     * change in settings, returns null if creation failed
+     */
+    @Nullable
+    public static KernelCpuThreadReader getSettingsModifiedReader(Context context) {
+        // Create the observer
+        KernelCpuThreadReaderSettingsObserver settingsObserver =
+                new KernelCpuThreadReaderSettingsObserver(context);
+        // Register the observer to listen for setting changes
+        Uri settingsUri =
+                Settings.Global.getUriFor(Settings.Global.KERNEL_CPU_THREAD_READER);
+        context.getContentResolver().registerContentObserver(
+                settingsUri, false, settingsObserver, UserHandle.USER_SYSTEM);
+        // Return the observer's reader
+        return settingsObserver.mKernelCpuThreadReader;
+    }
+
+    private KernelCpuThreadReaderSettingsObserver(Context context) {
+        super(BackgroundThread.getHandler());
+        mContext = context;
+        mKernelCpuThreadReader = KernelCpuThreadReader.create(
+                NUM_BUCKETS_DEFAULT,
+                UidPredicate.fromString(COLLECTED_UIDS_DEFAULT));
+    }
+
+    @Override
+    public void onChange(boolean selfChange, Uri uri, int userId) {
+        updateReader();
+    }
+
+    /**
+     * Update the reader with new settings
+     */
+    private void updateReader() {
+        if (mKernelCpuThreadReader == null) {
+            return;
+        }
+
+        final KeyValueListParser parser = new KeyValueListParser(',');
+        try {
+            parser.setString(Settings.Global.getString(
+                    mContext.getContentResolver(), Settings.Global.KERNEL_CPU_THREAD_READER));
+        } catch (IllegalArgumentException e) {
+            Slog.e(TAG, "Bad settings", e);
+            return;
+        }
+
+        final UidPredicate uidPredicate;
+        try {
+            uidPredicate = UidPredicate.fromString(
+                    parser.getString(COLLECTED_UIDS_SETTINGS_KEY, COLLECTED_UIDS_DEFAULT));
+        } catch (NumberFormatException e) {
+            Slog.w(TAG, "Failed to get UID predicate", e);
+            return;
+        }
+
+        mKernelCpuThreadReader.setNumBuckets(
+                parser.getInt(NUM_BUCKETS_SETTINGS_KEY, NUM_BUCKETS_DEFAULT));
+        mKernelCpuThreadReader.setUidPredicate(uidPredicate);
+    }
+
+    /**
+     * Check whether a UID belongs to a set of UIDs
+     */
+    @VisibleForTesting
+    public static class UidPredicate implements Predicate<Integer> {
+        private static final Pattern UID_RANGE_PATTERN = Pattern.compile("([0-9]+)-([0-9]+)");
+        private static final String UID_SPECIFIER_DELIMITER = ";";
+        private final List<Range<Integer>> mAcceptedUidRanges;
+
+        /**
+         * Create a UID predicate from a string representing a list of UID ranges
+         *
+         * <p>UID ranges are a pair of integers separated by a '-'. If you want to specify a single
+         * UID (e.g. UID 1000), you can use {@code 1000-1000}. Lists of ranges are separated by
+         * a single ';'. For example, this would be a valid string representation: {@code
+         * "1000-1999;2003-2003;2004-2004;2050-2060"}.
+         *
+         * <p>We do not use ',' to delimit as it is already used in separating different setting
+         * arguments.
+         *
+         * @throws NumberFormatException    if the input string is incorrectly formatted
+         * @throws IllegalArgumentException if an UID range has a lower end than start
+         */
+        @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+        public static UidPredicate fromString(String predicateString) throws NumberFormatException {
+            final List<Range<Integer>> acceptedUidRanges = new ArrayList<>();
+            for (String uidSpecifier : predicateString.split(UID_SPECIFIER_DELIMITER)) {
+                final Matcher uidRangeMatcher = UID_RANGE_PATTERN.matcher(uidSpecifier);
+                if (!uidRangeMatcher.matches()) {
+                    throw new NumberFormatException(
+                            "Failed to recognize as number range: " + uidSpecifier);
+                }
+                acceptedUidRanges.add(Range.create(
+                        Integer.parseInt(uidRangeMatcher.group(1)),
+                        Integer.parseInt(uidRangeMatcher.group(2))));
+            }
+            return new UidPredicate(acceptedUidRanges);
+        }
+
+        private UidPredicate(List<Range<Integer>> acceptedUidRanges) {
+            mAcceptedUidRanges = acceptedUidRanges;
+        }
+
+        @Override
+        public boolean test(Integer uid) {
+            for (int i = 0; i < mAcceptedUidRanges.size(); i++) {
+                if (mAcceptedUidRanges.get(i).contains(uid)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+}
diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
index bd7f852..d670208 100644
--- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java
+++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
@@ -293,6 +293,7 @@
                     Settings.Global.INTENT_FIREWALL_UPDATE_METADATA_URL,
                     Settings.Global.JOB_SCHEDULER_CONSTANTS,
                     Settings.Global.KEEP_PROFILE_IN_BACKGROUND,
+                    Settings.Global.KERNEL_CPU_THREAD_READER,
                     Settings.Global.LANG_ID_UPDATE_CONTENT_URL,
                     Settings.Global.LANG_ID_UPDATE_METADATA_URL,
                     Settings.Global.LAST_ACTIVE_USER_ID,
diff --git a/core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderEndToEndTest.java b/core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderEndToEndTest.java
index c03d1f3..3ddd8aa 100644
--- a/core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderEndToEndTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderEndToEndTest.java
@@ -20,6 +20,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
+import android.os.Process;
 import android.os.SystemClock;
 import android.support.test.filters.LargeTest;
 import android.support.test.runner.AndroidJUnit4;
@@ -121,7 +122,8 @@
         workFinishedLatch.await();
 
         // Get thread data from KernelCpuThreadReader
-        final KernelCpuThreadReader kernelCpuThreadReader = KernelCpuThreadReader.create();
+        final KernelCpuThreadReader kernelCpuThreadReader =
+                KernelCpuThreadReader.create(8, uid -> uid == Process.myUid());
         assertNotNull(kernelCpuThreadReader);
         final ProcessCpuUsage currentProcessCpuUsage =
                 kernelCpuThreadReader.getCurrentProcessCpuUsage();
diff --git a/core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderTest.java b/core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderTest.java
index 0c56b8a..b9744f5 100644
--- a/core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderTest.java
@@ -18,8 +18,10 @@
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.testng.Assert.assertThrows;
 
 import android.content.Context;
 import android.os.FileUtils;
@@ -99,6 +101,8 @@
                 THREAD_NAMES, THREAD_CPU_FREQUENCIES, THREAD_CPU_TIMES);
 
         final KernelCpuThreadReader kernelCpuThreadReader = new KernelCpuThreadReader(
+                8,
+                uid -> 1000 <= uid && uid < 2000,
                 mProcDirectory.toPath(),
                 mProcDirectory.toPath().resolve("self/task/" + THREAD_IDS[0] + "/time_in_state"),
                 processUtils);
@@ -138,11 +142,13 @@
                     new int[][]{{uid}});
         }
         final KernelCpuThreadReader kernelCpuThreadReader = new KernelCpuThreadReader(
+                8,
+                uidPredicate,
                 mProcDirectory.toPath(),
                 mProcDirectory.toPath().resolve(uids[0] + "/task/" + uids[0] + "/time_in_state"),
                 processUtils);
         ArrayList<KernelCpuThreadReader.ProcessCpuUsage> processCpuUsageByUids =
-                kernelCpuThreadReader.getProcessCpuUsageByUids(uidPredicate);
+                kernelCpuThreadReader.getProcessCpuUsageByUids();
         processCpuUsageByUids.sort(Comparator.comparing(usage -> usage.processId));
 
         assertEquals(expectedUids.length, processCpuUsageByUids.size());
@@ -350,4 +356,106 @@
                 4, KernelCpuThreadReader.FrequencyBucketCreator.getBigFrequenciesStartIndex(
                         new long[]{1, 2, 3, 4}));
     }
+
+    @Test
+    public void testUidPredicate_singleRange() {
+        KernelCpuThreadReaderSettingsObserver.UidPredicate uidPredicate =
+                KernelCpuThreadReaderSettingsObserver.UidPredicate.fromString("1000-1999");
+        assertTrue(uidPredicate.test(1000));
+        assertTrue(uidPredicate.test(1050));
+        assertTrue(uidPredicate.test(1999));
+        assertFalse(uidPredicate.test(2000));
+        assertFalse(uidPredicate.test(0));
+        assertFalse(uidPredicate.test(10000));
+        assertFalse(uidPredicate.test(-100));
+    }
+
+    @Test
+    public void testUidPredicate_singleUid() {
+        KernelCpuThreadReaderSettingsObserver.UidPredicate uidPredicate =
+                KernelCpuThreadReaderSettingsObserver.UidPredicate.fromString("1234-1234");
+        assertTrue(uidPredicate.test(1234));
+        assertFalse(uidPredicate.test(1235));
+        assertFalse(uidPredicate.test(1232));
+        assertFalse(uidPredicate.test(0));
+        assertFalse(uidPredicate.test(-1234));
+    }
+
+    @Test
+    public void testUidPredicate_uidAndRange() {
+        KernelCpuThreadReaderSettingsObserver.UidPredicate uidPredicate =
+                KernelCpuThreadReaderSettingsObserver.UidPredicate.fromString(
+                        "1000-1000;1050-1060");
+        assertTrue(uidPredicate.test(1000));
+        assertTrue(uidPredicate.test(1050));
+        assertTrue(uidPredicate.test(1054));
+        assertTrue(uidPredicate.test(1060));
+        assertFalse(uidPredicate.test(1040));
+        assertFalse(uidPredicate.test(1001));
+        assertFalse(uidPredicate.test(0));
+        assertFalse(uidPredicate.test(-1000));
+    }
+
+    @Test
+    public void testUidPredicate_multiple() {
+        KernelCpuThreadReaderSettingsObserver.UidPredicate uidPredicate =
+                KernelCpuThreadReaderSettingsObserver.UidPredicate.fromString(
+                        "1000-1000;1050-1060;1001-1001;2000-3000");
+        assertTrue(uidPredicate.test(1000));
+        assertTrue(uidPredicate.test(1001));
+        assertTrue(uidPredicate.test(1050));
+        assertTrue(uidPredicate.test(1054));
+        assertTrue(uidPredicate.test(1060));
+        assertTrue(uidPredicate.test(1001));
+        assertTrue(uidPredicate.test(2000));
+        assertTrue(uidPredicate.test(2444));
+        assertTrue(uidPredicate.test(3000));
+        assertFalse(uidPredicate.test(0));
+        assertFalse(uidPredicate.test(1040));
+        assertFalse(uidPredicate.test(3001));
+        assertFalse(uidPredicate.test(1999));
+    }
+
+    @Test
+    public void testUidPredicate_emptyRangeString() {
+        assertThrows(
+                NumberFormatException.class,
+                () -> KernelCpuThreadReaderSettingsObserver.UidPredicate.fromString(""));
+    }
+
+    @Test
+    public void testUidPredicate_singleNumber() {
+        assertThrows(
+                NumberFormatException.class,
+                () -> KernelCpuThreadReaderSettingsObserver.UidPredicate.fromString("1000"));
+    }
+
+    @Test
+    public void testUidPredicate_lettersInRange() {
+        assertThrows(
+                NumberFormatException.class,
+                () -> KernelCpuThreadReaderSettingsObserver.UidPredicate.fromString(
+                        "0-0;1-1;a;3-3"));
+    }
+
+    @Test
+    public void testUidPredicate_onlyLetters() {
+        assertThrows(
+                NumberFormatException.class,
+                () -> KernelCpuThreadReaderSettingsObserver.UidPredicate.fromString("abc"));
+    }
+
+    @Test
+    public void testUidPredicate_backwardsRange() {
+        assertThrows(
+                IllegalArgumentException.class,
+                () -> KernelCpuThreadReaderSettingsObserver.UidPredicate.fromString("20-10"));
+    }
+
+    @Test
+    public void testUidPredicate_comma() {
+        assertThrows(
+                NumberFormatException.class,
+                () -> KernelCpuThreadReaderSettingsObserver.UidPredicate.fromString("1-1,2-2,3-3"));
+    }
 }
diff --git a/services/core/java/com/android/server/stats/StatsCompanionService.java b/services/core/java/com/android/server/stats/StatsCompanionService.java
index c6d2870..f2d50f0 100644
--- a/services/core/java/com/android/server/stats/StatsCompanionService.java
+++ b/services/core/java/com/android/server/stats/StatsCompanionService.java
@@ -96,6 +96,7 @@
 import com.android.internal.os.BinderCallsStats.ExportedCallStat;
 import com.android.internal.os.KernelCpuSpeedReader;
 import com.android.internal.os.KernelCpuThreadReader;
+import com.android.internal.os.KernelCpuThreadReaderSettingsObserver;
 import com.android.internal.os.KernelCpuUidTimeReader.KernelCpuUidActiveTimeReader;
 import com.android.internal.os.KernelCpuUidTimeReader.KernelCpuUidClusterTimeReader;
 import com.android.internal.os.KernelCpuUidTimeReader.KernelCpuUidFreqTimeReader;
@@ -200,7 +201,7 @@
             "zygote64",
     };
 
-    private static final int CPU_TIME_PER_THREAD_FREQ_NUM_FREQUENCIES = 8;
+    private static final int CPU_TIME_PER_THREAD_FREQ_MAX_NUM_FREQUENCIES = 8;
 
     static final class CompanionHandler extends Handler {
         CompanionHandler(Looper looper) {
@@ -329,7 +330,8 @@
         handlerThread.start();
         mHandler = new CompanionHandler(handlerThread.getLooper());
 
-        mKernelCpuThreadReader = KernelCpuThreadReader.create();
+        mKernelCpuThreadReader =
+                KernelCpuThreadReaderSettingsObserver.getSettingsModifiedReader(mContext);
     }
 
     @Override
@@ -1675,8 +1677,8 @@
             return;
         }
         int[] cpuFrequencies = mKernelCpuThreadReader.getCpuFrequenciesKhz();
-        if (cpuFrequencies.length != CPU_TIME_PER_THREAD_FREQ_NUM_FREQUENCIES) {
-            Slog.w(TAG, "Expected " + CPU_TIME_PER_THREAD_FREQ_NUM_FREQUENCIES
+        if (cpuFrequencies.length > CPU_TIME_PER_THREAD_FREQ_MAX_NUM_FREQUENCIES) {
+            Slog.w(TAG, "Expected maximum " + CPU_TIME_PER_THREAD_FREQ_MAX_NUM_FREQUENCIES
                     + " frequencies, but got " + cpuFrequencies.length);
             return;
         }
@@ -1700,9 +1702,17 @@
                 e.writeInt(threadCpuUsage.threadId);
                 e.writeString(processCpuUsage.processName);
                 e.writeString(threadCpuUsage.threadName);
-                for (int k = 0; k < CPU_TIME_PER_THREAD_FREQ_NUM_FREQUENCIES; k++) {
-                    e.writeInt(cpuFrequencies[k]);
-                    e.writeInt(threadCpuUsage.usageTimesMillis[k]);
+                for (int k = 0; k < CPU_TIME_PER_THREAD_FREQ_MAX_NUM_FREQUENCIES; k++) {
+                    if (k < cpuFrequencies.length) {
+                        e.writeInt(cpuFrequencies[k]);
+                        e.writeInt(threadCpuUsage.usageTimesMillis[k]);
+                    } else {
+                        // If we have no more frequencies to write, we still must write empty data.
+                        // We know that this data is empty (and not just zero) because all
+                        // frequencies are expected to be greater than zero
+                        e.writeInt(0);
+                        e.writeInt(0);
+                    }
                 }
                 pulledData.add(e);
             }