content: /*
   * 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 = "0-0;1000-1000";
  
      /** Minimum total CPU usage to report */
      private static final String MINIMUM_TOTAL_CPU_USAGE_MILLIS_SETTINGS_KEY =
              "minimum_total_cpu_usage_millis";
  
      private static final int MINIMUM_TOTAL_CPU_USAGE_MILLIS_DEFAULT = 10000;
  
      private final Context mContext;
  
      @Nullable private final KernelCpuThreadReader mKernelCpuThreadReader;
  
      @Nullable private final KernelCpuThreadReaderDiff mKernelCpuThreadReaderDiff;
  
      /**
       * @return returns a created {@link KernelCpuThreadReader} that will be modified by any change
       *     in settings, returns null if creation failed
       */
      @Nullable
      public static KernelCpuThreadReaderDiff 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.mKernelCpuThreadReaderDiff;
      }
  
      private KernelCpuThreadReaderSettingsObserver(Context context) {
          super(BackgroundThread.getHandler());
          mContext = context;
          mKernelCpuThreadReader =
                  KernelCpuThreadReader.create(
                          NUM_BUCKETS_DEFAULT, UidPredicate.fromString(COLLECTED_UIDS_DEFAULT));
          mKernelCpuThreadReaderDiff =
                  new KernelCpuThreadReaderDiff(
                          mKernelCpuThreadReader, MINIMUM_TOTAL_CPU_USAGE_MILLIS_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);
          mKernelCpuThreadReaderDiff.setMinimumTotalCpuUsageMillis(
                  parser.getInt(
                          MINIMUM_TOTAL_CPU_USAGE_MILLIS_SETTINGS_KEY,
                          MINIMUM_TOTAL_CPU_USAGE_MILLIS_DEFAULT));
      }
  
      /** 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
          @SuppressWarnings("ForLoopReplaceableByForEach")
          public boolean test(Integer uid) {
              for (int i = 0; i < mAcceptedUidRanges.size(); i++) {
                  if (mAcceptedUidRanges.get(i).contains(uid)) {
                      return true;
                  }
              }
              return false;
          }
      }
  }
  