Add private APIs to watch noted app ops - framework.

This change adds simple APIs to watch for noted ops. We are now
reporting a callback for every note which could lead to too many
call but until this is found to be the case we stick to the simple
solution. We already have APIs to watch started and stopped ops
which in theory could have the same frequence as noted ops, so
this is consistent to our approach.

bug:117617221
bug:119323172

Test: atest androidx.test.runner.AndroidJUnitRunner

Change-Id: I46c4b00e765eb14df8c30471d2b80dda905b8125
diff --git a/Android.bp b/Android.bp
index 1b81306..65f1718 100644
--- a/Android.bp
+++ b/Android.bp
@@ -387,6 +387,7 @@
         "core/java/android/speech/tts/ITextToSpeechService.aidl",
         "core/java/com/android/internal/app/IAppOpsActiveCallback.aidl",
         "core/java/com/android/internal/app/IAppOpsCallback.aidl",
+        "core/java/com/android/internal/app/IAppOpsNotedCallback.aidl",
         "core/java/com/android/internal/app/IAppOpsService.aidl",
         "core/java/com/android/internal/app/IBatteryStats.aidl",
         "core/java/com/android/internal/app/ISoundTriggerService.aidl",
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 6905cb5..a278423 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -41,8 +41,10 @@
 import android.provider.Settings;
 import android.util.ArrayMap;
 
+import com.android.internal.annotations.GuardedBy;
 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.util.Preconditions;
 
@@ -86,10 +88,20 @@
      */
 
     final Context mContext;
+
     @UnsupportedAppUsage
     final IAppOpsService mService;
-    final ArrayMap<OnOpChangedListener, IAppOpsCallback> mModeWatchers = new ArrayMap<>();
-    final ArrayMap<OnOpActiveChangedListener, IAppOpsActiveCallback> mActiveWatchers =
+
+    @GuardedBy("mModeWatchers")
+    private final ArrayMap<OnOpChangedListener, IAppOpsCallback> mModeWatchers =
+            new ArrayMap<>();
+
+    @GuardedBy("mActiveWatchers")
+    private final ArrayMap<OnOpActiveChangedListener, IAppOpsActiveCallback> mActiveWatchers =
+            new ArrayMap<>();
+
+    @GuardedBy("mNotedWatchers")
+    private final ArrayMap<OnOpNotedListener, IAppOpsNotedCallback> mNotedWatchers =
             new ArrayMap<>();
 
     static IBinder sToken;
@@ -2471,6 +2483,23 @@
     }
 
     /**
+     * Callback for notification of an op being noted.
+     *
+     * @hide
+     */
+    public interface OnOpNotedListener {
+        /**
+         * Called when an op was noted.
+         *
+         * @param code The op code.
+         * @param uid The UID performing the operation.
+         * @param packageName The package performing the operation.
+         * @param result The result of the note.
+         */
+        void onOpNoted(String code, int uid, String packageName, int result);
+    }
+
+    /**
      * Callback for notification of changes to operation state.
      * This allows you to see the raw op codes instead of strings.
      * @hide
@@ -2819,7 +2848,7 @@
      */
     public void stopWatchingMode(OnOpChangedListener callback) {
         synchronized (mModeWatchers) {
-            IAppOpsCallback cb = mModeWatchers.get(callback);
+            IAppOpsCallback cb = mModeWatchers.remove(callback);
             if (cb != null) {
                 try {
                     mService.stopWatchingMode(cb);
@@ -2893,7 +2922,7 @@
     @TestApi
     public void stopWatchingActive(@NonNull OnOpActiveChangedListener callback) {
         synchronized (mActiveWatchers) {
-            final IAppOpsActiveCallback cb = mActiveWatchers.get(callback);
+            final IAppOpsActiveCallback cb = mActiveWatchers.remove(callback);
             if (cb != null) {
                 try {
                     mService.stopWatchingActive(cb);
@@ -2904,6 +2933,74 @@
         }
     }
 
+    /**
+     * Start watching for noted app ops. An app op may be immediate or long running.
+     * Immediate ops are noted while long running ones are started and stopped. This
+     * method allows registering a listener to be notified when an app op is noted. If
+     * an op is being noted by any package you will get a callback. To change the
+     * watched ops for a registered callback you need to unregister and register it again.
+     *
+     * <p> If you don't hold the {@link android.Manifest.permission#WATCH_APPOPS} permission
+     * you can watch changes only for your UID.
+     *
+     * @param ops The ops to watch.
+     * @param callback Where to report changes.
+     *
+     * @see #startWatchingActive(int[], OnOpActiveChangedListener)
+     * @see #stopWatchingNoted(OnOpNotedListener)
+     * @see #noteOp(String, int, String)
+     *
+     * @hide
+     */
+    @RequiresPermission(value=Manifest.permission.WATCH_APPOPS, conditional=true)
+    public void startWatchingNoted(@NonNull String[] ops, @NonNull OnOpNotedListener callback) {
+        IAppOpsNotedCallback cb;
+        synchronized (mNotedWatchers) {
+            cb = mNotedWatchers.get(callback);
+            if (cb != null) {
+                return;
+            }
+            cb = new IAppOpsNotedCallback.Stub() {
+                @Override
+                public void opNoted(int op, int uid, String packageName, int mode) {
+                    callback.onOpNoted(sOpToString[op], uid, packageName, mode);
+                }
+            };
+            mNotedWatchers.put(callback, cb);
+        }
+        try {
+            final int[] opCodes = new int[ops.length];
+            for (int i = 0; i < opCodes.length; i++) {
+                opCodes[i] = strOpToOp(ops[i]);
+            }
+            mService.startWatchingNoted(opCodes, cb);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Stop watching for noted app ops. An app op may be immediate or long running.
+     * Unregistering a non-registered callback has no effect.
+     *
+     * @see #startWatchingNoted(String[], OnOpNotedListener)
+     * @see #noteOp(String, int, String)
+     *
+     * @hide
+     */
+    public void stopWatchingNoted(@NonNull OnOpNotedListener callback) {
+        synchronized (mNotedWatchers) {
+            final IAppOpsNotedCallback cb = mNotedWatchers.get(callback);
+            if (cb != null) {
+                try {
+                    mService.stopWatchingNoted(cb);
+                } catch (RemoteException e) {
+                    throw e.rethrowFromSystemServer();
+                }
+            }
+        }
+    }
+
     private String buildSecurityExceptionMsg(int op, int uid, String packageName) {
         return packageName + " from uid " + uid + " not allowed to perform " + sOpNames[op];
     }
diff --git a/core/java/com/android/internal/app/IAppOpsNotedCallback.aidl b/core/java/com/android/internal/app/IAppOpsNotedCallback.aidl
new file mode 100644
index 0000000..fa5c30a
--- /dev/null
+++ b/core/java/com/android/internal/app/IAppOpsNotedCallback.aidl
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.app;
+
+// Iterface to observe op note/checks of ops
+oneway interface IAppOpsNotedCallback {
+    void opNoted(int op, int uid, String packageName, int mode);
+}
diff --git a/core/java/com/android/internal/app/IAppOpsService.aidl b/core/java/com/android/internal/app/IAppOpsService.aidl
index 049103b..e571656 100644
--- a/core/java/com/android/internal/app/IAppOpsService.aidl
+++ b/core/java/com/android/internal/app/IAppOpsService.aidl
@@ -21,6 +21,7 @@
 import android.os.Bundle;
 import com.android.internal.app.IAppOpsCallback;
 import com.android.internal.app.IAppOpsActiveCallback;
+import com.android.internal.app.IAppOpsNotedCallback;
 
 interface IAppOpsService {
     // These first methods are also called by native code, so must
@@ -61,4 +62,7 @@
     boolean isOperationActive(int code, int uid, String packageName);
 
     void startWatchingModeWithFlags(int op, String packageName, int flags, IAppOpsCallback callback);
+
+    void startWatchingNoted(in int[] ops, IAppOpsNotedCallback callback);
+    void stopWatchingNoted(IAppOpsNotedCallback callback);
 }
diff --git a/services/core/java/com/android/server/AppOpsService.java b/services/core/java/com/android/server/AppOpsService.java
index 8d912fa..f0ec69f 100644
--- a/services/core/java/com/android/server/AppOpsService.java
+++ b/services/core/java/com/android/server/AppOpsService.java
@@ -86,6 +86,7 @@
 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;
@@ -456,6 +457,7 @@
     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 {
@@ -475,6 +477,7 @@
             try {
                 mCallback.asBinder().linkToDeath(this, 0);
             } catch (RemoteException e) {
+                /*ignored*/
             }
         }
 
@@ -524,6 +527,7 @@
             try {
                 mCallback.asBinder().linkToDeath(this, 0);
             } catch (RemoteException e) {
+                /*ignored*/
             }
         }
 
@@ -552,6 +556,50 @@
         }
     }
 
+    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 {
@@ -1629,7 +1677,7 @@
             UidState uidState = getUidStateLocked(uid, false);
             if (uidState != null && uidState.opModes != null
                     && uidState.opModes.indexOfKey(code) >= 0) {
-                return uidState.opModes.get(code);
+                return uidState.evalMode(uidState.opModes.get(code));
             }
             Op op = getOpLocked(code, uid, packageName, false, true, false);
             if (op == null) {
@@ -1795,12 +1843,16 @@
             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;
@@ -1820,6 +1872,7 @@
                             + switchCode + " (" + code + ") uid " + uid + " package "
                             + packageName);
                     op.rejectTime[uidState.state] = System.currentTimeMillis();
+                    scheduleOpNotedIfNeededLocked(code, uid, packageName, uidMode);
                     return uidMode;
                 }
             } else {
@@ -1830,6 +1883,7 @@
                             + switchCode + " (" + code + ") uid " + uid + " package "
                             + packageName);
                     op.rejectTime[uidState.state] = System.currentTimeMillis();
+                    scheduleOpNotedIfNeededLocked(op.op, uid, packageName, mode);
                     return mode;
                 }
             }
@@ -1839,6 +1893,8 @@
             op.rejectTime[uidState.state] = 0;
             op.proxyUid = proxyUid;
             op.proxyPackageName = proxyPackageName;
+            scheduleOpNotedIfNeededLocked(code, uid, packageName,
+                    AppOpsManager.MODE_ALLOWED);
             return AppOpsManager.MODE_ALLOWED;
         }
     }
@@ -1886,10 +1942,50 @@
             }
             final int callbackCount = activeCallbacks.size();
             for (int i = 0; i < callbackCount; i++) {
-                // Apps ops are mapped to a singleton
-                if (i == 0) {
-                    activeCallbacks.valueAt(i).destroy();
-                }
+                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();
             }
         }
     }
@@ -2052,6 +2148,51 @@
         }
     }
 
+    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) {
@@ -3463,6 +3604,46 @@
                     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 && cb.mWatchingUid >= 0
+                            && 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) {
                 needSep = true;
                 boolean printedHeader = false;
diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml
index cf4d3a8..1b5ba26 100644
--- a/services/tests/servicestests/AndroidManifest.xml
+++ b/services/tests/servicestests/AndroidManifest.xml
@@ -25,7 +25,6 @@
     <uses-permission android:name="android.permission.READ_PHONE_STATE" />
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
     <uses-permission android:name="android.permission.BROADCAST_STICKY" />
-    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
     <uses-permission android:name="android.permission.UPDATE_DEVICE_STATS" />
     <uses-permission android:name="android.permission.MANAGE_APP_TOKENS" />
     <uses-permission android:name="android.permission.WAKE_LOCK" />
diff --git a/services/tests/servicestests/src/com/android/server/appops/AppOpsNotedWatcherTest.java b/services/tests/servicestests/src/com/android/server/appops/AppOpsNotedWatcherTest.java
new file mode 100644
index 0000000..52f434d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/appops/AppOpsNotedWatcherTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.appops;
+
+import android.Manifest;
+import android.app.AppOpsManager;
+import android.app.AppOpsManager.OnOpNotedListener;
+import android.content.Context;
+import android.os.Process;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+
+
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+/**
+ * Tests watching noted ops.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AppOpsNotedWatcherTest {
+
+    private static final long NOTIFICATION_TIMEOUT_MILLIS = 5000;
+
+    public void testWatchNotedOpsRequiresPermission() {
+        // Create a mock listener
+        final OnOpNotedListener listener = mock(OnOpNotedListener.class);
+
+        // Try to start watching noted ops
+        final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
+        try {
+            appOpsManager.startWatchingNoted(new String[]{AppOpsManager.OPSTR_FINE_LOCATION,
+                    AppOpsManager.OPSTR_RECORD_AUDIO}, listener);
+            fail("Watching noted ops shoudl require " + Manifest.permission.WATCH_APPOPS);
+        } catch (SecurityException expected) {
+            /*ignored*/
+        }
+    }
+
+    @Test
+    public void testWatchNotedOps() {
+        // Create a mock listener
+        final OnOpNotedListener listener = mock(OnOpNotedListener.class);
+
+        // Start watching noted ops
+        final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
+        appOpsManager.startWatchingNoted(new String[]{AppOpsManager.OPSTR_FINE_LOCATION,
+                AppOpsManager.OPSTR_CAMERA}, listener);
+
+        // Note some ops
+        appOpsManager.noteOp(AppOpsManager.OPSTR_FINE_LOCATION, Process.myUid(),
+                getContext().getPackageName());
+        appOpsManager.noteOp(AppOpsManager.OPSTR_CAMERA, Process.myUid(),
+                getContext().getPackageName());
+
+        // Verify that we got called for the ops being noted
+        final InOrder inOrder = inOrder(listener);
+        inOrder.verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
+                .times(1)).onOpNoted(eq(AppOpsManager.OPSTR_FINE_LOCATION),
+                eq(Process.myUid()), eq(getContext().getPackageName()),
+                eq(AppOpsManager.MODE_ALLOWED));
+        inOrder.verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS)
+                .times(1)).onOpNoted(eq(AppOpsManager.OPSTR_CAMERA),
+                eq(Process.myUid()), eq(getContext().getPackageName()),
+                eq(AppOpsManager.MODE_ALLOWED));
+
+        // Stop watching
+        appOpsManager.stopWatchingNoted(listener);
+
+        // This should be the only two callbacks we got
+        verifyNoMoreInteractions(listener);
+    }
+
+    private static Context getContext() {
+        return InstrumentationRegistry.getContext();
+    }
+}
\ No newline at end of file