content: /*
   * Copyright (C) 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 com.android.server;
  
  import android.annotation.NonNull;
  import android.app.ActivityManager;
  import android.app.ActivityManagerInternal;
  import android.app.AppOpsManager;
  import android.app.AppOpsManager.PackageOps;
  import android.app.IActivityManager;
  import android.app.IUidObserver;
  import android.app.usage.UsageStatsManager;
  import android.app.usage.UsageStatsManagerInternal;
  import android.app.usage.UsageStatsManagerInternal.AppIdleStateChangeListener;
  import android.content.BroadcastReceiver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.database.ContentObserver;
  import android.net.Uri;
  import android.os.BatteryManager;
  import android.os.Handler;
  import android.os.Looper;
  import android.os.Message;
  import android.os.PowerManager.ServiceType;
  import android.os.PowerManagerInternal;
  import android.os.RemoteException;
  import android.os.ServiceManager;
  import android.os.UserHandle;
  import android.provider.Settings;
  import android.util.ArraySet;
  import android.util.Pair;
  import android.util.Slog;
  import android.util.SparseBooleanArray;
  import android.util.SparseSetArray;
  import android.util.proto.ProtoOutputStream;
  
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.app.IAppOpsCallback;
  import com.android.internal.app.IAppOpsService;
  import com.android.internal.util.ArrayUtils;
  import com.android.internal.util.IndentingPrintWriter;
  import com.android.internal.util.Preconditions;
  import com.android.internal.util.StatLogger;
  import com.android.server.ForceAppStandbyTrackerProto.ExemptedPackage;
  import com.android.server.ForceAppStandbyTrackerProto.RunAnyInBackgroundRestrictedPackages;
  
  import java.io.PrintWriter;
  import java.util.Arrays;
  import java.util.List;
  
  /**
   * Class to keep track of the information related to "force app standby", which includes:
   * - OP_RUN_ANY_IN_BACKGROUND for each package
   * - UID foreground/active state
   * - User+system power save whitelist
   * - Temporary power save whitelist
   * - Global "force all apps standby" mode enforced by battery saver.
   *
   * Test:
    atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/AppStateTrackerTest.java
   */
  public class AppStateTracker {
      private static final String TAG = "AppStateTracker";
      private static final boolean DEBUG = false;
  
      private final Object mLock = new Object();
      private final Context mContext;
  
      @VisibleForTesting
      static final int TARGET_OP = AppOpsManager.OP_RUN_ANY_IN_BACKGROUND;
  
      IActivityManager mIActivityManager;
      ActivityManagerInternal mActivityManagerInternal;
      AppOpsManager mAppOpsManager;
      IAppOpsService mAppOpsService;
      PowerManagerInternal mPowerManagerInternal;
      StandbyTracker mStandbyTracker;
      UsageStatsManagerInternal mUsageStatsManagerInternal;
  
      private final MyHandler mHandler;
  
      @VisibleForTesting
      FeatureFlagsObserver mFlagsObserver;
  
      /**
       * Pair of (uid (not user-id), packageName) with OP_RUN_ANY_IN_BACKGROUND *not* allowed.
       */
      @GuardedBy("mLock")
      final ArraySet<Pair<Integer, String>> mRunAnyRestrictedPackages = new ArraySet<>();
  
      /** UIDs that are active. */
      @GuardedBy("mLock")
      final SparseBooleanArray mActiveUids = new SparseBooleanArray();
  
      /** UIDs that are in the foreground. */
      @GuardedBy("mLock")
      final SparseBooleanArray mForegroundUids = new SparseBooleanArray();
  
      /**
       * System except-idle + user whitelist in the device idle controller.
       */
      @GuardedBy("mLock")
      private int[] mPowerWhitelistedAllAppIds = new int[0];
  
      /**
       * User whitelisted apps in the device idle controller.
       */
      @GuardedBy("mLock")
      private int[] mPowerWhitelistedUserAppIds = new int[0];
  
      @GuardedBy("mLock")
      private int[] mTempWhitelistedAppIds = mPowerWhitelistedAllAppIds;
  
      /**
       * Per-user packages that are in the EXEMPT bucket.
       */
      @GuardedBy("mLock")
      private final SparseSetArray<String> mExemptedPackages = new SparseSetArray<>();
  
      @GuardedBy("mLock")
      final ArraySet<Listener> mListeners = new ArraySet<>();
  
      @GuardedBy("mLock")
      boolean mStarted;
  
      /**
       * Only used for small battery use-case.
       */
      @GuardedBy("mLock")
      boolean mIsPluggedIn;
  
      @GuardedBy("mLock")
      boolean mBatterySaverEnabled;
  
      /**
       * True if the forced app standby is currently enabled
       */
      @GuardedBy("mLock")
      boolean mForceAllAppsStandby;
  
      /**
       * True if the forced app standby for small battery devices feature is enabled in settings
       */
      @GuardedBy("mLock")
      boolean mForceAllAppStandbyForSmallBattery;
  
      /**
       * True if the forced app standby feature is enabled in settings
       */
      @GuardedBy("mLock")
      boolean mForcedAppStandbyEnabled;
  
      interface Stats {
          int UID_FG_STATE_CHANGED = 0;
          int UID_ACTIVE_STATE_CHANGED = 1;
          int RUN_ANY_CHANGED = 2;
          int ALL_UNWHITELISTED = 3;
          int ALL_WHITELIST_CHANGED = 4;
          int TEMP_WHITELIST_CHANGED = 5;
          int EXEMPT_CHANGED = 6;
          int FORCE_ALL_CHANGED = 7;
          int FORCE_APP_STANDBY_FEATURE_FLAG_CHANGED = 8;
  
          int IS_UID_ACTIVE_CACHED = 9;
          int IS_UID_ACTIVE_RAW = 10;
      }
  
      private final StatLogger mStatLogger = new StatLogger(new String[] {
              "UID_FG_STATE_CHANGED",
              "UID_ACTIVE_STATE_CHANGED",
              "RUN_ANY_CHANGED",
              "ALL_UNWHITELISTED",
              "ALL_WHITELIST_CHANGED",
              "TEMP_WHITELIST_CHANGED",
              "EXEMPT_CHANGED",
              "FORCE_ALL_CHANGED",
              "FORCE_APP_STANDBY_FEATURE_FLAG_CHANGED",
  
              "IS_UID_ACTIVE_CACHED",
              "IS_UID_ACTIVE_RAW",
      });
  
      @VisibleForTesting
      class FeatureFlagsObserver extends ContentObserver {
          FeatureFlagsObserver() {
              super(null);
          }
  
          void register() {
              mContext.getContentResolver().registerContentObserver(
                      Settings.Global.getUriFor(Settings.Global.FORCED_APP_STANDBY_ENABLED),
                      false, this);
  
              mContext.getContentResolver().registerContentObserver(Settings.Global.getUriFor(
                      Settings.Global.FORCED_APP_STANDBY_FOR_SMALL_BATTERY_ENABLED), false, this);
          }
  
          boolean isForcedAppStandbyEnabled() {
              return injectGetGlobalSettingInt(Settings.Global.FORCED_APP_STANDBY_ENABLED, 1) == 1;
          }
  
          boolean isForcedAppStandbyForSmallBatteryEnabled() {
              return injectGetGlobalSettingInt(
                      Settings.Global.FORCED_APP_STANDBY_FOR_SMALL_BATTERY_ENABLED, 0) == 1;
          }
  
          @Override
          public void onChange(boolean selfChange, Uri uri) {
              if (Settings.Global.getUriFor(Settings.Global.FORCED_APP_STANDBY_ENABLED).equals(uri)) {
                  final boolean enabled = isForcedAppStandbyEnabled();
                  synchronized (mLock) {
                      if (mForcedAppStandbyEnabled == enabled) {
                          return;
                      }
                      mForcedAppStandbyEnabled = enabled;
                      if (DEBUG) {
                          Slog.d(TAG,"Forced app standby feature flag changed: "
                                  + mForcedAppStandbyEnabled);
                      }
                  }
                  mHandler.notifyForcedAppStandbyFeatureFlagChanged();
              } else if (Settings.Global.getUriFor(
                      Settings.Global.FORCED_APP_STANDBY_FOR_SMALL_BATTERY_ENABLED).equals(uri)) {
                  final boolean enabled = isForcedAppStandbyForSmallBatteryEnabled();
                  synchronized (mLock) {
                      if (mForceAllAppStandbyForSmallBattery == enabled) {
                          return;
                      }
                      mForceAllAppStandbyForSmallBattery = enabled;
                      if (DEBUG) {
                          Slog.d(TAG, "Forced app standby for small battery feature flag changed: "
                                  + mForceAllAppStandbyForSmallBattery);
                      }
                      updateForceAllAppStandbyState();
                  }
              } else {
                  Slog.w(TAG, "Unexpected feature flag uri encountered: " + uri);
              }
          }
      }
  
      public static abstract class Listener {
          /**
           * This is called when the OP_RUN_ANY_IN_BACKGROUND appops changed for a package.
           */
          private void onRunAnyAppOpsChanged(AppStateTracker sender,
                  int uid, @NonNull String packageName) {
              updateJobsForUidPackage(uid, packageName, sender.isUidActive(uid));
  
              if (!sender.areAlarmsRestricted(uid, packageName, /*allowWhileIdle=*/ false)) {
                  unblockAlarmsForUidPackage(uid, packageName);
              } else if (!sender.areAlarmsRestricted(uid, packageName, /*allowWhileIdle=*/ true)){
                  // we need to deliver the allow-while-idle alarms for this uid, package
                  unblockAllUnrestrictedAlarms();
              }
  
              if (!sender.isRunAnyInBackgroundAppOpsAllowed(uid, packageName)) {
                  Slog.v(TAG, "Package " + packageName + "/" + uid
                          + " toggled into fg service restriction");
                  stopForegroundServicesForUidPackage(uid, packageName);
              }
          }
  
          /**
           * This is called when the foreground state changed for a UID.
           */
          private void onUidForegroundStateChanged(AppStateTracker sender, int uid) {
              onUidForeground(uid, sender.isUidInForeground(uid));
          }
  
          /**
           * This is called when the active/idle state changed for a UID.
           */
          private void onUidActiveStateChanged(AppStateTracker sender, int uid) {
              final boolean isActive = sender.isUidActive(uid);
  
              updateJobsForUid(uid, isActive);
  
              if (isActive) {
                  unblockAlarmsForUid(uid);
              }
          }
  
          /**
           * This is called when an app-id(s) is removed from the power save whitelist.
           */
          private void onPowerSaveUnwhitelisted(AppStateTracker sender) {
              updateAllJobs();
              unblockAllUnrestrictedAlarms();
          }
  
          /**
           * This is called when the power save whitelist changes, excluding the
           * {@link #onPowerSaveUnwhitelisted} case.
           */
          private void onPowerSaveWhitelistedChanged(AppStateTracker sender) {
              updateAllJobs();
          }
  
          /**
           * This is called when the temp whitelist changes.
           */
          private void onTempPowerSaveWhitelistChanged(AppStateTracker sender) {
  
              // TODO This case happens rather frequently; consider optimizing and update jobs
              // only for affected app-ids.
  
              updateAllJobs();
  
              // Note when an app is just put in the temp whitelist, we do *not* drain pending alarms.
          }
  
          /**
           * This is called when the EXEMPT bucket is updated.
           */
          private void onExemptChanged(AppStateTracker sender) {
              // This doesn't happen very often, so just re-evaluate all jobs / alarms.
              updateAllJobs();
              unblockAllUnrestrictedAlarms();
          }
  
          /**
           * This is called when the global "force all apps standby" flag changes.
           */
          private void onForceAllAppsStandbyChanged(AppStateTracker sender) {
              updateAllJobs();
  
              if (!sender.isForceAllAppsStandbyEnabled()) {
                  unblockAllUnrestrictedAlarms();
              }
          }
  
          /**
           * Called when the job restrictions for multiple UIDs might have changed, so the job
           * scheduler should re-evaluate all restrictions for all jobs.
           */
          public void updateAllJobs() {
          }
  
          /**
           * Called when the job restrictions for a UID might have changed, so the job
           * scheduler should re-evaluate all restrictions for all jobs.
           */
          public void updateJobsForUid(int uid, boolean isNowActive) {
          }
  
          /**
           * Called when the job restrictions for a UID - package might have changed, so the job
           * scheduler should re-evaluate all restrictions for all jobs.
           */
          public void updateJobsForUidPackage(int uid, String packageName, boolean isNowActive) {
          }
  
          /**
           * Called when an app goes into forced app standby and its foreground
           * services need to be removed from that state.
           */
          public void stopForegroundServicesForUidPackage(int uid, String packageName) {
          }
  
          /**
           * Called when the job restrictions for multiple UIDs might have changed, so the alarm
           * manager should re-evaluate all restrictions for all blocked jobs.
           */
          public void unblockAllUnrestrictedAlarms() {
          }
  
          /**
           * Called when all jobs for a specific UID are unblocked.
           */
          public void unblockAlarmsForUid(int uid) {
          }
  
          /**
           * Called when all alarms for a specific UID - package are unblocked.
           */
          public void unblockAlarmsForUidPackage(int uid, String packageName) {
          }
  
          /**
           * Called when a UID comes into the foreground or the background.
           *
           * @see #isUidInForeground(int)
           */
          public void onUidForeground(int uid, boolean foreground) {
          }
      }
  
      public AppStateTracker(Context context, Looper looper) {
          mContext = context;
          mHandler = new MyHandler(looper);
      }
  
      /**
       * Call it when the system is ready.
       */
      public void onSystemServicesReady() {
          synchronized (mLock) {
              if (mStarted) {
                  return;
              }
              mStarted = true;
  
              mIActivityManager = Preconditions.checkNotNull(injectIActivityManager());
              mActivityManagerInternal = Preconditions.checkNotNull(injectActivityManagerInternal());
              mAppOpsManager = Preconditions.checkNotNull(injectAppOpsManager());
              mAppOpsService = Preconditions.checkNotNull(injectIAppOpsService());
              mPowerManagerInternal = Preconditions.checkNotNull(injectPowerManagerInternal());
              mUsageStatsManagerInternal = Preconditions.checkNotNull(
                      injectUsageStatsManagerInternal());
  
              mFlagsObserver = new FeatureFlagsObserver();
              mFlagsObserver.register();
              mForcedAppStandbyEnabled = mFlagsObserver.isForcedAppStandbyEnabled();
              mForceAllAppStandbyForSmallBattery =
                      mFlagsObserver.isForcedAppStandbyForSmallBatteryEnabled();
              mStandbyTracker = new StandbyTracker();
              mUsageStatsManagerInternal.addAppIdleStateChangeListener(mStandbyTracker);
  
              try {
                  mIActivityManager.registerUidObserver(new UidObserver(),
                          ActivityManager.UID_OBSERVER_GONE
                                  | ActivityManager.UID_OBSERVER_IDLE
                                  | ActivityManager.UID_OBSERVER_ACTIVE
                                  | ActivityManager.UID_OBSERVER_PROCSTATE,
                          ActivityManager.PROCESS_STATE_UNKNOWN, null);
                  mAppOpsService.startWatchingMode(TARGET_OP, null,
                          new AppOpsWatcher());
              } catch (RemoteException e) {
                  // shouldn't happen.
              }
  
              IntentFilter filter = new IntentFilter();
              filter.addAction(Intent.ACTION_USER_REMOVED);
              filter.addAction(Intent.ACTION_BATTERY_CHANGED);
              mContext.registerReceiver(new MyReceiver(), filter);
  
              refreshForcedAppStandbyUidPackagesLocked();
  
              mPowerManagerInternal.registerLowPowerModeObserver(
                      ServiceType.FORCE_ALL_APPS_STANDBY,
                      (state) -> {
                          synchronized (mLock) {
                              mBatterySaverEnabled = state.batterySaverEnabled;
                              updateForceAllAppStandbyState();
                          }
                      });
  
              mBatterySaverEnabled = mPowerManagerInternal.getLowPowerState(
                      ServiceType.FORCE_ALL_APPS_STANDBY).batterySaverEnabled;
  
              updateForceAllAppStandbyState();
          }
      }
  
      @VisibleForTesting
      AppOpsManager injectAppOpsManager() {
          return mContext.getSystemService(AppOpsManager.class);
      }
  
      @VisibleForTesting
      IAppOpsService injectIAppOpsService() {
          return IAppOpsService.Stub.asInterface(
                  ServiceManager.getService(Context.APP_OPS_SERVICE));
      }
  
      @VisibleForTesting
      IActivityManager injectIActivityManager() {
          return ActivityManager.getService();
      }
  
      @VisibleForTesting
      ActivityManagerInternal injectActivityManagerInternal() {
          return LocalServices.getService(ActivityManagerInternal.class);
      }
  
      @VisibleForTesting
      PowerManagerInternal injectPowerManagerInternal() {
          return LocalServices.getService(PowerManagerInternal.class);
      }
  
      @VisibleForTesting
      UsageStatsManagerInternal injectUsageStatsManagerInternal() {
          return LocalServices.getService(UsageStatsManagerInternal.class);
      }
  
      @VisibleForTesting
      boolean isSmallBatteryDevice() {
          return ActivityManager.isSmallBatteryDevice();
      }
  
      @VisibleForTesting
      int injectGetGlobalSettingInt(String key, int def) {
          return Settings.Global.getInt(mContext.getContentResolver(), key, def);
      }
  
      /**
       * Update {@link #mRunAnyRestrictedPackages} with the current app ops state.
       */
      @GuardedBy("mLock")
      private void refreshForcedAppStandbyUidPackagesLocked() {
          mRunAnyRestrictedPackages.clear();
          final List<PackageOps> ops = mAppOpsManager.getPackagesForOps(
                  new int[] {TARGET_OP});
  
          if (ops == null) {
              return;
          }
          final int size = ops.size();
          for (int i = 0; i < size; i++) {
              final AppOpsManager.PackageOps pkg = ops.get(i);
              final List<AppOpsManager.OpEntry> entries = ops.get(i).getOps();
  
              for (int j = 0; j < entries.size(); j++) {
                  AppOpsManager.OpEntry ent = entries.get(j);
                  if (ent.getOp() != TARGET_OP) {
                      continue;
                  }
                  if (ent.getMode() != AppOpsManager.MODE_ALLOWED) {
                      mRunAnyRestrictedPackages.add(Pair.create(
                              pkg.getUid(), pkg.getPackageName()));
                  }
              }
          }
      }
  
      private void updateForceAllAppStandbyState() {
          synchronized (mLock) {
              if (mForceAllAppStandbyForSmallBattery && isSmallBatteryDevice()) {
                  toggleForceAllAppsStandbyLocked(!mIsPluggedIn);
              } else {
                  toggleForceAllAppsStandbyLocked(mBatterySaverEnabled);
              }
          }
      }
  
      /**
       * Update {@link #mForceAllAppsStandby} and notifies the listeners.
       */
      @GuardedBy("mLock")
      private void toggleForceAllAppsStandbyLocked(boolean enable) {
          if (enable == mForceAllAppsStandby) {
              return;
          }
          mForceAllAppsStandby = enable;
  
          mHandler.notifyForceAllAppsStandbyChanged();
      }
  
      @GuardedBy("mLock")
      private int findForcedAppStandbyUidPackageIndexLocked(int uid, @NonNull String packageName) {
          final int size = mRunAnyRestrictedPackages.size();
          if (size > 8) {
              return mRunAnyRestrictedPackages.indexOf(Pair.create(uid, packageName));
          }
          for (int i = 0; i < size; i++) {
              final Pair<Integer, String> pair = mRunAnyRestrictedPackages.valueAt(i);
  
              if ((pair.first == uid) && packageName.equals(pair.second)) {
                  return i;
              }
          }
          return -1;
      }
  
      /**
       * @return whether a uid package-name pair is in mRunAnyRestrictedPackages.
       */
      @GuardedBy("mLock")
      boolean isRunAnyRestrictedLocked(int uid, @NonNull String packageName) {
          return findForcedAppStandbyUidPackageIndexLocked(uid, packageName) >= 0;
      }
  
      /**
       * Add to / remove from {@link #mRunAnyRestrictedPackages}.
       */
      @GuardedBy("mLock")
      boolean updateForcedAppStandbyUidPackageLocked(int uid, @NonNull String packageName,
              boolean restricted) {
          final int index = findForcedAppStandbyUidPackageIndexLocked(uid, packageName);
          final boolean wasRestricted = index >= 0;
          if (wasRestricted == restricted) {
              return false;
          }
          if (restricted) {
              mRunAnyRestrictedPackages.add(Pair.create(uid, packageName));
          } else {
              mRunAnyRestrictedPackages.removeAt(index);
          }
          return true;
      }
  
      private static boolean addUidToArray(SparseBooleanArray array, int uid) {
          if (UserHandle.isCore(uid)) {
              return false;
          }
          if (array.get(uid)) {
              return false;
          }
          array.put(uid, true);
          return true;
      }
  
      private static boolean removeUidFromArray(SparseBooleanArray array, int uid, boolean remove) {
          if (UserHandle.isCore(uid)) {
              return false;
          }
          if (!array.get(uid)) {
              return false;
          }
          if (remove) {
              array.delete(uid);
          } else {
              array.put(uid, false);
          }
          return true;
      }
  
      private final class UidObserver extends IUidObserver.Stub {
          @Override
          public void onUidStateChanged(int uid, int procState, long procStateSeq) {
              mHandler.onUidStateChanged(uid, procState);
          }
  
          @Override
          public void onUidActive(int uid) {
              mHandler.onUidActive(uid);
          }
  
          @Override
          public void onUidGone(int uid, boolean disabled) {
              mHandler.onUidGone(uid, disabled);
          }
  
          @Override
          public void onUidIdle(int uid, boolean disabled) {
              mHandler.onUidIdle(uid, disabled);
          }
  
          @Override
          public void onUidCachedChanged(int uid, boolean cached) {
          }
      }
  
      private final class AppOpsWatcher extends IAppOpsCallback.Stub {
          @Override
          public void opChanged(int op, int uid, String packageName) throws RemoteException {
              boolean restricted = false;
              try {
                  restricted = mAppOpsService.checkOperation(TARGET_OP,
                          uid, packageName) != AppOpsManager.MODE_ALLOWED;
              } catch (RemoteException e) {
                  // Shouldn't happen
              }
              synchronized (mLock) {
                  if (updateForcedAppStandbyUidPackageLocked(uid, packageName, restricted)) {
                      mHandler.notifyRunAnyAppOpsChanged(uid, packageName);
                  }
              }
          }
      }
  
      private final class MyReceiver extends BroadcastReceiver {
          @Override
          public void onReceive(Context context, Intent intent) {
              if (Intent.ACTION_USER_REMOVED.equals(intent.getAction())) {
                  final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
                  if (userId > 0) {
                      mHandler.doUserRemoved(userId);
                  }
              } else if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) {
                  synchronized (mLock) {
                      mIsPluggedIn = (intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0);
                  }
                  updateForceAllAppStandbyState();
              }
          }
      }
  
      final class StandbyTracker extends AppIdleStateChangeListener {
          @Override
          public void onAppIdleStateChanged(String packageName, int userId, boolean idle,
                  int bucket, int reason) {
              if (DEBUG) {
                  Slog.d(TAG,"onAppIdleStateChanged: " + packageName + " u" + userId
                          + (idle ? " idle" : " active") + " " + bucket);
              }
              final boolean changed;
              if (bucket == UsageStatsManager.STANDBY_BUCKET_EXEMPTED) {
                  changed = mExemptedPackages.add(userId, packageName);
              } else {
                  changed = mExemptedPackages.remove(userId, packageName);
              }
              if (changed) {
                  mHandler.notifyExemptChanged();
              }
          }
  
          @Override
          public void onParoleStateChanged(boolean isParoleOn) {
          }
      }
  
      private Listener[] cloneListeners() {
          synchronized (mLock) {
              return mListeners.toArray(new Listener[mListeners.size()]);
          }
      }
  
      private class MyHandler extends Handler {
          private static final int MSG_UID_ACTIVE_STATE_CHANGED = 0;
          private static final int MSG_UID_FG_STATE_CHANGED = 1;
          private static final int MSG_RUN_ANY_CHANGED = 3;
          private static final int MSG_ALL_UNWHITELISTED = 4;
          private static final int MSG_ALL_WHITELIST_CHANGED = 5;
          private static final int MSG_TEMP_WHITELIST_CHANGED = 6;
          private static final int MSG_FORCE_ALL_CHANGED = 7;
          private static final int MSG_USER_REMOVED = 8;
          private static final int MSG_FORCE_APP_STANDBY_FEATURE_FLAG_CHANGED = 9;
          private static final int MSG_EXEMPT_CHANGED = 10;
  
          private static final int MSG_ON_UID_STATE_CHANGED = 11;
          private static final int MSG_ON_UID_ACTIVE = 12;
          private static final int MSG_ON_UID_GONE = 13;
          private static final int MSG_ON_UID_IDLE = 14;
  
          public MyHandler(Looper looper) {
              super(looper);
          }
  
          public void notifyUidActiveStateChanged(int uid) {
              obtainMessage(MSG_UID_ACTIVE_STATE_CHANGED, uid, 0).sendToTarget();
          }
  
          public void notifyUidForegroundStateChanged(int uid) {
              obtainMessage(MSG_UID_FG_STATE_CHANGED, uid, 0).sendToTarget();
          }
  
          public void notifyRunAnyAppOpsChanged(int uid, @NonNull String packageName) {
              obtainMessage(MSG_RUN_ANY_CHANGED, uid, 0, packageName).sendToTarget();
          }
  
          public void notifyAllUnwhitelisted() {
              removeMessages(MSG_ALL_UNWHITELISTED);
              obtainMessage(MSG_ALL_UNWHITELISTED).sendToTarget();
          }
  
          public void notifyAllWhitelistChanged() {
              removeMessages(MSG_ALL_WHITELIST_CHANGED);
              obtainMessage(MSG_ALL_WHITELIST_CHANGED).sendToTarget();
          }
  
          public void notifyTempWhitelistChanged() {
              removeMessages(MSG_TEMP_WHITELIST_CHANGED);
              obtainMessage(MSG_TEMP_WHITELIST_CHANGED).sendToTarget();
          }
  
          public void notifyForceAllAppsStandbyChanged() {
              removeMessages(MSG_FORCE_ALL_CHANGED);
              obtainMessage(MSG_FORCE_ALL_CHANGED).sendToTarget();
          }
  
          public void notifyForcedAppStandbyFeatureFlagChanged() {
              removeMessages(MSG_FORCE_APP_STANDBY_FEATURE_FLAG_CHANGED);
              obtainMessage(MSG_FORCE_APP_STANDBY_FEATURE_FLAG_CHANGED).sendToTarget();
          }
  
          public void notifyExemptChanged() {
              removeMessages(MSG_EXEMPT_CHANGED);
              obtainMessage(MSG_EXEMPT_CHANGED).sendToTarget();
          }
  
          public void doUserRemoved(int userId) {
              obtainMessage(MSG_USER_REMOVED, userId, 0).sendToTarget();
          }
  
          public void onUidStateChanged(int uid, int procState) {
              obtainMessage(MSG_ON_UID_STATE_CHANGED, uid, procState).sendToTarget();
          }
  
          public void onUidActive(int uid) {
              obtainMessage(MSG_ON_UID_ACTIVE, uid, 0).sendToTarget();
          }
  
          public void onUidGone(int uid, boolean disabled) {
              obtainMessage(MSG_ON_UID_GONE, uid, disabled ? 1 : 0).sendToTarget();
          }
  
          public void onUidIdle(int uid, boolean disabled) {
              obtainMessage(MSG_ON_UID_IDLE, uid, disabled ? 1 : 0).sendToTarget();
          }
  
          @Override
          public void handleMessage(Message msg) {
              switch (msg.what) {
                  case MSG_USER_REMOVED:
                      handleUserRemoved(msg.arg1);
                      return;
              }
  
              // Only notify the listeners when started.
              synchronized (mLock) {
                  if (!mStarted) {
                      return;
                  }
              }
              final AppStateTracker sender = AppStateTracker.this;
  
              long start = mStatLogger.getTime();
              switch (msg.what) {
                  case MSG_UID_ACTIVE_STATE_CHANGED:
                      for (Listener l : cloneListeners()) {
                          l.onUidActiveStateChanged(sender, msg.arg1);
                      }
                      mStatLogger.logDurationStat(Stats.UID_ACTIVE_STATE_CHANGED, start);
                      return;
  
                  case MSG_UID_FG_STATE_CHANGED:
                      for (Listener l : cloneListeners()) {
                          l.onUidForegroundStateChanged(sender, msg.arg1);
                      }
                      mStatLogger.logDurationStat(Stats.UID_FG_STATE_CHANGED, start);
                      return;
  
                  case MSG_RUN_ANY_CHANGED:
                      for (Listener l : cloneListeners()) {
                          l.onRunAnyAppOpsChanged(sender, msg.arg1, (String) msg.obj);
                      }
                      mStatLogger.logDurationStat(Stats.RUN_ANY_CHANGED, start);
                      return;
  
                  case MSG_ALL_UNWHITELISTED:
                      for (Listener l : cloneListeners()) {
                          l.onPowerSaveUnwhitelisted(sender);
                      }
                      mStatLogger.logDurationStat(Stats.ALL_UNWHITELISTED, start);
                      return;
  
                  case MSG_ALL_WHITELIST_CHANGED:
                      for (Listener l : cloneListeners()) {
                          l.onPowerSaveWhitelistedChanged(sender);
                      }
                      mStatLogger.logDurationStat(Stats.ALL_WHITELIST_CHANGED, start);
                      return;
  
                  case MSG_TEMP_WHITELIST_CHANGED:
                      for (Listener l : cloneListeners()) {
                          l.onTempPowerSaveWhitelistChanged(sender);
                      }
                      mStatLogger.logDurationStat(Stats.TEMP_WHITELIST_CHANGED, start);
                      return;
  
                  case MSG_EXEMPT_CHANGED:
                      for (Listener l : cloneListeners()) {
                          l.onExemptChanged(sender);
                      }
                      mStatLogger.logDurationStat(Stats.EXEMPT_CHANGED, start);
                      return;
  
                  case MSG_FORCE_ALL_CHANGED:
                      for (Listener l : cloneListeners()) {
                          l.onForceAllAppsStandbyChanged(sender);
                      }
                      mStatLogger.logDurationStat(Stats.FORCE_ALL_CHANGED, start);
                      return;
  
                  case MSG_FORCE_APP_STANDBY_FEATURE_FLAG_CHANGED:
                      // Feature flag for forced app standby changed.
                      final boolean unblockAlarms;
                      synchronized (mLock) {
                          unblockAlarms = !mForcedAppStandbyEnabled && !mForceAllAppsStandby;
                      }
                      for (Listener l : cloneListeners()) {
                          l.updateAllJobs();
                          if (unblockAlarms) {
                              l.unblockAllUnrestrictedAlarms();
                          }
                      }
                      mStatLogger.logDurationStat(
                              Stats.FORCE_APP_STANDBY_FEATURE_FLAG_CHANGED, start);
                      return;
  
                  case MSG_USER_REMOVED:
                      handleUserRemoved(msg.arg1);
                      return;
  
                  case MSG_ON_UID_STATE_CHANGED:
                      handleUidStateChanged(msg.arg1, msg.arg2);
                      return;
                  case MSG_ON_UID_ACTIVE:
                      handleUidActive(msg.arg1);
                      return;
                  case MSG_ON_UID_GONE:
                      handleUidGone(msg.arg1, msg.arg1 != 0);
                      return;
                  case MSG_ON_UID_IDLE:
                      handleUidIdle(msg.arg1, msg.arg1 != 0);
                      return;
              }
          }
  
          public void handleUidStateChanged(int uid, int procState) {
              synchronized (mLock) {
                  if (procState > ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND) {
                      if (removeUidFromArray(mForegroundUids, uid, false)) {
                          mHandler.notifyUidForegroundStateChanged(uid);
                      }
                  } else {
                      if (addUidToArray(mForegroundUids, uid)) {
                          mHandler.notifyUidForegroundStateChanged(uid);
                      }
                  }
              }
          }
  
          public void handleUidActive(int uid) {
              synchronized (mLock) {
                  if (addUidToArray(mActiveUids, uid)) {
                      mHandler.notifyUidActiveStateChanged(uid);
                  }
              }
          }
  
          public void handleUidGone(int uid, boolean disabled) {
              removeUid(uid, true);
          }
  
          public void handleUidIdle(int uid, boolean disabled) {
              // Just to avoid excessive memcpy, don't remove from the array in this case.
              removeUid(uid, false);
          }
  
          private void removeUid(int uid, boolean remove) {
              synchronized (mLock) {
                  if (removeUidFromArray(mActiveUids, uid, remove)) {
                      mHandler.notifyUidActiveStateChanged(uid);
                  }
                  if (removeUidFromArray(mForegroundUids, uid, remove)) {
                      mHandler.notifyUidForegroundStateChanged(uid);
                  }
              }
          }
      }
  
      void handleUserRemoved(int removedUserId) {
          synchronized (mLock) {
              for (int i = mRunAnyRestrictedPackages.size() - 1; i >= 0; i--) {
                  final Pair<Integer, String> pair = mRunAnyRestrictedPackages.valueAt(i);
                  final int uid = pair.first;
                  final int userId = UserHandle.getUserId(uid);
  
                  if (userId == removedUserId) {
                      mRunAnyRestrictedPackages.removeAt(i);
                  }
              }
              cleanUpArrayForUser(mActiveUids, removedUserId);
              cleanUpArrayForUser(mForegroundUids, removedUserId);
              mExemptedPackages.remove(removedUserId);
          }
      }
  
      private void cleanUpArrayForUser(SparseBooleanArray array, int removedUserId) {
          for (int i = array.size() - 1; i >= 0; i--) {
              final int uid = array.keyAt(i);
              final int userId = UserHandle.getUserId(uid);
  
              if (userId == removedUserId) {
                  array.removeAt(i);
              }
          }
      }
  
      /**
       * Called by device idle controller to update the power save whitelists.
       */
      public void setPowerSaveWhitelistAppIds(
              int[] powerSaveWhitelistExceptIdleAppIdArray,
              int[] powerSaveWhitelistUserAppIdArray,
              int[] tempWhitelistAppIdArray) {
          synchronized (mLock) {
              final int[] previousWhitelist = mPowerWhitelistedAllAppIds;
              final int[] previousTempWhitelist = mTempWhitelistedAppIds;
  
              mPowerWhitelistedAllAppIds = powerSaveWhitelistExceptIdleAppIdArray;
              mTempWhitelistedAppIds = tempWhitelistAppIdArray;
              mPowerWhitelistedUserAppIds = powerSaveWhitelistUserAppIdArray;
  
              if (isAnyAppIdUnwhitelisted(previousWhitelist, mPowerWhitelistedAllAppIds)) {
                  mHandler.notifyAllUnwhitelisted();
              } else if (!Arrays.equals(previousWhitelist, mPowerWhitelistedAllAppIds)) {
                  mHandler.notifyAllWhitelistChanged();
              }
  
              if (!Arrays.equals(previousTempWhitelist, mTempWhitelistedAppIds)) {
                  mHandler.notifyTempWhitelistChanged();
              }
  
          }
      }
  
      /**
       * @retunr true if a sorted app-id array {@code prevArray} has at least one element
       * that's not in a sorted app-id array {@code newArray}.
       */
      @VisibleForTesting
      static boolean isAnyAppIdUnwhitelisted(int[] prevArray, int[] newArray) {
          int i1 = 0;
          int i2 = 0;
          boolean prevFinished;
          boolean newFinished;
  
          for (;;) {
              prevFinished = i1 >= prevArray.length;
              newFinished = i2 >= newArray.length;
              if (prevFinished || newFinished) {
                  break;
              }
              int a1 = prevArray[i1];
              int a2 = newArray[i2];
  
              if (a1 == a2) {
                  i1++;
                  i2++;
                  continue;
              }
              if (a1 < a2) {
                  // prevArray has an element that's not in a2.
                  return true;
              }
              i2++;
          }
          if (prevFinished) {
              return false;
          }
          return newFinished;
      }
  
      // Public interface.
  
      /**
       * Register a new listener.
       */
      public void addListener(@NonNull Listener listener) {
          synchronized (mLock) {
              mListeners.add(listener);
          }
      }
  
      /**
       * @return whether alarms should be restricted for a UID package-name.
       */
      public boolean areAlarmsRestricted(int uid, @NonNull String packageName,
              boolean isExemptOnBatterySaver) {
          return isRestricted(uid, packageName, /*useTempWhitelistToo=*/ false,
                  isExemptOnBatterySaver);
      }
  
      /**
       * @return whether jobs should be restricted for a UID package-name.
       */
      public boolean areJobsRestricted(int uid, @NonNull String packageName,
              boolean hasForegroundExemption) {
          return isRestricted(uid, packageName, /*useTempWhitelistToo=*/ true,
                  hasForegroundExemption);
      }
  
      /**
       * @return whether foreground services should be suppressed in the background
       * due to forced app standby for the given app
       */
      public boolean areForegroundServicesRestricted(int uid, @NonNull String packageName) {
          synchronized (mLock) {
              return isRunAnyRestrictedLocked(uid, packageName);
          }
      }
  
      /**
       * @return whether force-app-standby is effective for a UID package-name.
       */
      private boolean isRestricted(int uid, @NonNull String packageName,
              boolean useTempWhitelistToo, boolean exemptOnBatterySaver) {
          if (isUidActive(uid)) {
              return false;
          }
          synchronized (mLock) {
              // Whitelisted?
              final int appId = UserHandle.getAppId(uid);
              if (ArrayUtils.contains(mPowerWhitelistedAllAppIds, appId)) {
                  return false;
              }
              if (useTempWhitelistToo &&
                      ArrayUtils.contains(mTempWhitelistedAppIds, appId)) {
                  return false;
              }
              if (mForcedAppStandbyEnabled && isRunAnyRestrictedLocked(uid, packageName)) {
                  return true;
              }
              if (exemptOnBatterySaver) {
                  return false;
              }
              final int userId = UserHandle.getUserId(uid);
              if (mExemptedPackages.contains(userId, packageName)) {
                  return false;
              }
              return mForceAllAppsStandby;
          }
      }
  
      /**
       * @return whether a UID is in active or not *based on cached information.*
       *
       * Note this information is based on the UID proc state callback, meaning it's updated
       * asynchronously and may subtly be stale. If the fresh data is needed, use
       * {@link #isUidActiveSynced} instead.
       */
      public boolean isUidActive(int uid) {
          if (UserHandle.isCore(uid)) {
              return true;
          }
          synchronized (mLock) {
              return mActiveUids.get(uid);
          }
      }
  
      /**
       * @return whether a UID is in active or not *right now.*
       *
       * This gives the fresh information, but may access the activity manager so is slower.
       */
      public boolean isUidActiveSynced(int uid) {
          if (isUidActive(uid)) { // Use the cached one first.
              return true;
          }
          final long start = mStatLogger.getTime();
  
          final boolean ret = mActivityManagerInternal.isUidActive(uid);
          mStatLogger.logDurationStat(Stats.IS_UID_ACTIVE_RAW, start);
  
          return ret;
      }
  
      /**
       * @return whether a UID is in the foreground or not.
       *
       * Note this information is based on the UID proc state callback, meaning it's updated
       * asynchronously and may subtly be stale. If the fresh data is needed, use
       * {@link ActivityManagerInternal#getUidProcessState} instead.
       */
      public boolean isUidInForeground(int uid) {
          if (UserHandle.isCore(uid)) {
              return true;
          }
          synchronized (mLock) {
              return mForegroundUids.get(uid);
          }
      }
  
      /**
       * @return whether force all apps standby is enabled or not.
       *
       */
      boolean isForceAllAppsStandbyEnabled() {
          synchronized (mLock) {
              return mForceAllAppsStandby;
          }
      }
  
      /**
       * @return whether a UID/package has {@code OP_RUN_ANY_IN_BACKGROUND} allowed or not.
       *
       * Note clients normally shouldn't need to access it. It's only for dumpsys.
       */
      public boolean isRunAnyInBackgroundAppOpsAllowed(int uid, @NonNull String packageName) {
          synchronized (mLock) {
              return !isRunAnyRestrictedLocked(uid, packageName);
          }
      }
  
      /**
       * @return whether a UID is in the user / system defined power-save whitelist or not.
       *
       * Note clients normally shouldn't need to access it. It's only for dumpsys.
       */
      public boolean isUidPowerSaveWhitelisted(int uid) {
          synchronized (mLock) {
              return ArrayUtils.contains(mPowerWhitelistedAllAppIds, UserHandle.getAppId(uid));
          }
      }
  
      /**
       * @param uid the uid to check for
       * @return whether a UID is in the user defined power-save whitelist or not.
       */
      public boolean isUidPowerSaveUserWhitelisted(int uid) {
          synchronized (mLock) {
              return ArrayUtils.contains(mPowerWhitelistedUserAppIds, UserHandle.getAppId(uid));
          }
      }
  
      /**
       * @return whether a UID is in the temp power-save whitelist or not.
       *
       * Note clients normally shouldn't need to access it. It's only for dumpsys.
       */
      public boolean isUidTempPowerSaveWhitelisted(int uid) {
          synchronized (mLock) {
              return ArrayUtils.contains(mTempWhitelistedAppIds, UserHandle.getAppId(uid));
          }
      }
  
      @Deprecated
      public void dump(PrintWriter pw, String prefix) {
          dump(new IndentingPrintWriter(pw, "  ").setIndent(prefix));
      }
  
      public void dump(IndentingPrintWriter pw) {
          synchronized (mLock) {
              pw.println("Forced App Standby Feature enabled: " + mForcedAppStandbyEnabled);
  
              pw.print("Force all apps standby: ");
              pw.println(isForceAllAppsStandbyEnabled());
  
              pw.print("Small Battery Device: ");
              pw.println(isSmallBatteryDevice());
  
              pw.print("Force all apps standby for small battery device: ");
              pw.println(mForceAllAppStandbyForSmallBattery);
  
              pw.print("Plugged In: ");
              pw.println(mIsPluggedIn);
  
              pw.print("Active uids: ");
              dumpUids(pw, mActiveUids);
  
              pw.print("Foreground uids: ");
              dumpUids(pw, mForegroundUids);
  
              pw.print("Except-idle + user whitelist appids: ");
              pw.println(Arrays.toString(mPowerWhitelistedAllAppIds));
  
              pw.print("User whitelist appids: ");
              pw.println(Arrays.toString(mPowerWhitelistedUserAppIds));
  
              pw.print("Temp whitelist appids: ");
              pw.println(Arrays.toString(mTempWhitelistedAppIds));
  
              pw.println("Exempted packages:");
              pw.increaseIndent();
              for (int i = 0; i < mExemptedPackages.size(); i++) {
                  pw.print("User ");
                  pw.print(mExemptedPackages.keyAt(i));
                  pw.println();
  
                  pw.increaseIndent();
                  for (int j = 0; j < mExemptedPackages.sizeAt(i); j++) {
                      pw.print(mExemptedPackages.valueAt(i, j));
                      pw.println();
                  }
                  pw.decreaseIndent();
              }
              pw.decreaseIndent();
              pw.println();
  
              pw.println("Restricted packages:");
              pw.increaseIndent();
              for (Pair<Integer, String> uidAndPackage : mRunAnyRestrictedPackages) {
                  pw.print(UserHandle.formatUid(uidAndPackage.first));
                  pw.print(" ");
                  pw.print(uidAndPackage.second);
                  pw.println();
              }
              pw.decreaseIndent();
  
              mStatLogger.dump(pw);
          }
      }
  
      private void dumpUids(PrintWriter pw, SparseBooleanArray array) {
          pw.print("[");
  
          String sep = "";
          for (int i = 0; i < array.size(); i++) {
              if (array.valueAt(i)) {
                  pw.print(sep);
                  pw.print(UserHandle.formatUid(array.keyAt(i)));
                  sep = " ";
              }
          }
          pw.println("]");
      }
  
      public void dumpProto(ProtoOutputStream proto, long fieldId) {
          synchronized (mLock) {
              final long token = proto.start(fieldId);
  
              proto.write(ForceAppStandbyTrackerProto.FORCE_ALL_APPS_STANDBY, mForceAllAppsStandby);
              proto.write(ForceAppStandbyTrackerProto.IS_SMALL_BATTERY_DEVICE,
                      isSmallBatteryDevice());
              proto.write(ForceAppStandbyTrackerProto.FORCE_ALL_APPS_STANDBY_FOR_SMALL_BATTERY,
                      mForceAllAppStandbyForSmallBattery);
              proto.write(ForceAppStandbyTrackerProto.IS_PLUGGED_IN, mIsPluggedIn);
  
              for (int i = 0; i < mActiveUids.size(); i++) {
                  if (mActiveUids.valueAt(i)) {
                      proto.write(ForceAppStandbyTrackerProto.ACTIVE_UIDS,
                              mActiveUids.keyAt(i));
                  }
              }
  
              for (int i = 0; i < mForegroundUids.size(); i++) {
                  if (mForegroundUids.valueAt(i)) {
                      proto.write(ForceAppStandbyTrackerProto.FOREGROUND_UIDS,
                              mForegroundUids.keyAt(i));
                  }
              }
  
              for (int appId : mPowerWhitelistedAllAppIds) {
                  proto.write(ForceAppStandbyTrackerProto.POWER_SAVE_WHITELIST_APP_IDS, appId);
              }
  
              for (int appId : mPowerWhitelistedUserAppIds) {
                  proto.write(ForceAppStandbyTrackerProto.POWER_SAVE_USER_WHITELIST_APP_IDS, appId);
              }
  
              for (int appId : mTempWhitelistedAppIds) {
                  proto.write(ForceAppStandbyTrackerProto.TEMP_POWER_SAVE_WHITELIST_APP_IDS, appId);
              }
  
              for (int i = 0; i < mExemptedPackages.size(); i++) {
                  for (int j = 0; j < mExemptedPackages.sizeAt(i); j++) {
                      final long token2 = proto.start(
                              ForceAppStandbyTrackerProto.EXEMPTED_PACKAGES);
  
                      proto.write(ExemptedPackage.USER_ID, mExemptedPackages.keyAt(i));
                      proto.write(ExemptedPackage.PACKAGE_NAME, mExemptedPackages.valueAt(i, j));
  
                      proto.end(token2);
                  }
              }
  
              for (Pair<Integer, String> uidAndPackage : mRunAnyRestrictedPackages) {
                  final long token2 = proto.start(
                          ForceAppStandbyTrackerProto.RUN_ANY_IN_BACKGROUND_RESTRICTED_PACKAGES);
                  proto.write(RunAnyInBackgroundRestrictedPackages.UID, uidAndPackage.first);
                  proto.write(RunAnyInBackgroundRestrictedPackages.PACKAGE_NAME,
                          uidAndPackage.second);
                  proto.end(token2);
              }
  
              mStatLogger.dumpProto(proto, ForceAppStandbyTrackerProto.STATS);
  
              proto.end(token);
          }
      }
  }
  