From 182862e595848fa2d2508e725ca880b78a7026a7 Mon Sep 17 00:00:00 2001 From: Adam Bookatz Date: Mon, 27 Apr 2020 21:58:22 -0700 Subject: [PATCH] Listener to watch op starts Currently, there is onOpNoted - tells listeners that noteOp has occurred onOpActiveChanged - tells listeners that an op's 'active' state has changed, i.e. that a successfull startOp or stopOp has happened There was, however, no way of telling a listener that a startOp has happened (regardless of whether it was successful). This cl introduces it, via a OnOpStartedListener. This is required by the ForegroundServiceAppOpSessionEnded atom, which counts the number of accepted vs. rejected attempts, and therefore also needs to know when a rejected start happened. This cl also contains some cosmetic moving of code so that startOperation() and noteOperationImpl() are almost exactly parallel. * Also * This cl fixes a bug I discovered in stopWatchingNoted, in which the callback wasn't fully removed. Consequently, if a callback was unregistered and then re-registered, the re-registration would mistakingly be ignored (in direct contradiction to the javadoc). Test: atest UidAtomTests#testForegroundServiceAccessAppOp Test: atest AppOpsStartedWatcherTest AppOpsActiveWatcherTest AppOpsNotedWatcherTest Test: manually monitor: adb shell cmd stats print-logs && adb logcat -v uid -s statsd | grep "statsd : {" | egrep '\(256\)' Bug: 152800926 Change-Id: Icdb9edf6b2b7c5807b339c1aabb32e882190b071 --- core/java/android/app/AppOpsManager.java | 94 +++++++- .../android/internal/app/IAppOpsService.aidl | 4 + .../internal/app/IAppOpsStartedCallback.aidl | 22 ++ .../com/android/server/am/ActiveServices.java | 24 +- .../android/server/appop/AppOpsService.java | 205 +++++++++++++++++- .../server/appop/AppOpsActiveWatcherTest.java | 16 ++ .../server/appop/AppOpsNotedWatcherTest.java | 49 +++-- .../appop/AppOpsStartedWatcherTest.java | 106 +++++++++ 8 files changed, 488 insertions(+), 32 deletions(-) create mode 100644 core/java/com/android/internal/app/IAppOpsStartedCallback.aidl create mode 100644 services/tests/servicestests/src/com/android/server/appop/AppOpsStartedWatcherTest.java diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 4d21c8d1a343e..d3212884200d3 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -66,6 +66,7 @@ import com.android.internal.app.IAppOpsAsyncNotedCallback; import com.android.internal.app.IAppOpsCallback; import com.android.internal.app.IAppOpsNotedCallback; import com.android.internal.app.IAppOpsService; +import com.android.internal.app.IAppOpsStartedCallback; import com.android.internal.app.MessageSamplingConfig; import com.android.internal.os.RuntimeInit; import com.android.internal.os.ZygoteInit; @@ -201,6 +202,10 @@ public class AppOpsManager { private final ArrayMap mActiveWatchers = new ArrayMap<>(); + @GuardedBy("mStartedWatchers") + private final ArrayMap mStartedWatchers = + new ArrayMap<>(); + @GuardedBy("mNotedWatchers") private final ArrayMap mNotedWatchers = new ArrayMap<>(); @@ -6367,6 +6372,25 @@ public class AppOpsManager { default void onOpActiveChanged(int op, int uid, String packageName, boolean active) { } } + /** + * Callback for notification of an op being started. + * + * @hide + */ + public interface OnOpStartedListener { + /** + * Called when an op was started. + * + * Note: This is only for op starts. It is not called when an op is noted or stopped. + * + * @param op The op code. + * @param uid The UID performing the operation. + * @param packageName The package performing the operation. + * @param result The result of the start. + */ + void onOpStarted(int op, int uid, String packageName, int result); + } + AppOpsManager(Context context, IAppOpsService service) { mContext = context; mService = service; @@ -6921,6 +6945,73 @@ public class AppOpsManager { } } + /** + * Start watching for started app-ops. + * An app-op may be long running and it has a clear start delimiter. + * If an op start is attempted 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. + * + *

If you don't hold the {@code android.Manifest.permission#WATCH_APPOPS} permission + * you can watch changes only for your UID. + * + * @param ops The operations to watch. + * @param callback Where to report changes. + * + * @see #stopWatchingStarted(OnOpStartedListener) + * @see #startWatchingActive(int[], OnOpActiveChangedListener) + * @see #startWatchingNoted(int[], OnOpNotedListener) + * @see #startOp(int, int, String, boolean, String, String) + * @see #finishOp(int, int, String, String) + * + * @hide + */ + @RequiresPermission(value=Manifest.permission.WATCH_APPOPS, conditional=true) + public void startWatchingStarted(@NonNull int[] ops, @NonNull OnOpStartedListener callback) { + IAppOpsStartedCallback cb; + synchronized (mStartedWatchers) { + if (mStartedWatchers.containsKey(callback)) { + return; + } + cb = new IAppOpsStartedCallback.Stub() { + @Override + public void opStarted(int op, int uid, String packageName, int mode) { + callback.onOpStarted(op, uid, packageName, mode); + } + }; + mStartedWatchers.put(callback, cb); + } + try { + mService.startWatchingStarted(ops, cb); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Stop watching for started app-ops. + * An app-op may be long running and it has a clear start delimiter. + * Henceforth, if an op start is attempted by any package, you will not get a callback. + * Unregistering a non-registered callback has no effect. + * + * @see #startWatchingStarted(int[], OnOpStartedListener) + * @see #startOp(int, int, String, boolean, String, String) + * + * @hide + */ + public void stopWatchingStarted(@NonNull OnOpStartedListener callback) { + synchronized (mStartedWatchers) { + final IAppOpsStartedCallback cb = mStartedWatchers.remove(callback); + if (cb != null) { + try { + mService.stopWatchingStarted(cb); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + } + /** * 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 @@ -6935,6 +7026,7 @@ public class AppOpsManager { * @param callback Where to report changes. * * @see #startWatchingActive(int[], OnOpActiveChangedListener) + * @see #startWatchingStarted(int[], OnOpStartedListener) * @see #stopWatchingNoted(OnOpNotedListener) * @see #noteOp(String, int, String, String, String) * @@ -6974,7 +7066,7 @@ public class AppOpsManager { */ public void stopWatchingNoted(@NonNull OnOpNotedListener callback) { synchronized (mNotedWatchers) { - final IAppOpsNotedCallback cb = mNotedWatchers.get(callback); + final IAppOpsNotedCallback cb = mNotedWatchers.remove(callback); if (cb != null) { try { mService.stopWatchingNoted(cb); diff --git a/core/java/com/android/internal/app/IAppOpsService.aidl b/core/java/com/android/internal/app/IAppOpsService.aidl index 9218823483286..06c21ab8832d7 100644 --- a/core/java/com/android/internal/app/IAppOpsService.aidl +++ b/core/java/com/android/internal/app/IAppOpsService.aidl @@ -27,6 +27,7 @@ import com.android.internal.app.IAppOpsCallback; import com.android.internal.app.IAppOpsActiveCallback; import com.android.internal.app.IAppOpsAsyncNotedCallback; import com.android.internal.app.IAppOpsNotedCallback; +import com.android.internal.app.IAppOpsStartedCallback; import com.android.internal.app.MessageSamplingConfig; interface IAppOpsService { @@ -91,6 +92,9 @@ interface IAppOpsService { void stopWatchingActive(IAppOpsActiveCallback callback); boolean isOperationActive(int code, int uid, String packageName); + void startWatchingStarted(in int[] ops, IAppOpsStartedCallback callback); + void stopWatchingStarted(IAppOpsStartedCallback callback); + void startWatchingModeWithFlags(int op, String packageName, int flags, IAppOpsCallback callback); void startWatchingNoted(in int[] ops, IAppOpsNotedCallback callback); diff --git a/core/java/com/android/internal/app/IAppOpsStartedCallback.aidl b/core/java/com/android/internal/app/IAppOpsStartedCallback.aidl new file mode 100644 index 0000000000000..ed521e656981d --- /dev/null +++ b/core/java/com/android/internal/app/IAppOpsStartedCallback.aidl @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2020 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 starts +oneway interface IAppOpsStartedCallback { + void opStarted(int op, int uid, String packageName, int mode); +} diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 419389f7abefe..cca604655f3d5 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -1634,22 +1634,24 @@ public final class ActiveServices { new AppOpsManager.OnOpNotedListener() { @Override public void onOpNoted(int op, int uid, String pkgName, int result) { - if (uid == mProcessRecord.uid && isNotTop()) { - incrementOpCount(op, result == AppOpsManager.MODE_ALLOWED); - } + incrementOpCountIfNeeded(op, uid, result); } }; - private final AppOpsManager.OnOpActiveChangedInternalListener mOpActiveCallback = - new AppOpsManager.OnOpActiveChangedInternalListener() { + private final AppOpsManager.OnOpStartedListener mOpStartedCallback = + new AppOpsManager.OnOpStartedListener() { @Override - public void onOpActiveChanged(int op, int uid, String pkgName, boolean active) { - if (uid == mProcessRecord.uid && active && isNotTop()) { - incrementOpCount(op, true); - } + public void onOpStarted(int op, int uid, String pkgName, int result) { + incrementOpCountIfNeeded(op, uid, result); } }; + private void incrementOpCountIfNeeded(int op, int uid, @AppOpsManager.Mode int result) { + if (uid == mProcessRecord.uid && isNotTop()) { + incrementOpCount(op, result == AppOpsManager.MODE_ALLOWED); + } + } + private boolean isNotTop() { return mProcessRecord.getCurProcState() != ActivityManager.PROCESS_STATE_TOP; } @@ -1674,7 +1676,7 @@ public final class ActiveServices { mNumFgs++; if (mNumFgs == 1) { mAppOpsManager.startWatchingNoted(LOGGED_AP_OPS, mOpNotedCallback); - mAppOpsManager.startWatchingActive(LOGGED_AP_OPS, mOpActiveCallback); + mAppOpsManager.startWatchingStarted(LOGGED_AP_OPS, mOpStartedCallback); } } @@ -1684,7 +1686,7 @@ public final class ActiveServices { mDestroyed = true; logFinalValues(); mAppOpsManager.stopWatchingNoted(mOpNotedCallback); - mAppOpsManager.stopWatchingActive(mOpActiveCallback); + mAppOpsManager.stopWatchingStarted(mOpStartedCallback); } } diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index d76ee685ca4d7..63e01e034d7ec 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -149,6 +149,7 @@ import com.android.internal.app.IAppOpsAsyncNotedCallback; import com.android.internal.app.IAppOpsCallback; import com.android.internal.app.IAppOpsNotedCallback; import com.android.internal.app.IAppOpsService; +import com.android.internal.app.IAppOpsStartedCallback; import com.android.internal.app.MessageSamplingConfig; import com.android.internal.os.Zygote; import com.android.internal.util.ArrayUtils; @@ -1292,6 +1293,7 @@ public class AppOpsService extends IAppOpsService.Stub { final ArrayMap> mPackageModeWatchers = new ArrayMap<>(); final ArrayMap mModeWatchers = new ArrayMap<>(); final ArrayMap> mActiveWatchers = new ArrayMap<>(); + final ArrayMap> mStartedWatchers = new ArrayMap<>(); final ArrayMap> mNotedWatchers = new ArrayMap<>(); final AudioRestrictionManager mAudioRestrictionManager = new AudioRestrictionManager(); @@ -1407,6 +1409,50 @@ public class AppOpsService extends IAppOpsService.Stub { } } + final class StartedCallback implements DeathRecipient { + final IAppOpsStartedCallback mCallback; + final int mWatchingUid; + final int mCallingUid; + final int mCallingPid; + + StartedCallback(IAppOpsStartedCallback 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("StartedCallback{"); + 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() { + stopWatchingStarted(mCallback); + } + } + final class NotedCallback implements DeathRecipient { final IAppOpsNotedCallback mCallback; final int mWatchingUid; @@ -3031,13 +3077,12 @@ public class AppOpsService extends IAppOpsService.Stub { return AppOpsManager.MODE_ERRORED; } final Op op = getOpLocked(ops, code, uid, true); - final AttributedOp attributedOp = op.getOrCreateAttribution(op, attributionTag); if (isOpRestrictedLocked(uid, code, packageName, bypass)) { scheduleOpNotedIfNeededLocked(code, uid, packageName, AppOpsManager.MODE_IGNORED); return AppOpsManager.MODE_IGNORED; } - final UidState uidState = ops.uidState; + final AttributedOp attributedOp = op.getOrCreateAttribution(op, attributionTag); if (attributedOp.isRunning()) { Slog.w(TAG, "Noting op not finished: uid " + uid + " pkg " + packageName + " code " + code + " startTime of in progress event=" @@ -3045,6 +3090,7 @@ public class AppOpsService extends IAppOpsService.Stub { } 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) { @@ -3076,10 +3122,9 @@ public class AppOpsService extends IAppOpsService.Stub { + packageName + (attributionTag == null ? "" : "." + attributionTag)); } + scheduleOpNotedIfNeededLocked(code, uid, packageName, AppOpsManager.MODE_ALLOWED); attributedOp.accessed(proxyUid, proxyPackageName, proxyAttributionTag, uidState.state, flags); - scheduleOpNotedIfNeededLocked(code, uid, packageName, - AppOpsManager.MODE_ALLOWED); if (shouldCollectAsyncNotedOp) { collectAsyncNotedOp(uid, packageName, code, attributionTag, message); @@ -3092,7 +3137,7 @@ public class AppOpsService extends IAppOpsService.Stub { // TODO moltmann: Allow watching for attribution ops @Override public void startWatchingActive(int[] ops, IAppOpsActiveCallback callback) { - int watchedUid = -1; + int watchedUid = Process.INVALID_UID; final int callingUid = Binder.getCallingUid(); final int callingPid = Binder.getCallingPid(); if (mContext.checkCallingOrSelfPermission(Manifest.permission.WATCH_APPOPS) @@ -3138,6 +3183,54 @@ public class AppOpsService extends IAppOpsService.Stub { } } + @Override + public void startWatchingStarted(int[] ops, @NonNull IAppOpsStartedCallback 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)); + Objects.requireNonNull(callback, "Callback cannot be null"); + + synchronized (this) { + SparseArray callbacks = mStartedWatchers.get(callback.asBinder()); + if (callbacks == null) { + callbacks = new SparseArray<>(); + mStartedWatchers.put(callback.asBinder(), callbacks); + } + + final StartedCallback startedCallback = new StartedCallback(callback, watchedUid, + callingUid, callingPid); + for (int op : ops) { + callbacks.put(op, startedCallback); + } + } + } + + @Override + public void stopWatchingStarted(IAppOpsStartedCallback callback) { + Objects.requireNonNull(callback, "Callback cannot be null"); + + synchronized (this) { + final SparseArray startedCallbacks = + mStartedWatchers.remove(callback.asBinder()); + if (startedCallbacks == null) { + return; + } + + final int callbackCount = startedCallbacks.size(); + for (int i = 0; i < callbackCount; i++) { + startedCallbacks.valueAt(i).destroy(); + } + } + } + @Override public void startWatchingNoted(@NonNull int[] ops, @NonNull IAppOpsNotedCallback callback) { int watchedUid = Process.INVALID_UID; @@ -3340,12 +3433,14 @@ public class AppOpsService extends IAppOpsService.Stub { final Ops ops = getOpsLocked(uid, resolvedPackageName, attributionTag, bypass, true /* edit */); if (ops == null) { + scheduleOpStartedIfNeededLocked(code, uid, packageName, AppOpsManager.MODE_IGNORED); 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, uid, true); if (isOpRestrictedLocked(uid, code, resolvedPackageName, bypass)) { + scheduleOpStartedIfNeededLocked(code, uid, packageName, AppOpsManager.MODE_IGNORED); return AppOpsManager.MODE_IGNORED; } final AttributedOp attributedOp = op.getOrCreateAttribution(op, attributionTag); @@ -3353,7 +3448,6 @@ public class AppOpsService extends IAppOpsService.Stub { 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. - final int opCode = op.op; if (uidState.opModes != null && uidState.opModes.indexOfKey(switchCode) >= 0) { final int uidMode = uidState.evalMode(code, uidState.opModes.get(switchCode)); if (uidMode != AppOpsManager.MODE_ALLOWED @@ -3362,6 +3456,7 @@ public class AppOpsService extends IAppOpsService.Stub { + switchCode + " (" + code + ") uid " + uid + " package " + resolvedPackageName); attributedOp.rejected(uidState.state, AppOpsManager.OP_FLAG_SELF); + scheduleOpStartedIfNeededLocked(code, uid, packageName, uidMode); return uidMode; } } else { @@ -3374,11 +3469,13 @@ public class AppOpsService extends IAppOpsService.Stub { + switchCode + " (" + code + ") uid " + uid + " package " + resolvedPackageName); attributedOp.rejected(uidState.state, AppOpsManager.OP_FLAG_SELF); + scheduleOpStartedIfNeededLocked(code, uid, packageName, mode); return mode; } } if (DEBUG) Slog.d(TAG, "startOperation: allowing code " + code + " uid " + uid + " package " + resolvedPackageName); + scheduleOpStartedIfNeededLocked(code, uid, packageName, AppOpsManager.MODE_ALLOWED); try { attributedOp.started(clientId, uidState.state); } catch (RemoteException e) { @@ -3480,6 +3577,52 @@ public class AppOpsService extends IAppOpsService.Stub { } } + private void scheduleOpStartedIfNeededLocked(int code, int uid, String pkgName, int result) { + ArraySet dispatchedCallbacks = null; + final int callbackListCount = mStartedWatchers.size(); + for (int i = 0; i < callbackListCount; i++) { + final SparseArray callbacks = mStartedWatchers.valueAt(i); + + StartedCallback 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::notifyOpStarted, + this, dispatchedCallbacks, code, uid, pkgName, result)); + } + + private void notifyOpStarted(ArraySet callbacks, + int code, int uid, String packageName, int result) { + final long identity = Binder.clearCallingIdentity(); + try { + final int callbackCount = callbacks.size(); + for (int i = 0; i < callbackCount; i++) { + final StartedCallback callback = callbacks.valueAt(i); + try { + callback.mCallback.opStarted(code, uid, packageName, result); + } catch (RemoteException e) { + /* do nothing */ + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + private void scheduleOpNotedIfNeededLocked(int code, int uid, String packageName, int result) { ArraySet dispatchedCallbacks = null; @@ -5185,6 +5328,56 @@ public class AppOpsService extends IAppOpsService.Stub { pw.println(cb); } } + if (mStartedWatchers.size() > 0 && dumpMode < 0) { + needSep = true; + boolean printedHeader = false; + + final int watchersSize = mStartedWatchers.size(); + for (int watcherNum = 0; watcherNum < watchersSize; watcherNum++) { + final SparseArray startedWatchers = + mStartedWatchers.valueAt(watcherNum); + if (startedWatchers.size() <= 0) { + continue; + } + + final StartedCallback cb = startedWatchers.valueAt(0); + if (dumpOp >= 0 && startedWatchers.indexOfKey(dumpOp) < 0) { + continue; + } + + if (dumpPackage != null + && dumpUid != UserHandle.getAppId(cb.mWatchingUid)) { + continue; + } + + if (!printedHeader) { + pw.println(" All op started watchers:"); + printedHeader = true; + } + + pw.print(" "); + pw.print(Integer.toHexString(System.identityHashCode( + mStartedWatchers.keyAt(watcherNum)))); + pw.println(" ->"); + + pw.print(" ["); + final int opCount = startedWatchers.size(); + for (int opNum = 0; opNum < opCount; opNum++) { + if (opNum > 0) { + pw.print(' '); + } + + pw.print(AppOpsManager.opToName(startedWatchers.keyAt(opNum))); + if (opNum < opCount - 1) { + pw.print(','); + } + } + pw.println("]"); + + pw.print(" "); + pw.println(cb); + } + } if (mNotedWatchers.size() > 0 && dumpMode < 0) { needSep = true; boolean printedHeader = false; diff --git a/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java b/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java index 41142f6b85051..98bc0673f79c1 100644 --- a/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java +++ b/services/tests/servicestests/src/com/android/server/appop/AppOpsActiveWatcherTest.java @@ -110,6 +110,22 @@ public class AppOpsActiveWatcherTest { // We should not be getting any callbacks verifyNoMoreInteractions(listener); + + // Start watching op again + appOpsManager.startWatchingActive(new String[] {AppOpsManager.OPSTR_CAMERA}, + getContext().getMainExecutor(), listener); + + // Start the op + appOpsManager.startOp(AppOpsManager.OP_CAMERA); + + // We should get the callback again (and since we reset the listener, we therefore expect 1) + verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS) + .times(1)).onOpActiveChanged(eq(AppOpsManager.OPSTR_CAMERA), + eq(Process.myUid()), eq(getContext().getPackageName()), eq(true)); + + // Finish up + appOpsManager.finishOp(AppOpsManager.OP_CAMERA); + appOpsManager.stopWatchingActive(listener); } @Test diff --git a/services/tests/servicestests/src/com/android/server/appop/AppOpsNotedWatcherTest.java b/services/tests/servicestests/src/com/android/server/appop/AppOpsNotedWatcherTest.java index 96f329b9161e6..1e602f84071de 100644 --- a/services/tests/servicestests/src/com/android/server/appop/AppOpsNotedWatcherTest.java +++ b/services/tests/servicestests/src/com/android/server/appop/AppOpsNotedWatcherTest.java @@ -16,26 +16,26 @@ 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.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import android.app.AppOpsManager; +import android.app.AppOpsManager.OnOpNotedListener; +import android.content.Context; +import android.os.Process; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InOrder; + /** * Tests watching noted ops. */ @@ -77,6 +77,27 @@ public class AppOpsNotedWatcherTest { // This should be the only two callbacks we got verifyNoMoreInteractions(listener); + + // Note the op again and verify it isn't being watched + appOpsManager.noteOp(AppOpsManager.OP_FINE_LOCATION); + verifyNoMoreInteractions(listener); + + // Start watching again + appOpsManager.startWatchingNoted(new int[]{AppOpsManager.OP_FINE_LOCATION, + AppOpsManager.OP_CAMERA}, listener); + + // Note the op again + appOpsManager.noteOp(AppOpsManager.OP_FINE_LOCATION, Process.myUid(), + getContext().getPackageName()); + + // Verify it's watched again + verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS) + .times(2)).onOpNoted(eq(AppOpsManager.OP_FINE_LOCATION), + eq(Process.myUid()), eq(getContext().getPackageName()), + eq(AppOpsManager.MODE_ALLOWED)); + + // Finish up + appOpsManager.stopWatchingNoted(listener); } private static Context getContext() { diff --git a/services/tests/servicestests/src/com/android/server/appop/AppOpsStartedWatcherTest.java b/services/tests/servicestests/src/com/android/server/appop/AppOpsStartedWatcherTest.java new file mode 100644 index 0000000000000..1aa697b04f1da --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/appop/AppOpsStartedWatcherTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2020 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 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.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import android.app.AppOpsManager; +import android.app.AppOpsManager.OnOpStartedListener; +import android.content.Context; +import android.os.Process; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InOrder; + +/** Tests watching started ops. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class AppOpsStartedWatcherTest { + + private static final long NOTIFICATION_TIMEOUT_MILLIS = 5000; + + @Test + public void testWatchStartedOps() { + // Create a mock listener + final OnOpStartedListener listener = mock(OnOpStartedListener.class); + + // Start watching started ops + final AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class); + appOpsManager.startWatchingStarted(new int[]{AppOpsManager.OP_FINE_LOCATION, + AppOpsManager.OP_CAMERA}, listener); + + // Start some ops + appOpsManager.startOp(AppOpsManager.OP_FINE_LOCATION); + appOpsManager.startOp(AppOpsManager.OP_CAMERA); + appOpsManager.startOp(AppOpsManager.OP_RECORD_AUDIO); + + // Verify that we got called for the ops being started + final InOrder inOrder = inOrder(listener); + inOrder.verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS) + .times(1)).onOpStarted(eq(AppOpsManager.OP_FINE_LOCATION), + eq(Process.myUid()), eq(getContext().getPackageName()), + eq(AppOpsManager.MODE_ALLOWED)); + inOrder.verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS) + .times(1)).onOpStarted(eq(AppOpsManager.OP_CAMERA), + eq(Process.myUid()), eq(getContext().getPackageName()), + eq(AppOpsManager.MODE_ALLOWED)); + + // Stop watching + appOpsManager.stopWatchingStarted(listener); + + // This should be the only two callbacks we got + verifyNoMoreInteractions(listener); + + // Start the op again and verify it isn't being watched + appOpsManager.startOp(AppOpsManager.OP_FINE_LOCATION); + appOpsManager.finishOp(AppOpsManager.OP_FINE_LOCATION); + verifyNoMoreInteractions(listener); + + // Start watching an op again (only CAMERA this time) + appOpsManager.startWatchingStarted(new int[]{AppOpsManager.OP_CAMERA}, listener); + + // Note the ops again + appOpsManager.startOp(AppOpsManager.OP_CAMERA); + appOpsManager.startOp(AppOpsManager.OP_FINE_LOCATION); + + // Verify it's watched again + verify(listener, timeout(NOTIFICATION_TIMEOUT_MILLIS) + .times(2)).onOpStarted(eq(AppOpsManager.OP_CAMERA), + eq(Process.myUid()), eq(getContext().getPackageName()), + eq(AppOpsManager.MODE_ALLOWED)); + verifyNoMoreInteractions(listener); + + // Finish up + appOpsManager.finishOp(AppOpsManager.OP_CAMERA); + appOpsManager.finishOp(AppOpsManager.OP_FINE_LOCATION); + appOpsManager.stopWatchingStarted(listener); + } + + private static Context getContext() { + return InstrumentationRegistry.getContext(); + } +}