diff options
| author | 2018-12-18 23:22:02 +0000 | |
|---|---|---|
| committer | 2018-12-18 23:22:02 +0000 | |
| commit | 4374ef8506e252bb72f189a26b56666016075662 (patch) | |
| tree | 7c6065177cb859a36712f8f35c4a16e7d35bcbce | |
| parent | 7a17e757fbcb0fe5b0dbd1bdcc76e645fec3e1f8 (diff) | |
| parent | b3d2ae26449495f44eb284f07a54cbf744ae50d9 (diff) | |
Merge "Add private APIs to watch noted app ops - framework."
| -rw-r--r-- | Android.bp | 1 | ||||
| -rw-r--r-- | core/java/android/app/AppOpsManager.java | 105 | ||||
| -rw-r--r-- | core/java/com/android/internal/app/IAppOpsNotedCallback.aidl | 22 | ||||
| -rw-r--r-- | core/java/com/android/internal/app/IAppOpsService.aidl | 4 | ||||
| -rw-r--r-- | services/core/java/com/android/server/AppOpsService.java | 191 | ||||
| -rw-r--r-- | services/tests/servicestests/AndroidManifest.xml | 1 | ||||
| -rw-r--r-- | services/tests/servicestests/src/com/android/server/appops/AppOpsNotedWatcherTest.java | 100 |
7 files changed, 414 insertions, 10 deletions
diff --git a/Android.bp b/Android.bp index 763139d311cc..dc9c4d4b5a85 100644 --- a/Android.bp +++ b/Android.bp @@ -388,6 +388,7 @@ java_defaults { "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 6905cb5cea73..a2784237247c 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -41,8 +41,10 @@ import android.os.UserManager; 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 @@ public class AppOpsManager { */ 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 @@ public class AppOpsManager { } /** + * 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 class AppOpsManager { */ 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 @@ public class AppOpsManager { @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 @@ public class AppOpsManager { } } + /** + * 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 000000000000..fa5c30a03e78 --- /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 049103bfebb2..e571656b9135 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.content.pm.ParceledListSlice; 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 @@ interface IAppOpsService { 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 8d912fadf6d1..f0ec69f488b1 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.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; @@ -456,6 +457,7 @@ public class AppOpsService extends IAppOpsService.Stub { 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 @@ public class AppOpsService extends IAppOpsService.Stub { try { mCallback.asBinder().linkToDeath(this, 0); } catch (RemoteException e) { + /*ignored*/ } } @@ -524,6 +527,7 @@ public class AppOpsService extends IAppOpsService.Stub { try { mCallback.asBinder().linkToDeath(this, 0); } catch (RemoteException e) { + /*ignored*/ } } @@ -552,6 +556,50 @@ public class AppOpsService extends IAppOpsService.Stub { } } + 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 @@ public class AppOpsService extends IAppOpsService.Stub { 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 @@ public class AppOpsService extends IAppOpsService.Stub { 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 @@ public class AppOpsService extends IAppOpsService.Stub { + switchCode + " (" + code + ") uid " + uid + " package " + packageName); op.rejectTime[uidState.state] = System.currentTimeMillis(); + scheduleOpNotedIfNeededLocked(code, uid, packageName, uidMode); return uidMode; } } else { @@ -1830,6 +1883,7 @@ public class AppOpsService extends IAppOpsService.Stub { + switchCode + " (" + code + ") uid " + uid + " package " + packageName); op.rejectTime[uidState.state] = System.currentTimeMillis(); + scheduleOpNotedIfNeededLocked(op.op, uid, packageName, mode); return mode; } } @@ -1839,6 +1893,8 @@ public class AppOpsService extends IAppOpsService.Stub { 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 @@ public class AppOpsService extends IAppOpsService.Stub { } 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 @@ public class AppOpsService extends IAppOpsService.Stub { } } + 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 @@ public class AppOpsService extends IAppOpsService.Stub { 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 cf4d3a8070f9..1b5ba263e6dd 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 000000000000..52f434db3be3 --- /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 |