Historical app ops.

This change is the main check in for the historical app op feature.
The idea is to store a historical data about past app op rejections,
accesses, and durations per op for any UID state indefinitely.

Keeping all operations on record is not practical as app ops are
very frequently performed. To address this we are storing aggregated
data as snapshots where we store for every UID and its packages
how many times each op was accessed, rejected, lasted as an aggregate.

To allow history scaling indefinitely we are taking a logarithmic
approach with only the most recent state stored in memory and all
preceding state stored on disk. State on disk is stored in separate
files where each preceding file, i.e. for an older period, would
cover X times longer period with X number of snapshots covering
X times longer period. Initially X is ten but can be tweaked. For
example, the first file could contain data for ten days with daily
snapshots, while the file for older period would have data
for a hundred days with snapshots every ten days, etc.

The implementation is optimized for fast history update and no impact
on system runtime performance and minimizing memory footprint. We
are lazily persisting state to disk on a dedicated thread as this is
slow. We are also reading the relevant historical files on a query
as this is very rare as opposed to state updates.

The base snapshot interval, i.e. snapshot time span, in the initial
iteration and the logarithmic step are configurable. These can be
changed dynamically and the history would be rewriten to take this
into account.

Test: atest CtsAppOpsTestCases

bug:111061782

Change-Id: I55c32c79911ba12b2ace58d2a782b8df1e6bff60
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
new file mode 100644
index 0000000..7ede6dc
--- /dev/null
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -0,0 +1,4391 @@
+/*
+ * Copyright (C) 2012 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.appop;
+
+import static android.app.AppOpsManager.OP_PLAY_AUDIO;
+import static android.app.AppOpsManager.OP_NONE;
+import static android.app.AppOpsManager.UID_STATE_BACKGROUND;
+import static android.app.AppOpsManager.UID_STATE_CACHED;
+import static android.app.AppOpsManager.UID_STATE_FOREGROUND;
+import static android.app.AppOpsManager.UID_STATE_FOREGROUND_SERVICE;
+import static android.app.AppOpsManager.UID_STATE_LAST_NON_RESTRICTED;
+import static android.app.AppOpsManager.UID_STATE_PERSISTENT;
+import static android.app.AppOpsManager.UID_STATE_TOP;
+import static android.app.AppOpsManager._NUM_UID_STATE;
+import static android.app.AppOpsManager.modeToName;
+import static android.app.AppOpsManager.opToName;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.ActivityThread;
+import android.app.AppGlobals;
+import android.app.AppOpsManager;
+import android.app.AppOpsManager.HistoricalOps;
+import android.app.AppOpsManagerInternal;
+import android.app.AppOpsManagerInternal.CheckOpsDelegate;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
+import android.content.pm.UserInfo;
+import android.database.ContentObserver;
+import android.media.AudioAttributes;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ServiceManager;
+import android.os.ShellCallback;
+import android.os.ShellCommand;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.os.storage.StorageManager;
+import android.os.storage.StorageManagerInternal;
+import android.provider.Settings;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.AtomicFile;
+import android.util.KeyValueListParser;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
+import android.util.TimeUtils;
+import android.util.Xml;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.IAppOpsActiveCallback;
+import com.android.internal.app.IAppOpsCallback;
+import com.android.internal.app.IAppOpsNotedCallback;
+import com.android.internal.app.IAppOpsService;
+import com.android.internal.os.Zygote;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.DumpUtils;
+import com.android.internal.util.FastXmlSerializer;
+import com.android.internal.util.Preconditions;
+import com.android.internal.util.XmlUtils;
+import com.android.internal.util.function.pooled.PooledLambda;
+
+import com.android.server.LocalServices;
+import com.android.server.LockGuard;
+import libcore.util.EmptyArray;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+public class AppOpsService extends IAppOpsService.Stub {
+    static final String TAG = "AppOps";
+    static final boolean DEBUG = false;
+
+    private static final int NO_VERSION = -1;
+    /** Increment by one every time and add the corresponding upgrade logic in
+     *  {@link #upgradeLocked(int)} below. The first version was 1 */
+    private static final int CURRENT_VERSION = 1;
+
+    // Write at most every 30 minutes.
+    static final long WRITE_DELAY = DEBUG ? 1000 : 30*60*1000;
+
+    // Constant meaning that any UID should be matched when dispatching callbacks
+    private static final int UID_ANY = -2;
+
+    // Map from process states to the uid states we track.
+    private static final int[] PROCESS_STATE_TO_UID_STATE = new int[] {
+        UID_STATE_PERSISTENT,           // ActivityManager.PROCESS_STATE_PERSISTENT
+        UID_STATE_PERSISTENT,           // ActivityManager.PROCESS_STATE_PERSISTENT_UI
+        UID_STATE_TOP,                  // ActivityManager.PROCESS_STATE_TOP
+        UID_STATE_FOREGROUND_SERVICE,   // ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE
+        UID_STATE_FOREGROUND,           // ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE
+        UID_STATE_FOREGROUND,           // ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND
+        UID_STATE_BACKGROUND,           // ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
+        UID_STATE_BACKGROUND,           // ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND
+        UID_STATE_BACKGROUND,           // ActivityManager.PROCESS_STATE_BACKUP
+        UID_STATE_BACKGROUND,           // ActivityManager.PROCESS_STATE_SERVICE
+        UID_STATE_BACKGROUND,           // ActivityManager.PROCESS_STATE_RECEIVER
+        UID_STATE_CACHED,               // ActivityManager.PROCESS_STATE_TOP_SLEEPING
+        UID_STATE_CACHED,               // ActivityManager.PROCESS_STATE_HEAVY_WEIGHT
+        UID_STATE_CACHED,               // ActivityManager.PROCESS_STATE_HOME
+        UID_STATE_CACHED,               // ActivityManager.PROCESS_STATE_LAST_ACTIVITY
+        UID_STATE_CACHED,               // ActivityManager.PROCESS_STATE_CACHED_ACTIVITY
+        UID_STATE_CACHED,               // ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT
+        UID_STATE_CACHED,               // ActivityManager.PROCESS_STATE_CACHED_RECENT
+        UID_STATE_CACHED,               // ActivityManager.PROCESS_STATE_CACHED_EMPTY
+        UID_STATE_CACHED,               // ActivityManager.PROCESS_STATE_NONEXISTENT
+    };
+
+    static final String[] UID_STATE_NAMES = new String[] {
+            "pers ",    // UID_STATE_PERSISTENT
+            "top  ",    // UID_STATE_TOP
+            "fgsvc",    // UID_STATE_FOREGROUND_SERVICE
+            "fg   ",    // UID_STATE_FOREGROUND
+            "bg   ",    // UID_STATE_BACKGROUND
+            "cch  ",    // UID_STATE_CACHED
+    };
+
+    static final String[] UID_STATE_TIME_ATTRS = new String[] {
+            "tp",       // UID_STATE_PERSISTENT
+            "tt",       // UID_STATE_TOP
+            "tfs",      // UID_STATE_FOREGROUND_SERVICE
+            "tf",       // UID_STATE_FOREGROUND
+            "tb",       // UID_STATE_BACKGROUND
+            "tc",       // UID_STATE_CACHED
+    };
+
+    static final String[] UID_STATE_REJECT_ATTRS = new String[] {
+            "rp",       // UID_STATE_PERSISTENT
+            "rt",       // UID_STATE_TOP
+            "rfs",      // UID_STATE_FOREGROUND_SERVICE
+            "rf",       // UID_STATE_FOREGROUND
+            "rb",       // UID_STATE_BACKGROUND
+            "rc",       // UID_STATE_CACHED
+    };
+
+    Context mContext;
+    final AtomicFile mFile;
+    final Handler mHandler;
+
+    private final AppOpsManagerInternalImpl mAppOpsManagerInternal
+            = new AppOpsManagerInternalImpl();
+
+    boolean mWriteScheduled;
+    boolean mFastWriteScheduled;
+    final Runnable mWriteRunner = new Runnable() {
+        public void run() {
+            synchronized (AppOpsService.this) {
+                mWriteScheduled = false;
+                mFastWriteScheduled = false;
+                AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
+                    @Override protected Void doInBackground(Void... params) {
+                        writeState();
+                        return null;
+                    }
+                };
+                task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[])null);
+            }
+        }
+    };
+
+    @VisibleForTesting
+    final SparseArray<UidState> mUidStates = new SparseArray<>();
+
+    private final HistoricalRegistry mHistoricalRegistry = new HistoricalRegistry(this);
+
+    long mLastRealtime;
+
+    /*
+     * These are app op restrictions imposed per user from various parties.
+     */
+    private final ArrayMap<IBinder, ClientRestrictionState> mOpUserRestrictions = new ArrayMap<>();
+
+    SparseIntArray mProfileOwners;
+
+    @GuardedBy("this")
+    private CheckOpsDelegate mCheckOpsDelegate;
+
+    /**
+     * All times are in milliseconds. These constants are kept synchronized with the system
+     * global Settings. Any access to this class or its fields should be done while
+     * holding the AppOpsService lock.
+     */
+    private final class Constants extends ContentObserver {
+        // Key names stored in the settings value.
+        private static final String KEY_TOP_STATE_SETTLE_TIME = "top_state_settle_time";
+        private static final String KEY_FG_SERVICE_STATE_SETTLE_TIME
+                = "fg_service_state_settle_time";
+        private static final String KEY_BG_STATE_SETTLE_TIME = "bg_state_settle_time";
+
+        /**
+         * How long we want for a drop in uid state from top to settle before applying it.
+         * @see Settings.Global#APP_OPS_CONSTANTS
+         * @see #KEY_TOP_STATE_SETTLE_TIME
+         */
+        public long TOP_STATE_SETTLE_TIME;
+
+        /**
+         * How long we want for a drop in uid state from foreground to settle before applying it.
+         * @see Settings.Global#APP_OPS_CONSTANTS
+         * @see #KEY_FG_SERVICE_STATE_SETTLE_TIME
+         */
+        public long FG_SERVICE_STATE_SETTLE_TIME;
+
+        /**
+         * How long we want for a drop in uid state from background to settle before applying it.
+         * @see Settings.Global#APP_OPS_CONSTANTS
+         * @see #KEY_BG_STATE_SETTLE_TIME
+         */
+        public long BG_STATE_SETTLE_TIME;
+
+        private final KeyValueListParser mParser = new KeyValueListParser(',');
+        private ContentResolver mResolver;
+
+        public Constants(Handler handler) {
+            super(handler);
+            updateConstants();
+        }
+
+        public void startMonitoring(ContentResolver resolver) {
+            mResolver = resolver;
+            mResolver.registerContentObserver(
+                    Settings.Global.getUriFor(Settings.Global.APP_OPS_CONSTANTS),
+                    false, this);
+            updateConstants();
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            updateConstants();
+        }
+
+        private void updateConstants() {
+            String value = mResolver != null ? Settings.Global.getString(mResolver,
+                    Settings.Global.APP_OPS_CONSTANTS) : "";
+
+            synchronized (AppOpsService.this) {
+                try {
+                    mParser.setString(value);
+                } catch (IllegalArgumentException e) {
+                    // Failed to parse the settings string, log this and move on
+                    // with defaults.
+                    Slog.e(TAG, "Bad app ops settings", e);
+                }
+                TOP_STATE_SETTLE_TIME = mParser.getDurationMillis(
+                        KEY_TOP_STATE_SETTLE_TIME, 30 * 1000L);
+                FG_SERVICE_STATE_SETTLE_TIME = mParser.getDurationMillis(
+                        KEY_FG_SERVICE_STATE_SETTLE_TIME, 10 * 1000L);
+                BG_STATE_SETTLE_TIME = mParser.getDurationMillis(
+                        KEY_BG_STATE_SETTLE_TIME, 1 * 1000L);
+            }
+        }
+
+        void dump(PrintWriter pw) {
+            pw.println("  Settings:");
+
+            pw.print("    "); pw.print(KEY_TOP_STATE_SETTLE_TIME); pw.print("=");
+            TimeUtils.formatDuration(TOP_STATE_SETTLE_TIME, pw);
+            pw.println();
+            pw.print("    "); pw.print(KEY_FG_SERVICE_STATE_SETTLE_TIME); pw.print("=");
+            TimeUtils.formatDuration(FG_SERVICE_STATE_SETTLE_TIME, pw);
+            pw.println();
+            pw.print("    "); pw.print(KEY_BG_STATE_SETTLE_TIME); pw.print("=");
+            TimeUtils.formatDuration(BG_STATE_SETTLE_TIME, pw);
+            pw.println();
+        }
+    }
+
+    private final Constants mConstants;
+
+    @VisibleForTesting
+    static final class UidState {
+        public final int uid;
+
+        public int state = UID_STATE_CACHED;
+        public int pendingState = UID_STATE_CACHED;
+        public long pendingStateCommitTime;
+
+        public int startNesting;
+        public ArrayMap<String, Ops> pkgOps;
+        public SparseIntArray opModes;
+
+        // true indicates there is an interested observer, false there isn't but it has such an op
+        public SparseBooleanArray foregroundOps;
+        public boolean hasForegroundWatchers;
+
+        public UidState(int uid) {
+            this.uid = uid;
+        }
+
+        public void clear() {
+            pkgOps = null;
+            opModes = null;
+        }
+
+        public boolean isDefault() {
+            return (pkgOps == null || pkgOps.isEmpty())
+                    && (opModes == null || opModes.size() <= 0);
+        }
+
+        int evalMode(int mode) {
+            if (mode == AppOpsManager.MODE_FOREGROUND) {
+                return state <= UID_STATE_LAST_NON_RESTRICTED
+                        ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED;
+            }
+            return mode;
+        }
+
+        private void evalForegroundWatchers(int op, SparseArray<ArraySet<ModeCallback>> watchers,
+                SparseBooleanArray which) {
+            boolean curValue = which.get(op, false);
+            ArraySet<ModeCallback> callbacks = watchers.get(op);
+            if (callbacks != null) {
+                for (int cbi = callbacks.size() - 1; !curValue && cbi >= 0; cbi--) {
+                    if ((callbacks.valueAt(cbi).mFlags
+                            & AppOpsManager.WATCH_FOREGROUND_CHANGES) != 0) {
+                        hasForegroundWatchers = true;
+                        curValue = true;
+                    }
+                }
+            }
+            which.put(op, curValue);
+        }
+
+        public void evalForegroundOps(SparseArray<ArraySet<ModeCallback>> watchers) {
+            SparseBooleanArray which = null;
+            hasForegroundWatchers = false;
+            if (opModes != null) {
+                for (int i = opModes.size() - 1; i >= 0; i--) {
+                    if (opModes.valueAt(i) == AppOpsManager.MODE_FOREGROUND) {
+                        if (which == null) {
+                            which = new SparseBooleanArray();
+                        }
+                        evalForegroundWatchers(opModes.keyAt(i), watchers, which);
+                    }
+                }
+            }
+            if (pkgOps != null) {
+                for (int i = pkgOps.size() - 1; i >= 0; i--) {
+                    Ops ops = pkgOps.valueAt(i);
+                    for (int j = ops.size() - 1; j >= 0; j--) {
+                        if (ops.valueAt(j).mode == AppOpsManager.MODE_FOREGROUND) {
+                            if (which == null) {
+                                which = new SparseBooleanArray();
+                            }
+                            evalForegroundWatchers(ops.keyAt(j), watchers, which);
+                        }
+                    }
+                }
+            }
+            foregroundOps = which;
+        }
+    }
+
+    final static class Ops extends SparseArray<Op> {
+        final String packageName;
+        final UidState uidState;
+        final boolean isPrivileged;
+
+        Ops(String _packageName, UidState _uidState, boolean _isPrivileged) {
+            packageName = _packageName;
+            uidState = _uidState;
+            isPrivileged = _isPrivileged;
+        }
+    }
+
+    final static class Op {
+        final UidState uidState;
+        final int uid;
+        final String packageName;
+        final int op;
+        int proxyUid = -1;
+        String proxyPackageName;
+        int mode;
+        int duration;
+        long time[] = new long[_NUM_UID_STATE];
+        long rejectTime[] = new long[_NUM_UID_STATE];
+        int startNesting;
+        long startRealtime;
+
+        Op(UidState _uidState, String _packageName, int _op) {
+            uidState = _uidState;
+            uid = _uidState.uid;
+            packageName = _packageName;
+            op = _op;
+            mode = AppOpsManager.opToDefaultMode(op);
+        }
+
+        boolean hasAnyTime() {
+            for (int i = 0; i < AppOpsManager._NUM_UID_STATE; i++) {
+                if (time[i] != 0) {
+                    return true;
+                }
+                if (rejectTime[i] != 0) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        int getMode() {
+            return uidState.evalMode(mode);
+        }
+    }
+
+    final SparseArray<ArraySet<ModeCallback>> mOpModeWatchers = new SparseArray<>();
+    final ArrayMap<String, ArraySet<ModeCallback>> mPackageModeWatchers = new ArrayMap<>();
+    final ArrayMap<IBinder, ModeCallback> mModeWatchers = new ArrayMap<>();
+    final ArrayMap<IBinder, SparseArray<ActiveCallback>> mActiveWatchers = new ArrayMap<>();
+    final ArrayMap<IBinder, SparseArray<NotedCallback>> mNotedWatchers = new ArrayMap<>();
+    final SparseArray<SparseArray<Restriction>> mAudioRestrictions = new SparseArray<>();
+
+    final class ModeCallback implements DeathRecipient {
+        final IAppOpsCallback mCallback;
+        final int mWatchingUid;
+        final int mFlags;
+        final int mCallingUid;
+        final int mCallingPid;
+
+        ModeCallback(IAppOpsCallback callback, int watchingUid, int flags, int callingUid,
+                int callingPid) {
+            mCallback = callback;
+            mWatchingUid = watchingUid;
+            mFlags = flags;
+            mCallingUid = callingUid;
+            mCallingPid = callingPid;
+            try {
+                mCallback.asBinder().linkToDeath(this, 0);
+            } catch (RemoteException e) {
+                /*ignored*/
+            }
+        }
+
+        public boolean isWatchingUid(int uid) {
+            return uid == UID_ANY || mWatchingUid < 0 || mWatchingUid == uid;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder(128);
+            sb.append("ModeCallback{");
+            sb.append(Integer.toHexString(System.identityHashCode(this)));
+            sb.append(" watchinguid=");
+            UserHandle.formatUid(sb, mWatchingUid);
+            sb.append(" flags=0x");
+            sb.append(Integer.toHexString(mFlags));
+            sb.append(" from uid=");
+            UserHandle.formatUid(sb, mCallingUid);
+            sb.append(" pid=");
+            sb.append(mCallingPid);
+            sb.append('}');
+            return sb.toString();
+        }
+
+        void unlinkToDeath() {
+            mCallback.asBinder().unlinkToDeath(this, 0);
+        }
+
+        @Override
+        public void binderDied() {
+            stopWatchingMode(mCallback);
+        }
+    }
+
+    final class ActiveCallback implements DeathRecipient {
+        final IAppOpsActiveCallback mCallback;
+        final int mWatchingUid;
+        final int mCallingUid;
+        final int mCallingPid;
+
+        ActiveCallback(IAppOpsActiveCallback callback, int watchingUid, int callingUid,
+                int callingPid) {
+            mCallback = callback;
+            mWatchingUid = watchingUid;
+            mCallingUid = callingUid;
+            mCallingPid = callingPid;
+            try {
+                mCallback.asBinder().linkToDeath(this, 0);
+            } catch (RemoteException e) {
+                /*ignored*/
+            }
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder(128);
+            sb.append("ActiveCallback{");
+            sb.append(Integer.toHexString(System.identityHashCode(this)));
+            sb.append(" watchinguid=");
+            UserHandle.formatUid(sb, mWatchingUid);
+            sb.append(" from uid=");
+            UserHandle.formatUid(sb, mCallingUid);
+            sb.append(" pid=");
+            sb.append(mCallingPid);
+            sb.append('}');
+            return sb.toString();
+        }
+
+        void destroy() {
+            mCallback.asBinder().unlinkToDeath(this, 0);
+        }
+
+        @Override
+        public void binderDied() {
+            stopWatchingActive(mCallback);
+        }
+    }
+
+    final class NotedCallback implements DeathRecipient {
+        final IAppOpsNotedCallback mCallback;
+        final int mWatchingUid;
+        final int mCallingUid;
+        final int mCallingPid;
+
+        NotedCallback(IAppOpsNotedCallback callback, int watchingUid, int callingUid,
+                int callingPid) {
+            mCallback = callback;
+            mWatchingUid = watchingUid;
+            mCallingUid = callingUid;
+            mCallingPid = callingPid;
+            try {
+                mCallback.asBinder().linkToDeath(this, 0);
+            } catch (RemoteException e) {
+                /*ignored*/
+            }
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder(128);
+            sb.append("NotedCallback{");
+            sb.append(Integer.toHexString(System.identityHashCode(this)));
+            sb.append(" watchinguid=");
+            UserHandle.formatUid(sb, mWatchingUid);
+            sb.append(" from uid=");
+            UserHandle.formatUid(sb, mCallingUid);
+            sb.append(" pid=");
+            sb.append(mCallingPid);
+            sb.append('}');
+            return sb.toString();
+        }
+
+        void destroy() {
+            mCallback.asBinder().unlinkToDeath(this, 0);
+        }
+
+        @Override
+        public void binderDied() {
+            stopWatchingNoted(mCallback);
+        }
+    }
+
+    final ArrayMap<IBinder, ClientState> mClients = new ArrayMap<>();
+
+    final class ClientState extends Binder implements DeathRecipient {
+        final ArrayList<Op> mStartedOps = new ArrayList<>();
+        final IBinder mAppToken;
+        final int mPid;
+
+        ClientState(IBinder appToken) {
+            mAppToken = appToken;
+            mPid = Binder.getCallingPid();
+            // Watch only for remote processes dying
+            if (!(appToken instanceof Binder)) {
+                try {
+                    mAppToken.linkToDeath(this, 0);
+                } catch (RemoteException e) {
+                    /* do nothing */
+                }
+            }
+        }
+
+        @Override
+        public String toString() {
+            return "ClientState{" +
+                    "mAppToken=" + mAppToken +
+                    ", " + "pid=" + mPid +
+                    '}';
+        }
+
+        @Override
+        public void binderDied() {
+            synchronized (AppOpsService.this) {
+                for (int i=mStartedOps.size()-1; i>=0; i--) {
+                    finishOperationLocked(mStartedOps.get(i), /*finishNested*/ true);
+                }
+                mClients.remove(mAppToken);
+            }
+        }
+    }
+
+    public AppOpsService(File storagePath, Handler handler) {
+        LockGuard.installLock(this, LockGuard.INDEX_APP_OPS);
+        mFile = new AtomicFile(storagePath, "appops");
+        mHandler = handler;
+        mConstants = new Constants(mHandler);
+        readState();
+    }
+
+    public void publish(Context context) {
+        mContext = context;
+        ServiceManager.addService(Context.APP_OPS_SERVICE, asBinder());
+        LocalServices.addService(AppOpsManagerInternal.class, mAppOpsManagerInternal);
+    }
+
+    public void systemReady() {
+        mConstants.startMonitoring(mContext.getContentResolver());
+        mHistoricalRegistry.systemReady(mContext.getContentResolver());
+
+        synchronized (this) {
+            boolean changed = false;
+            for (int i = mUidStates.size() - 1; i >= 0; i--) {
+                UidState uidState = mUidStates.valueAt(i);
+
+                String[] packageNames = getPackagesForUid(uidState.uid);
+                if (ArrayUtils.isEmpty(packageNames)) {
+                    uidState.clear();
+                    mUidStates.removeAt(i);
+                    changed = true;
+                    continue;
+                }
+
+                ArrayMap<String, Ops> pkgs = uidState.pkgOps;
+                if (pkgs == null) {
+                    continue;
+                }
+
+                Iterator<Ops> it = pkgs.values().iterator();
+                while (it.hasNext()) {
+                    Ops ops = it.next();
+                    int curUid = -1;
+                    try {
+                        curUid = AppGlobals.getPackageManager().getPackageUid(ops.packageName,
+                                PackageManager.MATCH_UNINSTALLED_PACKAGES,
+                                UserHandle.getUserId(ops.uidState.uid));
+                    } catch (RemoteException ignored) {
+                    }
+                    if (curUid != ops.uidState.uid) {
+                        Slog.i(TAG, "Pruning old package " + ops.packageName
+                                + "/" + ops.uidState + ": new uid=" + curUid);
+                        it.remove();
+                        changed = true;
+                    }
+                }
+
+                if (uidState.isDefault()) {
+                    mUidStates.removeAt(i);
+                }
+            }
+            if (changed) {
+                scheduleFastWriteLocked();
+            }
+        }
+
+        final IntentFilter packageSuspendFilter = new IntentFilter();
+        packageSuspendFilter.addAction(Intent.ACTION_PACKAGES_UNSUSPENDED);
+        packageSuspendFilter.addAction(Intent.ACTION_PACKAGES_SUSPENDED);
+        mContext.registerReceiver(new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                final int[] changedUids = intent.getIntArrayExtra(Intent.EXTRA_CHANGED_UID_LIST);
+                final String[] changedPkgs = intent.getStringArrayExtra(
+                        Intent.EXTRA_CHANGED_PACKAGE_LIST);
+                final ArraySet<ModeCallback> callbacks = mOpModeWatchers.get(OP_PLAY_AUDIO);
+                for (int i = 0; i < changedUids.length; i++) {
+                    final int changedUid = changedUids[i];
+                    final String changedPkg = changedPkgs[i];
+                    // We trust packagemanager to insert matching uid and packageNames in the extras
+                    mHandler.sendMessage(PooledLambda.obtainMessage(AppOpsService::notifyOpChanged,
+                            AppOpsService.this, callbacks, OP_PLAY_AUDIO, changedUid, changedPkg));
+                }
+            }
+        }, packageSuspendFilter);
+
+        PackageManagerInternal packageManagerInternal = LocalServices.getService(
+                PackageManagerInternal.class);
+        packageManagerInternal.setExternalSourcesPolicy(
+                new PackageManagerInternal.ExternalSourcesPolicy() {
+                    @Override
+                    public int getPackageTrustedToInstallApps(String packageName, int uid) {
+                        int appOpMode = checkOperation(AppOpsManager.OP_REQUEST_INSTALL_PACKAGES,
+                                uid, packageName);
+                        switch (appOpMode) {
+                            case AppOpsManager.MODE_ALLOWED:
+                                return PackageManagerInternal.ExternalSourcesPolicy.USER_TRUSTED;
+                            case AppOpsManager.MODE_ERRORED:
+                                return PackageManagerInternal.ExternalSourcesPolicy.USER_BLOCKED;
+                            default:
+                                return PackageManagerInternal.ExternalSourcesPolicy.USER_DEFAULT;
+                        }
+                    }
+                });
+
+        if (!StorageManager.hasIsolatedStorage()) {
+            StorageManagerInternal storageManagerInternal = LocalServices.getService(
+                    StorageManagerInternal.class);
+            storageManagerInternal.addExternalStoragePolicy(
+                    new StorageManagerInternal.ExternalStorageMountPolicy() {
+                        @Override
+                        public int getMountMode(int uid, String packageName) {
+                            if (Process.isIsolated(uid)) {
+                                return Zygote.MOUNT_EXTERNAL_NONE;
+                            }
+                            if (noteOperation(AppOpsManager.OP_READ_EXTERNAL_STORAGE, uid,
+                                    packageName) != AppOpsManager.MODE_ALLOWED) {
+                                return Zygote.MOUNT_EXTERNAL_NONE;
+                            }
+                            if (noteOperation(AppOpsManager.OP_WRITE_EXTERNAL_STORAGE, uid,
+                                    packageName) != AppOpsManager.MODE_ALLOWED) {
+                                return Zygote.MOUNT_EXTERNAL_READ;
+                            }
+                            return Zygote.MOUNT_EXTERNAL_WRITE;
+                        }
+
+                        @Override
+                        public boolean hasExternalStorage(int uid, String packageName) {
+                            final int mountMode = getMountMode(uid, packageName);
+                            return mountMode == Zygote.MOUNT_EXTERNAL_READ
+                                    || mountMode == Zygote.MOUNT_EXTERNAL_WRITE;
+                        }
+                    });
+        }
+    }
+
+    public void packageRemoved(int uid, String packageName) {
+        synchronized (this) {
+            UidState uidState = mUidStates.get(uid);
+            if (uidState == null) {
+                return;
+            }
+
+            Ops ops = null;
+
+            // Remove any package state if such.
+            if (uidState.pkgOps != null) {
+                ops = uidState.pkgOps.remove(packageName);
+            }
+
+            // If we just nuked the last package state check if the UID is valid.
+            if (ops != null && uidState.pkgOps.isEmpty()
+                    && getPackagesForUid(uid).length <= 0) {
+                mUidStates.remove(uid);
+            }
+
+            // Finish ops other packages started on behalf of the package.
+            final int clientCount = mClients.size();
+            for (int i = 0; i < clientCount; i++) {
+                final ClientState client = mClients.valueAt(i);
+                if (client.mStartedOps == null) {
+                    continue;
+                }
+                final int opCount = client.mStartedOps.size();
+                for (int j = opCount - 1; j >= 0; j--) {
+                    final Op op = client.mStartedOps.get(j);
+                    if (uid == op.uid && packageName.equals(op.packageName)) {
+                        finishOperationLocked(op, /*finishNested*/ true);
+                        client.mStartedOps.remove(j);
+                        if (op.startNesting <= 0) {
+                            scheduleOpActiveChangedIfNeededLocked(op.op,
+                                    uid, packageName, false);
+                        }
+                    }
+                }
+            }
+
+            if (ops != null) {
+                scheduleFastWriteLocked();
+
+                final int opCount = ops.size();
+                for (int i = 0; i < opCount; i++) {
+                    final Op op = ops.valueAt(i);
+                    if (op.duration == -1) {
+                        scheduleOpActiveChangedIfNeededLocked(
+                                op.op, op.uid, op.packageName, false);
+                    }
+                }
+            }
+        }
+    }
+
+    public void uidRemoved(int uid) {
+        synchronized (this) {
+            if (mUidStates.indexOfKey(uid) >= 0) {
+                mUidStates.remove(uid);
+                scheduleFastWriteLocked();
+            }
+        }
+    }
+
+    public void updateUidProcState(int uid, int procState) {
+        synchronized (this) {
+            final UidState uidState = getUidStateLocked(uid, true);
+            final int newState = PROCESS_STATE_TO_UID_STATE[procState];
+            if (uidState != null && uidState.pendingState != newState) {
+                final int oldPendingState = uidState.pendingState;
+                uidState.pendingState = newState;
+                if (newState < uidState.state || newState <= UID_STATE_LAST_NON_RESTRICTED) {
+                    // We are moving to a more important state, or the new state is in the
+                    // foreground, then always do it immediately.
+                    commitUidPendingStateLocked(uidState);
+                } else if (uidState.pendingStateCommitTime == 0) {
+                    // We are moving to a less important state for the first time,
+                    // delay the application for a bit.
+                    final long settleTime;
+                    if (uidState.state <= UID_STATE_TOP) {
+                        settleTime = mConstants.TOP_STATE_SETTLE_TIME;
+                    } else if (uidState.state <= UID_STATE_FOREGROUND_SERVICE) {
+                        settleTime = mConstants.FG_SERVICE_STATE_SETTLE_TIME;
+                    } else {
+                        settleTime = mConstants.BG_STATE_SETTLE_TIME;
+                    }
+                    uidState.pendingStateCommitTime = SystemClock.elapsedRealtime() + settleTime;
+                }
+                if (uidState.startNesting != 0) {
+                    // There is some actively running operation...  need to find it
+                    // and appropriately update its state.
+                    final long now = System.currentTimeMillis();
+                    for (int i = uidState.pkgOps.size() - 1; i >= 0; i--) {
+                        final Ops ops = uidState.pkgOps.valueAt(i);
+                        for (int j = ops.size() - 1; j >= 0; j--) {
+                            final Op op = ops.valueAt(j);
+                            if (op.startNesting > 0) {
+                                op.time[oldPendingState] = now;
+                                op.time[newState] = now;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    public void shutdown() {
+        Slog.w(TAG, "Writing app ops before shutdown...");
+        boolean doWrite = false;
+        synchronized (this) {
+            if (mWriteScheduled) {
+                mWriteScheduled = false;
+                doWrite = true;
+            }
+        }
+        if (doWrite) {
+            writeState();
+        }
+    }
+
+    private ArrayList<AppOpsManager.OpEntry> collectOps(Ops pkgOps, int[] ops) {
+        ArrayList<AppOpsManager.OpEntry> resOps = null;
+        final long elapsedNow = SystemClock.elapsedRealtime();
+        if (ops == null) {
+            resOps = new ArrayList<>();
+            for (int j=0; j<pkgOps.size(); j++) {
+                Op curOp = pkgOps.valueAt(j);
+                final boolean running = curOp.duration == -1;
+                long duration = running
+                        ? (elapsedNow - curOp.startRealtime)
+                        : curOp.duration;
+                resOps.add(new AppOpsManager.OpEntry(curOp.op, curOp.mode, curOp.time,
+                        curOp.rejectTime, (int) duration, running, curOp.proxyUid,
+                        curOp.proxyPackageName));
+            }
+        } else {
+            for (int j=0; j<ops.length; j++) {
+                Op curOp = pkgOps.get(ops[j]);
+                if (curOp != null) {
+                    if (resOps == null) {
+                        resOps = new ArrayList<>();
+                    }
+                    final boolean running = curOp.duration == -1;
+                    final long duration = running
+                            ? (elapsedNow - curOp.startRealtime)
+                            : curOp.duration;
+                    resOps.add(new AppOpsManager.OpEntry(curOp.op, curOp.mode, curOp.time,
+                            curOp.rejectTime, (int) duration, running, curOp.proxyUid,
+                            curOp.proxyPackageName));
+                }
+            }
+        }
+        return resOps;
+    }
+
+    private ArrayList<AppOpsManager.OpEntry> collectOps(SparseIntArray uidOps, int[] ops) {
+        if (uidOps == null) {
+            return null;
+        }
+        ArrayList<AppOpsManager.OpEntry> resOps = null;
+        if (ops == null) {
+            resOps = new ArrayList<>();
+            for (int j=0; j<uidOps.size(); j++) {
+                resOps.add(new AppOpsManager.OpEntry(uidOps.keyAt(j), uidOps.valueAt(j),
+                        0, 0, 0, -1, null));
+            }
+        } else {
+            for (int j=0; j<ops.length; j++) {
+                int index = uidOps.indexOfKey(ops[j]);
+                if (index >= 0) {
+                    if (resOps == null) {
+                        resOps = new ArrayList<>();
+                    }
+                    resOps.add(new AppOpsManager.OpEntry(uidOps.keyAt(index), uidOps.valueAt(index),
+                            0, 0, 0, -1, null));
+                }
+            }
+        }
+        return resOps;
+    }
+
+    @Override
+    public List<AppOpsManager.PackageOps> getPackagesForOps(int[] ops) {
+        mContext.enforcePermission(android.Manifest.permission.GET_APP_OPS_STATS,
+                Binder.getCallingPid(), Binder.getCallingUid(), null);
+        ArrayList<AppOpsManager.PackageOps> res = null;
+        synchronized (this) {
+            final int uidStateCount = mUidStates.size();
+            for (int i = 0; i < uidStateCount; i++) {
+                UidState uidState = mUidStates.valueAt(i);
+                if (uidState.pkgOps == null || uidState.pkgOps.isEmpty()) {
+                    continue;
+                }
+                ArrayMap<String, Ops> packages = uidState.pkgOps;
+                final int packageCount = packages.size();
+                for (int j = 0; j < packageCount; j++) {
+                    Ops pkgOps = packages.valueAt(j);
+                    ArrayList<AppOpsManager.OpEntry> resOps = collectOps(pkgOps, ops);
+                    if (resOps != null) {
+                        if (res == null) {
+                            res = new ArrayList<AppOpsManager.PackageOps>();
+                        }
+                        AppOpsManager.PackageOps resPackage = new AppOpsManager.PackageOps(
+                                pkgOps.packageName, pkgOps.uidState.uid, resOps);
+                        res.add(resPackage);
+                    }
+                }
+            }
+        }
+        return res;
+    }
+
+    @Override
+    public List<AppOpsManager.PackageOps> getOpsForPackage(int uid, String packageName,
+            int[] ops) {
+        mContext.enforcePermission(android.Manifest.permission.GET_APP_OPS_STATS,
+                Binder.getCallingPid(), Binder.getCallingUid(), null);
+        String resolvedPackageName = resolvePackageName(uid, packageName);
+        if (resolvedPackageName == null) {
+            return Collections.emptyList();
+        }
+        synchronized (this) {
+            Ops pkgOps = getOpsRawLocked(uid, resolvedPackageName, false /* edit */,
+                    false /* uidMismatchExpected */);
+            if (pkgOps == null) {
+                return null;
+            }
+            ArrayList<AppOpsManager.OpEntry> resOps = collectOps(pkgOps, ops);
+            if (resOps == null) {
+                return null;
+            }
+            ArrayList<AppOpsManager.PackageOps> res = new ArrayList<AppOpsManager.PackageOps>();
+            AppOpsManager.PackageOps resPackage = new AppOpsManager.PackageOps(
+                    pkgOps.packageName, pkgOps.uidState.uid, resOps);
+            res.add(resPackage);
+            return res;
+        }
+    }
+
+    @Override
+    public void getHistoricalOps(int uid, @NonNull String packageName,
+            @Nullable String[] opNames, long beginTimeMillis, long endTimeMillis,
+            @NonNull RemoteCallback callback) {
+        Preconditions.checkArgument(uid == Process.INVALID_UID || uid >= 0,
+                "uid must be " + Process.INVALID_UID + " or non negative");
+        Preconditions.checkArgument(beginTimeMillis >= 0 && beginTimeMillis < endTimeMillis,
+                "beginTimeMillis must be non negative and lesser than endTimeMillis");
+        Preconditions.checkNotNull(callback, "callback cannot be null");
+        checkValidOpsOrNull(opNames);
+
+        mContext.enforcePermission(android.Manifest.permission.GET_APP_OPS_STATS,
+                Binder.getCallingPid(), Binder.getCallingUid(), "getHistoricalOps");
+
+        if (mHistoricalRegistry.getMode() == AppOpsManager.HISTORICAL_MODE_DISABLED) {
+            // TODO (bug:122218838): Remove once the feature fully enabled.
+            getHistoricalPackagesOpsCompat(uid, packageName, opNames, beginTimeMillis,
+                    endTimeMillis, callback);
+        } else {
+            // Must not hold the appops lock
+            mHistoricalRegistry.getHistoricalOps(uid, packageName, opNames,
+                    beginTimeMillis, endTimeMillis, callback);
+        }
+    }
+
+    private void getHistoricalPackagesOpsCompat(int uid, @NonNull String packageName,
+            @Nullable String[] opNames, long beginTimeMillis, long endTimeMillis,
+            @NonNull RemoteCallback callback) {
+        synchronized (AppOpsService.this) {
+            final HistoricalOps ops = new HistoricalOps(beginTimeMillis, endTimeMillis);
+            if (opNames == null) {
+                opNames = AppOpsManager.getOpStrs();
+            }
+            final int uidStateCount = mUidStates.size();
+            for (int uidIdx = 0; uidIdx < uidStateCount; uidIdx++) {
+                final UidState uidState = mUidStates.valueAt(uidIdx);
+                if (uidState.pkgOps == null || uidState.pkgOps.isEmpty()
+                        || (uid != Process.INVALID_UID && uid != uidState.uid)) {
+                    continue;
+                }
+                final ArrayMap<String, Ops> packages = uidState.pkgOps;
+                final int packageCount = packages.size();
+                for (int pkgIdx = 0; pkgIdx < packageCount; pkgIdx++) {
+                    final Ops pkgOps = packages.valueAt(pkgIdx);
+                    if (packageName != null && !packageName.equals(pkgOps.packageName)) {
+                        continue;
+                    }
+                    final int opCount = opNames.length;
+                    for (int opIdx = 0; opIdx < opCount; opIdx++) {
+                        final String opName = opNames[opIdx];
+                        if (!ArrayUtils.contains(opNames, opName)) {
+                            continue;
+                        }
+                        final int opCode = AppOpsManager.strOpToOp(opName);
+                        final Op op = pkgOps.get(opCode);
+                        if (op == null) {
+                            continue;
+                        }
+                        final int stateCount = AppOpsManager._NUM_UID_STATE;
+                        for (int stateIdx = 0; stateIdx < stateCount; stateIdx++) {
+                            if (op.rejectTime[stateIdx] != 0) {
+                                ops.increaseRejectCount(opCode, uidState.uid,
+                                        pkgOps.packageName, stateIdx, 1);
+                            } else if (op.time[stateIdx] != 0) {
+                                ops.increaseAccessCount(opCode, uidState.uid,
+                                        pkgOps.packageName, stateIdx, 1);
+                            }
+                        }
+                    }
+                }
+            }
+            final Bundle payload = new Bundle();
+            payload.putParcelable(AppOpsManager.KEY_HISTORICAL_OPS, ops);
+            callback.sendResult(payload);
+        }
+    }
+
+    @Override
+    public void getHistoricalOpsFromDiskRaw(int uid, @NonNull String packageName,
+            @Nullable String[] opNames, long beginTimeMillis, long endTimeMillis,
+            @NonNull RemoteCallback callback) {
+        Preconditions.checkArgument(uid == Process.INVALID_UID || uid >= 0,
+                "uid must be " + Process.INVALID_UID + " or non negative");
+        Preconditions.checkArgument(beginTimeMillis >= 0 && beginTimeMillis < endTimeMillis,
+                "beginTimeMillis must be non negative and lesser than endTimeMillis");
+        Preconditions.checkNotNull(callback, "callback cannot be null");
+        checkValidOpsOrNull(opNames);
+
+        mContext.enforcePermission(android.Manifest.permission.GET_APP_OPS_STATS,
+                Binder.getCallingPid(), Binder.getCallingUid(), "getHistoricalOps");
+
+        // Must not hold the appops lock
+        mHistoricalRegistry.getHistoricalOpsFromDiskRaw(uid, packageName, opNames,
+                beginTimeMillis, endTimeMillis, callback);
+    }
+
+    @Override
+    public List<AppOpsManager.PackageOps> getUidOps(int uid, int[] ops) {
+        mContext.enforcePermission(android.Manifest.permission.GET_APP_OPS_STATS,
+                Binder.getCallingPid(), Binder.getCallingUid(), null);
+        synchronized (this) {
+            UidState uidState = getUidStateLocked(uid, false);
+            if (uidState == null) {
+                return null;
+            }
+            ArrayList<AppOpsManager.OpEntry> resOps = collectOps(uidState.opModes, ops);
+            if (resOps == null) {
+                return null;
+            }
+            ArrayList<AppOpsManager.PackageOps> res = new ArrayList<AppOpsManager.PackageOps>();
+            AppOpsManager.PackageOps resPackage = new AppOpsManager.PackageOps(
+                    null, uidState.uid, resOps);
+            res.add(resPackage);
+            return res;
+        }
+    }
+
+    private void pruneOp(Op op, int uid, String packageName) {
+        if (!op.hasAnyTime()) {
+            Ops ops = getOpsRawLocked(uid, packageName, false /* edit */,
+                    false /* uidMismatchExpected */);
+            if (ops != null) {
+                ops.remove(op.op);
+                if (ops.size() <= 0) {
+                    UidState uidState = ops.uidState;
+                    ArrayMap<String, Ops> pkgOps = uidState.pkgOps;
+                    if (pkgOps != null) {
+                        pkgOps.remove(ops.packageName);
+                        if (pkgOps.isEmpty()) {
+                            uidState.pkgOps = null;
+                        }
+                        if (uidState.isDefault()) {
+                            mUidStates.remove(uid);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    void enforceManageAppOpsModes(int callingPid, int callingUid, int targetUid) {
+        if (callingPid == Process.myPid()) {
+            return;
+        }
+        final int callingUser = UserHandle.getUserId(callingUid);
+        synchronized (this) {
+            if (mProfileOwners != null && mProfileOwners.get(callingUser, -1) == callingUid) {
+                if (targetUid >= 0 && callingUser == UserHandle.getUserId(targetUid)) {
+                    // Profile owners are allowed to change modes but only for apps
+                    // within their user.
+                    return;
+                }
+            }
+        }
+        mContext.enforcePermission(android.Manifest.permission.MANAGE_APP_OPS_MODES,
+                Binder.getCallingPid(), Binder.getCallingUid(), null);
+    }
+
+    @Override
+    public void setUidMode(int code, int uid, int mode) {
+        if (DEBUG) {
+            Slog.i(TAG, "uid " + uid + " OP_" + opToName(code) + " := " + modeToName(mode)
+                    + " by uid " + Binder.getCallingUid());
+        }
+
+        enforceManageAppOpsModes(Binder.getCallingPid(), Binder.getCallingUid(), uid);
+        verifyIncomingOp(code);
+        code = AppOpsManager.opToSwitch(code);
+
+        synchronized (this) {
+            final int defaultMode = AppOpsManager.opToDefaultMode(code);
+
+            UidState uidState = getUidStateLocked(uid, false);
+            if (uidState == null) {
+                if (mode == defaultMode) {
+                    return;
+                }
+                uidState = new UidState(uid);
+                uidState.opModes = new SparseIntArray();
+                uidState.opModes.put(code, mode);
+                mUidStates.put(uid, uidState);
+                scheduleWriteLocked();
+            } else if (uidState.opModes == null) {
+                if (mode != defaultMode) {
+                    uidState.opModes = new SparseIntArray();
+                    uidState.opModes.put(code, mode);
+                    scheduleWriteLocked();
+                }
+            } else {
+                if (uidState.opModes.indexOfKey(code) >= 0 && uidState.opModes.get(code) == mode) {
+                    return;
+                }
+                if (mode == defaultMode) {
+                    uidState.opModes.delete(code);
+                    if (uidState.opModes.size() <= 0) {
+                        uidState.opModes = null;
+                    }
+                } else {
+                    uidState.opModes.put(code, mode);
+                }
+                scheduleWriteLocked();
+            }
+        }
+
+        String[] uidPackageNames = getPackagesForUid(uid);
+        ArrayMap<ModeCallback, ArraySet<String>> callbackSpecs = null;
+
+        synchronized (this) {
+            ArraySet<ModeCallback> callbacks = mOpModeWatchers.get(code);
+            if (callbacks != null) {
+                final int callbackCount = callbacks.size();
+                for (int i = 0; i < callbackCount; i++) {
+                    ModeCallback callback = callbacks.valueAt(i);
+                    ArraySet<String> changedPackages = new ArraySet<>();
+                    Collections.addAll(changedPackages, uidPackageNames);
+                    if (callbackSpecs == null) {
+                        callbackSpecs = new ArrayMap<>();
+                    }
+                    callbackSpecs.put(callback, changedPackages);
+                }
+            }
+
+            for (String uidPackageName : uidPackageNames) {
+                callbacks = mPackageModeWatchers.get(uidPackageName);
+                if (callbacks != null) {
+                    if (callbackSpecs == null) {
+                        callbackSpecs = new ArrayMap<>();
+                    }
+                    final int callbackCount = callbacks.size();
+                    for (int i = 0; i < callbackCount; i++) {
+                        ModeCallback callback = callbacks.valueAt(i);
+                        ArraySet<String> changedPackages = callbackSpecs.get(callback);
+                        if (changedPackages == null) {
+                            changedPackages = new ArraySet<>();
+                            callbackSpecs.put(callback, changedPackages);
+                        }
+                        changedPackages.add(uidPackageName);
+                    }
+                }
+            }
+        }
+
+        if (callbackSpecs == null) {
+            return;
+        }
+
+        for (int i = 0; i < callbackSpecs.size(); i++) {
+            final ModeCallback callback = callbackSpecs.keyAt(i);
+            final ArraySet<String> reportedPackageNames = callbackSpecs.valueAt(i);
+            if (reportedPackageNames == null) {
+                mHandler.sendMessage(PooledLambda.obtainMessage(
+                        AppOpsService::notifyOpChanged,
+                        this, callback, code, uid, (String) null));
+
+            } else {
+                final int reportedPackageCount = reportedPackageNames.size();
+                for (int j = 0; j < reportedPackageCount; j++) {
+                    final String reportedPackageName = reportedPackageNames.valueAt(j);
+                    mHandler.sendMessage(PooledLambda.obtainMessage(
+                            AppOpsService::notifyOpChanged,
+                            this, callback, code, uid, reportedPackageName));
+                }
+            }
+        }
+    }
+
+    @Override
+    public void setMode(int code, int uid, String packageName, int mode) {
+        setMode(code, uid, packageName, mode, true, false);
+    }
+
+    /**
+     * Sets the mode for a certain op and uid.
+     *
+     * @param code The op code to set
+     * @param uid The UID for which to set
+     * @param packageName The package for which to set
+     * @param mode The new mode to set
+     * @param verifyUid Iff {@code true}, check that the package name belongs to the uid
+     * @param isPrivileged Whether the package is privileged. (Only used if {@code verifyUid ==
+     *                     false})
+     */
+    private void setMode(int code, int uid, @NonNull String packageName, int mode,
+            boolean verifyUid, boolean isPrivileged) {
+        enforceManageAppOpsModes(Binder.getCallingPid(), Binder.getCallingUid(), uid);
+        verifyIncomingOp(code);
+        ArraySet<ModeCallback> repCbs = null;
+        code = AppOpsManager.opToSwitch(code);
+        synchronized (this) {
+            UidState uidState = getUidStateLocked(uid, false);
+            Op op = getOpLocked(code, uid, packageName, true, verifyUid, isPrivileged);
+            if (op != null) {
+                if (op.mode != mode) {
+                    op.mode = mode;
+                    if (uidState != null) {
+                        uidState.evalForegroundOps(mOpModeWatchers);
+                    }
+                    ArraySet<ModeCallback> cbs = mOpModeWatchers.get(code);
+                    if (cbs != null) {
+                        if (repCbs == null) {
+                            repCbs = new ArraySet<>();
+                        }
+                        repCbs.addAll(cbs);
+                    }
+                    cbs = mPackageModeWatchers.get(packageName);
+                    if (cbs != null) {
+                        if (repCbs == null) {
+                            repCbs = new ArraySet<>();
+                        }
+                        repCbs.addAll(cbs);
+                    }
+                    if (mode == AppOpsManager.opToDefaultMode(op.op)) {
+                        // If going into the default mode, prune this op
+                        // if there is nothing else interesting in it.
+                        pruneOp(op, uid, packageName);
+                    }
+                    scheduleFastWriteLocked();
+                }
+            }
+        }
+        if (repCbs != null) {
+            mHandler.sendMessage(PooledLambda.obtainMessage(
+                    AppOpsService::notifyOpChanged,
+                    this, repCbs, code, uid, packageName));
+        }
+    }
+
+    private void notifyOpChanged(ArraySet<ModeCallback> callbacks, int code,
+            int uid, String packageName) {
+        for (int i = 0; i < callbacks.size(); i++) {
+            final ModeCallback callback = callbacks.valueAt(i);
+            notifyOpChanged(callback, code, uid, packageName);
+        }
+    }
+
+    private void notifyOpChanged(ModeCallback callback, int code,
+            int uid, String packageName) {
+        if (uid != UID_ANY && callback.mWatchingUid >= 0 && callback.mWatchingUid != uid) {
+            return;
+        }
+        // There are components watching for mode changes such as window manager
+        // and location manager which are in our process. The callbacks in these
+        // components may require permissions our remote caller does not have.
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            callback.mCallback.opChanged(code, uid, packageName);
+        } catch (RemoteException e) {
+            /* ignore */
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    private static HashMap<ModeCallback, ArrayList<ChangeRec>> addCallbacks(
+            HashMap<ModeCallback, ArrayList<ChangeRec>> callbacks,
+            int op, int uid, String packageName, ArraySet<ModeCallback> cbs) {
+        if (cbs == null) {
+            return callbacks;
+        }
+        if (callbacks == null) {
+            callbacks = new HashMap<>();
+        }
+        boolean duplicate = false;
+        final int N = cbs.size();
+        for (int i=0; i<N; i++) {
+            ModeCallback cb = cbs.valueAt(i);
+            ArrayList<ChangeRec> reports = callbacks.get(cb);
+            if (reports == null) {
+                reports = new ArrayList<>();
+                callbacks.put(cb, reports);
+            } else {
+                final int reportCount = reports.size();
+                for (int j = 0; j < reportCount; j++) {
+                    ChangeRec report = reports.get(j);
+                    if (report.op == op && report.pkg.equals(packageName)) {
+                        duplicate = true;
+                        break;
+                    }
+                }
+            }
+            if (!duplicate) {
+                reports.add(new ChangeRec(op, uid, packageName));
+            }
+        }
+        return callbacks;
+    }
+
+    static final class ChangeRec {
+        final int op;
+        final int uid;
+        final String pkg;
+
+        ChangeRec(int _op, int _uid, String _pkg) {
+            op = _op;
+            uid = _uid;
+            pkg = _pkg;
+        }
+    }
+
+    @Override
+    public void resetAllModes(int reqUserId, String reqPackageName) {
+        final int callingPid = Binder.getCallingPid();
+        final int callingUid = Binder.getCallingUid();
+        reqUserId = ActivityManager.handleIncomingUser(callingPid, callingUid, reqUserId,
+                true, true, "resetAllModes", null);
+
+        int reqUid = -1;
+        if (reqPackageName != null) {
+            try {
+                reqUid = AppGlobals.getPackageManager().getPackageUid(
+                        reqPackageName, PackageManager.MATCH_UNINSTALLED_PACKAGES, reqUserId);
+            } catch (RemoteException e) {
+                /* ignore - local call */
+            }
+        }
+
+        enforceManageAppOpsModes(callingPid, callingUid, reqUid);
+
+        HashMap<ModeCallback, ArrayList<ChangeRec>> callbacks = null;
+        synchronized (this) {
+            boolean changed = false;
+            for (int i = mUidStates.size() - 1; i >= 0; i--) {
+                UidState uidState = mUidStates.valueAt(i);
+
+                SparseIntArray opModes = uidState.opModes;
+                if (opModes != null && (uidState.uid == reqUid || reqUid == -1)) {
+                    final int uidOpCount = opModes.size();
+                    for (int j = uidOpCount - 1; j >= 0; j--) {
+                        final int code = opModes.keyAt(j);
+                        if (AppOpsManager.opAllowsReset(code)) {
+                            opModes.removeAt(j);
+                            if (opModes.size() <= 0) {
+                                uidState.opModes = null;
+                            }
+                            for (String packageName : getPackagesForUid(uidState.uid)) {
+                                callbacks = addCallbacks(callbacks, code, uidState.uid, packageName,
+                                        mOpModeWatchers.get(code));
+                                callbacks = addCallbacks(callbacks, code, uidState.uid, packageName,
+                                        mPackageModeWatchers.get(packageName));
+                            }
+                        }
+                    }
+                }
+
+                if (uidState.pkgOps == null) {
+                    continue;
+                }
+
+                if (reqUserId != UserHandle.USER_ALL
+                        && reqUserId != UserHandle.getUserId(uidState.uid)) {
+                    // Skip any ops for a different user
+                    continue;
+                }
+
+                Map<String, Ops> packages = uidState.pkgOps;
+                Iterator<Map.Entry<String, Ops>> it = packages.entrySet().iterator();
+                boolean uidChanged = false;
+                while (it.hasNext()) {
+                    Map.Entry<String, Ops> ent = it.next();
+                    String packageName = ent.getKey();
+                    if (reqPackageName != null && !reqPackageName.equals(packageName)) {
+                        // Skip any ops for a different package
+                        continue;
+                    }
+                    Ops pkgOps = ent.getValue();
+                    for (int j=pkgOps.size()-1; j>=0; j--) {
+                        Op curOp = pkgOps.valueAt(j);
+                        if (AppOpsManager.opAllowsReset(curOp.op)
+                                && curOp.mode != AppOpsManager.opToDefaultMode(curOp.op)) {
+                            curOp.mode = AppOpsManager.opToDefaultMode(curOp.op);
+                            changed = true;
+                            uidChanged = true;
+                            callbacks = addCallbacks(callbacks, curOp.op, curOp.uid, packageName,
+                                    mOpModeWatchers.get(curOp.op));
+                            callbacks = addCallbacks(callbacks, curOp.op, curOp.uid, packageName,
+                                    mPackageModeWatchers.get(packageName));
+                            if (!curOp.hasAnyTime()) {
+                                pkgOps.removeAt(j);
+                            }
+                        }
+                    }
+                    if (pkgOps.size() == 0) {
+                        it.remove();
+                    }
+                }
+                if (uidState.isDefault()) {
+                    mUidStates.remove(uidState.uid);
+                }
+                if (uidChanged) {
+                    uidState.evalForegroundOps(mOpModeWatchers);
+                }
+            }
+
+            if (changed) {
+                scheduleFastWriteLocked();
+            }
+        }
+        if (callbacks != null) {
+            for (Map.Entry<ModeCallback, ArrayList<ChangeRec>> ent : callbacks.entrySet()) {
+                ModeCallback cb = ent.getKey();
+                ArrayList<ChangeRec> reports = ent.getValue();
+                for (int i=0; i<reports.size(); i++) {
+                    ChangeRec rep = reports.get(i);
+                    mHandler.sendMessage(PooledLambda.obtainMessage(
+                            AppOpsService::notifyOpChanged,
+                            this, cb, rep.op, rep.uid, rep.pkg));
+                }
+            }
+        }
+    }
+
+    private void evalAllForegroundOpsLocked() {
+        for (int uidi = mUidStates.size() - 1; uidi >= 0; uidi--) {
+            final UidState uidState = mUidStates.valueAt(uidi);
+            if (uidState.foregroundOps != null) {
+                uidState.evalForegroundOps(mOpModeWatchers);
+            }
+        }
+    }
+
+    @Override
+    public void startWatchingMode(int op, String packageName, IAppOpsCallback callback) {
+        startWatchingModeWithFlags(op, packageName, 0, callback);
+    }
+
+    @Override
+    public void startWatchingModeWithFlags(int op, String packageName, int flags,
+            IAppOpsCallback callback) {
+        int watchedUid = -1;
+        final int callingUid = Binder.getCallingUid();
+        final int callingPid = Binder.getCallingPid();
+        // TODO: should have a privileged permission to protect this.
+        // Also, if the caller has requested WATCH_FOREGROUND_CHANGES, should we require
+        // the USAGE_STATS permission since this can provide information about when an
+        // app is in the foreground?
+        Preconditions.checkArgumentInRange(op, AppOpsManager.OP_NONE,
+                AppOpsManager._NUM_OP - 1, "Invalid op code: " + op);
+        if (callback == null) {
+            return;
+        }
+        synchronized (this) {
+            op = (op != AppOpsManager.OP_NONE) ? AppOpsManager.opToSwitch(op) : op;
+            ModeCallback cb = mModeWatchers.get(callback.asBinder());
+            if (cb == null) {
+                cb = new ModeCallback(callback, watchedUid, flags, callingUid, callingPid);
+                mModeWatchers.put(callback.asBinder(), cb);
+            }
+            if (op != AppOpsManager.OP_NONE) {
+                ArraySet<ModeCallback> cbs = mOpModeWatchers.get(op);
+                if (cbs == null) {
+                    cbs = new ArraySet<>();
+                    mOpModeWatchers.put(op, cbs);
+                }
+                cbs.add(cb);
+            }
+            if (packageName != null) {
+                ArraySet<ModeCallback> cbs = mPackageModeWatchers.get(packageName);
+                if (cbs == null) {
+                    cbs = new ArraySet<>();
+                    mPackageModeWatchers.put(packageName, cbs);
+                }
+                cbs.add(cb);
+            }
+            evalAllForegroundOpsLocked();
+        }
+    }
+
+    @Override
+    public void stopWatchingMode(IAppOpsCallback callback) {
+        if (callback == null) {
+            return;
+        }
+        synchronized (this) {
+            ModeCallback cb = mModeWatchers.remove(callback.asBinder());
+            if (cb != null) {
+                cb.unlinkToDeath();
+                for (int i=mOpModeWatchers.size()-1; i>=0; i--) {
+                    ArraySet<ModeCallback> cbs = mOpModeWatchers.valueAt(i);
+                    cbs.remove(cb);
+                    if (cbs.size() <= 0) {
+                        mOpModeWatchers.removeAt(i);
+                    }
+                }
+                for (int i=mPackageModeWatchers.size()-1; i>=0; i--) {
+                    ArraySet<ModeCallback> cbs = mPackageModeWatchers.valueAt(i);
+                    cbs.remove(cb);
+                    if (cbs.size() <= 0) {
+                        mPackageModeWatchers.removeAt(i);
+                    }
+                }
+            }
+            evalAllForegroundOpsLocked();
+        }
+    }
+
+    @Override
+    public IBinder getToken(IBinder clientToken) {
+        synchronized (this) {
+            ClientState cs = mClients.get(clientToken);
+            if (cs == null) {
+                cs = new ClientState(clientToken);
+                mClients.put(clientToken, cs);
+            }
+            return cs;
+        }
+    }
+
+    public CheckOpsDelegate getAppOpsServiceDelegate() {
+        synchronized (this) {
+            return mCheckOpsDelegate;
+        }
+    }
+
+    public void setAppOpsServiceDelegate(CheckOpsDelegate delegate) {
+        synchronized (this) {
+            mCheckOpsDelegate = delegate;
+        }
+    }
+
+    @Override
+    public int checkOperationRaw(int code, int uid, String packageName) {
+        return checkOperationInternal(code, uid, packageName, true /*raw*/);
+    }
+
+    @Override
+    public int checkOperation(int code, int uid, String packageName) {
+        return checkOperationInternal(code, uid, packageName, false /*raw*/);
+    }
+
+    private int checkOperationInternal(int code, int uid, String packageName, boolean raw) {
+        final CheckOpsDelegate delegate;
+        synchronized (this) {
+            delegate = mCheckOpsDelegate;
+        }
+        if (delegate == null) {
+            return checkOperationImpl(code, uid, packageName, raw);
+        }
+        return delegate.checkOperation(code, uid, packageName, raw,
+                    AppOpsService.this::checkOperationImpl);
+    }
+
+    private int checkOperationImpl(int code, int uid, String packageName,
+                boolean raw) {
+        verifyIncomingUid(uid);
+        verifyIncomingOp(code);
+        String resolvedPackageName = resolvePackageName(uid, packageName);
+        if (resolvedPackageName == null) {
+            return AppOpsManager.MODE_IGNORED;
+        }
+        return checkOperationUnchecked(code, uid, resolvedPackageName, raw);
+    }
+
+    private int checkOperationUnchecked(int code, int uid, String packageName,
+                boolean raw) {
+        synchronized (this) {
+            if (isOpRestrictedLocked(uid, code, packageName)) {
+                return AppOpsManager.MODE_IGNORED;
+            }
+            code = AppOpsManager.opToSwitch(code);
+            UidState uidState = getUidStateLocked(uid, false);
+            if (uidState != null && uidState.opModes != null
+                    && uidState.opModes.indexOfKey(code) >= 0) {
+                final int rawMode = uidState.opModes.get(code);
+                return raw ? rawMode : uidState.evalMode(rawMode);
+            }
+            Op op = getOpLocked(code, uid, packageName, false, true, false);
+            if (op == null) {
+                return AppOpsManager.opToDefaultMode(code);
+            }
+            return op.mode;
+        }
+    }
+
+    @Override
+    public int checkAudioOperation(int code, int usage, int uid, String packageName) {
+        final CheckOpsDelegate delegate;
+        synchronized (this) {
+            delegate = mCheckOpsDelegate;
+        }
+        if (delegate == null) {
+            return checkAudioOperationImpl(code, usage, uid, packageName);
+        }
+        return delegate.checkAudioOperation(code, usage, uid, packageName,
+                AppOpsService.this::checkAudioOperationImpl);
+    }
+
+    private int checkAudioOperationImpl(int code, int usage, int uid, String packageName) {
+        boolean suspended;
+        try {
+            suspended = isPackageSuspendedForUser(packageName, uid);
+        } catch (IllegalArgumentException ex) {
+            // Package not found.
+            suspended = false;
+        }
+
+        if (suspended) {
+            Slog.i(TAG, "Audio disabled for suspended package=" + packageName
+                    + " for uid=" + uid);
+            return AppOpsManager.MODE_IGNORED;
+        }
+
+        synchronized (this) {
+            final int mode = checkRestrictionLocked(code, usage, uid, packageName);
+            if (mode != AppOpsManager.MODE_ALLOWED) {
+                return mode;
+            }
+        }
+        return checkOperation(code, uid, packageName);
+    }
+
+    private boolean isPackageSuspendedForUser(String pkg, int uid) {
+        try {
+            return AppGlobals.getPackageManager().isPackageSuspendedForUser(
+                    pkg, UserHandle.getUserId(uid));
+        } catch (RemoteException re) {
+            throw new SecurityException("Could not talk to package manager service");
+        }
+    }
+
+    private int checkRestrictionLocked(int code, int usage, int uid, String packageName) {
+        final SparseArray<Restriction> usageRestrictions = mAudioRestrictions.get(code);
+        if (usageRestrictions != null) {
+            final Restriction r = usageRestrictions.get(usage);
+            if (r != null && !r.exceptionPackages.contains(packageName)) {
+                return r.mode;
+            }
+        }
+        return AppOpsManager.MODE_ALLOWED;
+    }
+
+    @Override
+    public void setAudioRestriction(int code, int usage, int uid, int mode,
+            String[] exceptionPackages) {
+        enforceManageAppOpsModes(Binder.getCallingPid(), Binder.getCallingUid(), uid);
+        verifyIncomingUid(uid);
+        verifyIncomingOp(code);
+        synchronized (this) {
+            SparseArray<Restriction> usageRestrictions = mAudioRestrictions.get(code);
+            if (usageRestrictions == null) {
+                usageRestrictions = new SparseArray<Restriction>();
+                mAudioRestrictions.put(code, usageRestrictions);
+            }
+            usageRestrictions.remove(usage);
+            if (mode != AppOpsManager.MODE_ALLOWED) {
+                final Restriction r = new Restriction();
+                r.mode = mode;
+                if (exceptionPackages != null) {
+                    final int N = exceptionPackages.length;
+                    r.exceptionPackages = new ArraySet<String>(N);
+                    for (int i = 0; i < N; i++) {
+                        final String pkg = exceptionPackages[i];
+                        if (pkg != null) {
+                            r.exceptionPackages.add(pkg.trim());
+                        }
+                    }
+                }
+                usageRestrictions.put(usage, r);
+            }
+        }
+
+        mHandler.sendMessage(PooledLambda.obtainMessage(
+                AppOpsService::notifyWatchersOfChange, this, code, UID_ANY));
+    }
+
+    @Override
+    public int checkPackage(int uid, String packageName) {
+        Preconditions.checkNotNull(packageName);
+        synchronized (this) {
+            Ops ops = getOpsRawLocked(uid, packageName, true /* edit */,
+                    true /* uidMismatchExpected */);
+            if (ops != null) {
+                return AppOpsManager.MODE_ALLOWED;
+            } else {
+                return AppOpsManager.MODE_ERRORED;
+            }
+        }
+    }
+
+    @Override
+    public int noteProxyOperation(int code, int proxyUid,
+            String proxyPackageName, int proxiedUid, String proxiedPackageName) {
+        verifyIncomingUid(proxyUid);
+        verifyIncomingOp(code);
+        String resolveProxyPackageName = resolvePackageName(proxyUid, proxyPackageName);
+        if (resolveProxyPackageName == null) {
+            return AppOpsManager.MODE_IGNORED;
+        }
+        final int proxyMode = noteOperationUnchecked(code, proxyUid,
+                resolveProxyPackageName, -1, null);
+        if (proxyMode != AppOpsManager.MODE_ALLOWED || Binder.getCallingUid() == proxiedUid) {
+            return proxyMode;
+        }
+        String resolveProxiedPackageName = resolvePackageName(proxiedUid, proxiedPackageName);
+        if (resolveProxiedPackageName == null) {
+            return AppOpsManager.MODE_IGNORED;
+        }
+        return noteOperationUnchecked(code, proxiedUid, resolveProxiedPackageName,
+                proxyMode, resolveProxyPackageName);
+    }
+
+    @Override
+    public int noteOperation(int code, int uid, String packageName) {
+        final CheckOpsDelegate delegate;
+        synchronized (this) {
+            delegate = mCheckOpsDelegate;
+        }
+        if (delegate == null) {
+            return noteOperationImpl(code, uid, packageName);
+        }
+        return delegate.noteOperation(code, uid, packageName,
+                AppOpsService.this::noteOperationImpl);
+    }
+
+    private int noteOperationImpl(int code, int uid, String packageName) {
+        verifyIncomingUid(uid);
+        verifyIncomingOp(code);
+        String resolvedPackageName = resolvePackageName(uid, packageName);
+        if (resolvedPackageName == null) {
+            return AppOpsManager.MODE_IGNORED;
+        }
+        return noteOperationUnchecked(code, uid, resolvedPackageName, 0, null);
+    }
+
+    private int noteOperationUnchecked(int code, int uid, String packageName,
+            int proxyUid, String proxyPackageName) {
+        synchronized (this) {
+            final Ops ops = getOpsRawLocked(uid, packageName, true /* edit */,
+                    false /* uidMismatchExpected */);
+            if (ops == null) {
+                scheduleOpNotedIfNeededLocked(code, uid, packageName,
+                        AppOpsManager.MODE_IGNORED);
+                if (DEBUG) Slog.d(TAG, "noteOperation: no op for code " + code + " uid " + uid
+                        + " package " + packageName);
+                return AppOpsManager.MODE_ERRORED;
+            }
+            final Op op = getOpLocked(ops, code, true);
+            if (isOpRestrictedLocked(uid, code, packageName)) {
+                scheduleOpNotedIfNeededLocked(code, uid, packageName,
+                        AppOpsManager.MODE_IGNORED);
+                return AppOpsManager.MODE_IGNORED;
+            }
+            final UidState uidState = ops.uidState;
+            if (op.duration == -1) {
+                Slog.w(TAG, "Noting op not finished: uid " + uid + " pkg " + packageName
+                        + " code " + code + " time=" + op.time[uidState.state]
+                        + " duration=" + op.duration);
+            }
+            op.duration = 0;
+            final int switchCode = AppOpsManager.opToSwitch(code);
+            // If there is a non-default per UID policy (we set UID op mode only if
+            // non-default) it takes over, otherwise use the per package policy.
+            if (uidState.opModes != null && uidState.opModes.indexOfKey(switchCode) >= 0) {
+                final int uidMode = uidState.evalMode(uidState.opModes.get(switchCode));
+                if (uidMode != AppOpsManager.MODE_ALLOWED) {
+                    if (DEBUG) Slog.d(TAG, "noteOperation: uid reject #" + uidMode + " for code "
+                            + switchCode + " (" + code + ") uid " + uid + " package "
+                            + packageName);
+                    op.rejectTime[uidState.state] = System.currentTimeMillis();
+                    scheduleOpNotedIfNeededLocked(code, uid, packageName, uidMode);
+                    mHistoricalRegistry.incrementOpRejected(op.op, uid, packageName,
+                            uidState.state);
+                    return uidMode;
+                }
+            } else {
+                final Op switchOp = switchCode != code ? getOpLocked(ops, switchCode, true) : op;
+                final int mode = switchOp.getMode();
+                if (mode != AppOpsManager.MODE_ALLOWED) {
+                    if (DEBUG) Slog.d(TAG, "noteOperation: reject #" + mode + " for code "
+                            + switchCode + " (" + code + ") uid " + uid + " package "
+                            + packageName);
+                    op.rejectTime[uidState.state] = System.currentTimeMillis();
+                    scheduleOpNotedIfNeededLocked(op.op, uid, packageName, mode);
+                    mHistoricalRegistry.incrementOpRejected(op.op, uid, packageName,
+                            uidState.state);
+                    return mode;
+                }
+            }
+            if (DEBUG) Slog.d(TAG, "noteOperation: allowing code " + code + " uid " + uid
+                    + " package " + packageName);
+            op.time[uidState.state] = System.currentTimeMillis();
+            mHistoricalRegistry.incrementOpAccessedCount(op.op, uid, packageName,
+                    uidState.state);
+            op.rejectTime[uidState.state] = 0;
+            op.proxyUid = proxyUid;
+            op.proxyPackageName = proxyPackageName;
+            scheduleOpNotedIfNeededLocked(code, uid, packageName,
+                    AppOpsManager.MODE_ALLOWED);
+            return AppOpsManager.MODE_ALLOWED;
+        }
+    }
+
+    @Override
+    public void startWatchingActive(int[] ops, IAppOpsActiveCallback callback) {
+        int watchedUid = -1;
+        final int callingUid = Binder.getCallingUid();
+        final int callingPid = Binder.getCallingPid();
+        if (mContext.checkCallingOrSelfPermission(Manifest.permission.WATCH_APPOPS)
+                != PackageManager.PERMISSION_GRANTED) {
+            watchedUid = callingUid;
+        }
+        if (ops != null) {
+            Preconditions.checkArrayElementsInRange(ops, 0,
+                    AppOpsManager._NUM_OP - 1, "Invalid op code in: " + Arrays.toString(ops));
+        }
+        if (callback == null) {
+            return;
+        }
+        synchronized (this) {
+            SparseArray<ActiveCallback> callbacks = mActiveWatchers.get(callback.asBinder());
+            if (callbacks == null) {
+                callbacks = new SparseArray<>();
+                mActiveWatchers.put(callback.asBinder(), callbacks);
+            }
+            final ActiveCallback activeCallback = new ActiveCallback(callback, watchedUid,
+                    callingUid, callingPid);
+            for (int op : ops) {
+                callbacks.put(op, activeCallback);
+            }
+        }
+    }
+
+    @Override
+    public void stopWatchingActive(IAppOpsActiveCallback callback) {
+        if (callback == null) {
+            return;
+        }
+        synchronized (this) {
+            final SparseArray<ActiveCallback> activeCallbacks =
+                    mActiveWatchers.remove(callback.asBinder());
+            if (activeCallbacks == null) {
+                return;
+            }
+            final int callbackCount = activeCallbacks.size();
+            for (int i = 0; i < callbackCount; i++) {
+                activeCallbacks.valueAt(i).destroy();
+            }
+        }
+    }
+
+    @Override
+    public void startWatchingNoted(@NonNull int[] ops, @NonNull IAppOpsNotedCallback callback) {
+        int watchedUid = Process.INVALID_UID;
+        final int callingUid = Binder.getCallingUid();
+        final int callingPid = Binder.getCallingPid();
+        if (mContext.checkCallingOrSelfPermission(Manifest.permission.WATCH_APPOPS)
+                != PackageManager.PERMISSION_GRANTED) {
+            watchedUid = callingUid;
+        }
+        Preconditions.checkArgument(!ArrayUtils.isEmpty(ops), "Ops cannot be null or empty");
+        Preconditions.checkArrayElementsInRange(ops, 0, AppOpsManager._NUM_OP - 1,
+                "Invalid op code in: " + Arrays.toString(ops));
+        Preconditions.checkNotNull(callback, "Callback cannot be null");
+        synchronized (this) {
+            SparseArray<NotedCallback> callbacks = mNotedWatchers.get(callback.asBinder());
+            if (callbacks == null) {
+                callbacks = new SparseArray<>();
+                mNotedWatchers.put(callback.asBinder(), callbacks);
+            }
+            final NotedCallback notedCallback = new NotedCallback(callback, watchedUid,
+                    callingUid, callingPid);
+            for (int op : ops) {
+                callbacks.put(op, notedCallback);
+            }
+        }
+    }
+
+    @Override
+    public void stopWatchingNoted(IAppOpsNotedCallback callback) {
+        Preconditions.checkNotNull(callback, "Callback cannot be null");
+        synchronized (this) {
+            final SparseArray<NotedCallback> notedCallbacks =
+                    mNotedWatchers.remove(callback.asBinder());
+            if (notedCallbacks == null) {
+                return;
+            }
+            final int callbackCount = notedCallbacks.size();
+            for (int i = 0; i < callbackCount; i++) {
+                notedCallbacks.valueAt(i).destroy();
+            }
+        }
+    }
+
+    @Override
+    public int startOperation(IBinder token, int code, int uid, String packageName,
+            boolean startIfModeDefault) {
+        verifyIncomingUid(uid);
+        verifyIncomingOp(code);
+        String resolvedPackageName = resolvePackageName(uid, packageName);
+        if (resolvedPackageName == null) {
+            return  AppOpsManager.MODE_IGNORED;
+        }
+        ClientState client = (ClientState)token;
+        synchronized (this) {
+            final Ops ops = getOpsRawLocked(uid, resolvedPackageName, true /* edit */,
+                    false /* uidMismatchExpected */);
+            if (ops == null) {
+                if (DEBUG) Slog.d(TAG, "startOperation: no op for code " + code + " uid " + uid
+                        + " package " + resolvedPackageName);
+                return AppOpsManager.MODE_ERRORED;
+            }
+            final Op op = getOpLocked(ops, code, true);
+            if (isOpRestrictedLocked(uid, code, resolvedPackageName)) {
+                return AppOpsManager.MODE_IGNORED;
+            }
+            final int switchCode = AppOpsManager.opToSwitch(code);
+            final UidState uidState = ops.uidState;
+            // If there is a non-default per UID policy (we set UID op mode only if
+            // non-default) it takes over, otherwise use the per package policy.
+            if (uidState.opModes != null && uidState.opModes.indexOfKey(switchCode) >= 0) {
+                final int uidMode = uidState.evalMode(uidState.opModes.get(switchCode));
+                if (uidMode != AppOpsManager.MODE_ALLOWED
+                        && (!startIfModeDefault || uidMode != AppOpsManager.MODE_DEFAULT)) {
+                    if (DEBUG) Slog.d(TAG, "noteOperation: uid reject #" + uidMode + " for code "
+                            + switchCode + " (" + code + ") uid " + uid + " package "
+                            + resolvedPackageName);
+                    op.rejectTime[uidState.state] = System.currentTimeMillis();
+                    mHistoricalRegistry.incrementOpRejected(op.op, uid, packageName,
+                            uidState.state);
+                    return uidMode;
+                }
+            } else {
+                final Op switchOp = switchCode != code ? getOpLocked(ops, switchCode, true) : op;
+                final int mode = switchOp.getMode();
+                if (mode != AppOpsManager.MODE_ALLOWED
+                        && (!startIfModeDefault || mode != AppOpsManager.MODE_DEFAULT)) {
+                    if (DEBUG) Slog.d(TAG, "startOperation: reject #" + mode + " for code "
+                            + switchCode + " (" + code + ") uid " + uid + " package "
+                            + resolvedPackageName);
+                    op.rejectTime[uidState.state] = System.currentTimeMillis();
+                    mHistoricalRegistry.incrementOpRejected(op.op, uid, packageName,
+                            uidState.state);
+                    return mode;
+                }
+            }
+            if (DEBUG) Slog.d(TAG, "startOperation: allowing code " + code + " uid " + uid
+                    + " package " + resolvedPackageName);
+            if (op.startNesting == 0) {
+                op.startRealtime = SystemClock.elapsedRealtime();
+                op.time[uidState.state] = System.currentTimeMillis();
+                mHistoricalRegistry.incrementOpAccessedCount(op.op, uid, packageName,
+                        uidState.state);
+                op.rejectTime[uidState.state] = 0;
+                op.duration = -1;
+                scheduleOpActiveChangedIfNeededLocked(code, uid, packageName, true);
+            }
+            op.startNesting++;
+            uidState.startNesting++;
+            if (client.mStartedOps != null) {
+                client.mStartedOps.add(op);
+            }
+        }
+
+        return AppOpsManager.MODE_ALLOWED;
+    }
+
+    @Override
+    public void finishOperation(IBinder token, int code, int uid, String packageName) {
+        verifyIncomingUid(uid);
+        verifyIncomingOp(code);
+        String resolvedPackageName = resolvePackageName(uid, packageName);
+        if (resolvedPackageName == null) {
+            return;
+        }
+        if (!(token instanceof ClientState)) {
+            return;
+        }
+        ClientState client = (ClientState) token;
+        synchronized (this) {
+            Op op = getOpLocked(code, uid, resolvedPackageName, true, true, false);
+            if (op == null) {
+                return;
+            }
+            if (!client.mStartedOps.remove(op)) {
+                // We finish ops when packages get removed to guarantee no dangling
+                // started ops. However, some part of the system may asynchronously
+                // finish ops for an already gone package. Hence, finishing an op
+                // for a non existing package is fine and we don't log as a wtf.
+                final long identity = Binder.clearCallingIdentity();
+                try {
+                    if (LocalServices.getService(PackageManagerInternal.class).getPackageUid(
+                            resolvedPackageName, 0, UserHandle.getUserId(uid)) < 0) {
+                        Slog.i(TAG, "Finishing op=" + AppOpsManager.opToName(code)
+                                + " for non-existing package=" + resolvedPackageName
+                                + " in uid=" + uid);
+                        return;
+                    }
+                } finally {
+                    Binder.restoreCallingIdentity(identity);
+                }
+                Slog.wtf(TAG, "Operation not started: uid=" + op.uid + " pkg="
+                        + op.packageName + " op=" + AppOpsManager.opToName(op.op));
+                return;
+            }
+            finishOperationLocked(op, /*finishNested*/ false);
+            if (op.startNesting <= 0) {
+                scheduleOpActiveChangedIfNeededLocked(code, uid, packageName, false);
+            }
+        }
+    }
+
+    private void scheduleOpActiveChangedIfNeededLocked(int code, int uid, String packageName,
+            boolean active) {
+        ArraySet<ActiveCallback> dispatchedCallbacks = null;
+        final int callbackListCount = mActiveWatchers.size();
+        for (int i = 0; i < callbackListCount; i++) {
+            final SparseArray<ActiveCallback> callbacks = mActiveWatchers.valueAt(i);
+            ActiveCallback callback = callbacks.get(code);
+            if (callback != null) {
+                if (callback.mWatchingUid >= 0 && callback.mWatchingUid != uid) {
+                    continue;
+                }
+                if (dispatchedCallbacks == null) {
+                    dispatchedCallbacks = new ArraySet<>();
+                }
+                dispatchedCallbacks.add(callback);
+            }
+        }
+        if (dispatchedCallbacks == null) {
+            return;
+        }
+        mHandler.sendMessage(PooledLambda.obtainMessage(
+                AppOpsService::notifyOpActiveChanged,
+                this, dispatchedCallbacks, code, uid, packageName, active));
+    }
+
+    private void notifyOpActiveChanged(ArraySet<ActiveCallback> callbacks,
+            int code, int uid, String packageName, boolean active) {
+        // There are components watching for mode changes such as window manager
+        // and location manager which are in our process. The callbacks in these
+        // components may require permissions our remote caller does not have.
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            final int callbackCount = callbacks.size();
+            for (int i = 0; i < callbackCount; i++) {
+                final ActiveCallback callback = callbacks.valueAt(i);
+                try {
+                    callback.mCallback.opActiveChanged(code, uid, packageName, active);
+                } catch (RemoteException e) {
+                    /* do nothing */
+                }
+            }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    private void scheduleOpNotedIfNeededLocked(int code, int uid, String packageName,
+            int result) {
+        ArraySet<NotedCallback> dispatchedCallbacks = null;
+        final int callbackListCount = mNotedWatchers.size();
+        for (int i = 0; i < callbackListCount; i++) {
+            final SparseArray<NotedCallback> callbacks = mNotedWatchers.valueAt(i);
+            final NotedCallback callback = callbacks.get(code);
+            if (callback != null) {
+                if (callback.mWatchingUid >= 0 && callback.mWatchingUid != uid) {
+                    continue;
+                }
+                if (dispatchedCallbacks == null) {
+                    dispatchedCallbacks = new ArraySet<>();
+                }
+                dispatchedCallbacks.add(callback);
+            }
+        }
+        if (dispatchedCallbacks == null) {
+            return;
+        }
+        mHandler.sendMessage(PooledLambda.obtainMessage(
+                AppOpsService::notifyOpChecked,
+                this, dispatchedCallbacks, code, uid, packageName, result));
+    }
+
+    private void notifyOpChecked(ArraySet<NotedCallback> callbacks,
+            int code, int uid, String packageName, int result) {
+        // There are components watching for checks in our process. The callbacks in
+        // these components may require permissions our remote caller does not have.
+        final long identity = Binder.clearCallingIdentity();
+        try {
+            final int callbackCount = callbacks.size();
+            for (int i = 0; i < callbackCount; i++) {
+                final NotedCallback callback = callbacks.valueAt(i);
+                try {
+                    callback.mCallback.opNoted(code, uid, packageName, result);
+                } catch (RemoteException e) {
+                    /* do nothing */
+                }
+            }
+        } finally {
+            Binder.restoreCallingIdentity(identity);
+        }
+    }
+
+    @Override
+    public int permissionToOpCode(String permission) {
+        if (permission == null) {
+            return AppOpsManager.OP_NONE;
+        }
+        return AppOpsManager.permissionToOpCode(permission);
+    }
+
+    void finishOperationLocked(Op op, boolean finishNested) {
+        if (op.startNesting <= 1 || finishNested) {
+            if (op.startNesting == 1 || finishNested) {
+                op.duration = (int)(SystemClock.elapsedRealtime() - op.startRealtime);
+                mHistoricalRegistry.increaseOpAccessDuration(op.op, op.uid, op.packageName,
+                        op.uidState.state, op.duration);
+                op.time[op.uidState.state] = System.currentTimeMillis();
+            } else {
+                Slog.w(TAG, "Finishing op nesting under-run: uid " + op.uid + " pkg "
+                        + op.packageName + " code " + op.op + " time=" + op.time
+                        + " duration=" + op.duration + " nesting=" + op.startNesting);
+            }
+            if (op.startNesting >= 1) {
+                op.uidState.startNesting -= op.startNesting;
+            }
+            op.startNesting = 0;
+        } else {
+            op.startNesting--;
+            op.uidState.startNesting--;
+        }
+    }
+
+    private void verifyIncomingUid(int uid) {
+        if (uid == Binder.getCallingUid()) {
+            return;
+        }
+        if (Binder.getCallingPid() == Process.myPid()) {
+            return;
+        }
+        mContext.enforcePermission(android.Manifest.permission.UPDATE_APP_OPS_STATS,
+                Binder.getCallingPid(), Binder.getCallingUid(), null);
+    }
+
+    private void verifyIncomingOp(int op) {
+        if (op >= 0 && op < AppOpsManager._NUM_OP) {
+            return;
+        }
+        throw new IllegalArgumentException("Bad operation #" + op);
+    }
+
+    private UidState getUidStateLocked(int uid, boolean edit) {
+        UidState uidState = mUidStates.get(uid);
+        if (uidState == null) {
+            if (!edit) {
+                return null;
+            }
+            uidState = new UidState(uid);
+            mUidStates.put(uid, uidState);
+        } else {
+            if (uidState.pendingStateCommitTime != 0) {
+                if (uidState.pendingStateCommitTime < mLastRealtime) {
+                    commitUidPendingStateLocked(uidState);
+                } else {
+                    mLastRealtime = SystemClock.elapsedRealtime();
+                    if (uidState.pendingStateCommitTime < mLastRealtime) {
+                        commitUidPendingStateLocked(uidState);
+                    }
+                }
+            }
+        }
+        return uidState;
+    }
+
+    private void commitUidPendingStateLocked(UidState uidState) {
+        final boolean lastForeground = uidState.state <= UID_STATE_LAST_NON_RESTRICTED;
+        final boolean nowForeground = uidState.pendingState <= UID_STATE_LAST_NON_RESTRICTED;
+        uidState.state = uidState.pendingState;
+        uidState.pendingStateCommitTime = 0;
+        if (uidState.hasForegroundWatchers && lastForeground != nowForeground) {
+            for (int fgi = uidState.foregroundOps.size() - 1; fgi >= 0; fgi--) {
+                if (!uidState.foregroundOps.valueAt(fgi)) {
+                    continue;
+                }
+                final int code = uidState.foregroundOps.keyAt(fgi);
+
+                final ArraySet<ModeCallback> callbacks = mOpModeWatchers.get(code);
+                if (callbacks != null) {
+                    for (int cbi = callbacks.size() - 1; cbi >= 0; cbi--) {
+                        final ModeCallback callback = callbacks.valueAt(cbi);
+                        if ((callback.mFlags & AppOpsManager.WATCH_FOREGROUND_CHANGES) == 0
+                                || !callback.isWatchingUid(uidState.uid)) {
+                            continue;
+                        }
+                        boolean doAllPackages = uidState.opModes != null
+                                && uidState.opModes.indexOfKey(code) >= 0
+                                && uidState.opModes.get(code) == AppOpsManager.MODE_FOREGROUND;
+                        if (uidState.pkgOps != null) {
+                            for (int pkgi = uidState.pkgOps.size() - 1; pkgi >= 0; pkgi--) {
+                                final Op op = uidState.pkgOps.valueAt(pkgi).get(code);
+                                if (doAllPackages || (op != null
+                                        && op.mode == AppOpsManager.MODE_FOREGROUND)) {
+                                    mHandler.sendMessage(PooledLambda.obtainMessage(
+                                            AppOpsService::notifyOpChanged,
+                                            this, callback, code, uidState.uid,
+                                            uidState.pkgOps.keyAt(pkgi)));
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private Ops getOpsRawLocked(int uid, String packageName, boolean edit,
+            boolean uidMismatchExpected) {
+        UidState uidState = getUidStateLocked(uid, edit);
+        if (uidState == null) {
+            return null;
+        }
+
+        if (uidState.pkgOps == null) {
+            if (!edit) {
+                return null;
+            }
+            uidState.pkgOps = new ArrayMap<>();
+        }
+
+        Ops ops = uidState.pkgOps.get(packageName);
+        if (ops == null) {
+            if (!edit) {
+                return null;
+            }
+            boolean isPrivileged = false;
+            // This is the first time we have seen this package name under this uid,
+            // so let's make sure it is valid.
+            if (uid != 0) {
+                final long ident = Binder.clearCallingIdentity();
+                try {
+                    int pkgUid = -1;
+                    try {
+                        ApplicationInfo appInfo = ActivityThread.getPackageManager()
+                                .getApplicationInfo(packageName,
+                                        PackageManager.MATCH_DIRECT_BOOT_AWARE
+                                                | PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
+                                        UserHandle.getUserId(uid));
+                        if (appInfo != null) {
+                            pkgUid = appInfo.uid;
+                            isPrivileged = (appInfo.privateFlags
+                                    & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0;
+                        } else {
+                            pkgUid = resolveUid(packageName);
+                            if (pkgUid >= 0) {
+                                isPrivileged = false;
+                            }
+                        }
+                    } catch (RemoteException e) {
+                        Slog.w(TAG, "Could not contact PackageManager", e);
+                    }
+                    if (pkgUid != uid) {
+                        // Oops!  The package name is not valid for the uid they are calling
+                        // under.  Abort.
+                        if (!uidMismatchExpected) {
+                            RuntimeException ex = new RuntimeException("here");
+                            ex.fillInStackTrace();
+                            Slog.w(TAG, "Bad call: specified package " + packageName
+                                    + " under uid " + uid + " but it is really " + pkgUid, ex);
+                        }
+                        return null;
+                    }
+                } finally {
+                    Binder.restoreCallingIdentity(ident);
+                }
+            }
+            ops = new Ops(packageName, uidState, isPrivileged);
+            uidState.pkgOps.put(packageName, ops);
+        }
+        return ops;
+    }
+
+    /**
+     * Get the state of all ops for a package, <b>don't verify that package belongs to uid</b>.
+     *
+     * <p>Usually callers should use {@link #getOpLocked} and not call this directly.
+     *
+     * @param uid The uid the of the package
+     * @param packageName The package name for which to get the state for
+     * @param edit Iff {@code true} create the {@link Ops} object if not yet created
+     * @param isPrivileged Whether the package is privileged or not
+     *
+     * @return The {@link Ops state} of all ops for the package
+     */
+    private @Nullable Ops getOpsRawNoVerifyLocked(int uid, @NonNull String packageName,
+            boolean edit, boolean isPrivileged) {
+        UidState uidState = getUidStateLocked(uid, edit);
+        if (uidState == null) {
+            return null;
+        }
+
+        if (uidState.pkgOps == null) {
+            if (!edit) {
+                return null;
+            }
+            uidState.pkgOps = new ArrayMap<>();
+        }
+
+        Ops ops = uidState.pkgOps.get(packageName);
+        if (ops == null) {
+            if (!edit) {
+                return null;
+            }
+            ops = new Ops(packageName, uidState, isPrivileged);
+            uidState.pkgOps.put(packageName, ops);
+        }
+        return ops;
+    }
+
+    private void scheduleWriteLocked() {
+        if (!mWriteScheduled) {
+            mWriteScheduled = true;
+            mHandler.postDelayed(mWriteRunner, WRITE_DELAY);
+        }
+    }
+
+    private void scheduleFastWriteLocked() {
+        if (!mFastWriteScheduled) {
+            mWriteScheduled = true;
+            mFastWriteScheduled = true;
+            mHandler.removeCallbacks(mWriteRunner);
+            mHandler.postDelayed(mWriteRunner, 10*1000);
+        }
+    }
+
+    /**
+     * Get the state of an op for a uid.
+     *
+     * @param code The code of the op
+     * @param uid The uid the of the package
+     * @param packageName The package name for which to get the state for
+     * @param edit Iff {@code true} create the {@link Op} object if not yet created
+     * @param verifyUid Iff {@code true} check that the package belongs to the uid
+     * @param isPrivileged Whether the package is privileged or not (only used if {@code verifyUid
+     *                     == false})
+     *
+     * @return The {@link Op state} of the op
+     */
+    private @Nullable Op getOpLocked(int code, int uid, @NonNull String packageName, boolean edit,
+            boolean verifyUid, boolean isPrivileged) {
+        Ops ops;
+
+        if (verifyUid) {
+            ops = getOpsRawLocked(uid, packageName, edit, false /* uidMismatchExpected */);
+        }  else {
+            ops = getOpsRawNoVerifyLocked(uid, packageName, edit, isPrivileged);
+        }
+
+        if (ops == null) {
+            return null;
+        }
+        return getOpLocked(ops, code, edit);
+    }
+
+    private Op getOpLocked(Ops ops, int code, boolean edit) {
+        Op op = ops.get(code);
+        if (op == null) {
+            if (!edit) {
+                return null;
+            }
+            op = new Op(ops.uidState, ops.packageName, code);
+            ops.put(code, op);
+        }
+        if (edit) {
+            scheduleWriteLocked();
+        }
+        return op;
+    }
+
+    private boolean isOpRestrictedLocked(int uid, int code, String packageName) {
+        int userHandle = UserHandle.getUserId(uid);
+        final int restrictionSetCount = mOpUserRestrictions.size();
+
+        for (int i = 0; i < restrictionSetCount; i++) {
+            // For each client, check that the given op is not restricted, or that the given
+            // package is exempt from the restriction.
+            ClientRestrictionState restrictionState = mOpUserRestrictions.valueAt(i);
+            if (restrictionState.hasRestriction(code, packageName, userHandle)) {
+                if (AppOpsManager.opAllowSystemBypassRestriction(code)) {
+                    // If we are the system, bypass user restrictions for certain codes
+                    synchronized (this) {
+                        Ops ops = getOpsRawLocked(uid, packageName, true /* edit */,
+                                false /* uidMismatchExpected */);
+                        if ((ops != null) && ops.isPrivileged) {
+                            return false;
+                        }
+                    }
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    void readState() {
+        int oldVersion = NO_VERSION;
+        synchronized (mFile) {
+            synchronized (this) {
+                FileInputStream stream;
+                try {
+                    stream = mFile.openRead();
+                } catch (FileNotFoundException e) {
+                    Slog.i(TAG, "No existing app ops " + mFile.getBaseFile() + "; starting empty");
+                    return;
+                }
+                boolean success = false;
+                mUidStates.clear();
+                try {
+                    XmlPullParser parser = Xml.newPullParser();
+                    parser.setInput(stream, StandardCharsets.UTF_8.name());
+                    int type;
+                    while ((type = parser.next()) != XmlPullParser.START_TAG
+                            && type != XmlPullParser.END_DOCUMENT) {
+                        ;
+                    }
+
+                    if (type != XmlPullParser.START_TAG) {
+                        throw new IllegalStateException("no start tag found");
+                    }
+
+                    final String versionString = parser.getAttributeValue(null, "v");
+                    if (versionString != null) {
+                        oldVersion = Integer.parseInt(versionString);
+                    }
+
+                    int outerDepth = parser.getDepth();
+                    while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                            && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+                        if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                            continue;
+                        }
+
+                        String tagName = parser.getName();
+                        if (tagName.equals("pkg")) {
+                            readPackage(parser);
+                        } else if (tagName.equals("uid")) {
+                            readUidOps(parser);
+                        } else {
+                            Slog.w(TAG, "Unknown element under <app-ops>: "
+                                    + parser.getName());
+                            XmlUtils.skipCurrentTag(parser);
+                        }
+                    }
+                    success = true;
+                } catch (IllegalStateException e) {
+                    Slog.w(TAG, "Failed parsing " + e);
+                } catch (NullPointerException e) {
+                    Slog.w(TAG, "Failed parsing " + e);
+                } catch (NumberFormatException e) {
+                    Slog.w(TAG, "Failed parsing " + e);
+                } catch (XmlPullParserException e) {
+                    Slog.w(TAG, "Failed parsing " + e);
+                } catch (IOException e) {
+                    Slog.w(TAG, "Failed parsing " + e);
+                } catch (IndexOutOfBoundsException e) {
+                    Slog.w(TAG, "Failed parsing " + e);
+                } finally {
+                    if (!success) {
+                        mUidStates.clear();
+                    }
+                    try {
+                        stream.close();
+                    } catch (IOException e) {
+                    }
+                }
+            }
+        }
+        synchronized (this) {
+            upgradeLocked(oldVersion);
+        }
+    }
+
+    private void upgradeRunAnyInBackgroundLocked() {
+        for (int i = 0; i < mUidStates.size(); i++) {
+            final UidState uidState = mUidStates.valueAt(i);
+            if (uidState == null) {
+                continue;
+            }
+            if (uidState.opModes != null) {
+                final int idx = uidState.opModes.indexOfKey(AppOpsManager.OP_RUN_IN_BACKGROUND);
+                if (idx >= 0) {
+                    uidState.opModes.put(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND,
+                            uidState.opModes.valueAt(idx));
+                }
+            }
+            if (uidState.pkgOps == null) {
+                continue;
+            }
+            boolean changed = false;
+            for (int j = 0; j < uidState.pkgOps.size(); j++) {
+                Ops ops = uidState.pkgOps.valueAt(j);
+                if (ops != null) {
+                    final Op op = ops.get(AppOpsManager.OP_RUN_IN_BACKGROUND);
+                    if (op != null && op.mode != AppOpsManager.opToDefaultMode(op.op)) {
+                        final Op copy = new Op(op.uidState, op.packageName,
+                                AppOpsManager.OP_RUN_ANY_IN_BACKGROUND);
+                        copy.mode = op.mode;
+                        ops.put(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, copy);
+                        changed = true;
+                    }
+                }
+            }
+            if (changed) {
+                uidState.evalForegroundOps(mOpModeWatchers);
+            }
+        }
+    }
+
+    private void upgradeLocked(int oldVersion) {
+        if (oldVersion >= CURRENT_VERSION) {
+            return;
+        }
+        Slog.d(TAG, "Upgrading app-ops xml from version " + oldVersion + " to " + CURRENT_VERSION);
+        switch (oldVersion) {
+            case NO_VERSION:
+                upgradeRunAnyInBackgroundLocked();
+                // fall through
+            case 1:
+                // for future upgrades
+        }
+        scheduleFastWriteLocked();
+    }
+
+    void readUidOps(XmlPullParser parser) throws NumberFormatException,
+            XmlPullParserException, IOException {
+        final int uid = Integer.parseInt(parser.getAttributeValue(null, "n"));
+        int outerDepth = parser.getDepth();
+        int type;
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                continue;
+            }
+
+            String tagName = parser.getName();
+            if (tagName.equals("op")) {
+                final int code = Integer.parseInt(parser.getAttributeValue(null, "n"));
+                final int mode = Integer.parseInt(parser.getAttributeValue(null, "m"));
+                UidState uidState = getUidStateLocked(uid, true);
+                if (uidState.opModes == null) {
+                    uidState.opModes = new SparseIntArray();
+                }
+                uidState.opModes.put(code, mode);
+            } else {
+                Slog.w(TAG, "Unknown element under <uid-ops>: "
+                        + parser.getName());
+                XmlUtils.skipCurrentTag(parser);
+            }
+        }
+    }
+
+    void readPackage(XmlPullParser parser) throws NumberFormatException,
+            XmlPullParserException, IOException {
+        String pkgName = parser.getAttributeValue(null, "n");
+        int outerDepth = parser.getDepth();
+        int type;
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                continue;
+            }
+
+            String tagName = parser.getName();
+            if (tagName.equals("uid")) {
+                readUid(parser, pkgName);
+            } else {
+                Slog.w(TAG, "Unknown element under <pkg>: "
+                        + parser.getName());
+                XmlUtils.skipCurrentTag(parser);
+            }
+        }
+    }
+
+    void readUid(XmlPullParser parser, String pkgName) throws NumberFormatException,
+            XmlPullParserException, IOException {
+        int uid = Integer.parseInt(parser.getAttributeValue(null, "n"));
+        String isPrivilegedString = parser.getAttributeValue(null, "p");
+        boolean isPrivileged = false;
+        if (isPrivilegedString == null) {
+            try {
+                IPackageManager packageManager = ActivityThread.getPackageManager();
+                if (packageManager != null) {
+                    ApplicationInfo appInfo = ActivityThread.getPackageManager()
+                            .getApplicationInfo(pkgName, 0, UserHandle.getUserId(uid));
+                    if (appInfo != null) {
+                        isPrivileged = (appInfo.privateFlags
+                                & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0;
+                    }
+                } else {
+                    // Could not load data, don't add to cache so it will be loaded later.
+                    return;
+                }
+            } catch (RemoteException e) {
+                Slog.w(TAG, "Could not contact PackageManager", e);
+            }
+        } else {
+            isPrivileged = Boolean.parseBoolean(isPrivilegedString);
+        }
+        int outerDepth = parser.getDepth();
+        int type;
+        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+                && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                continue;
+            }
+
+            String tagName = parser.getName();
+            if (tagName.equals("op")) {
+                UidState uidState = getUidStateLocked(uid, true);
+                if (uidState.pkgOps == null) {
+                    uidState.pkgOps = new ArrayMap<>();
+                }
+
+                Op op = new Op(uidState, pkgName,
+                        Integer.parseInt(parser.getAttributeValue(null, "n")));
+
+                for (int i = parser.getAttributeCount()-1; i >= 0; i--) {
+                    final String name = parser.getAttributeName(i);
+                    final String value = parser.getAttributeValue(i);
+                    switch (name) {
+                        case "m":
+                            op.mode = Integer.parseInt(value);
+                            break;
+                        case "d":
+                            op.duration = Integer.parseInt(value);
+                            break;
+                        case "pu":
+                            op.proxyUid = Integer.parseInt(value);
+                            break;
+                        case "pp":
+                            op.proxyPackageName = value;
+                            break;
+                        case "tp":
+                            op.time[AppOpsManager.UID_STATE_PERSISTENT] = Long.parseLong(value);
+                            break;
+                        case "tt":
+                            op.time[AppOpsManager.UID_STATE_TOP] = Long.parseLong(value);
+                            break;
+                        case "tfs":
+                            op.time[AppOpsManager.UID_STATE_FOREGROUND_SERVICE]
+                                    = Long.parseLong(value);
+                            break;
+                        case "tf":
+                            op.time[AppOpsManager.UID_STATE_FOREGROUND] = Long.parseLong(value);
+                            break;
+                        case "tb":
+                            op.time[AppOpsManager.UID_STATE_BACKGROUND] = Long.parseLong(value);
+                            break;
+                        case "tc":
+                            op.time[AppOpsManager.UID_STATE_CACHED] = Long.parseLong(value);
+                            break;
+                        case "rp":
+                            op.rejectTime[AppOpsManager.UID_STATE_PERSISTENT]
+                                    = Long.parseLong(value);
+                            break;
+                        case "rt":
+                            op.rejectTime[AppOpsManager.UID_STATE_TOP] = Long.parseLong(value);
+                            break;
+                        case "rfs":
+                            op.rejectTime[AppOpsManager.UID_STATE_FOREGROUND_SERVICE]
+                                    = Long.parseLong(value);
+                            break;
+                        case "rf":
+                            op.rejectTime[AppOpsManager.UID_STATE_FOREGROUND]
+                                    = Long.parseLong(value);
+                            break;
+                        case "rb":
+                            op.rejectTime[AppOpsManager.UID_STATE_BACKGROUND]
+                                    = Long.parseLong(value);
+                            break;
+                        case "rc":
+                            op.rejectTime[AppOpsManager.UID_STATE_CACHED]
+                                    = Long.parseLong(value);
+                            break;
+                        case "t":
+                            // Backwards compat.
+                            op.time[AppOpsManager.UID_STATE_TOP] = Long.parseLong(value);
+                            break;
+                        case "r":
+                            // Backwards compat.
+                            op.rejectTime[AppOpsManager.UID_STATE_TOP] = Long.parseLong(value);
+                            break;
+                        default:
+                            Slog.w(TAG, "Unknown attribute in 'op' tag: " + name);
+                            break;
+                    }
+                }
+
+                Ops ops = uidState.pkgOps.get(pkgName);
+                if (ops == null) {
+                    ops = new Ops(pkgName, uidState, isPrivileged);
+                    uidState.pkgOps.put(pkgName, ops);
+                }
+                ops.put(op.op, op);
+            } else {
+                Slog.w(TAG, "Unknown element under <pkg>: "
+                        + parser.getName());
+                XmlUtils.skipCurrentTag(parser);
+            }
+        }
+        UidState uidState = getUidStateLocked(uid, false);
+        if (uidState != null) {
+            uidState.evalForegroundOps(mOpModeWatchers);
+        }
+    }
+
+    void writeState() {
+        synchronized (mFile) {
+            FileOutputStream stream;
+            try {
+                stream = mFile.startWrite();
+            } catch (IOException e) {
+                Slog.w(TAG, "Failed to write state: " + e);
+                return;
+            }
+
+            List<AppOpsManager.PackageOps> allOps = getPackagesForOps(null);
+
+            try {
+                XmlSerializer out = new FastXmlSerializer();
+                out.setOutput(stream, StandardCharsets.UTF_8.name());
+                out.startDocument(null, true);
+                out.startTag(null, "app-ops");
+                out.attribute(null, "v", String.valueOf(CURRENT_VERSION));
+
+                final int uidStateCount = mUidStates.size();
+                for (int i = 0; i < uidStateCount; i++) {
+                    UidState uidState = mUidStates.valueAt(i);
+                    if (uidState.opModes != null && uidState.opModes.size() > 0) {
+                        out.startTag(null, "uid");
+                        out.attribute(null, "n", Integer.toString(uidState.uid));
+                        SparseIntArray uidOpModes = uidState.opModes;
+                        final int opCount = uidOpModes.size();
+                        for (int j = 0; j < opCount; j++) {
+                            final int op = uidOpModes.keyAt(j);
+                            final int mode = uidOpModes.valueAt(j);
+                            out.startTag(null, "op");
+                            out.attribute(null, "n", Integer.toString(op));
+                            out.attribute(null, "m", Integer.toString(mode));
+                            out.endTag(null, "op");
+                        }
+                        out.endTag(null, "uid");
+                    }
+                }
+
+                if (allOps != null) {
+                    String lastPkg = null;
+                    for (int i=0; i<allOps.size(); i++) {
+                        AppOpsManager.PackageOps pkg = allOps.get(i);
+                        if (!pkg.getPackageName().equals(lastPkg)) {
+                            if (lastPkg != null) {
+                                out.endTag(null, "pkg");
+                            }
+                            lastPkg = pkg.getPackageName();
+                            out.startTag(null, "pkg");
+                            out.attribute(null, "n", lastPkg);
+                        }
+                        out.startTag(null, "uid");
+                        out.attribute(null, "n", Integer.toString(pkg.getUid()));
+                        synchronized (this) {
+                            Ops ops = getOpsRawLocked(pkg.getUid(), pkg.getPackageName(),
+                                    false /* edit */, false /* uidMismatchExpected */);
+                            // Should always be present as the list of PackageOps is generated
+                            // from Ops.
+                            if (ops != null) {
+                                out.attribute(null, "p", Boolean.toString(ops.isPrivileged));
+                            } else {
+                                out.attribute(null, "p", Boolean.toString(false));
+                            }
+                        }
+                        List<AppOpsManager.OpEntry> ops = pkg.getOps();
+                        for (int j=0; j<ops.size(); j++) {
+                            AppOpsManager.OpEntry op = ops.get(j);
+                            out.startTag(null, "op");
+                            out.attribute(null, "n", Integer.toString(op.getOp()));
+                            if (op.getMode() != AppOpsManager.opToDefaultMode(op.getOp())) {
+                                out.attribute(null, "m", Integer.toString(op.getMode()));
+                            }
+                            for (int k = 0; k < _NUM_UID_STATE; k++) {
+                                final long time = op.getLastTimeFor(k);
+                                if (time != 0) {
+                                    out.attribute(null, UID_STATE_TIME_ATTRS[k],
+                                            Long.toString(time));
+                                }
+                                final long rejectTime = op.getLastRejectTimeFor(k);
+                                if (rejectTime != 0) {
+                                    out.attribute(null, UID_STATE_REJECT_ATTRS[k],
+                                            Long.toString(rejectTime));
+                                }
+                            }
+                            int dur = op.getDuration();
+                            if (dur != 0) {
+                                out.attribute(null, "d", Integer.toString(dur));
+                            }
+                            int proxyUid = op.getProxyUid();
+                            if (proxyUid != -1) {
+                                out.attribute(null, "pu", Integer.toString(proxyUid));
+                            }
+                            String proxyPackageName = op.getProxyPackageName();
+                            if (proxyPackageName != null) {
+                                out.attribute(null, "pp", proxyPackageName);
+                            }
+                            out.endTag(null, "op");
+                        }
+                        out.endTag(null, "uid");
+                    }
+                    if (lastPkg != null) {
+                        out.endTag(null, "pkg");
+                    }
+                }
+
+                out.endTag(null, "app-ops");
+                out.endDocument();
+                mFile.finishWrite(stream);
+            } catch (IOException e) {
+                Slog.w(TAG, "Failed to write state, restoring backup.", e);
+                mFile.failWrite(stream);
+            }
+        }
+    }
+
+    static class Shell extends ShellCommand {
+        final IAppOpsService mInterface;
+        final AppOpsService mInternal;
+
+        int userId = UserHandle.USER_SYSTEM;
+        String packageName;
+        String opStr;
+        String modeStr;
+        int op;
+        int mode;
+        int packageUid;
+        int nonpackageUid;
+        final static Binder sBinder = new Binder();
+        IBinder mToken;
+
+        Shell(IAppOpsService iface, AppOpsService internal) {
+            mInterface = iface;
+            mInternal = internal;
+            try {
+                mToken = mInterface.getToken(sBinder);
+            } catch (RemoteException e) {
+            }
+        }
+
+        @Override
+        public int onCommand(String cmd) {
+            return onShellCommand(this, cmd);
+        }
+
+        @Override
+        public void onHelp() {
+            PrintWriter pw = getOutPrintWriter();
+            dumpCommandHelp(pw);
+        }
+
+        static private int strOpToOp(String op, PrintWriter err) {
+            try {
+                return AppOpsManager.strOpToOp(op);
+            } catch (IllegalArgumentException e) {
+            }
+            try {
+                return Integer.parseInt(op);
+            } catch (NumberFormatException e) {
+            }
+            try {
+                return AppOpsManager.strDebugOpToOp(op);
+            } catch (IllegalArgumentException e) {
+                err.println("Error: " + e.getMessage());
+                return -1;
+            }
+        }
+
+        static int strModeToMode(String modeStr, PrintWriter err) {
+            for (int i = AppOpsManager.MODE_NAMES.length - 1; i >= 0; i--) {
+                if (AppOpsManager.MODE_NAMES[i].equals(modeStr)) {
+                    return i;
+                }
+            }
+            try {
+                return Integer.parseInt(modeStr);
+            } catch (NumberFormatException e) {
+            }
+            err.println("Error: Mode " + modeStr + " is not valid");
+            return -1;
+        }
+
+        int parseUserOpMode(int defMode, PrintWriter err) throws RemoteException {
+            userId = UserHandle.USER_CURRENT;
+            opStr = null;
+            modeStr = null;
+            for (String argument; (argument = getNextArg()) != null;) {
+                if ("--user".equals(argument)) {
+                    userId = UserHandle.parseUserArg(getNextArgRequired());
+                } else {
+                    if (opStr == null) {
+                        opStr = argument;
+                    } else if (modeStr == null) {
+                        modeStr = argument;
+                        break;
+                    }
+                }
+            }
+            if (opStr == null) {
+                err.println("Error: Operation not specified.");
+                return -1;
+            }
+            op = strOpToOp(opStr, err);
+            if (op < 0) {
+                return -1;
+            }
+            if (modeStr != null) {
+                if ((mode=strModeToMode(modeStr, err)) < 0) {
+                    return -1;
+                }
+            } else {
+                mode = defMode;
+            }
+            return 0;
+        }
+
+        int parseUserPackageOp(boolean reqOp, PrintWriter err) throws RemoteException {
+            userId = UserHandle.USER_CURRENT;
+            packageName = null;
+            opStr = null;
+            for (String argument; (argument = getNextArg()) != null;) {
+                if ("--user".equals(argument)) {
+                    userId = UserHandle.parseUserArg(getNextArgRequired());
+                } else {
+                    if (packageName == null) {
+                        packageName = argument;
+                    } else if (opStr == null) {
+                        opStr = argument;
+                        break;
+                    }
+                }
+            }
+            if (packageName == null) {
+                err.println("Error: Package name not specified.");
+                return -1;
+            } else if (opStr == null && reqOp) {
+                err.println("Error: Operation not specified.");
+                return -1;
+            }
+            if (opStr != null) {
+                op = strOpToOp(opStr, err);
+                if (op < 0) {
+                    return -1;
+                }
+            } else {
+                op = AppOpsManager.OP_NONE;
+            }
+            if (userId == UserHandle.USER_CURRENT) {
+                userId = ActivityManager.getCurrentUser();
+            }
+            nonpackageUid = -1;
+            try {
+                nonpackageUid = Integer.parseInt(packageName);
+            } catch (NumberFormatException e) {
+            }
+            if (nonpackageUid == -1 && packageName.length() > 1 && packageName.charAt(0) == 'u'
+                    && packageName.indexOf('.') < 0) {
+                int i = 1;
+                while (i < packageName.length() && packageName.charAt(i) >= '0'
+                        && packageName.charAt(i) <= '9') {
+                    i++;
+                }
+                if (i > 1 && i < packageName.length()) {
+                    String userStr = packageName.substring(1, i);
+                    try {
+                        int user = Integer.parseInt(userStr);
+                        char type = packageName.charAt(i);
+                        i++;
+                        int startTypeVal = i;
+                        while (i < packageName.length() && packageName.charAt(i) >= '0'
+                                && packageName.charAt(i) <= '9') {
+                            i++;
+                        }
+                        if (i > startTypeVal) {
+                            String typeValStr = packageName.substring(startTypeVal, i);
+                            try {
+                                int typeVal = Integer.parseInt(typeValStr);
+                                if (type == 'a') {
+                                    nonpackageUid = UserHandle.getUid(user,
+                                            typeVal + Process.FIRST_APPLICATION_UID);
+                                } else if (type == 's') {
+                                    nonpackageUid = UserHandle.getUid(user, typeVal);
+                                }
+                            } catch (NumberFormatException e) {
+                            }
+                        }
+                    } catch (NumberFormatException e) {
+                    }
+                }
+            }
+            if (nonpackageUid != -1) {
+                packageName = null;
+            } else {
+                packageUid = resolveUid(packageName);
+                if (packageUid < 0) {
+                    packageUid = AppGlobals.getPackageManager().getPackageUid(packageName,
+                            PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
+                }
+                if (packageUid < 0) {
+                    err.println("Error: No UID for " + packageName + " in user " + userId);
+                    return -1;
+                }
+            }
+            return 0;
+        }
+    }
+
+    @Override public void onShellCommand(FileDescriptor in, FileDescriptor out,
+            FileDescriptor err, String[] args, ShellCallback callback,
+            ResultReceiver resultReceiver) {
+        (new Shell(this, this)).exec(this, in, out, err, args, callback, resultReceiver);
+    }
+
+    static void dumpCommandHelp(PrintWriter pw) {
+        pw.println("AppOps service (appops) commands:");
+        pw.println("  help");
+        pw.println("    Print this help text.");
+        pw.println("  start [--user <USER_ID>] <PACKAGE | UID> <OP> ");
+        pw.println("    Starts a given operation for a particular application.");
+        pw.println("  stop [--user <USER_ID>] <PACKAGE | UID> <OP> ");
+        pw.println("    Stops a given operation for a particular application.");
+        pw.println("  set [--user <USER_ID>] <PACKAGE | UID> <OP> <MODE>");
+        pw.println("    Set the mode for a particular application and operation.");
+        pw.println("  get [--user <USER_ID>] <PACKAGE | UID> [<OP>]");
+        pw.println("    Return the mode for a particular application and optional operation.");
+        pw.println("  query-op [--user <USER_ID>] <OP> [<MODE>]");
+        pw.println("    Print all packages that currently have the given op in the given mode.");
+        pw.println("  reset [--user <USER_ID>] [<PACKAGE>]");
+        pw.println("    Reset the given application or all applications to default modes.");
+        pw.println("  write-settings");
+        pw.println("    Immediately write pending changes to storage.");
+        pw.println("  read-settings");
+        pw.println("    Read the last written settings, replacing current state in RAM.");
+        pw.println("  options:");
+        pw.println("    <PACKAGE> an Android package name.");
+        pw.println("    <OP>      an AppOps operation.");
+        pw.println("    <MODE>    one of allow, ignore, deny, or default");
+        pw.println("    <USER_ID> the user id under which the package is installed. If --user is not");
+        pw.println("              specified, the current user is assumed.");
+    }
+
+    static int onShellCommand(Shell shell, String cmd) {
+        if (cmd == null) {
+            return shell.handleDefaultCommands(cmd);
+        }
+        PrintWriter pw = shell.getOutPrintWriter();
+        PrintWriter err = shell.getErrPrintWriter();
+        try {
+            switch (cmd) {
+                case "set": {
+                    int res = shell.parseUserPackageOp(true, err);
+                    if (res < 0) {
+                        return res;
+                    }
+                    String modeStr = shell.getNextArg();
+                    if (modeStr == null) {
+                        err.println("Error: Mode not specified.");
+                        return -1;
+                    }
+
+                    final int mode = shell.strModeToMode(modeStr, err);
+                    if (mode < 0) {
+                        return -1;
+                    }
+
+                    if (shell.packageName != null) {
+                        shell.mInterface.setMode(shell.op, shell.packageUid, shell.packageName,
+                                mode);
+                    } else {
+                        shell.mInterface.setUidMode(shell.op, shell.nonpackageUid, mode);
+                    }
+                    return 0;
+                }
+                case "get": {
+                    int res = shell.parseUserPackageOp(false, err);
+                    if (res < 0) {
+                        return res;
+                    }
+
+                    List<AppOpsManager.PackageOps> ops = new ArrayList<>();
+                    if (shell.packageName != null) {
+                        // Uid mode overrides package mode, so make sure it's also reported
+                        List<AppOpsManager.PackageOps> r = shell.mInterface.getUidOps(
+                                shell.packageUid,
+                                shell.op != AppOpsManager.OP_NONE ? new int[]{shell.op} : null);
+                        if (r != null) {
+                            ops.addAll(r);
+                        }
+                        r = shell.mInterface.getOpsForPackage(
+                                shell.packageUid, shell.packageName,
+                                shell.op != AppOpsManager.OP_NONE ? new int[]{shell.op} : null);
+                        if (r != null) {
+                            ops.addAll(r);
+                        }
+                    } else {
+                        ops = shell.mInterface.getUidOps(
+                                shell.nonpackageUid,
+                                shell.op != AppOpsManager.OP_NONE ? new int[]{shell.op} : null);
+                    }
+                    if (ops == null || ops.size() <= 0) {
+                        pw.println("No operations.");
+                        if (shell.op > AppOpsManager.OP_NONE && shell.op < AppOpsManager._NUM_OP) {
+                            pw.println("Default mode: " + AppOpsManager.modeToName(
+                                    AppOpsManager.opToDefaultMode(shell.op)));
+                        }
+                        return 0;
+                    }
+                    final long now = System.currentTimeMillis();
+                    for (int i=0; i<ops.size(); i++) {
+                        AppOpsManager.PackageOps packageOps = ops.get(i);
+                        if (packageOps.getPackageName() == null) {
+                            pw.print("Uid mode: ");
+                        }
+                        List<AppOpsManager.OpEntry> entries = packageOps.getOps();
+                        for (int j=0; j<entries.size(); j++) {
+                            AppOpsManager.OpEntry ent = entries.get(j);
+                            pw.print(AppOpsManager.opToName(ent.getOp()));
+                            pw.print(": ");
+                            pw.print(AppOpsManager.modeToName(ent.getMode()));
+                            if (ent.getTime() != 0) {
+                                pw.print("; time=");
+                                TimeUtils.formatDuration(now - ent.getTime(), pw);
+                                pw.print(" ago");
+                            }
+                            if (ent.getRejectTime() != 0) {
+                                pw.print("; rejectTime=");
+                                TimeUtils.formatDuration(now - ent.getRejectTime(), pw);
+                                pw.print(" ago");
+                            }
+                            if (ent.getDuration() == -1) {
+                                pw.print(" (running)");
+                            } else if (ent.getDuration() != 0) {
+                                pw.print("; duration=");
+                                TimeUtils.formatDuration(ent.getDuration(), pw);
+                            }
+                            pw.println();
+                        }
+                    }
+                    return 0;
+                }
+                case "query-op": {
+                    int res = shell.parseUserOpMode(AppOpsManager.MODE_IGNORED, err);
+                    if (res < 0) {
+                        return res;
+                    }
+                    List<AppOpsManager.PackageOps> ops = shell.mInterface.getPackagesForOps(
+                            new int[] {shell.op});
+                    if (ops == null || ops.size() <= 0) {
+                        pw.println("No operations.");
+                        return 0;
+                    }
+                    for (int i=0; i<ops.size(); i++) {
+                        final AppOpsManager.PackageOps pkg = ops.get(i);
+                        boolean hasMatch = false;
+                        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() == shell.op && ent.getMode() == shell.mode) {
+                                hasMatch = true;
+                                break;
+                            }
+                        }
+                        if (hasMatch) {
+                            pw.println(pkg.getPackageName());
+                        }
+                    }
+                    return 0;
+                }
+                case "reset": {
+                    String packageName = null;
+                    int userId = UserHandle.USER_CURRENT;
+                    for (String argument; (argument = shell.getNextArg()) != null;) {
+                        if ("--user".equals(argument)) {
+                            String userStr = shell.getNextArgRequired();
+                            userId = UserHandle.parseUserArg(userStr);
+                        } else {
+                            if (packageName == null) {
+                                packageName = argument;
+                            } else {
+                                err.println("Error: Unsupported argument: " + argument);
+                                return -1;
+                            }
+                        }
+                    }
+
+                    if (userId == UserHandle.USER_CURRENT) {
+                        userId = ActivityManager.getCurrentUser();
+                    }
+
+                    shell.mInterface.resetAllModes(userId, packageName);
+                    pw.print("Reset all modes for: ");
+                    if (userId == UserHandle.USER_ALL) {
+                        pw.print("all users");
+                    } else {
+                        pw.print("user "); pw.print(userId);
+                    }
+                    pw.print(", ");
+                    if (packageName == null) {
+                        pw.println("all packages");
+                    } else {
+                        pw.print("package "); pw.println(packageName);
+                    }
+                    return 0;
+                }
+                case "write-settings": {
+                    shell.mInternal.enforceManageAppOpsModes(Binder.getCallingPid(),
+                            Binder.getCallingUid(), -1);
+                    long token = Binder.clearCallingIdentity();
+                    try {
+                        synchronized (shell.mInternal) {
+                            shell.mInternal.mHandler.removeCallbacks(shell.mInternal.mWriteRunner);
+                        }
+                        shell.mInternal.writeState();
+                        pw.println("Current settings written.");
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
+                    return 0;
+                }
+                case "read-settings": {
+                    shell.mInternal.enforceManageAppOpsModes(Binder.getCallingPid(),
+                            Binder.getCallingUid(), -1);
+                    long token = Binder.clearCallingIdentity();
+                    try {
+                        shell.mInternal.readState();
+                        pw.println("Last settings read.");
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
+                    return 0;
+                }
+                case "start": {
+                    int res = shell.parseUserPackageOp(true, err);
+                    if (res < 0) {
+                        return res;
+                    }
+
+                    if (shell.packageName != null) {
+                        shell.mInterface.startOperation(shell.mToken,
+                                shell.op, shell.packageUid, shell.packageName, true);
+                    } else {
+                        return -1;
+                    }
+                    return 0;
+                }
+                case "stop": {
+                    int res = shell.parseUserPackageOp(true, err);
+                    if (res < 0) {
+                        return res;
+                    }
+
+                    if (shell.packageName != null) {
+                        shell.mInterface.finishOperation(shell.mToken,
+                                shell.op, shell.packageUid, shell.packageName);
+                    } else {
+                        return -1;
+                    }
+                    return 0;
+                }
+                default:
+                    return shell.handleDefaultCommands(cmd);
+            }
+        } catch (RemoteException e) {
+            pw.println("Remote exception: " + e);
+        }
+        return -1;
+    }
+
+    private void dumpHelp(PrintWriter pw) {
+        pw.println("AppOps service (appops) dump options:");
+        pw.println("  -h");
+        pw.println("    Print this help text.");
+        pw.println("  --op [OP]");
+        pw.println("    Limit output to data associated with the given app op code.");
+        pw.println("  --mode [MODE]");
+        pw.println("    Limit output to data associated with the given app op mode.");
+        pw.println("  --package [PACKAGE]");
+        pw.println("    Limit output to data associated with the given package name.");
+        pw.println("  --watchers");
+        pw.println("    Only output the watcher sections.");
+    }
+
+    private void dumpTimesLocked(PrintWriter pw, String firstPrefix, String prefix, long[] times,
+            long now, SimpleDateFormat sdf, Date date) {
+        boolean hasTime = false;
+        for (int i = 0; i < _NUM_UID_STATE; i++) {
+            if (times[i] != 0) {
+                hasTime = true;
+                break;
+            }
+        }
+        if (!hasTime) {
+            return;
+        }
+        boolean first = true;
+        for (int i = 0; i < _NUM_UID_STATE; i++) {
+            if (times[i] != 0) {
+                pw.print(first ? firstPrefix : prefix);
+                first = false;
+                pw.print(UID_STATE_NAMES[i]);
+                pw.print(" = ");
+                date.setTime(times[i]);
+                pw.print(sdf.format(date));
+                pw.print(" (");
+                TimeUtils.formatDuration(times[i]-now, pw);
+                pw.println(")");
+            }
+        }
+    }
+
+    @Override
+    protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        if (!DumpUtils.checkDumpAndUsageStatsPermission(mContext, TAG, pw)) return;
+
+        int dumpOp = OP_NONE;
+        String dumpPackage = null;
+        int dumpUid = Process.INVALID_UID;
+        int dumpMode = -1;
+        boolean dumpWatchers = false;
+
+        if (args != null) {
+            for (int i=0; i<args.length; i++) {
+                String arg = args[i];
+                if ("-h".equals(arg)) {
+                    dumpHelp(pw);
+                    return;
+                } else if ("-a".equals(arg)) {
+                    // dump all data
+                } else if ("--op".equals(arg)) {
+                    i++;
+                    if (i >= args.length) {
+                        pw.println("No argument for --op option");
+                        return;
+                    }
+                    dumpOp = Shell.strOpToOp(args[i], pw);
+                    if (dumpOp < 0) {
+                        return;
+                    }
+                } else if ("--package".equals(arg)) {
+                    i++;
+                    if (i >= args.length) {
+                        pw.println("No argument for --package option");
+                        return;
+                    }
+                    dumpPackage = args[i];
+                    try {
+                        dumpUid = AppGlobals.getPackageManager().getPackageUid(dumpPackage,
+                                PackageManager.MATCH_KNOWN_PACKAGES | PackageManager.MATCH_INSTANT,
+                                0);
+                    } catch (RemoteException e) {
+                    }
+                    if (dumpUid < 0) {
+                        pw.println("Unknown package: " + dumpPackage);
+                        return;
+                    }
+                    dumpUid = UserHandle.getAppId(dumpUid);
+                } else if ("--mode".equals(arg)) {
+                    i++;
+                    if (i >= args.length) {
+                        pw.println("No argument for --mode option");
+                        return;
+                    }
+                    dumpMode = Shell.strModeToMode(args[i], pw);
+                    if (dumpMode < 0) {
+                        return;
+                    }
+                } else if ("--watchers".equals(arg)) {
+                    dumpWatchers = true;
+                } else if (arg.length() > 0 && arg.charAt(0) == '-'){
+                    pw.println("Unknown option: " + arg);
+                    return;
+                } else {
+                    pw.println("Unknown command: " + arg);
+                    return;
+                }
+            }
+        }
+
+        synchronized (this) {
+            pw.println("Current AppOps Service state:");
+            mConstants.dump(pw);
+            pw.println();
+            final long now = System.currentTimeMillis();
+            final long nowElapsed = SystemClock.elapsedRealtime();
+            final long nowUptime = SystemClock.uptimeMillis();
+            final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+            final Date date = new Date();
+            boolean needSep = false;
+            if (dumpOp < 0 && dumpMode < 0 && dumpPackage == null && mProfileOwners != null
+                    && !dumpWatchers) {
+                pw.println("  Profile owners:");
+                for (int poi = 0; poi < mProfileOwners.size(); poi++) {
+                    pw.print("    User #");
+                    pw.print(mProfileOwners.keyAt(poi));
+                    pw.print(": ");
+                    UserHandle.formatUid(pw, mProfileOwners.valueAt(poi));
+                    pw.println();
+                }
+                pw.println();
+            }
+            if (mOpModeWatchers.size() > 0) {
+                boolean printedHeader = false;
+                for (int i=0; i<mOpModeWatchers.size(); i++) {
+                    if (dumpOp >= 0 && dumpOp != mOpModeWatchers.keyAt(i)) {
+                        continue;
+                    }
+                    boolean printedOpHeader = false;
+                    ArraySet<ModeCallback> callbacks = mOpModeWatchers.valueAt(i);
+                    for (int j=0; j<callbacks.size(); j++) {
+                        final ModeCallback cb = callbacks.valueAt(j);
+                        if (dumpPackage != null
+                                && dumpUid != UserHandle.getAppId(cb.mWatchingUid)) {
+                            continue;
+                        }
+                        needSep = true;
+                        if (!printedHeader) {
+                            pw.println("  Op mode watchers:");
+                            printedHeader = true;
+                        }
+                        if (!printedOpHeader) {
+                            pw.print("    Op ");
+                            pw.print(AppOpsManager.opToName(mOpModeWatchers.keyAt(i)));
+                            pw.println(":");
+                            printedOpHeader = true;
+                        }
+                        pw.print("      #"); pw.print(j); pw.print(": ");
+                        pw.println(cb);
+                    }
+                }
+            }
+            if (mPackageModeWatchers.size() > 0 && dumpOp < 0) {
+                boolean printedHeader = false;
+                for (int i=0; i<mPackageModeWatchers.size(); i++) {
+                    if (dumpPackage != null && !dumpPackage.equals(mPackageModeWatchers.keyAt(i))) {
+                        continue;
+                    }
+                    needSep = true;
+                    if (!printedHeader) {
+                        pw.println("  Package mode watchers:");
+                        printedHeader = true;
+                    }
+                    pw.print("    Pkg "); pw.print(mPackageModeWatchers.keyAt(i));
+                    pw.println(":");
+                    ArraySet<ModeCallback> callbacks = mPackageModeWatchers.valueAt(i);
+                    for (int j=0; j<callbacks.size(); j++) {
+                        pw.print("      #"); pw.print(j); pw.print(": ");
+                        pw.println(callbacks.valueAt(j));
+                    }
+                }
+            }
+            if (mModeWatchers.size() > 0 && dumpOp < 0) {
+                boolean printedHeader = false;
+                for (int i=0; i<mModeWatchers.size(); i++) {
+                    final ModeCallback cb = mModeWatchers.valueAt(i);
+                    if (dumpPackage != null
+                            && dumpUid != UserHandle.getAppId(cb.mWatchingUid)) {
+                        continue;
+                    }
+                    needSep = true;
+                    if (!printedHeader) {
+                        pw.println("  All op mode watchers:");
+                        printedHeader = true;
+                    }
+                    pw.print("    ");
+                    pw.print(Integer.toHexString(System.identityHashCode(mModeWatchers.keyAt(i))));
+                    pw.print(": "); pw.println(cb);
+                }
+            }
+            if (mActiveWatchers.size() > 0 && dumpMode < 0) {
+                needSep = true;
+                boolean printedHeader = false;
+                for (int i = 0; i < mActiveWatchers.size(); i++) {
+                    final SparseArray<ActiveCallback> activeWatchers = mActiveWatchers.valueAt(i);
+                    if (activeWatchers.size() <= 0) {
+                        continue;
+                    }
+                    final ActiveCallback cb = activeWatchers.valueAt(0);
+                    if (dumpOp >= 0 && activeWatchers.indexOfKey(dumpOp) < 0) {
+                        continue;
+                    }
+                    if (dumpPackage != null
+                            && dumpUid != UserHandle.getAppId(cb.mWatchingUid)) {
+                        continue;
+                    }
+                    if (!printedHeader) {
+                        pw.println("  All op active watchers:");
+                        printedHeader = true;
+                    }
+                    pw.print("    ");
+                    pw.print(Integer.toHexString(System.identityHashCode(
+                            mActiveWatchers.keyAt(i))));
+                    pw.println(" ->");
+                    pw.print("        [");
+                    final int opCount = activeWatchers.size();
+                    for (i = 0; i < opCount; i++) {
+                        if (i > 0) {
+                            pw.print(' ');
+                        }
+                        pw.print(AppOpsManager.opToName(activeWatchers.keyAt(i)));
+                        if (i < opCount - 1) {
+                            pw.print(',');
+                        }
+                    }
+                    pw.println("]");
+                    pw.print("        ");
+                    pw.println(cb);
+                }
+            }
+            if (mNotedWatchers.size() > 0 && dumpMode < 0) {
+                needSep = true;
+                boolean printedHeader = false;
+                for (int i = 0; i < mNotedWatchers.size(); i++) {
+                    final SparseArray<NotedCallback> notedWatchers = mNotedWatchers.valueAt(i);
+                    if (notedWatchers.size() <= 0) {
+                        continue;
+                    }
+                    final NotedCallback cb = notedWatchers.valueAt(0);
+                    if (dumpOp >= 0 && notedWatchers.indexOfKey(dumpOp) < 0) {
+                        continue;
+                    }
+                    if (dumpPackage != null
+                            && dumpUid != UserHandle.getAppId(cb.mWatchingUid)) {
+                        continue;
+                    }
+                    if (!printedHeader) {
+                        pw.println("  All op noted watchers:");
+                        printedHeader = true;
+                    }
+                    pw.print("    ");
+                    pw.print(Integer.toHexString(System.identityHashCode(
+                            mNotedWatchers.keyAt(i))));
+                    pw.println(" ->");
+                    pw.print("        [");
+                    final int opCount = notedWatchers.size();
+                    for (i = 0; i < opCount; i++) {
+                        if (i > 0) {
+                            pw.print(' ');
+                        }
+                        pw.print(AppOpsManager.opToName(notedWatchers.keyAt(i)));
+                        if (i < opCount - 1) {
+                            pw.print(',');
+                        }
+                    }
+                    pw.println("]");
+                    pw.print("        ");
+                    pw.println(cb);
+                }
+            }
+            if (mClients.size() > 0 && dumpMode < 0 && !dumpWatchers) {
+                needSep = true;
+                boolean printedHeader = false;
+                for (int i=0; i<mClients.size(); i++) {
+                    boolean printedClient = false;
+                    ClientState cs = mClients.valueAt(i);
+                    if (cs.mStartedOps.size() > 0) {
+                        boolean printedStarted = false;
+                        for (int j=0; j<cs.mStartedOps.size(); j++) {
+                            Op op = cs.mStartedOps.get(j);
+                            if (dumpOp >= 0 && op.op != dumpOp) {
+                                continue;
+                            }
+                            if (dumpPackage != null && !dumpPackage.equals(op.packageName)) {
+                                continue;
+                            }
+                            if (!printedHeader) {
+                                pw.println("  Clients:");
+                                printedHeader = true;
+                            }
+                            if (!printedClient) {
+                                pw.print("    "); pw.print(mClients.keyAt(i)); pw.println(":");
+                                pw.print("      "); pw.println(cs);
+                                printedClient = true;
+                            }
+                            if (!printedStarted) {
+                                pw.println("      Started ops:");
+                                printedStarted = true;
+                            }
+                            pw.print("        "); pw.print("uid="); pw.print(op.uid);
+                            pw.print(" pkg="); pw.print(op.packageName);
+                            pw.print(" op="); pw.println(AppOpsManager.opToName(op.op));
+                        }
+                    }
+                }
+            }
+            if (mAudioRestrictions.size() > 0 && dumpOp < 0 && dumpPackage != null
+                    && dumpMode < 0 && !dumpWatchers) {
+                boolean printedHeader = false;
+                for (int o=0; o<mAudioRestrictions.size(); o++) {
+                    final String op = AppOpsManager.opToName(mAudioRestrictions.keyAt(o));
+                    final SparseArray<Restriction> restrictions = mAudioRestrictions.valueAt(o);
+                    for (int i=0; i<restrictions.size(); i++) {
+                        if (!printedHeader){
+                            pw.println("  Audio Restrictions:");
+                            printedHeader = true;
+                            needSep = true;
+                        }
+                        final int usage = restrictions.keyAt(i);
+                        pw.print("    "); pw.print(op);
+                        pw.print(" usage="); pw.print(AudioAttributes.usageToString(usage));
+                        Restriction r = restrictions.valueAt(i);
+                        pw.print(": mode="); pw.println(AppOpsManager.modeToName(r.mode));
+                        if (!r.exceptionPackages.isEmpty()) {
+                            pw.println("      Exceptions:");
+                            for (int j=0; j<r.exceptionPackages.size(); j++) {
+                                pw.print("        "); pw.println(r.exceptionPackages.valueAt(j));
+                            }
+                        }
+                    }
+                }
+            }
+            if (needSep) {
+                pw.println();
+            }
+            for (int i=0; i<mUidStates.size(); i++) {
+                UidState uidState = mUidStates.valueAt(i);
+                final SparseIntArray opModes = uidState.opModes;
+                final ArrayMap<String, Ops> pkgOps = uidState.pkgOps;
+
+                if (dumpWatchers) {
+                    continue;
+                }
+                if (dumpOp >= 0 || dumpPackage != null || dumpMode >= 0) {
+                    boolean hasOp = dumpOp < 0 || (uidState.opModes != null
+                            && uidState.opModes.indexOfKey(dumpOp) >= 0);
+                    boolean hasPackage = dumpPackage == null;
+                    boolean hasMode = dumpMode < 0;
+                    if (!hasMode && opModes != null) {
+                        for (int opi = 0; !hasMode && opi < opModes.size(); opi++) {
+                            if (opModes.valueAt(opi) == dumpMode) {
+                                hasMode = true;
+                            }
+                        }
+                    }
+                    if (pkgOps != null) {
+                        for (int pkgi = 0;
+                                 (!hasOp || !hasPackage || !hasMode) && pkgi < pkgOps.size();
+                                 pkgi++) {
+                            Ops ops = pkgOps.valueAt(pkgi);
+                            if (!hasOp && ops != null && ops.indexOfKey(dumpOp) >= 0) {
+                                hasOp = true;
+                            }
+                            if (!hasMode) {
+                                for (int opi = 0; !hasMode && opi < ops.size(); opi++) {
+                                    if (ops.valueAt(opi).mode == dumpMode) {
+                                        hasMode = true;
+                                    }
+                                }
+                            }
+                            if (!hasPackage && dumpPackage.equals(ops.packageName)) {
+                                hasPackage = true;
+                            }
+                        }
+                    }
+                    if (uidState.foregroundOps != null && !hasOp) {
+                        if (uidState.foregroundOps.indexOfKey(dumpOp) > 0) {
+                            hasOp = true;
+                        }
+                    }
+                    if (!hasOp || !hasPackage || !hasMode) {
+                        continue;
+                    }
+                }
+
+                pw.print("  Uid "); UserHandle.formatUid(pw, uidState.uid); pw.println(":");
+                pw.print("    state=");
+                pw.println(UID_STATE_NAMES[uidState.state]);
+                if (uidState.state != uidState.pendingState) {
+                    pw.print("    pendingState=");
+                    pw.println(UID_STATE_NAMES[uidState.pendingState]);
+                }
+                if (uidState.pendingStateCommitTime != 0) {
+                    pw.print("    pendingStateCommitTime=");
+                    TimeUtils.formatDuration(uidState.pendingStateCommitTime, nowElapsed, pw);
+                    pw.println();
+                }
+                if (uidState.startNesting != 0) {
+                    pw.print("    startNesting=");
+                    pw.println(uidState.startNesting);
+                }
+                if (uidState.foregroundOps != null && (dumpMode < 0
+                        || dumpMode == AppOpsManager.MODE_FOREGROUND)) {
+                    pw.println("    foregroundOps:");
+                    for (int j = 0; j < uidState.foregroundOps.size(); j++) {
+                        if (dumpOp >= 0 && dumpOp != uidState.foregroundOps.keyAt(j)) {
+                            continue;
+                        }
+                        pw.print("      ");
+                        pw.print(AppOpsManager.opToName(uidState.foregroundOps.keyAt(j)));
+                        pw.print(": ");
+                        pw.println(uidState.foregroundOps.valueAt(j) ? "WATCHER" : "SILENT");
+                    }
+                    pw.print("    hasForegroundWatchers=");
+                    pw.println(uidState.hasForegroundWatchers);
+                }
+                needSep = true;
+
+                if (opModes != null) {
+                    final int opModeCount = opModes.size();
+                    for (int j = 0; j < opModeCount; j++) {
+                        final int code = opModes.keyAt(j);
+                        final int mode = opModes.valueAt(j);
+                        if (dumpOp >= 0 && dumpOp != code) {
+                            continue;
+                        }
+                        if (dumpMode >= 0 && dumpMode != mode) {
+                            continue;
+                        }
+                        pw.print("      "); pw.print(AppOpsManager.opToName(code));
+                        pw.print(": mode="); pw.println(AppOpsManager.modeToName(mode));
+                    }
+                }
+
+                if (pkgOps == null) {
+                    continue;
+                }
+
+                for (int pkgi = 0; pkgi < pkgOps.size(); pkgi++) {
+                    final Ops ops = pkgOps.valueAt(pkgi);
+                    if (dumpPackage != null && !dumpPackage.equals(ops.packageName)) {
+                        continue;
+                    }
+                    boolean printedPackage = false;
+                    for (int j=0; j<ops.size(); j++) {
+                        final Op op = ops.valueAt(j);
+                        if (dumpOp >= 0 && dumpOp != op.op) {
+                            continue;
+                        }
+                        if (dumpMode >= 0 && dumpMode != op.mode) {
+                            continue;
+                        }
+                        if (!printedPackage) {
+                            pw.print("    Package "); pw.print(ops.packageName); pw.println(":");
+                            printedPackage = true;
+                        }
+                        pw.print("      "); pw.print(AppOpsManager.opToName(op.op));
+                        pw.print(" ("); pw.print(AppOpsManager.modeToName(op.mode));
+                        final int switchOp = AppOpsManager.opToSwitch(op.op);
+                        if (switchOp != op.op) {
+                            pw.print(" / switch ");
+                            pw.print(AppOpsManager.opToName(switchOp));
+                            final Op switchObj = ops.get(switchOp);
+                            int mode = switchObj != null
+                                    ? switchObj.mode : AppOpsManager.opToDefaultMode(switchOp);
+                            pw.print("="); pw.print(AppOpsManager.modeToName(mode));
+                        }
+                        pw.println("): ");
+                        dumpTimesLocked(pw,
+                                "          Access: ",
+                                "                  ", op.time, now, sdf, date);
+                        dumpTimesLocked(pw,
+                                "          Reject: ",
+                                "                  ", op.rejectTime, now, sdf, date);
+                        if (op.duration == -1) {
+                            pw.print("          Running start at: ");
+                            TimeUtils.formatDuration(nowElapsed-op.startRealtime, pw);
+                            pw.println();
+                        } else if (op.duration != 0) {
+                            pw.print("          duration=");
+                            TimeUtils.formatDuration(op.duration, pw);
+                            pw.println();
+                        }
+                        if (op.startNesting != 0) {
+                            pw.print("          startNesting=");
+                            pw.println(op.startNesting);
+                        }
+                    }
+                }
+            }
+            if (needSep) {
+                pw.println();
+            }
+
+            final int userRestrictionCount = mOpUserRestrictions.size();
+            for (int i = 0; i < userRestrictionCount; i++) {
+                IBinder token = mOpUserRestrictions.keyAt(i);
+                ClientRestrictionState restrictionState = mOpUserRestrictions.valueAt(i);
+                boolean printedTokenHeader = false;
+
+                if (dumpMode >= 0 || dumpWatchers) {
+                    continue;
+                }
+
+                final int restrictionCount = restrictionState.perUserRestrictions != null
+                        ? restrictionState.perUserRestrictions.size() : 0;
+                if (restrictionCount > 0 && dumpPackage == null) {
+                    boolean printedOpsHeader = false;
+                    for (int j = 0; j < restrictionCount; j++) {
+                        int userId = restrictionState.perUserRestrictions.keyAt(j);
+                        boolean[] restrictedOps = restrictionState.perUserRestrictions.valueAt(j);
+                        if (restrictedOps == null) {
+                            continue;
+                        }
+                        if (dumpOp >= 0 && (dumpOp >= restrictedOps.length
+                                || !restrictedOps[dumpOp])) {
+                            continue;
+                        }
+                        if (!printedTokenHeader) {
+                            pw.println("  User restrictions for token " + token + ":");
+                            printedTokenHeader = true;
+                        }
+                        if (!printedOpsHeader) {
+                            pw.println("      Restricted ops:");
+                            printedOpsHeader = true;
+                        }
+                        StringBuilder restrictedOpsValue = new StringBuilder();
+                        restrictedOpsValue.append("[");
+                        final int restrictedOpCount = restrictedOps.length;
+                        for (int k = 0; k < restrictedOpCount; k++) {
+                            if (restrictedOps[k]) {
+                                if (restrictedOpsValue.length() > 1) {
+                                    restrictedOpsValue.append(", ");
+                                }
+                                restrictedOpsValue.append(AppOpsManager.opToName(k));
+                            }
+                        }
+                        restrictedOpsValue.append("]");
+                        pw.print("        "); pw.print("user: "); pw.print(userId);
+                                pw.print(" restricted ops: "); pw.println(restrictedOpsValue);
+                    }
+                }
+
+                final int excludedPackageCount = restrictionState.perUserExcludedPackages != null
+                        ? restrictionState.perUserExcludedPackages.size() : 0;
+                if (excludedPackageCount > 0 && dumpOp < 0) {
+                    boolean printedPackagesHeader = false;
+                    for (int j = 0; j < excludedPackageCount; j++) {
+                        int userId = restrictionState.perUserExcludedPackages.keyAt(j);
+                        String[] packageNames = restrictionState.perUserExcludedPackages.valueAt(j);
+                        if (packageNames == null) {
+                            continue;
+                        }
+                        boolean hasPackage;
+                        if (dumpPackage != null) {
+                            hasPackage = false;
+                            for (String pkg : packageNames) {
+                                if (dumpPackage.equals(pkg)) {
+                                    hasPackage = true;
+                                    break;
+                                }
+                            }
+                        } else {
+                            hasPackage = true;
+                        }
+                        if (!hasPackage) {
+                            continue;
+                        }
+                        if (!printedTokenHeader) {
+                            pw.println("  User restrictions for token " + token + ":");
+                            printedTokenHeader = true;
+                        }
+                        if (!printedPackagesHeader) {
+                            pw.println("      Excluded packages:");
+                            printedPackagesHeader = true;
+                        }
+                        pw.print("        "); pw.print("user: "); pw.print(userId);
+                                pw.print(" packages: "); pw.println(Arrays.toString(packageNames));
+                    }
+                }
+            }
+        }
+
+        // Must not hold the appops lock
+        mHistoricalRegistry.dump("  ", pw, dumpUid, dumpPackage, dumpOp);
+    }
+
+    private static final class Restriction {
+        private static final ArraySet<String> NO_EXCEPTIONS = new ArraySet<String>();
+        int mode;
+        ArraySet<String> exceptionPackages = NO_EXCEPTIONS;
+    }
+
+    @Override
+    public void setUserRestrictions(Bundle restrictions, IBinder token, int userHandle) {
+        checkSystemUid("setUserRestrictions");
+        Preconditions.checkNotNull(restrictions);
+        Preconditions.checkNotNull(token);
+        for (int i = 0; i < AppOpsManager._NUM_OP; i++) {
+            String restriction = AppOpsManager.opToRestriction(i);
+            if (restriction != null) {
+                setUserRestrictionNoCheck(i, restrictions.getBoolean(restriction, false), token,
+                        userHandle, null);
+            }
+        }
+    }
+
+    @Override
+    public void setUserRestriction(int code, boolean restricted, IBinder token, int userHandle,
+            String[] exceptionPackages) {
+        if (Binder.getCallingPid() != Process.myPid()) {
+            mContext.enforcePermission(Manifest.permission.MANAGE_APP_OPS_RESTRICTIONS,
+                    Binder.getCallingPid(), Binder.getCallingUid(), null);
+        }
+        if (userHandle != UserHandle.getCallingUserId()) {
+            if (mContext.checkCallingOrSelfPermission(Manifest.permission
+                    .INTERACT_ACROSS_USERS_FULL) != PackageManager.PERMISSION_GRANTED
+                && mContext.checkCallingOrSelfPermission(Manifest.permission
+                    .INTERACT_ACROSS_USERS) != PackageManager.PERMISSION_GRANTED) {
+                throw new SecurityException("Need INTERACT_ACROSS_USERS_FULL or"
+                        + " INTERACT_ACROSS_USERS to interact cross user ");
+            }
+        }
+        verifyIncomingOp(code);
+        Preconditions.checkNotNull(token);
+        setUserRestrictionNoCheck(code, restricted, token, userHandle, exceptionPackages);
+    }
+
+    private void setUserRestrictionNoCheck(int code, boolean restricted, IBinder token,
+            int userHandle, String[] exceptionPackages) {
+        synchronized (AppOpsService.this) {
+            ClientRestrictionState restrictionState = mOpUserRestrictions.get(token);
+
+            if (restrictionState == null) {
+                try {
+                    restrictionState = new ClientRestrictionState(token);
+                } catch (RemoteException e) {
+                    return;
+                }
+                mOpUserRestrictions.put(token, restrictionState);
+            }
+
+            if (restrictionState.setRestriction(code, restricted, exceptionPackages, userHandle)) {
+                mHandler.sendMessage(PooledLambda.obtainMessage(
+                        AppOpsService::notifyWatchersOfChange, this, code, UID_ANY));
+            }
+
+            if (restrictionState.isDefault()) {
+                mOpUserRestrictions.remove(token);
+                restrictionState.destroy();
+            }
+        }
+    }
+
+    private void notifyWatchersOfChange(int code, int uid) {
+        final ArraySet<ModeCallback> clonedCallbacks;
+        synchronized (this) {
+            ArraySet<ModeCallback> callbacks = mOpModeWatchers.get(code);
+            if (callbacks == null) {
+                return;
+            }
+            clonedCallbacks = new ArraySet<>(callbacks);
+        }
+
+        notifyOpChanged(clonedCallbacks,  code, uid, null);
+    }
+
+    @Override
+    public void removeUser(int userHandle) throws RemoteException {
+        checkSystemUid("removeUser");
+        synchronized (AppOpsService.this) {
+            final int tokenCount = mOpUserRestrictions.size();
+            for (int i = tokenCount - 1; i >= 0; i--) {
+                ClientRestrictionState opRestrictions = mOpUserRestrictions.valueAt(i);
+                opRestrictions.removeUser(userHandle);
+            }
+            removeUidsForUserLocked(userHandle);
+        }
+    }
+
+    @Override
+    public boolean isOperationActive(int code, int uid, String packageName) {
+        if (Binder.getCallingUid() != uid) {
+            if (mContext.checkCallingOrSelfPermission(Manifest.permission.WATCH_APPOPS)
+                    != PackageManager.PERMISSION_GRANTED) {
+                return false;
+            }
+        }
+        verifyIncomingOp(code);
+        final String resolvedPackageName = resolvePackageName(uid, packageName);
+        if (resolvedPackageName == null) {
+            return false;
+        }
+        synchronized (AppOpsService.this) {
+            for (int i = mClients.size() - 1; i >= 0; i--) {
+                final ClientState client = mClients.valueAt(i);
+                for (int j = client.mStartedOps.size() - 1; j >= 0; j--) {
+                    final Op op = client.mStartedOps.get(j);
+                    if (op.op == code && op.uid == uid) return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public void setHistoryParameters(@AppOpsManager.HistoricalMode int mode,
+            long baseSnapshotInterval, int compressionStep) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS,
+                "setHistoryParameters");
+        // Must not hold the appops lock
+        mHistoricalRegistry.setHistoryParameters(mode, baseSnapshotInterval, compressionStep);
+    }
+
+    @Override
+    public void offsetHistory(long offsetMillis) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS,
+                "offsetHistory");
+        // Must not hold the appops lock
+        mHistoricalRegistry.offsetHistory(offsetMillis);
+    }
+
+    @Override
+    public void addHistoricalOps(HistoricalOps ops) {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS,
+                "addHistoricalOps");
+        // Must not hold the appops lock
+        mHistoricalRegistry.addHistoricalOps(ops);
+    }
+
+    @Override
+    public void resetHistoryParameters() {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS,
+                "resetHistoryParameters");
+        // Must not hold the appops lock
+        mHistoricalRegistry.resetHistoryParameters();
+    }
+
+    @Override
+    public void clearHistory() {
+        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS,
+                "clearHistory");
+        // Must not hold the appops lock
+        mHistoricalRegistry.clearHistory();
+    }
+
+    private void removeUidsForUserLocked(int userHandle) {
+        for (int i = mUidStates.size() - 1; i >= 0; --i) {
+            final int uid = mUidStates.keyAt(i);
+            if (UserHandle.getUserId(uid) == userHandle) {
+                mUidStates.removeAt(i);
+            }
+        }
+    }
+
+    private void checkSystemUid(String function) {
+        int uid = Binder.getCallingUid();
+        if (uid != Process.SYSTEM_UID) {
+            throw new SecurityException(function + " must by called by the system");
+        }
+    }
+
+    private static String resolvePackageName(int uid, String packageName)  {
+        if (uid == Process.ROOT_UID) {
+            return "root";
+        } else if (uid == Process.SHELL_UID) {
+            return "com.android.shell";
+        } else if (uid == Process.MEDIA_UID) {
+            return "media";
+        } else if (uid == Process.AUDIOSERVER_UID) {
+            return "audioserver";
+        } else if (uid == Process.CAMERASERVER_UID) {
+            return "cameraserver";
+        } else if (uid == Process.SYSTEM_UID && packageName == null) {
+            return "android";
+        }
+        return packageName;
+    }
+
+    private static int resolveUid(String packageName)  {
+        if (packageName == null) {
+            return -1;
+        }
+        switch (packageName) {
+            case "root":
+                return Process.ROOT_UID;
+            case "shell":
+                return Process.SHELL_UID;
+            case "media":
+                return Process.MEDIA_UID;
+            case "audioserver":
+                return Process.AUDIOSERVER_UID;
+            case "cameraserver":
+                return Process.CAMERASERVER_UID;
+        }
+        return -1;
+    }
+
+    private static String[] getPackagesForUid(int uid) {
+        String[] packageNames = null;
+
+        // Very early during boot the package manager is not yet or not yet fully started. At this
+        // time there are no packages yet.
+        if (AppGlobals.getPackageManager() != null) {
+            try {
+                packageNames = AppGlobals.getPackageManager().getPackagesForUid(uid);
+            } catch (RemoteException e) {
+                /* ignore - local call */
+            }
+        }
+        if (packageNames == null) {
+            return EmptyArray.STRING;
+        }
+        return packageNames;
+    }
+
+    private static void checkValidOpsOrNull(String[] opNames) {
+        if (opNames != null) {
+            for (String opName : opNames) {
+                if (AppOpsManager.strOpToOp(opName) == AppOpsManager.OP_NONE) {
+                    throw new IllegalArgumentException("Unknown op: " + opName);
+                }
+            }
+        }
+    }
+
+    private final class ClientRestrictionState implements DeathRecipient {
+        private final IBinder token;
+        SparseArray<boolean[]> perUserRestrictions;
+        SparseArray<String[]> perUserExcludedPackages;
+
+        public ClientRestrictionState(IBinder token)
+                throws RemoteException {
+            token.linkToDeath(this, 0);
+            this.token = token;
+        }
+
+        public boolean setRestriction(int code, boolean restricted,
+                String[] excludedPackages, int userId) {
+            boolean changed = false;
+
+            if (perUserRestrictions == null && restricted) {
+                perUserRestrictions = new SparseArray<>();
+            }
+
+            int[] users;
+            if (userId == UserHandle.USER_ALL) {
+                List<UserInfo> liveUsers = UserManager.get(mContext).getUsers(false);
+
+                users = new int[liveUsers.size()];
+                for (int i = 0; i < liveUsers.size(); i++) {
+                    users[i] = liveUsers.get(i).id;
+                }
+            } else {
+                users = new int[]{userId};
+            }
+
+            if (perUserRestrictions != null) {
+                int numUsers = users.length;
+
+                for (int i = 0; i < numUsers; i++) {
+                    int thisUserId = users[i];
+
+                    boolean[] userRestrictions = perUserRestrictions.get(thisUserId);
+                    if (userRestrictions == null && restricted) {
+                        userRestrictions = new boolean[AppOpsManager._NUM_OP];
+                        perUserRestrictions.put(thisUserId, userRestrictions);
+                    }
+                    if (userRestrictions != null && userRestrictions[code] != restricted) {
+                        userRestrictions[code] = restricted;
+                        if (!restricted && isDefault(userRestrictions)) {
+                            perUserRestrictions.remove(thisUserId);
+                            userRestrictions = null;
+                        }
+                        changed = true;
+                    }
+
+                    if (userRestrictions != null) {
+                        final boolean noExcludedPackages = ArrayUtils.isEmpty(excludedPackages);
+                        if (perUserExcludedPackages == null && !noExcludedPackages) {
+                            perUserExcludedPackages = new SparseArray<>();
+                        }
+                        if (perUserExcludedPackages != null && !Arrays.equals(excludedPackages,
+                                perUserExcludedPackages.get(thisUserId))) {
+                            if (noExcludedPackages) {
+                                perUserExcludedPackages.remove(thisUserId);
+                                if (perUserExcludedPackages.size() <= 0) {
+                                    perUserExcludedPackages = null;
+                                }
+                            } else {
+                                perUserExcludedPackages.put(thisUserId, excludedPackages);
+                            }
+                            changed = true;
+                        }
+                    }
+                }
+            }
+
+            return changed;
+        }
+
+        public boolean hasRestriction(int restriction, String packageName, int userId) {
+            if (perUserRestrictions == null) {
+                return false;
+            }
+            boolean[] restrictions = perUserRestrictions.get(userId);
+            if (restrictions == null) {
+                return false;
+            }
+            if (!restrictions[restriction]) {
+                return false;
+            }
+            if (perUserExcludedPackages == null) {
+                return true;
+            }
+            String[] perUserExclusions = perUserExcludedPackages.get(userId);
+            if (perUserExclusions == null) {
+                return true;
+            }
+            return !ArrayUtils.contains(perUserExclusions, packageName);
+        }
+
+        public void removeUser(int userId) {
+            if (perUserExcludedPackages != null) {
+                perUserExcludedPackages.remove(userId);
+                if (perUserExcludedPackages.size() <= 0) {
+                    perUserExcludedPackages = null;
+                }
+            }
+            if (perUserRestrictions != null) {
+                perUserRestrictions.remove(userId);
+                if (perUserRestrictions.size() <= 0) {
+                    perUserRestrictions = null;
+                }
+            }
+        }
+
+        public boolean isDefault() {
+            return perUserRestrictions == null || perUserRestrictions.size() <= 0;
+        }
+
+        @Override
+        public void binderDied() {
+            synchronized (AppOpsService.this) {
+                mOpUserRestrictions.remove(token);
+                if (perUserRestrictions == null) {
+                    return;
+                }
+                final int userCount = perUserRestrictions.size();
+                for (int i = 0; i < userCount; i++) {
+                    final boolean[] restrictions = perUserRestrictions.valueAt(i);
+                    final int restrictionCount = restrictions.length;
+                    for (int j = 0; j < restrictionCount; j++) {
+                        if (restrictions[j]) {
+                            final int changedCode = j;
+                            mHandler.post(() -> notifyWatchersOfChange(changedCode, UID_ANY));
+                        }
+                    }
+                }
+                destroy();
+            }
+        }
+
+        public void destroy() {
+            token.unlinkToDeath(this, 0);
+        }
+
+        private boolean isDefault(boolean[] array) {
+            if (ArrayUtils.isEmpty(array)) {
+                return true;
+            }
+            for (boolean value : array) {
+                if (value) {
+                    return false;
+                }
+            }
+            return true;
+        }
+    }
+
+    private final class AppOpsManagerInternalImpl extends AppOpsManagerInternal {
+        @Override public void setDeviceAndProfileOwners(SparseIntArray owners) {
+            synchronized (AppOpsService.this) {
+                mProfileOwners = owners;
+            }
+        }
+
+        @Override
+        public void setUidMode(int code, int uid, int mode) {
+            AppOpsService.this.setUidMode(code, uid, mode);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/appop/HistoricalRegistry.java b/services/core/java/com/android/server/appop/HistoricalRegistry.java
new file mode 100644
index 0000000..8d7811f
--- /dev/null
+++ b/services/core/java/com/android/server/appop/HistoricalRegistry.java
@@ -0,0 +1,1495 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.appop;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AppOpsManager;
+import android.app.AppOpsManager.HistoricalMode;
+import android.app.AppOpsManager.HistoricalOp;
+import android.app.AppOpsManager.HistoricalOps;
+import android.app.AppOpsManager.HistoricalPackageOps;
+import android.app.AppOpsManager.HistoricalUidOps;
+import android.app.AppOpsManager.UidState;
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Message;
+import android.os.Process;
+import android.os.RemoteCallback;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.ArraySet;
+import android.util.Slog;
+import android.util.TimeUtils;
+import android.util.Xml;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.os.AtomicDirectory;
+import com.android.internal.os.BackgroundThread;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.XmlUtils;
+import com.android.internal.util.function.pooled.PooledLambda;
+import com.android.server.FgThread;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This class managers historical app op state. This includes reading, persistence,
+ * accounting, querying.
+ * <p>
+ * The history is kept forever in multiple files. Each file time contains the
+ * relative offset from the current time which time is encoded in the file name.
+ * The files contain historical app op state snapshots which have times that
+ * are relative to the time of the container file.
+ *
+ * The data in the files are stored in a logarithmic fashion where where every
+ * subsequent file would contain data for ten times longer interval with ten
+ * times more time distance between snapshots. Hence, the more time passes
+ * the lesser the fidelity.
+ * <p>
+ * For example, the first file would contain data for 1 days with snapshots
+ * every 0.1 days, the next file would contain data for the period 1 to 10
+ * days with snapshots every 1 days, and so on.
+ * <p>
+ * THREADING AND LOCKING: Reported ops must be processed as quickly as possible.
+ * We keep ops pending to be persisted in memory and write to disk on a background
+ * thread. Hence, methods that report op changes are locking only the in memory
+ * state guarded by the mInMemoryLock which happens to be the app ops service lock
+ * avoiding a lock addition on the critical path. When a query comes we need to
+ * evaluate it based off both in memory and on disk state. This means they need to
+ * be frozen with respect to each other and not change from the querying caller's
+ * perspective. To achieve this we add a dedicated mOnDiskLock to guard the on
+ * disk state. To have fast critical path we need to limit the locking of the
+ * mInMemoryLock, thus for operations that touch in memory and on disk state one
+ * must grab first the mOnDiskLock and then the mInMemoryLock and limit the
+ * in memory lock to extraction of relevant data. Locking order is critical to
+ * avoid deadlocks. The convention is that xxxDLocked suffix means the method
+ * must be called with the mOnDiskLock lock, xxxMLocked suffix means the method
+ * must be called with the mInMemoryLock, xxxDMLocked suffix means the method
+ * must be called with the mOnDiskLock and mInMemoryLock locks acquired in that
+ * exact order.
+ */
+// TODO (bug:122218838): Make sure we handle start of epoch time
+// TODO (bug:122218838): Validate changed time is handled correctly
+final class HistoricalRegistry {
+    private static final boolean DEBUG = false;
+
+    private static final String LOG_TAG = HistoricalRegistry.class.getSimpleName();
+
+    private static final String PARAMETER_DELIMITER = ",";
+    private static final String PARAMETER_ASSIGNMENT = "=";
+
+    @GuardedBy("mLock")
+    private @NonNull LinkedList<HistoricalOps> mPendingWrites = new LinkedList<>();
+
+    // Lock for read/write access to on disk state
+    private final Object mOnDiskLock = new Object();
+
+    //Lock for read/write access to in memory state
+    private final @NonNull Object mInMemoryLock;
+
+    private static final int MSG_WRITE_PENDING_HISTORY = 1;
+
+    // See mMode
+    private static final int DEFAULT_MODE = AppOpsManager.HISTORICAL_MODE_DISABLED;
+
+    // See mBaseSnapshotInterval
+    private static final long DEFAULT_SNAPSHOT_INTERVAL_MILLIS = TimeUnit.MINUTES.toMillis(15);
+
+    // See mIntervalCompressionMultiplier
+    private static final long DEFAULT_COMPRESSION_STEP = 10;
+
+    /**
+     * Whether history is enabled.
+     */
+    @GuardedBy("mInMemoryLock")
+    private int mMode = AppOpsManager.HISTORICAL_MODE_DISABLED;
+
+    /**
+     * This granularity has been chosen to allow clean delineation for intervals
+     * humans understand, 15 min, 60, min, a day, a week, a month (30 days).
+     */
+    @GuardedBy("mInMemoryLock")
+    private long mBaseSnapshotInterval = DEFAULT_SNAPSHOT_INTERVAL_MILLIS;
+
+    /**
+     * The compression between steps. Each subsequent step is this much longer
+     * in terms of duration and each snapshot is this much more apart from the
+     * previous step.
+     */
+    @GuardedBy("mInMemoryLock")
+    private long mIntervalCompressionMultiplier = DEFAULT_COMPRESSION_STEP;
+
+    // The current ops to which to add statistics.
+    @GuardedBy("mInMemoryLock")
+    private @Nullable HistoricalOps mCurrentHistoricalOps;
+
+    // The time we should write the next snapshot.
+    @GuardedBy("mInMemoryLock")
+    private long mNextPersistDueTimeMillis;
+
+    // How much to offset the history on the next write.
+    @GuardedBy("mInMemoryLock")
+    private long mPendingHistoryOffsetMillis;
+
+    // Object managing persistence (read/write)
+    @GuardedBy("mOnDiskLock")
+    private Persistence mPersistence = new Persistence(mBaseSnapshotInterval,
+            mIntervalCompressionMultiplier);
+
+    HistoricalRegistry(@NonNull Object lock) {
+        mInMemoryLock = lock;
+        if (mMode != AppOpsManager.HISTORICAL_MODE_DISABLED) {
+            synchronized (mInMemoryLock) {
+                // When starting always adjust history to now.
+                mPendingHistoryOffsetMillis = System.currentTimeMillis()
+                        - mPersistence.getLastPersistTimeMillisDLocked();
+            }
+        }
+    }
+
+    void systemReady(@NonNull ContentResolver resolver) {
+        updateParametersFromSetting(resolver);
+        final Uri uri = Settings.Global.getUriFor(Settings.Global.APPOP_HISTORY_PARAMETERS);
+        resolver.registerContentObserver(uri, false, new ContentObserver(
+                FgThread.getHandler()) {
+            @Override
+            public void onChange(boolean selfChange) {
+                updateParametersFromSetting(resolver);
+            }
+        });
+    }
+
+    private void updateParametersFromSetting(@NonNull ContentResolver resolver) {
+        final String setting = Settings.Global.getString(resolver,
+                Settings.Global.APPOP_HISTORY_PARAMETERS);
+        if (setting == null) {
+            return;
+        }
+        String modeValue = null;
+        String baseSnapshotIntervalValue = null;
+        String intervalMultiplierValue = null;
+        final String[] parameters = setting.split(PARAMETER_DELIMITER);
+        for (String parameter : parameters) {
+            final String[] parts = parameter.split(PARAMETER_ASSIGNMENT);
+            if (parts.length == 2) {
+                final String key = parts[0].trim();
+                switch (key) {
+                    case Settings.Global.APPOP_HISTORY_MODE: {
+                        modeValue = parts[1].trim();
+                    } break;
+                    case Settings.Global.APPOP_HISTORY_BASE_INTERVAL_MILLIS: {
+                        baseSnapshotIntervalValue = parts[1].trim();
+                    } break;
+                    case Settings.Global.APPOP_HISTORY_INTERVAL_MULTIPLIER: {
+                        intervalMultiplierValue = parts[1].trim();
+                    } break;
+                    default: {
+                        Slog.w(LOG_TAG, "Unknown parameter: " + parameter);
+                    }
+                }
+            }
+        }
+        if (modeValue != null && baseSnapshotIntervalValue != null
+                && intervalMultiplierValue != null) {
+            try {
+                final int mode = AppOpsManager.parseHistoricalMode(modeValue);
+                final long baseSnapshotInterval = Long.parseLong(baseSnapshotIntervalValue);
+                final int intervalCompressionMultiplier = Integer.parseInt(intervalMultiplierValue);
+                setHistoryParameters(mode, baseSnapshotInterval,intervalCompressionMultiplier);
+                return;
+            } catch (NumberFormatException ignored) {}
+        }
+        Slog.w(LOG_TAG, "Bad value for" + Settings.Global.APPOP_HISTORY_PARAMETERS
+                + "=" + setting + " resetting!");
+    }
+
+    void dump(String prefix, PrintWriter pw,  int filterUid,
+            String filterPackage, int filterOp) {
+        synchronized (mOnDiskLock) {
+            synchronized (mInMemoryLock) {
+                pw.println();
+                pw.print(prefix);
+                pw.print("History:");
+
+                pw.print("  mode=");
+                pw.println(AppOpsManager.historicalModeToString(mMode));
+
+                final StringDumpVisitor visitor = new StringDumpVisitor(prefix + "  ",
+                        pw, filterUid, filterPackage, filterOp);
+                final long nowMillis = System.currentTimeMillis();
+
+                // Dump in memory state first
+                final HistoricalOps currentOps = getUpdatedPendingHistoricalOpsMLocked(
+                        nowMillis);
+                makeRelativeToEpochStart(currentOps, nowMillis);
+                currentOps.accept(visitor);
+
+                final List<HistoricalOps> ops = mPersistence.readHistoryDLocked();
+                if (ops != null) {
+                    // TODO (bug:122218838): Make sure this is properly dumped
+                    final long remainingToFillBatchMillis = mNextPersistDueTimeMillis
+                            - nowMillis - mBaseSnapshotInterval;
+                    final int opCount = ops.size();
+                    for (int i = 0; i < opCount; i++) {
+                        final HistoricalOps op = ops.get(i);
+                        op.offsetBeginAndEndTime(remainingToFillBatchMillis);
+                        makeRelativeToEpochStart(op, nowMillis);
+                        op.accept(visitor);
+                    }
+                } else {
+                    pw.println("  Empty");
+                }
+            }
+        }
+    }
+
+    @HistoricalMode int getMode() {
+        synchronized (mInMemoryLock) {
+            return mMode;
+        }
+    }
+
+    @Nullable void getHistoricalOpsFromDiskRaw(int uid, @NonNull String packageName,
+            @Nullable String[] opNames, long beginTimeMillis, long endTimeMillis,
+            @NonNull RemoteCallback callback) {
+        final HistoricalOps result = new HistoricalOps(beginTimeMillis, endTimeMillis);
+        mPersistence.collectHistoricalOpsDLocked(result, uid, packageName, opNames,
+                beginTimeMillis, endTimeMillis);
+        final Bundle payload = new Bundle();
+        payload.putParcelable(AppOpsManager.KEY_HISTORICAL_OPS, result);
+        callback.sendResult(payload);
+    }
+
+    @Nullable void getHistoricalOps(int uid, @NonNull String packageName,
+            @Nullable String[] opNames, long beginTimeMillis, long endTimeMillis,
+            @NonNull RemoteCallback callback) {
+        final long currentTimeMillis = System.currentTimeMillis();
+        if (endTimeMillis == Long.MAX_VALUE) {
+            endTimeMillis = currentTimeMillis;
+        }
+
+        // Argument times are based off epoch start while our internal store is
+        // based off now, so take this into account.
+        final long inMemoryAdjBeginTimeMillis = Math.max(currentTimeMillis - endTimeMillis, 0);
+        final long inMemoryAdjEndTimeMillis = Math.max(currentTimeMillis - beginTimeMillis, 0);
+        final HistoricalOps result = new HistoricalOps(inMemoryAdjBeginTimeMillis,
+                inMemoryAdjEndTimeMillis);
+
+        synchronized (mOnDiskLock) {
+            final List<HistoricalOps> pendingWrites;
+            final HistoricalOps currentOps;
+            synchronized (mInMemoryLock) {
+                currentOps = getUpdatedPendingHistoricalOpsMLocked(currentTimeMillis);
+                if (!(inMemoryAdjBeginTimeMillis >= currentOps.getEndTimeMillis()
+                        || inMemoryAdjEndTimeMillis <= currentOps.getBeginTimeMillis())) {
+                    // Some of the current batch falls into the query, so extract that.
+                    final HistoricalOps currentOpsCopy = new HistoricalOps(currentOps);
+                    currentOpsCopy.filter(uid, packageName, opNames, inMemoryAdjBeginTimeMillis,
+                            inMemoryAdjEndTimeMillis);
+                    result.merge(currentOpsCopy);
+                }
+                pendingWrites = new ArrayList<>(mPendingWrites);
+                mPendingWrites.clear();
+            }
+
+            // If the query was only for in-memory state - done.
+            if (inMemoryAdjEndTimeMillis > currentOps.getEndTimeMillis()) {
+                // If there is a write in flight we need to force it now
+                persistPendingHistory(pendingWrites);
+                // Collect persisted state.
+                final long onDiskAndInMemoryOffsetMillis = currentTimeMillis
+                        - mNextPersistDueTimeMillis + mBaseSnapshotInterval;
+                final long onDiskAdjBeginTimeMillis = Math.max(inMemoryAdjBeginTimeMillis
+                        - onDiskAndInMemoryOffsetMillis, 0);
+                final long onDiskAdjEndTimeMillis = Math.max(inMemoryAdjEndTimeMillis
+                        - onDiskAndInMemoryOffsetMillis, 0);
+                mPersistence.collectHistoricalOpsDLocked(result, uid, packageName, opNames,
+                        onDiskAdjBeginTimeMillis, onDiskAdjEndTimeMillis);
+            }
+
+            // Rebase the result time to be since epoch.
+            result.setBeginAndEndTime(beginTimeMillis, endTimeMillis);
+
+            // Send back the result.
+            final Bundle payload = new Bundle();
+            payload.putParcelable(AppOpsManager.KEY_HISTORICAL_OPS, result);
+            callback.sendResult(payload);
+        }
+    }
+
+    void incrementOpAccessedCount(int op, int uid, @NonNull String packageName,
+            @UidState int uidState) {
+        synchronized (mInMemoryLock) {
+            if (mMode == AppOpsManager.HISTORICAL_MODE_ENABLED_ACTIVE) {
+                getUpdatedPendingHistoricalOpsMLocked(System.currentTimeMillis())
+                        .increaseAccessCount(op, uid, packageName, uidState, 1);
+
+            }
+        }
+    }
+
+    void incrementOpRejected(int op, int uid, @NonNull String packageName,
+            @UidState int uidState) {
+        synchronized (mInMemoryLock) {
+            if (mMode == AppOpsManager.HISTORICAL_MODE_ENABLED_ACTIVE) {
+                getUpdatedPendingHistoricalOpsMLocked(System.currentTimeMillis())
+                        .increaseRejectCount(op, uid, packageName, uidState, 1);
+            }
+        }
+    }
+
+    void increaseOpAccessDuration(int op, int uid, @NonNull String packageName,
+            @UidState int uidState, long increment) {
+        synchronized (mInMemoryLock) {
+            if (mMode == AppOpsManager.HISTORICAL_MODE_ENABLED_ACTIVE) {
+                getUpdatedPendingHistoricalOpsMLocked(System.currentTimeMillis())
+                        .increaseAccessDuration(op, uid, packageName, uidState, increment);
+            }
+        }
+    }
+
+    void setHistoryParameters(@HistoricalMode int mode,
+            long baseSnapshotInterval, long intervalCompressionMultiplier) {
+        synchronized (mOnDiskLock) {
+            synchronized (mInMemoryLock) {
+                boolean resampleHistory = false;
+                Slog.i(LOG_TAG, "New history parameters: mode:"
+                        + AppOpsManager.historicalModeToString(mMode) + " baseSnapshotInterval:"
+                        + baseSnapshotInterval + " intervalCompressionMultiplier:"
+                        + intervalCompressionMultiplier);
+                if (mMode != mode) {
+                    mMode = mode;
+                    if (mMode == AppOpsManager.HISTORICAL_MODE_DISABLED) {
+                        clearHistoryOnDiskLocked();
+                    }
+                }
+                if (mBaseSnapshotInterval != baseSnapshotInterval) {
+                    mBaseSnapshotInterval = baseSnapshotInterval;
+                    resampleHistory = true;
+                }
+                if (mIntervalCompressionMultiplier != intervalCompressionMultiplier) {
+                    mIntervalCompressionMultiplier = intervalCompressionMultiplier;
+                    resampleHistory = true;
+                }
+                if (resampleHistory) {
+                    resampleHistoryOnDiskInMemoryDMLocked(0);
+                }
+            }
+        }
+    }
+
+    void offsetHistory(long offsetMillis) {
+        synchronized (mOnDiskLock) {
+            synchronized (mInMemoryLock) {
+                final List<HistoricalOps> history = mPersistence.readHistoryDLocked();
+                clearHistory();
+                if (history != null) {
+                    final int historySize = history.size();
+                    for (int i = 0; i < historySize; i++) {
+                        final HistoricalOps ops = history.get(i);
+                        ops.offsetBeginAndEndTime(offsetMillis);
+                    }
+                    if (offsetMillis < 0) {
+                        pruneFutureOps(history);
+                    }
+                    mPersistence.persistHistoricalOpsDLocked(history);
+                }
+            }
+        }
+    }
+
+    void addHistoricalOps(HistoricalOps ops) {
+        final List<HistoricalOps> pendingWrites;
+        synchronized (mInMemoryLock) {
+            // The history files start from mBaseSnapshotInterval - take this into account.
+            ops.offsetBeginAndEndTime(mBaseSnapshotInterval);
+            mPendingWrites.offerFirst(ops);
+            pendingWrites = new ArrayList<>(mPendingWrites);
+            mPendingWrites.clear();
+        }
+        persistPendingHistory(pendingWrites);
+    }
+
+    private void resampleHistoryOnDiskInMemoryDMLocked(long offsetMillis) {
+        mPersistence = new Persistence(mBaseSnapshotInterval, mIntervalCompressionMultiplier);
+        offsetHistory(offsetMillis);
+    }
+
+    void resetHistoryParameters() {
+        setHistoryParameters(DEFAULT_MODE, DEFAULT_SNAPSHOT_INTERVAL_MILLIS,
+                DEFAULT_COMPRESSION_STEP);
+    }
+
+    void clearHistory() {
+        synchronized (mOnDiskLock) {
+            clearHistoryOnDiskLocked();
+        }
+    }
+
+    private void clearHistoryOnDiskLocked() {
+        BackgroundThread.getHandler().removeMessages(MSG_WRITE_PENDING_HISTORY);
+        synchronized (mInMemoryLock) {
+            mCurrentHistoricalOps = null;
+            mNextPersistDueTimeMillis = System.currentTimeMillis();
+            mPendingWrites.clear();
+        }
+        mPersistence.clearHistoryDLocked();
+    }
+
+    private @NonNull HistoricalOps getUpdatedPendingHistoricalOpsMLocked(long now) {
+        if (mCurrentHistoricalOps != null) {
+            final long remainingTimeMillis = mNextPersistDueTimeMillis - now;
+            if (remainingTimeMillis > mBaseSnapshotInterval) {
+                // If time went backwards we need to push history to the future with the
+                // overflow over our snapshot interval. If time went forward do nothing
+                // as we would naturally push history into the past on the next write.
+                mPendingHistoryOffsetMillis = remainingTimeMillis - mBaseSnapshotInterval;
+            }
+            final long elapsedTimeMillis = mBaseSnapshotInterval - remainingTimeMillis;
+            mCurrentHistoricalOps.setEndTime(elapsedTimeMillis);
+            if (remainingTimeMillis > 0) {
+                if (DEBUG) {
+                    Slog.i(LOG_TAG, "Returning current in-memory state");
+                }
+                return mCurrentHistoricalOps;
+            }
+            if (mCurrentHistoricalOps.isEmpty()) {
+                mCurrentHistoricalOps.setBeginAndEndTime(0, 0);
+                mNextPersistDueTimeMillis = now + mBaseSnapshotInterval;
+                return mCurrentHistoricalOps;
+            }
+            // The current batch is full, so persist taking into account overdue persist time.
+            mCurrentHistoricalOps.offsetBeginAndEndTime(mBaseSnapshotInterval);
+            mCurrentHistoricalOps.setBeginTime(mCurrentHistoricalOps.getEndTimeMillis()
+                    - mBaseSnapshotInterval);
+            final long overdueTimeMillis = Math.abs(remainingTimeMillis);
+            mCurrentHistoricalOps.offsetBeginAndEndTime(overdueTimeMillis);
+            schedulePersistHistoricalOpsMLocked(mCurrentHistoricalOps);
+        }
+        // The current batch is in the future, i.e. not complete yet.
+        mCurrentHistoricalOps = new HistoricalOps(0, 0);
+        mNextPersistDueTimeMillis = now + mBaseSnapshotInterval;
+        if (DEBUG) {
+            Slog.i(LOG_TAG, "Returning new in-memory state");
+        }
+        return mCurrentHistoricalOps;
+    }
+
+    private void persistPendingHistory() {
+        final List<HistoricalOps> pendingWrites;
+        synchronized (mOnDiskLock) {
+            synchronized (mInMemoryLock) {
+                pendingWrites = new ArrayList<>(mPendingWrites);
+                mPendingWrites.clear();
+                if (mPendingHistoryOffsetMillis != 0) {
+                    resampleHistoryOnDiskInMemoryDMLocked(mPendingHistoryOffsetMillis);
+                    mPendingHistoryOffsetMillis = 0;
+                }
+            }
+            persistPendingHistory(pendingWrites);
+        }
+    }
+    private void persistPendingHistory(@NonNull List<HistoricalOps> pendingWrites) {
+        synchronized (mOnDiskLock) {
+            BackgroundThread.getHandler().removeMessages(MSG_WRITE_PENDING_HISTORY);
+            if (pendingWrites.isEmpty()) {
+                return;
+            }
+            final int opCount = pendingWrites.size();
+            // Pending writes are offset relative to each other, so take this
+            // into account to persist everything in one shot - single write.
+            for (int i = 0; i < opCount; i++) {
+                final HistoricalOps current = pendingWrites.get(i);
+                if (i > 0) {
+                    final HistoricalOps previous = pendingWrites.get(i - 1);
+                    current.offsetBeginAndEndTime(previous.getBeginTimeMillis());
+                }
+            }
+            mPersistence.persistHistoricalOpsDLocked(pendingWrites);
+        }
+    }
+
+    private void schedulePersistHistoricalOpsMLocked(@NonNull HistoricalOps ops) {
+        final Message message = PooledLambda.obtainMessage(
+                HistoricalRegistry::persistPendingHistory, HistoricalRegistry.this);
+        message.what = MSG_WRITE_PENDING_HISTORY;
+        BackgroundThread.getHandler().sendMessage(message);
+        mPendingWrites.offerFirst(ops);
+    }
+
+    private static void makeRelativeToEpochStart(@NonNull HistoricalOps ops, long nowMillis) {
+        ops.setBeginAndEndTime(nowMillis - ops.getEndTimeMillis(),
+                nowMillis- ops.getBeginTimeMillis());
+    }
+
+    private void pruneFutureOps(@NonNull List<HistoricalOps> ops) {
+        final int opCount = ops.size();
+        for (int i = opCount - 1; i >= 0; i--) {
+            final HistoricalOps op = ops.get(i);
+            if (op.getEndTimeMillis() <= mBaseSnapshotInterval) {
+                ops.remove(i);
+            } else if (op.getBeginTimeMillis() < mBaseSnapshotInterval) {
+                final double filterScale = (double) (op.getEndTimeMillis() - mBaseSnapshotInterval)
+                        / (double) op.getDurationMillis();
+                Persistence.spliceFromBeginning(op, filterScale);
+            }
+        }
+    }
+
+    private static final class Persistence {
+        private static final boolean DEBUG = false;
+
+        private static final String LOG_TAG = Persistence.class.getSimpleName();
+
+        private static final String HISTORY_FILE_SUFFIX = ".xml";
+
+        private static final String TAG_HISTORY = "history";
+        private static final String TAG_OPS = "ops";
+        private static final String TAG_UID = "uid";
+        private static final String TAG_PACKAGE = "package";
+        private static final String TAG_OP = "op";
+        private static final String TAG_STATE = "state";
+
+        private static final String ATTR_VERSION = "version";
+        private static final String ATTR_NAME = "name";
+        private static final String ATTR_ACCESS_COUNT = "accessCount";
+        private static final String ATTR_REJECT_COUNT = "rejectCount";
+        private static final String ATTR_ACCESS_DURATION = "accessDuration";
+        private static final String ATTR_BEGIN_TIME = "beginTime";
+        private static final String ATTR_END_TIME = "endTime";
+        private static final String ATTR_OVERFLOW = "overflow";
+
+        private static final int CURRENT_VERSION = 1;
+
+        private final long mBaseSnapshotInterval;
+        private final long mIntervalCompressionMultiplier;
+
+        Persistence(long baseSnapshotInterval, long intervalCompressionMultiplier) {
+            mBaseSnapshotInterval = baseSnapshotInterval;
+            mIntervalCompressionMultiplier = intervalCompressionMultiplier;
+        }
+
+        private final AtomicDirectory mHistoricalAppOpsDir = new AtomicDirectory(
+                new File(new File(Environment.getDataSystemDeDirectory(), "appops"), "history"));
+
+        private File generateFile(@NonNull File baseDir, int depth) {
+            final long globalBeginMillis = computeGlobalIntervalBeginMillis(depth);
+            return new File(baseDir, Long.toString(globalBeginMillis) + HISTORY_FILE_SUFFIX);
+        }
+
+        void clearHistoryDLocked() {
+            mHistoricalAppOpsDir.delete();
+        }
+
+        void persistHistoricalOpsDLocked(@NonNull List<HistoricalOps> ops) {
+            if (DEBUG) {
+                Slog.i(LOG_TAG, "Persisting ops:\n" + opsToDebugString(ops));
+                enforceOpsWellFormed(ops);
+            }
+            try {
+                final File newBaseDir = mHistoricalAppOpsDir.startWrite();
+                final File oldBaseDir = mHistoricalAppOpsDir.getBackupDirectory();
+                handlePersistHistoricalOpsRecursiveDLocked(newBaseDir, oldBaseDir, ops, 0);
+                mHistoricalAppOpsDir.finishWrite();
+            } catch (Throwable t) {
+                Slog.wtf(LOG_TAG, "Failed to write historical app ops, restoring backup", t);
+                mHistoricalAppOpsDir.failWrite();
+            }
+        }
+
+        @Nullable List<HistoricalOps> readHistoryRawDLocked() {
+            return collectHistoricalOpsBaseDLocked(Process.INVALID_UID /*filterUid*/,
+                    null /*filterPackageName*/, null /*filterOpNames*/,
+                    0 /*filterBeginTimeMills*/, Long.MAX_VALUE /*filterEndTimeMills*/);
+        }
+
+        @Nullable List<HistoricalOps> readHistoryDLocked() {
+            final List<HistoricalOps> result = readHistoryRawDLocked();
+            // Take into account in memory state duration.
+            if (result != null) {
+                final int opCount = result.size();
+                for (int i = 0; i < opCount; i++) {
+                    result.get(i).offsetBeginAndEndTime(mBaseSnapshotInterval);
+                }
+            }
+            return result;
+        }
+
+        long getLastPersistTimeMillisDLocked() {
+            try {
+                final File baseDir = mHistoricalAppOpsDir.startRead();
+                final File file = generateFile(baseDir, 0);
+                if (file.exists()) {
+                    return file.lastModified();
+                }
+                mHistoricalAppOpsDir.finishRead();
+            } catch (IOException e) {
+                Slog.wtf("Error reading historical app ops. Deleting history.", e);
+                mHistoricalAppOpsDir.delete();
+            }
+            return 0;
+        }
+
+        private void collectHistoricalOpsDLocked(@NonNull HistoricalOps currentOps,
+                int filterUid, @NonNull String filterPackageName, @Nullable String[] filterOpNames,
+                long filterBeingMillis, long filterEndMillis) {
+            final List<HistoricalOps> readOps = collectHistoricalOpsBaseDLocked(filterUid,
+                    filterPackageName, filterOpNames, filterBeingMillis, filterEndMillis);
+            if (readOps != null) {
+                final int readCount = readOps.size();
+                for (int i = 0; i < readCount; i++) {
+                    final HistoricalOps readOp = readOps.get(i);
+                    currentOps.merge(readOp);
+                }
+             }
+        }
+
+        private @Nullable LinkedList<HistoricalOps> collectHistoricalOpsBaseDLocked(
+                int filterUid, @NonNull String filterPackageName, @Nullable String[] filterOpNames,
+                long filterBeginTimeMillis, long filterEndTimeMillis) {
+            try {
+                final File baseDir = mHistoricalAppOpsDir.startRead();
+                final File[] files = baseDir.listFiles();
+                if (files == null) {
+                    return null;
+                }
+                final ArraySet<File> historyFiles = new ArraySet<>(files.length);
+                for (File file : files) {
+                    if (file.isFile() && file.getName().endsWith(HISTORY_FILE_SUFFIX)) {
+                        historyFiles.add(file);
+                    }
+                }
+                final long[] globalContentOffsetMillis = {0};
+                final LinkedList<HistoricalOps> ops = collectHistoricalOpsRecursiveDLocked(
+                        baseDir, filterUid, filterPackageName, filterOpNames, filterBeginTimeMillis,
+                        filterEndTimeMillis, globalContentOffsetMillis, null /*outOps*/,
+                        0 /*depth*/, historyFiles);
+                mHistoricalAppOpsDir.finishRead();
+                return ops;
+            } catch (IOException | XmlPullParserException e) {
+                Slog.wtf("Error reading historical app ops. Deleting history.", e);
+                mHistoricalAppOpsDir.delete();
+            }
+            return null;
+        }
+
+        private @Nullable LinkedList<HistoricalOps> collectHistoricalOpsRecursiveDLocked(
+                @NonNull File baseDir, int filterUid, @NonNull String filterPackageName,
+                @Nullable String[] filterOpNames, long filterBeginTimeMillis,
+                long filterEndTimeMillis, @NonNull long[] globalContentOffsetMillis,
+                @Nullable LinkedList<HistoricalOps> outOps, int depth,
+                @NonNull ArraySet<File> historyFiles)
+                throws IOException, XmlPullParserException {
+            final long previousIntervalEndMillis = (long) Math.pow(mIntervalCompressionMultiplier,
+                    depth) * mBaseSnapshotInterval;
+            final long currentIntervalEndMillis = (long) Math.pow(mIntervalCompressionMultiplier,
+                    depth + 1) * mBaseSnapshotInterval;
+
+            filterBeginTimeMillis = Math.max(filterBeginTimeMillis - previousIntervalEndMillis, 0);
+            filterEndTimeMillis = filterEndTimeMillis - previousIntervalEndMillis;
+
+            // Read historical data at this level
+            final List<HistoricalOps> readOps = readHistoricalOpsLocked(baseDir,
+                    previousIntervalEndMillis, currentIntervalEndMillis, filterUid,
+                    filterPackageName, filterOpNames, filterBeginTimeMillis, filterEndTimeMillis,
+                    globalContentOffsetMillis, depth, historyFiles);
+
+            // Empty is a special signal to stop diving
+            if (readOps != null && readOps.isEmpty()) {
+                return outOps;
+            }
+
+            // Collect older historical data from subsequent levels
+            outOps = collectHistoricalOpsRecursiveDLocked(
+                    baseDir, filterUid, filterPackageName, filterOpNames, filterBeginTimeMillis,
+                    filterEndTimeMillis, globalContentOffsetMillis, outOps, depth + 1,
+                    historyFiles);
+
+            // Make older historical data relative to the current historical level
+            if (outOps != null) {
+                final int opCount = outOps.size();
+                for (int i = 0; i < opCount; i++) {
+                    final HistoricalOps collectedOp = outOps.get(i);
+                    collectedOp.offsetBeginAndEndTime(currentIntervalEndMillis);
+                }
+            }
+
+            if (readOps != null) {
+                if (outOps == null) {
+                    outOps = new LinkedList<>();
+                }
+                // Add the read ops to output
+                final int opCount = readOps.size();
+                for (int i = opCount - 1; i >= 0; i--) {
+                    outOps.offerFirst(readOps.get(i));
+                }
+            }
+
+            return outOps;
+        }
+
+        private boolean createHardLinkToExistingFile(@NonNull File fromFile, @NonNull File toFile)
+                throws IOException {
+            if (!fromFile.exists()) {
+                return false;
+            }
+            Files.createLink(toFile.toPath(), fromFile.toPath());
+            return true;
+        }
+
+        private void handlePersistHistoricalOpsRecursiveDLocked(@NonNull File newBaseDir,
+                @NonNull File oldBaseDir, @Nullable List<HistoricalOps> passedOps, int depth)
+                throws IOException, XmlPullParserException {
+            final long previousIntervalEndMillis = (long) Math.pow(mIntervalCompressionMultiplier,
+                    depth) * mBaseSnapshotInterval;
+            final long currentIntervalEndMillis = (long) Math.pow(mIntervalCompressionMultiplier,
+                    depth + 1) * mBaseSnapshotInterval;
+
+            if (passedOps == null || passedOps.isEmpty()) {
+                // If there is an old file we need to copy it over to the new state.
+                final File oldFile = generateFile(oldBaseDir, depth);
+                final File newFile = generateFile(newBaseDir, depth);
+                if (createHardLinkToExistingFile(oldFile, newFile)) {
+                    handlePersistHistoricalOpsRecursiveDLocked(newBaseDir, oldBaseDir,
+                            passedOps, depth + 1);
+                }
+                return;
+            }
+
+            if (DEBUG) {
+                enforceOpsWellFormed(passedOps);
+            }
+
+            // Normalize passed ops time to be based off this interval start
+            final int passedOpCount = passedOps.size();
+            for (int i = 0; i < passedOpCount; i++) {
+                final HistoricalOps passedOp = passedOps.get(i);
+                passedOp.offsetBeginAndEndTime(-previousIntervalEndMillis);
+            }
+
+            if (DEBUG) {
+                enforceOpsWellFormed(passedOps);
+            }
+
+            // Read persisted ops for this interval
+            final List<HistoricalOps> existingOps = readHistoricalOpsLocked(oldBaseDir,
+                    previousIntervalEndMillis, currentIntervalEndMillis,
+                    Process.INVALID_UID /*filterUid*/, null /*filterPackageName*/,
+                    null /*filterOpNames*/, Long.MIN_VALUE /*filterBeginTimeMillis*/,
+                    Long.MAX_VALUE /*filterEndTimeMillis*/, null, depth,
+                    null /*historyFiles*/);
+
+            if (DEBUG) {
+                enforceOpsWellFormed(existingOps);
+            }
+
+            // Offset existing ops to account for elapsed time
+            final int existingOpCount = existingOps.size();
+            if (existingOpCount > 0) {
+                // Compute elapsed time
+                final long elapsedTimeMillis = passedOps.get(passedOps.size() - 1)
+                        .getEndTimeMillis();
+                for (int i = 0; i < existingOpCount; i++) {
+                    final HistoricalOps existingOp = existingOps.get(i);
+                    existingOp.offsetBeginAndEndTime(elapsedTimeMillis);
+                }
+            }
+
+            if (DEBUG) {
+                enforceOpsWellFormed(existingOps);
+            }
+
+            final long slotDurationMillis = previousIntervalEndMillis;
+
+            // Consolidate passed ops at the current slot duration ensuring each snapshot is
+            // full. To achieve this we put all passed and existing ops in a list and will
+            // merge them to ensure each represents a snapshot at the current granularity.
+            final List<HistoricalOps> allOps = new LinkedList<>();
+            allOps.addAll(passedOps);
+            allOps.addAll(existingOps);
+
+            if (DEBUG) {
+                enforceOpsWellFormed(allOps);
+            }
+
+            // Compute ops to persist and overflow ops
+            List<HistoricalOps> persistedOps = null;
+            List<HistoricalOps> overflowedOps = null;
+
+            // We move a snapshot into the next level only if the start time is
+            // after the end of the current interval. This avoids rewriting all
+            // files to propagate the information added to the history on every
+            // iteration. Instead, we would rewrite the next level file only if
+            // an entire snapshot from the previous level is being propagated.
+            // The trade off is that we need to store how much the last snapshot
+            // of the current interval overflows past the interval end. We write
+            // the overflow data to avoid parsing all snapshots on query.
+            long intervalOverflowMillis = 0;
+            final int opCount = allOps.size();
+            for (int i = 0; i < opCount; i++) {
+                final HistoricalOps op = allOps.get(i);
+                final HistoricalOps persistedOp;
+                final HistoricalOps overflowedOp;
+                if (op.getEndTimeMillis() <= currentIntervalEndMillis) {
+                    persistedOp = op;
+                    overflowedOp = null;
+                } else if (op.getBeginTimeMillis() < currentIntervalEndMillis) {
+                    persistedOp = op;
+                    intervalOverflowMillis = op.getEndTimeMillis() - currentIntervalEndMillis;
+                    if (intervalOverflowMillis > previousIntervalEndMillis) {
+                        final double splitScale = (double) intervalOverflowMillis
+                                / op.getDurationMillis();
+                        overflowedOp = spliceFromEnd(op, splitScale);
+                        intervalOverflowMillis = op.getEndTimeMillis() - currentIntervalEndMillis;
+                    } else {
+                        overflowedOp = null;
+                    }
+                } else {
+                    persistedOp = null;
+                    overflowedOp = op;
+                }
+                if (persistedOp != null) {
+                    if (persistedOps == null) {
+                        persistedOps = new ArrayList<>();
+                    }
+                    persistedOps.add(persistedOp);
+                }
+                if (overflowedOp != null) {
+                    if (overflowedOps == null) {
+                        overflowedOps = new ArrayList<>();
+                    }
+                    overflowedOps.add(overflowedOp);
+                }
+            }
+
+            if (DEBUG) {
+                enforceOpsWellFormed(persistedOps);
+                enforceOpsWellFormed(overflowedOps);
+            }
+
+            if (persistedOps != null) {
+                normalizeSnapshotForSlotDuration(persistedOps, slotDurationMillis);
+                final File newFile = generateFile(newBaseDir, depth);
+                writeHistoricalOpsDLocked(persistedOps, intervalOverflowMillis, newFile);
+                if (DEBUG) {
+                    Slog.i(LOG_TAG, "Persisted at depth: " + depth
+                            + " ops:\n" + opsToDebugString(persistedOps));
+                    enforceOpsWellFormed(persistedOps);
+                }
+            }
+
+            handlePersistHistoricalOpsRecursiveDLocked(newBaseDir, oldBaseDir,
+                    overflowedOps, depth + 1);
+        }
+
+        private @NonNull List<HistoricalOps> readHistoricalOpsLocked(File baseDir,
+                long intervalBeginMillis, long intervalEndMillis, int filterUid,
+                @Nullable String filterPackageName, @Nullable String[] filterOpNames,
+                long filterBeginTimeMillis, long filterEndTimeMillis,
+                @Nullable long[] cumulativeOverflowMillis, int depth,
+                @NonNull ArraySet<File> historyFiles)
+                throws IOException, XmlPullParserException {
+            final File file = generateFile(baseDir, depth);
+            if (historyFiles != null) {
+                historyFiles.remove(file);
+            }
+            if (filterBeginTimeMillis >= filterEndTimeMillis
+                    || filterEndTimeMillis < intervalBeginMillis) {
+                // Don't go deeper
+                return Collections.emptyList();
+            }
+            if (filterBeginTimeMillis >= (intervalEndMillis
+                    + ((intervalEndMillis - intervalBeginMillis) / mIntervalCompressionMultiplier)
+                    + (cumulativeOverflowMillis != null ? cumulativeOverflowMillis[0] : 0))
+                    || !file.exists()) {
+                if (historyFiles == null || historyFiles.isEmpty()) {
+                    // Don't go deeper
+                    return Collections.emptyList();
+                } else {
+                    // Keep diving
+                    return null;
+                }
+            }
+            return readHistoricalOpsLocked(file, filterUid, filterPackageName, filterOpNames,
+                    filterBeginTimeMillis, filterEndTimeMillis, cumulativeOverflowMillis);
+        }
+
+        private @Nullable List<HistoricalOps> readHistoricalOpsLocked(@NonNull File file,
+                int filterUid, @Nullable String filterPackageName, @Nullable String[] filterOpNames,
+                long filterBeginTimeMillis, long filterEndTimeMillis,
+                @Nullable long[] cumulativeOverflowMillis)
+                throws IOException, XmlPullParserException {
+            if (DEBUG) {
+                Slog.i(LOG_TAG, "Reading ops from:" + file);
+            }
+            List<HistoricalOps> allOps = null;
+            try (FileInputStream stream = new FileInputStream(file)) {
+                final XmlPullParser parser = Xml.newPullParser();
+                parser.setInput(stream, StandardCharsets.UTF_8.name());
+                XmlUtils.beginDocument(parser, TAG_HISTORY);
+                final long overflowMillis = XmlUtils.readLongAttribute(parser, ATTR_OVERFLOW, 0);
+                final int depth = parser.getDepth();
+                while (XmlUtils.nextElementWithin(parser, depth)) {
+                    if (TAG_OPS.equals(parser.getName())) {
+                        final HistoricalOps ops = readeHistoricalOpsDLocked(parser,
+                                filterUid, filterPackageName, filterOpNames, filterBeginTimeMillis,
+                                filterEndTimeMillis, cumulativeOverflowMillis);
+                        if (ops == null) {
+                            continue;
+                        }
+                        if (ops.isEmpty()) {
+                            XmlUtils.skipCurrentTag(parser);
+                            continue;
+                        }
+                        if (allOps == null) {
+                            allOps = new ArrayList<>();
+                        }
+                        allOps.add(ops);
+                    }
+                }
+                if (cumulativeOverflowMillis != null) {
+                    cumulativeOverflowMillis[0] += overflowMillis;
+                }
+            } catch (FileNotFoundException e) {
+                Slog.i(LOG_TAG, "No history file: " + file.getName());
+                return Collections.emptyList();
+            }
+            if (DEBUG) {
+                if (allOps != null) {
+                    Slog.i(LOG_TAG, "Read from file: " + file + "ops:\n"
+                            + opsToDebugString(allOps));
+                    enforceOpsWellFormed(allOps);
+                }
+            }
+            return allOps;
+        }
+
+        private @Nullable HistoricalOps readeHistoricalOpsDLocked(
+                @NonNull XmlPullParser parser, int filterUid, @Nullable String filterPackageName,
+                @Nullable String[] filterOpNames, long filterBeginTimeMillis,
+                long filterEndTimeMillis, @Nullable long[] cumulativeOverflowMillis)
+                throws IOException, XmlPullParserException {
+            final long beginTimeMillis = XmlUtils.readLongAttribute(parser, ATTR_BEGIN_TIME, 0)
+                    + (cumulativeOverflowMillis != null ? cumulativeOverflowMillis[0] : 0);
+            final long endTimeMillis = XmlUtils.readLongAttribute(parser, ATTR_END_TIME, 0)
+                    + (cumulativeOverflowMillis != null ? cumulativeOverflowMillis[0] : 0);
+            // Keep reading as subsequent records may start matching
+            if (filterEndTimeMillis < beginTimeMillis) {
+                return null;
+            }
+            // Stop reading as subsequent records will not match
+            if (filterBeginTimeMillis > endTimeMillis) {
+                return new HistoricalOps(0, 0);
+            }
+            final long filteredBeginTimeMillis = Math.max(beginTimeMillis, filterBeginTimeMillis);
+            final long filteredEndTimeMillis = Math.min(endTimeMillis, filterEndTimeMillis);
+            final double filterScale = (double) (filteredEndTimeMillis - filteredBeginTimeMillis)
+                    / (double) (endTimeMillis - beginTimeMillis);
+            HistoricalOps ops = null;
+            final int depth = parser.getDepth();
+            while (XmlUtils.nextElementWithin(parser, depth)) {
+                if (TAG_UID.equals(parser.getName())) {
+                    final HistoricalOps returnedOps = readHistoricalUidOpsDLocked(ops, parser,
+                            filterUid, filterPackageName, filterOpNames, filterScale);
+                    if (ops == null) {
+                        ops = returnedOps;
+                    }
+                }
+            }
+            if (ops != null) {
+                ops.setBeginAndEndTime(filteredBeginTimeMillis, filteredEndTimeMillis);
+            }
+            return ops;
+        }
+
+        private @Nullable HistoricalOps readHistoricalUidOpsDLocked(
+                @Nullable HistoricalOps ops, @NonNull XmlPullParser parser, int filterUid,
+                @Nullable String filterPackageName, @Nullable String[] filterOpNames,
+                double filterScale) throws IOException, XmlPullParserException {
+            final int uid = XmlUtils.readIntAttribute(parser, ATTR_NAME);
+            if (filterUid != Process.INVALID_UID && filterUid != uid) {
+                XmlUtils.skipCurrentTag(parser);
+                return null;
+            }
+            final int depth = parser.getDepth();
+            while (XmlUtils.nextElementWithin(parser, depth)) {
+                if (TAG_PACKAGE.equals(parser.getName())) {
+                    final HistoricalOps returnedOps = readHistoricalPackageOpsDLocked(ops,
+                            uid, parser, filterPackageName, filterOpNames, filterScale);
+                    if (ops == null) {
+                        ops = returnedOps;
+                    }
+                }
+            }
+            return ops;
+        }
+
+        private @Nullable HistoricalOps readHistoricalPackageOpsDLocked(
+                @Nullable HistoricalOps ops, int uid, @NonNull XmlPullParser parser,
+                @Nullable String filterPackageName, @Nullable String[] filterOpNames,
+                double filterScale) throws IOException, XmlPullParserException {
+            final String packageName = XmlUtils.readStringAttribute(parser, ATTR_NAME);
+            if (filterPackageName != null && !filterPackageName.equals(packageName)) {
+                XmlUtils.skipCurrentTag(parser);
+                return null;
+            }
+            final int depth = parser.getDepth();
+            while (XmlUtils.nextElementWithin(parser, depth)) {
+                if (TAG_OP.equals(parser.getName())) {
+                    final HistoricalOps returnedOps = readHistoricalOpDLocked(ops, uid,
+                            packageName, parser, filterOpNames, filterScale);
+                    if (ops == null) {
+                        ops = returnedOps;
+                    }
+                }
+            }
+            return ops;
+        }
+
+        private @Nullable HistoricalOps readHistoricalOpDLocked(@Nullable HistoricalOps ops,
+                int uid, String packageName, @NonNull XmlPullParser parser,
+                @Nullable String[] filterOpNames, double filterScale)
+                throws IOException, XmlPullParserException {
+            final int op = XmlUtils.readIntAttribute(parser, ATTR_NAME);
+            if (filterOpNames != null && !ArrayUtils.contains(filterOpNames,
+                    AppOpsManager.opToName(op))) {
+                XmlUtils.skipCurrentTag(parser);
+                return null;
+            }
+            final int depth = parser.getDepth();
+            while (XmlUtils.nextElementWithin(parser, depth)) {
+                if (TAG_STATE.equals(parser.getName())) {
+                    final HistoricalOps returnedOps = readUidStateDLocked(ops, uid,
+                            packageName, op, parser, filterScale);
+                    if (ops == null) {
+                        ops = returnedOps;
+                    }
+                }
+            }
+            return ops;
+        }
+
+        private @Nullable HistoricalOps readUidStateDLocked(@Nullable HistoricalOps ops,
+                int uid, String packageName, int op, @NonNull XmlPullParser parser,
+                double filterScale) throws IOException {
+            final int uidState = XmlUtils.readIntAttribute(parser, ATTR_NAME);
+            long accessCount = XmlUtils.readLongAttribute(parser, ATTR_ACCESS_COUNT, 0);
+            if (accessCount > 0) {
+                if (!Double.isNaN(filterScale)) {
+                    accessCount = (long) HistoricalOps.round(
+                            (double) accessCount * filterScale);
+                }
+                if (ops == null) {
+                    ops = new HistoricalOps(0, 0);
+                }
+                ops.increaseAccessCount(op, uid, packageName, uidState, accessCount);
+            }
+            long rejectCount = XmlUtils.readLongAttribute(parser, ATTR_REJECT_COUNT, 0);
+            if (rejectCount > 0) {
+                if (!Double.isNaN(filterScale)) {
+                    rejectCount = (long) HistoricalOps.round(
+                            (double) rejectCount * filterScale);
+                }
+                if (ops == null) {
+                    ops = new HistoricalOps(0, 0);
+                }
+                ops.increaseRejectCount(op, uid, packageName, uidState, rejectCount);
+            }
+            long accessDuration =  XmlUtils.readLongAttribute(parser, ATTR_ACCESS_DURATION, 0);
+            if (accessDuration > 0) {
+                if (!Double.isNaN(filterScale)) {
+                    accessDuration = (long) HistoricalOps.round(
+                            (double) accessDuration * filterScale);
+                }
+                if (ops == null) {
+                    ops = new HistoricalOps(0, 0);
+                }
+                ops.increaseAccessDuration(op, uid, packageName, uidState, accessDuration);
+            }
+            return ops;
+        }
+
+        private void writeHistoricalOpsDLocked(@Nullable List<HistoricalOps> allOps,
+                long intervalOverflowMillis, @NonNull File file) throws IOException {
+            final FileOutputStream output = mHistoricalAppOpsDir.openWrite(file);
+            try {
+                final XmlSerializer serializer = Xml.newSerializer();
+                serializer.setOutput(output, StandardCharsets.UTF_8.name());
+                serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output",
+                        true);
+                serializer.startDocument(null, true);
+                serializer.startTag(null, TAG_HISTORY);
+                serializer.attribute(null, ATTR_VERSION, String.valueOf(CURRENT_VERSION));
+                if (intervalOverflowMillis != 0) {
+                    serializer.attribute(null, ATTR_OVERFLOW,
+                            Long.toString(intervalOverflowMillis));
+                }
+                if (allOps != null) {
+                    final int opsCount = allOps.size();
+                    for (int i = 0; i < opsCount; i++) {
+                        final HistoricalOps ops = allOps.get(i);
+                        writeHistoricalOpDLocked(ops, serializer);
+                    }
+                }
+                serializer.endTag(null, TAG_HISTORY);
+                serializer.endDocument();
+                mHistoricalAppOpsDir.closeWrite(output);
+            } catch (IOException e) {
+                mHistoricalAppOpsDir.failWrite(output);
+                throw e;
+            }
+        }
+
+        private void writeHistoricalOpDLocked(@NonNull HistoricalOps ops,
+                @NonNull XmlSerializer serializer) throws IOException {
+            serializer.startTag(null, TAG_OPS);
+            serializer.attribute(null, ATTR_BEGIN_TIME, Long.toString(ops.getBeginTimeMillis()));
+            serializer.attribute(null, ATTR_END_TIME, Long.toString(ops.getEndTimeMillis()));
+            final int uidCount = ops.getUidCount();
+            for (int i = 0; i < uidCount; i++) {
+                final HistoricalUidOps uidOp = ops.getUidOpsAt(i);
+                writeHistoricalUidOpsDLocked(uidOp, serializer);
+            }
+            serializer.endTag(null, TAG_OPS);
+        }
+
+        private void writeHistoricalUidOpsDLocked(@NonNull HistoricalUidOps uidOps,
+                @NonNull XmlSerializer serializer) throws IOException {
+            serializer.startTag(null, TAG_UID);
+            serializer.attribute(null, ATTR_NAME, Integer.toString(uidOps.getUid()));
+            final int packageCount = uidOps.getPackageCount();
+            for (int i = 0; i < packageCount; i++) {
+                final HistoricalPackageOps packageOps = uidOps.getPackageOpsAt(i);
+                writeHistoricalPackageOpsDLocked(packageOps, serializer);
+            }
+            serializer.endTag(null, TAG_UID);
+        }
+
+        private void writeHistoricalPackageOpsDLocked(@NonNull HistoricalPackageOps packageOps,
+                @NonNull XmlSerializer serializer) throws IOException {
+            serializer.startTag(null, TAG_PACKAGE);
+            serializer.attribute(null, ATTR_NAME, packageOps.getPackageName());
+            final int opCount = packageOps.getOpCount();
+            for (int i = 0; i < opCount; i++) {
+                final HistoricalOp op = packageOps.getOpAt(i);
+                writeHistoricalOpDLocked(op, serializer);
+            }
+            serializer.endTag(null, TAG_PACKAGE);
+        }
+
+        private void writeHistoricalOpDLocked(@NonNull HistoricalOp op,
+                @NonNull XmlSerializer serializer) throws IOException {
+            serializer.startTag(null, TAG_OP);
+            serializer.attribute(null, ATTR_NAME, Integer.toString(op.getOpCode()));
+            for (int uidState = 0; uidState < AppOpsManager._NUM_UID_STATE; uidState++) {
+                writeUidStateOnLocked(op, uidState, serializer);
+            }
+            serializer.endTag(null, TAG_OP);
+        }
+
+        private void writeUidStateOnLocked(@NonNull HistoricalOp op, @UidState int uidState,
+                @NonNull XmlSerializer serializer) throws IOException {
+            final long accessCount = op.getAccessCount(uidState);
+            final long rejectCount = op.getRejectCount(uidState);
+            final long accessDuration = op.getAccessDuration(uidState);
+            if (accessCount == 0 && rejectCount == 0 && accessDuration == 0) {
+                return;
+            }
+            serializer.startTag(null, TAG_STATE);
+            serializer.attribute(null, ATTR_NAME, Integer.toString(uidState));
+            if (accessCount > 0) {
+                serializer.attribute(null, ATTR_ACCESS_COUNT, Long.toString(accessCount));
+            }
+            if (rejectCount > 0) {
+                serializer.attribute(null, ATTR_REJECT_COUNT, Long.toString(rejectCount));
+            }
+            if (accessDuration > 0) {
+                serializer.attribute(null, ATTR_ACCESS_DURATION, Long.toString(accessDuration));
+            }
+            serializer.endTag(null, TAG_STATE);
+        }
+
+        private static void enforceOpsWellFormed(@NonNull List<HistoricalOps> ops) {
+            if (ops == null) {
+                return;
+            }
+            HistoricalOps previous;
+            HistoricalOps current = null;
+            final int opsCount = ops.size();
+            for (int i = 0; i < opsCount; i++) {
+                previous = current;
+                current = ops.get(i);
+                if (current.isEmpty()) {
+                    throw new IllegalStateException("Empty ops:\n"
+                            + opsToDebugString(ops));
+                }
+                if (current.getEndTimeMillis() < current.getBeginTimeMillis()) {
+                    throw new IllegalStateException("Begin after end:\n"
+                            + opsToDebugString(ops));
+                }
+                if (previous != null) {
+                    if (previous.getEndTimeMillis() > current.getBeginTimeMillis()) {
+                        throw new IllegalStateException("Intersecting ops:\n"
+                                + opsToDebugString(ops));
+                    }
+                    if (previous.getBeginTimeMillis() > current.getBeginTimeMillis()) {
+                        throw new IllegalStateException("Non increasing ops:\n"
+                                + opsToDebugString(ops));
+                    }
+                }
+            }
+        }
+
+        private long computeGlobalIntervalBeginMillis(int depth) {
+            long beginTimeMillis = 0;
+            for (int i = 0; i < depth + 1; i++) {
+                beginTimeMillis += Math.pow(mIntervalCompressionMultiplier, i);
+            }
+            return beginTimeMillis * mBaseSnapshotInterval;
+        }
+
+        private static @NonNull HistoricalOps spliceFromEnd(@NonNull HistoricalOps ops,
+                double spliceRatio) {
+            if (DEBUG) {
+                Slog.w(LOG_TAG, "Splicing from end:" + ops + " ratio:" + spliceRatio);
+            }
+            final HistoricalOps splice = ops.spliceFromEnd(spliceRatio);
+            if (DEBUG) {
+                Slog.w(LOG_TAG, "Spliced into:" + ops + " and:" + splice);
+            }
+            return splice;
+        }
+
+
+        private static @NonNull HistoricalOps spliceFromBeginning(@NonNull HistoricalOps ops,
+                double spliceRatio) {
+            if (DEBUG) {
+                Slog.w(LOG_TAG, "Splicing from beginning:" + ops + " ratio:" + spliceRatio);
+            }
+            final HistoricalOps splice = ops.spliceFromBeginning(spliceRatio);
+            if (DEBUG) {
+                Slog.w(LOG_TAG, "Spliced into:" + ops + " and:" + splice);
+            }
+            return splice;
+        }
+
+        private static void normalizeSnapshotForSlotDuration(@NonNull List<HistoricalOps> ops,
+                long slotDurationMillis) {
+            if (DEBUG) {
+                Slog.i(LOG_TAG, "Normalizing for slot duration: " + slotDurationMillis
+                        + " ops:\n" + opsToDebugString(ops));
+                enforceOpsWellFormed(ops);
+            }
+            long slotBeginTimeMillis;
+            final int opCount = ops.size();
+            for (int processedIdx = opCount - 1; processedIdx >= 0; processedIdx--) {
+                final HistoricalOps processedOp = ops.get(processedIdx);
+                slotBeginTimeMillis = Math.max(processedOp.getEndTimeMillis()
+                        - slotDurationMillis, 0);
+                for (int candidateIdx = processedIdx - 1; candidateIdx >= 0; candidateIdx--) {
+                    final HistoricalOps candidateOp = ops.get(candidateIdx);
+                    final long candidateSlotIntersectionMillis = candidateOp.getEndTimeMillis()
+                            - Math.min(slotBeginTimeMillis, processedOp.getBeginTimeMillis());
+                    if (candidateSlotIntersectionMillis <= 0) {
+                        break;
+                    }
+                    final float candidateSplitRatio = candidateSlotIntersectionMillis
+                            / (float) candidateOp.getDurationMillis();
+                    if (Float.compare(candidateSplitRatio, 1.0f) >= 0) {
+                        ops.remove(candidateIdx);
+                        processedIdx--;
+                        processedOp.merge(candidateOp);
+                    } else {
+                        final HistoricalOps endSplice = spliceFromEnd(candidateOp,
+                                candidateSplitRatio);
+                        if (endSplice != null) {
+                            processedOp.merge(endSplice);
+                        }
+                        if (candidateOp.isEmpty()) {
+                            ops.remove(candidateIdx);
+                            processedIdx--;
+                        }
+                    }
+                }
+            }
+            if (DEBUG) {
+                Slog.i(LOG_TAG, "Normalized for slot duration: " + slotDurationMillis
+                        + " ops:\n" + opsToDebugString(ops));
+                enforceOpsWellFormed(ops);
+            }
+        }
+
+        private static @NonNull String opsToDebugString(@NonNull List<HistoricalOps> ops) {
+            StringBuilder builder = new StringBuilder();
+            final int opCount = ops.size();
+            for (int i = 0; i < opCount; i++) {
+                builder.append("  ");
+                builder.append(ops.get(i));
+                if (i < opCount - 1) {
+                    builder.append('\n');
+                }
+            }
+            return builder.toString();
+        }
+    }
+
+    private final class StringDumpVisitor implements AppOpsManager.HistoricalOpsVisitor {
+        private final long mNow = System.currentTimeMillis();
+
+        private final SimpleDateFormat mDateFormatter = new SimpleDateFormat(
+                "yyyy-MM-dd HH:mm:ss.SSS");
+        private final Date mDate = new Date();
+
+        private final @NonNull String mOpsPrefix;
+        private final @NonNull String mUidPrefix;
+        private final @NonNull String mPackagePrefix;
+        private final @NonNull String mEntryPrefix;
+        private final @NonNull String mUidStatePrefix;
+        private final @NonNull PrintWriter mWriter;
+        private final int mFilterUid;
+        private final String mFilterPackage;
+        private final int mFilterOp;
+
+        StringDumpVisitor(@NonNull String prefix, @NonNull PrintWriter writer,
+                int filterUid, String filterPackage, int filterOp) {
+            mOpsPrefix = prefix + "  ";
+            mUidPrefix = mOpsPrefix + "  ";
+            mPackagePrefix = mUidPrefix + "  ";
+            mEntryPrefix = mPackagePrefix + "  ";
+            mUidStatePrefix = mEntryPrefix + "  ";
+            mWriter = writer;
+            mFilterUid = filterUid;
+            mFilterPackage = filterPackage;
+            mFilterOp = filterOp;
+        }
+
+        @Override
+        public void visitHistoricalOps(HistoricalOps ops) {
+            mWriter.println();
+            mWriter.print(mOpsPrefix);
+            mWriter.println("snapshot:");
+            mWriter.print(mUidPrefix);
+            mWriter.print("begin = ");
+            mDate.setTime(ops.getBeginTimeMillis());
+            mWriter.print(mDateFormatter.format(mDate));
+            mWriter.print("  (");
+            TimeUtils.formatDuration(ops.getBeginTimeMillis() - mNow, mWriter);
+            mWriter.println(")");
+            mWriter.print(mUidPrefix);
+            mWriter.print("end = ");
+            mDate.setTime(ops.getEndTimeMillis());
+            mWriter.print(mDateFormatter.format(mDate));
+            mWriter.print("  (");
+            TimeUtils.formatDuration(ops.getEndTimeMillis() - mNow, mWriter);
+            mWriter.println(")");
+        }
+
+        @Override
+        public void visitHistoricalUidOps(HistoricalUidOps ops) {
+            if (mFilterUid != Process.INVALID_UID && mFilterUid != ops.getUid()) {
+                return;
+            }
+            mWriter.println();
+            mWriter.print(mUidPrefix);
+            mWriter.print("Uid ");
+            UserHandle.formatUid(mWriter, ops.getUid());
+            mWriter.println(":");
+        }
+
+        @Override
+        public void visitHistoricalPackageOps(HistoricalPackageOps ops) {
+            if (mFilterPackage != null && !mFilterPackage.equals(ops.getPackageName())) {
+                return;
+            }
+            mWriter.print(mPackagePrefix);
+            mWriter.print("Package ");
+            mWriter.print(ops.getPackageName());
+            mWriter.println(":");
+        }
+
+        @Override
+        public void visitHistoricalOp(HistoricalOp ops) {
+            if (mFilterOp != AppOpsManager.OP_NONE && mFilterOp != ops.getOpCode()) {
+                return;
+            }
+            mWriter.print(mEntryPrefix);
+            mWriter.print(AppOpsManager.opToName(ops.getOpCode()));
+            mWriter.println(":");
+            for (int uidState = 0; uidState < AppOpsManager._NUM_UID_STATE; uidState++) {
+                boolean printedUidState = false;
+                final long accessCount = ops.getAccessCount(uidState);
+                if (accessCount > 0) {
+                    if (!printedUidState) {
+                        mWriter.print(mUidStatePrefix);
+                        mWriter.print(AppOpsManager.uidStateToString(uidState));
+                        mWriter.print("[");
+                        printedUidState = true;
+                    }
+                    mWriter.print("access=");
+                    mWriter.print(accessCount);
+                }
+                final long rejectCount = ops.getRejectCount(uidState);
+                if (rejectCount > 0) {
+                    if (!printedUidState) {
+                        mWriter.print(mUidStatePrefix);
+                        mWriter.print(AppOpsManager.uidStateToString(uidState));
+                        mWriter.print("[");
+                        printedUidState = true;
+                    } else {
+                        mWriter.print(",");
+                    }
+                    mWriter.print("reject=");
+                    mWriter.print(rejectCount);
+                }
+                final long accessDuration = ops.getAccessDuration(uidState);
+                if (accessDuration > 0) {
+                    if (!printedUidState) {
+                        mWriter.print(mUidStatePrefix);
+                        mWriter.print(AppOpsManager.uidStateToString(uidState));
+                        printedUidState = true;
+                    } else {
+                        mWriter.print(",");
+                    }
+                    mWriter.print("duration=");
+                    mWriter.print(accessDuration);
+                }
+                if (printedUidState) {
+                    mWriter.println("]");
+                }
+            }
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/appop/TEST_MAPPING b/services/core/java/com/android/server/appop/TEST_MAPPING
new file mode 100644
index 0000000..4901d3a
--- /dev/null
+++ b/services/core/java/com/android/server/appop/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+  "presubmit": [
+    {
+      "name": "CtsAppOpsTestCases",
+    }
+  ]
+}