Merge "Listener to watch op starts" into rvc-dev am: 316bc6ff66 am: 2d3310ec59 am: 911b6104c8

Change-Id: I3a7c8242e1bde2a129cfc2bfe2dedb9d4f459b35
This commit is contained in:
Adam Bookatz
2020-05-05 01:37:23 +00:00
committed by Automerger Merge Worker
8 changed files with 488 additions and 32 deletions

View File

@@ -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<OnOpActiveChangedListener, IAppOpsActiveCallback> mActiveWatchers =
new ArrayMap<>();
@GuardedBy("mStartedWatchers")
private final ArrayMap<OnOpStartedListener, IAppOpsStartedCallback> mStartedWatchers =
new ArrayMap<>();
@GuardedBy("mNotedWatchers")
private final ArrayMap<OnOpNotedListener, IAppOpsNotedCallback> 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.
*
* <p> 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);

View File

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

View File

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

View File

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

View File

@@ -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<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<StartedCallback>> mStartedWatchers = new ArrayMap<>();
final ArrayMap<IBinder, SparseArray<NotedCallback>> 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<StartedCallback> 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<StartedCallback> 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<StartedCallback> dispatchedCallbacks = null;
final int callbackListCount = mStartedWatchers.size();
for (int i = 0; i < callbackListCount; i++) {
final SparseArray<StartedCallback> 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<StartedCallback> 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<NotedCallback> 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<StartedCallback> 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;

View File

@@ -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

View File

@@ -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() {

View File

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