/*
 * 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.ActivityManagerInternal.AppBackgroundRestrictionListener;
import android.app.AppOpsManager;
import android.app.AppOpsManager.PackageOps;
import android.app.IActivityManager;
import android.app.usage.UsageStatsManager;
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.IndentingPrintWriter;
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.StatLogger;
import com.android.modules.expresslog.Counter;
import com.android.server.AppStateTrackerProto.ExemptedPackage;
import com.android.server.AppStateTrackerProto.RunAnyInBackgroundRestrictedPackages;
import com.android.server.usage.AppStandbyInternal;
import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener;

import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;

/**
 * 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 exemption list
 * - Temporary power save exemption list
 * - Global "force all apps standby" mode enforced by battery saver.
 *
 * Test: atest com.android.server.AppStateTrackerTest
 */
public class AppStateTrackerImpl implements AppStateTracker {
    private static final boolean DEBUG = false;

    private static final String APP_RESTRICTION_COUNTER_METRIC_ID =
            "battery.value_app_background_restricted";

    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;
    AppStandbyInternal mAppStandbyInternal;

    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();

    /**
     * System except-idle + user exemption list in the device idle controller.
     */
    @GuardedBy("mLock")
    private int[] mPowerExemptAllAppIds = new int[0];

    /**
     * User exempted apps in the device idle controller.
     */
    @GuardedBy("mLock")
    private int[] mPowerExemptUserAppIds = new int[0];

    @GuardedBy("mLock")
    private int[] mTempExemptAppIds = mPowerExemptAllAppIds;

    /**
     * Per-user packages that are in the EXEMPTED bucket.
     */
    @GuardedBy("mLock")
    @VisibleForTesting
    final SparseSetArray<String> mExemptedBucketPackages = 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;

    /**
     * A lock-free set of (uid, packageName) pairs in background restricted mode.
     *
     * <p>
     * It's basically shadowing the {@link #mRunAnyRestrictedPackages}, any mutations on it would
     * result in copy-on-write.
     * </p>
     */
    volatile Set<Pair<Integer, String>> mBackgroundRestrictedUidPackages = Collections.emptySet();

    @Override
    public void addBackgroundRestrictedAppListener(
            @NonNull BackgroundRestrictedAppListener listener) {
        addListener(new Listener() {
            @Override
            public void updateBackgroundRestrictedForUidPackage(int uid, String packageName,
                    boolean restricted) {
                listener.updateBackgroundRestrictedForUidPackage(uid, packageName, restricted);
            }
        });
    }

    @Override
    public boolean isAppBackgroundRestricted(int uid, @NonNull String packageName) {
        final Set<Pair<Integer, String>> bgRestrictedUidPkgs = mBackgroundRestrictedUidPackages;
        return bgRestrictedUidPkgs.contains(Pair.create(uid, packageName));
    }

    interface Stats {
        int UID_FG_STATE_CHANGED = 0;
        int UID_ACTIVE_STATE_CHANGED = 1;
        int RUN_ANY_CHANGED = 2;
        int ALL_UNEXEMPTED = 3;
        int ALL_EXEMPTION_LIST_CHANGED = 4;
        int TEMP_EXEMPTION_LIST_CHANGED = 5;
        int EXEMPTED_BUCKET_CHANGED = 6;
        int FORCE_ALL_CHANGED = 7;

        int IS_UID_ACTIVE_CACHED = 8;
        int IS_UID_ACTIVE_RAW = 9;
    }

    private final StatLogger mStatLogger = new StatLogger(new String[] {
            "UID_FG_STATE_CHANGED",
            "UID_ACTIVE_STATE_CHANGED",
            "RUN_ANY_CHANGED",
            "ALL_UNEXEMPTED",
            "ALL_EXEMPTION_LIST_CHANGED",
            "TEMP_EXEMPTION_LIST_CHANGED",
            "EXEMPTED_BUCKET_CHANGED",
            "FORCE_ALL_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_FOR_SMALL_BATTERY_ENABLED), false, this);
        }

        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_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);
            }
        }
    }

    private final AppBackgroundRestrictionListener mAppBackgroundRestrictionListener =
            new AppBackgroundRestrictionListener() {
        @Override
        public void onAutoRestrictedBucketFeatureFlagChanged(boolean autoRestrictedBucket) {
            mHandler.notifyAutoRestrictedBucketFeatureFlagChanged(autoRestrictedBucket);
        }
    };

    /**
     * Listener for any state changes that affect any app's eligibility to run.
     */
    public abstract static class Listener {
        /**
         * This is called when the OP_RUN_ANY_IN_BACKGROUND appops changed for a package.
         */
        private void onRunAnyAppOpsChanged(AppStateTrackerImpl sender,
                int uid, @NonNull String packageName) {
            updateJobsForUidPackage(uid, packageName, sender.isUidActive(uid));

            if (!sender.areAlarmsRestricted(uid, packageName)) {
                unblockAlarmsForUidPackage(uid, packageName);
            }

            if (!sender.isRunAnyInBackgroundAppOpsAllowed(uid, packageName)) {
                Slog.v(TAG, "Package " + packageName + "/" + uid
                        + " toggled into fg service restriction");
                updateBackgroundRestrictedForUidPackage(uid, packageName, true);
            } else {
                Slog.v(TAG, "Package " + packageName + "/" + uid
                        + " toggled out of fg service restriction");
                updateBackgroundRestrictedForUidPackage(uid, packageName, false);
            }
        }

        /**
         * This is called when the active/idle state changed for a UID.
         */
        private void onUidActiveStateChanged(AppStateTrackerImpl sender, int uid) {
            final boolean isActive = sender.isUidActive(uid);

            updateJobsForUid(uid, isActive);
            updateAlarmsForUid(uid);

            if (isActive) {
                unblockAlarmsForUid(uid);
            }
        }

        /**
         * This is called when an app-id(s) is removed from the power save allow-list.
         */
        private void onPowerSaveUnexempted(AppStateTrackerImpl sender) {
            updateAllJobs();
            updateAllAlarms();
        }

        /**
         * This is called when the power save exemption list changes, excluding the
         * {@link #onPowerSaveUnexempted} case.
         */
        private void onPowerSaveExemptionListChanged(AppStateTrackerImpl sender) {
            updateAllJobs();
            updateAllAlarms();
            unblockAllUnrestrictedAlarms();
        }

        /**
         * This is called when the temp exemption list changes.
         */
        private void onTempPowerSaveExemptionListChanged(AppStateTrackerImpl 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 exemption list, we do *not* drain pending
            // alarms.
        }

        /**
         * This is called when the EXEMPTED bucket is updated.
         */
        private void onExemptedBucketChanged(AppStateTrackerImpl sender) {
            // This doesn't happen very often, so just re-evaluate all jobs / alarms.
            updateAllJobs();
            updateAllAlarms();
        }

        /**
         * This is called when the global "force all apps standby" flag changes.
         */
        private void onForceAllAppsStandbyChanged(AppStateTrackerImpl sender) {
            updateAllJobs();
            updateAllAlarms();
        }

        /**
         * Called when toggling the feature flag of moving to restricted standby bucket
         * automatically on background-restricted.
         */
        private void onAutoRestrictedBucketFeatureFlagChanged(AppStateTrackerImpl sender,
                boolean autoRestrictedBucket) {
            updateAllJobs();
            if (autoRestrictedBucket) {
                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 in/out of background restricted mode.
         */
        public void updateBackgroundRestrictedForUidPackage(int uid, String packageName,
                boolean restricted) {
        }

        /**
         * Called when all alarms need to be re-evaluated for eligibility based on
         * {@link #areAlarmsRestrictedByBatterySaver}.
         */
        public void updateAllAlarms() {
        }

        /**
         * Called when the given uid state changes to active / idle.
         */
        public void updateAlarmsForUid(int uid) {
        }

        /**
         * 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 an ephemeral uid goes to the background, so its alarms need to be removed.
         */
        public void removeAlarmsForUid(int uid) {
        }

        /**
         * Called when a uid goes into cached, so its alarms using a listener should be removed.
         */
        public void handleUidCachedChanged(int uid, boolean cached) {
        }
    }

    public AppStateTrackerImpl(Context context, Looper looper) {
        mContext = context;
        mHandler = new MyHandler(looper);
    }

    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
            switch (intent.getAction()) {
                case Intent.ACTION_USER_REMOVED:
                    if (userId > 0) {
                        mHandler.doUserRemoved(userId);
                    }
                    break;
                case Intent.ACTION_BATTERY_CHANGED:
                    synchronized (mLock) {
                        mIsPluggedIn = (intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0);
                    }
                    updateForceAllAppStandbyState();
                    break;
                case Intent.ACTION_PACKAGE_REMOVED:
                    if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
                        final String pkgName = intent.getData().getSchemeSpecificPart();
                        final int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
                        // No need to notify for state change as all the alarms and jobs should be
                        // removed too.
                        synchronized (mLock) {
                            mExemptedBucketPackages.remove(userId, pkgName);
                            mRunAnyRestrictedPackages.remove(Pair.create(uid, pkgName));
                            updateBackgroundRestrictedUidPackagesLocked();
                            mActiveUids.delete(uid);
                        }
                    }
                    break;
            }
        }
    };

    /**
     * Call it when the system is ready.
     */
    public void onSystemServicesReady() {
        synchronized (mLock) {
            if (mStarted) {
                return;
            }
            mStarted = true;

            mIActivityManager = Objects.requireNonNull(injectIActivityManager());
            mActivityManagerInternal = Objects.requireNonNull(injectActivityManagerInternal());
            mAppOpsManager = Objects.requireNonNull(injectAppOpsManager());
            mAppOpsService = Objects.requireNonNull(injectIAppOpsService());
            mPowerManagerInternal = Objects.requireNonNull(injectPowerManagerInternal());
            mAppStandbyInternal = Objects.requireNonNull(injectAppStandbyInternal());

            mFlagsObserver = new FeatureFlagsObserver();
            mFlagsObserver.register();
            mForceAllAppStandbyForSmallBattery =
                    mFlagsObserver.isForcedAppStandbyForSmallBatteryEnabled();
            mStandbyTracker = new StandbyTracker();
            mAppStandbyInternal.addListener(mStandbyTracker);
            mActivityManagerInternal.addAppBackgroundRestrictionListener(
                    mAppBackgroundRestrictionListener);

            try {
                mIActivityManager.registerUidObserver(new UidObserver(),
                        ActivityManager.UID_OBSERVER_GONE
                                | ActivityManager.UID_OBSERVER_IDLE
                                | ActivityManager.UID_OBSERVER_ACTIVE
                                | ActivityManager.UID_OBSERVER_CACHED,
                        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(mReceiver, filter);

            filter = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED);
            filter.addDataScheme(IntentFilter.SCHEME_PACKAGE);
            mContext.registerReceiver(mReceiver, 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
    AppStandbyInternal injectAppStandbyInternal() {
        return LocalServices.getService(AppStandbyInternal.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()));
                }
            }
        }
        updateBackgroundRestrictedUidPackagesLocked();
    }

    /**
     * Update the {@link #mBackgroundRestrictedUidPackages} upon mutations on
     * {@link #mRunAnyRestrictedPackages}.
     */
    @GuardedBy("mLock")
    private void updateBackgroundRestrictedUidPackagesLocked() {
        Set<Pair<Integer, String>> fasUidPkgs = new ArraySet<>();
        for (int i = 0, size = mRunAnyRestrictedPackages.size(); i < size; i++) {
            fasUidPkgs.add(mRunAnyRestrictedPackages.valueAt(i));
        }
        mBackgroundRestrictedUidPackages = Collections.unmodifiableSet(fasUidPkgs);
    }

    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);
        }
        updateBackgroundRestrictedUidPackagesLocked();
        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 android.app.UidObserver {
        @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) {
            mHandler.onUidCachedChanged(uid, cached);
        }
    }

    private final class AppOpsWatcher extends IAppOpsCallback.Stub {
        @Override
        public void opChanged(int op, int uid, String packageName,
                String persistentDeviceId) throws RemoteException {
            boolean restricted = false;
            try {
                restricted = mAppOpsService.checkOperation(TARGET_OP,
                        uid, packageName) != AppOpsManager.MODE_ALLOWED;
            } catch (RemoteException e) {
                // Shouldn't happen
            }
            if (restricted) {
                Counter.logIncrementWithUid(APP_RESTRICTION_COUNTER_METRIC_ID, uid);
            }
            synchronized (mLock) {
                if (updateForcedAppStandbyUidPackageLocked(uid, packageName, restricted)) {
                    mHandler.notifyRunAnyAppOpsChanged(uid, packageName);
                }
            }
        }
    }

    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);
            }
            synchronized (mLock) {
                final boolean changed;
                if (bucket == UsageStatsManager.STANDBY_BUCKET_EXEMPTED) {
                    changed = mExemptedBucketPackages.add(userId, packageName);
                } else {
                    changed = mExemptedBucketPackages.remove(userId, packageName);
                }
                if (changed) {
                    mHandler.notifyExemptedBucketChanged();
                }
            }
        }
    }

    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;
        // Unused ids 1, 2.
        private static final int MSG_RUN_ANY_CHANGED = 3;
        private static final int MSG_ALL_UNEXEMPTED = 4;
        private static final int MSG_ALL_EXEMPTION_LIST_CHANGED = 5;
        private static final int MSG_TEMP_EXEMPTION_LIST_CHANGED = 6;
        private static final int MSG_FORCE_ALL_CHANGED = 7;
        private static final int MSG_USER_REMOVED = 8;
        // Unused id 9.
        private static final int MSG_EXEMPTED_BUCKET_CHANGED = 10;
        private static final int MSG_AUTO_RESTRICTED_BUCKET_FEATURE_FLAG_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;
        private static final int MSG_ON_UID_CACHED = 15;

        MyHandler(Looper looper) {
            super(looper);
        }

        public void notifyUidActiveStateChanged(int uid) {
            obtainMessage(MSG_UID_ACTIVE_STATE_CHANGED, uid, 0).sendToTarget();
        }

        public void notifyRunAnyAppOpsChanged(int uid, @NonNull String packageName) {
            obtainMessage(MSG_RUN_ANY_CHANGED, uid, 0, packageName).sendToTarget();
        }

        public void notifyAllUnexempted() {
            removeMessages(MSG_ALL_UNEXEMPTED);
            obtainMessage(MSG_ALL_UNEXEMPTED).sendToTarget();
        }

        public void notifyAllExemptionListChanged() {
            removeMessages(MSG_ALL_EXEMPTION_LIST_CHANGED);
            obtainMessage(MSG_ALL_EXEMPTION_LIST_CHANGED).sendToTarget();
        }

        public void notifyTempExemptionListChanged() {
            removeMessages(MSG_TEMP_EXEMPTION_LIST_CHANGED);
            obtainMessage(MSG_TEMP_EXEMPTION_LIST_CHANGED).sendToTarget();
        }

        public void notifyForceAllAppsStandbyChanged() {
            removeMessages(MSG_FORCE_ALL_CHANGED);
            obtainMessage(MSG_FORCE_ALL_CHANGED).sendToTarget();
        }

        public void notifyExemptedBucketChanged() {
            removeMessages(MSG_EXEMPTED_BUCKET_CHANGED);
            obtainMessage(MSG_EXEMPTED_BUCKET_CHANGED).sendToTarget();
        }

        public void notifyAutoRestrictedBucketFeatureFlagChanged(boolean autoRestrictedBucket) {
            removeMessages(MSG_AUTO_RESTRICTED_BUCKET_FEATURE_FLAG_CHANGED);
            obtainMessage(MSG_AUTO_RESTRICTED_BUCKET_FEATURE_FLAG_CHANGED,
                    autoRestrictedBucket ? 1 : 0, 0).sendToTarget();
        }

        public void doUserRemoved(int userId) {
            obtainMessage(MSG_USER_REMOVED, userId, 0).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();
        }

        public void onUidCachedChanged(int uid, boolean cached) {
            obtainMessage(MSG_ON_UID_CACHED, uid, cached ? 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 AppStateTrackerImpl sender = AppStateTrackerImpl.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_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_UNEXEMPTED:
                    for (Listener l : cloneListeners()) {
                        l.onPowerSaveUnexempted(sender);
                    }
                    mStatLogger.logDurationStat(Stats.ALL_UNEXEMPTED, start);
                    return;

                case MSG_ALL_EXEMPTION_LIST_CHANGED:
                    for (Listener l : cloneListeners()) {
                        l.onPowerSaveExemptionListChanged(sender);
                    }
                    mStatLogger.logDurationStat(Stats.ALL_EXEMPTION_LIST_CHANGED, start);
                    return;

                case MSG_TEMP_EXEMPTION_LIST_CHANGED:
                    for (Listener l : cloneListeners()) {
                        l.onTempPowerSaveExemptionListChanged(sender);
                    }
                    mStatLogger.logDurationStat(Stats.TEMP_EXEMPTION_LIST_CHANGED, start);
                    return;

                case MSG_EXEMPTED_BUCKET_CHANGED:
                    for (Listener l : cloneListeners()) {
                        l.onExemptedBucketChanged(sender);
                    }
                    mStatLogger.logDurationStat(Stats.EXEMPTED_BUCKET_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_USER_REMOVED:
                    handleUserRemoved(msg.arg1);
                    return;

                case MSG_AUTO_RESTRICTED_BUCKET_FEATURE_FLAG_CHANGED:
                    final boolean autoRestrictedBucket = msg.arg1 == 1;
                    for (Listener l : cloneListeners()) {
                        l.onAutoRestrictedBucketFeatureFlagChanged(sender, autoRestrictedBucket);
                    }
                    return;

                case MSG_ON_UID_ACTIVE:
                    handleUidActive(msg.arg1);
                    return;
                case MSG_ON_UID_GONE:
                    handleUidGone(msg.arg1);
                    if (msg.arg2 != 0) {
                        handleUidDisabled(msg.arg1);
                    }
                    return;
                case MSG_ON_UID_IDLE:
                    handleUidIdle(msg.arg1);
                    if (msg.arg2 != 0) {
                        handleUidDisabled(msg.arg1);
                    }
                    return;
                case MSG_ON_UID_CACHED:
                    handleUidCached(msg.arg1, (msg.arg2 != 0));
                    return;
            }
        }

        private void handleUidCached(int uid, boolean cached) {
            for (Listener l : cloneListeners()) {
                l.handleUidCachedChanged(uid, cached);
            }
        }

        private void handleUidDisabled(int uid) {
            for (Listener l : cloneListeners()) {
                l.removeAlarmsForUid(uid);
            }
        }

        public void handleUidActive(int uid) {
            synchronized (mLock) {
                if (addUidToArray(mActiveUids, uid)) {
                    mHandler.notifyUidActiveStateChanged(uid);
                }
            }
        }

        public void handleUidGone(int uid) {
            removeUid(uid, true);
        }

        public void handleUidIdle(int uid) {
            // 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);
                }
            }
        }
    }

    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);
                }
            }
            updateBackgroundRestrictedUidPackagesLocked();
            cleanUpArrayForUser(mActiveUids, removedUserId);
            mExemptedBucketPackages.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 exemption lists.
     */
    public void setPowerSaveExemptionListAppIds(
            int[] powerSaveExemptionListExceptIdleAppIdArray,
            int[] powerSaveExemptionListUserAppIdArray,
            int[] tempExemptionListAppIdArray) {
        synchronized (mLock) {
            final int[] previousExemptionList = mPowerExemptAllAppIds;
            final int[] previousTempExemptionList = mTempExemptAppIds;

            mPowerExemptAllAppIds = powerSaveExemptionListExceptIdleAppIdArray;
            mTempExemptAppIds = tempExemptionListAppIdArray;
            mPowerExemptUserAppIds = powerSaveExemptionListUserAppIdArray;

            if (isAnyAppIdUnexempt(previousExemptionList, mPowerExemptAllAppIds)) {
                mHandler.notifyAllUnexempted();
            } else if (!Arrays.equals(previousExemptionList, mPowerExemptAllAppIds)) {
                mHandler.notifyAllExemptionListChanged();
            }

            if (!Arrays.equals(previousTempExemptionList, mTempExemptAppIds)) {
                mHandler.notifyTempExemptionListChanged();
            }

        }
    }

    /**
     * @return 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 isAnyAppIdUnexempt(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 listener to get callbacks when any state changes.
     */
    public void addListener(@NonNull Listener listener) {
        synchronized (mLock) {
            mListeners.add(listener);
        }
    }

    /**
     * @return whether alarms should be restricted for a UID package-name, due to explicit
     * user-forced app standby. Use {{@link #areAlarmsRestrictedByBatterySaver} to check for
     * restrictions induced by battery saver.
     */
    public boolean areAlarmsRestricted(int uid, @NonNull String packageName) {
        if (isUidActive(uid)) {
            return false;
        }
        synchronized (mLock) {
            final int appId = UserHandle.getAppId(uid);
            if (ArrayUtils.contains(mPowerExemptAllAppIds, appId)) {
                return false;
            }
            // If apps will be put into restricted standby bucket automatically on user-forced
            // app standby, instead of blocking alarms completely, let the restricted standby bucket
            // policy take care of it.
            return (!mActivityManagerInternal.isBgAutoRestrictedBucketFeatureFlagEnabled()
                    && isRunAnyRestrictedLocked(uid, packageName));
        }
    }

    /**
     * @return whether alarms should be restricted when due to battery saver.
     */
    public boolean areAlarmsRestrictedByBatterySaver(int uid, @NonNull String packageName) {
        if (isUidActive(uid)) {
            return false;
        }
        synchronized (mLock) {
            final int appId = UserHandle.getAppId(uid);
            if (ArrayUtils.contains(mPowerExemptAllAppIds, appId)) {
                return false;
            }
            final int userId = UserHandle.getUserId(uid);
            if (mAppStandbyInternal.isAppIdleEnabled() && !mAppStandbyInternal.isInParole()
                    && mExemptedBucketPackages.contains(userId, packageName)) {
                return false;
            }
            return mForceAllAppsStandby;
        }
    }


    /**
     * @return whether jobs should be restricted for a UID package-name. This could be due to
     * battery saver or user-forced app standby
     */
    public boolean areJobsRestricted(int uid, @NonNull String packageName,
            boolean hasForegroundExemption) {
        if (isUidActive(uid)) {
            return false;
        }
        synchronized (mLock) {
            final int appId = UserHandle.getAppId(uid);
            if (ArrayUtils.contains(mPowerExemptAllAppIds, appId)
                    || ArrayUtils.contains(mTempExemptAppIds, appId)) {
                return false;
            }
            // If apps will be put into restricted standby bucket automatically on user-forced
            // app standby, instead of blocking jobs completely, let the restricted standby bucket
            // policy take care of it.
            if (!mActivityManagerInternal.isBgAutoRestrictedBucketFeatureFlagEnabled()
                    && isRunAnyRestrictedLocked(uid, packageName)) {
                return true;
            }
            if (hasForegroundExemption) {
                return false;
            }
            final int userId = UserHandle.getUserId(uid);
            if (mAppStandbyInternal.isAppIdleEnabled() && !mAppStandbyInternal.isInParole()
                    && mExemptedBucketPackages.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 force all apps standby is enabled or not.
     */
    public 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 exemption list or not.
     *
     * Note clients normally shouldn't need to access it. It's only for dumpsys.
     */
    public boolean isUidPowerSaveExempt(int uid) {
        synchronized (mLock) {
            return ArrayUtils.contains(mPowerExemptAllAppIds, UserHandle.getAppId(uid));
        }
    }

    /**
     * @param uid the uid to check for
     * @return whether a UID is in the user defined power-save exemption list or not.
     */
    public boolean isUidPowerSaveUserExempt(int uid) {
        synchronized (mLock) {
            return ArrayUtils.contains(mPowerExemptUserAppIds, UserHandle.getAppId(uid));
        }
    }

    /**
     * @return whether a UID is in the temp power-save exemption list or not.
     *
     * Note clients normally shouldn't need to access it. It's only for dumpsys.
     */
    public boolean isUidTempPowerSaveExempt(int uid) {
        synchronized (mLock) {
            return ArrayUtils.contains(mTempExemptAppIds, UserHandle.getAppId(uid));
        }
    }

    /**
     * Dump the internal state to the given PrintWriter. Can be included in the dump
     * of a binder service to be output on the shell command "dumpsys".
     */
    public void dump(IndentingPrintWriter pw) {
        synchronized (mLock) {
            pw.println("Current AppStateTracker State:");

            pw.increaseIndent();
            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("Except-idle + user exemption list appids: ");
            pw.println(Arrays.toString(mPowerExemptAllAppIds));

            pw.print("User exemption list appids: ");
            pw.println(Arrays.toString(mPowerExemptUserAppIds));

            pw.print("Temp exemption list appids: ");
            pw.println(Arrays.toString(mTempExemptAppIds));

            pw.println("Exempted bucket packages:");
            pw.increaseIndent();
            for (int i = 0; i < mExemptedBucketPackages.size(); i++) {
                pw.print("User ");
                pw.print(mExemptedBucketPackages.keyAt(i));
                pw.println();

                pw.increaseIndent();
                for (int j = 0; j < mExemptedBucketPackages.sizeAt(i); j++) {
                    pw.print(mExemptedBucketPackages.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);
            pw.decreaseIndent();
        }
    }

    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("]");
    }

    /**
     * Proto version of {@link #dump(IndentingPrintWriter)}
     */
    public void dumpProto(ProtoOutputStream proto, long fieldId) {
        synchronized (mLock) {
            final long token = proto.start(fieldId);

            proto.write(AppStateTrackerProto.FORCE_ALL_APPS_STANDBY,
                    isForceAllAppsStandbyEnabled());
            proto.write(AppStateTrackerProto.IS_SMALL_BATTERY_DEVICE, isSmallBatteryDevice());
            proto.write(AppStateTrackerProto.FORCE_ALL_APPS_STANDBY_FOR_SMALL_BATTERY,
                    mForceAllAppStandbyForSmallBattery);
            proto.write(AppStateTrackerProto.IS_PLUGGED_IN, mIsPluggedIn);

            for (int i = 0; i < mActiveUids.size(); i++) {
                if (mActiveUids.valueAt(i)) {
                    proto.write(AppStateTrackerProto.ACTIVE_UIDS, mActiveUids.keyAt(i));
                }
            }

            for (int appId : mPowerExemptAllAppIds) {
                proto.write(AppStateTrackerProto.POWER_SAVE_EXEMPT_APP_IDS, appId);
            }

            for (int appId : mPowerExemptUserAppIds) {
                proto.write(AppStateTrackerProto.POWER_SAVE_USER_EXEMPT_APP_IDS, appId);
            }

            for (int appId : mTempExemptAppIds) {
                proto.write(AppStateTrackerProto.TEMP_POWER_SAVE_EXEMPT_APP_IDS, appId);
            }

            for (int i = 0; i < mExemptedBucketPackages.size(); i++) {
                for (int j = 0; j < mExemptedBucketPackages.sizeAt(i); j++) {
                    final long token2 = proto.start(AppStateTrackerProto.EXEMPTED_BUCKET_PACKAGES);

                    proto.write(ExemptedPackage.USER_ID, mExemptedBucketPackages.keyAt(i));
                    proto.write(ExemptedPackage.PACKAGE_NAME,
                            mExemptedBucketPackages.valueAt(i, j));

                    proto.end(token2);
                }
            }

            for (Pair<Integer, String> uidAndPackage : mRunAnyRestrictedPackages) {
                final long token2 = proto.start(
                        AppStateTrackerProto.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, AppStateTrackerProto.STATS);

            proto.end(token);
        }
    }
}