Merge "[DO NOT MERGE] Keep track of audio recordings that are silenced" into rvc-qpr-dev

This commit is contained in:
TreeHugger Robot
2020-08-11 19:32:24 +00:00
committed by Android (Google) Code Review
3 changed files with 205 additions and 13 deletions

View File

@@ -25,7 +25,9 @@ public class AppOpItem {
private int mUid; private int mUid;
private String mPackageName; private String mPackageName;
private long mTimeStarted; private long mTimeStarted;
private String mState; private StringBuilder mState;
// This is only used for items with mCode == AppOpsManager.OP_RECORD_AUDIO
private boolean mSilenced;
public AppOpItem(int code, int uid, String packageName, long timeStarted) { public AppOpItem(int code, int uid, String packageName, long timeStarted) {
this.mCode = code; this.mCode = code;
@@ -36,9 +38,8 @@ public class AppOpItem {
.append("AppOpItem(") .append("AppOpItem(")
.append("Op code=").append(code).append(", ") .append("Op code=").append(code).append(", ")
.append("UID=").append(uid).append(", ") .append("UID=").append(uid).append(", ")
.append("Package name=").append(packageName) .append("Package name=").append(packageName).append(", ")
.append(")") .append("Paused=");
.toString();
} }
public int getCode() { public int getCode() {
@@ -57,8 +58,16 @@ public class AppOpItem {
return mTimeStarted; return mTimeStarted;
} }
public void setSilenced(boolean silenced) {
mSilenced = silenced;
}
public boolean isSilenced() {
return mSilenced;
}
@Override @Override
public String toString() { public String toString() {
return mState; return mState.append(mSilenced).append(")").toString();
} }
} }

View File

@@ -19,12 +19,15 @@ package com.android.systemui.appops;
import android.app.AppOpsManager; import android.app.AppOpsManager;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.media.AudioManager;
import android.media.AudioRecordingConfiguration;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.os.UserHandle; import android.os.UserHandle;
import android.util.ArrayMap; import android.util.ArrayMap;
import android.util.ArraySet; import android.util.ArraySet;
import android.util.Log; import android.util.Log;
import android.util.SparseArray;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
@@ -62,6 +65,7 @@ public class AppOpsControllerImpl implements AppOpsController,
private static final boolean DEBUG = false; private static final boolean DEBUG = false;
private final AppOpsManager mAppOps; private final AppOpsManager mAppOps;
private final AudioManager mAudioManager;
private H mBGHandler; private H mBGHandler;
private final List<AppOpsController.Callback> mCallbacks = new ArrayList<>(); private final List<AppOpsController.Callback> mCallbacks = new ArrayList<>();
private final ArrayMap<Integer, Set<Callback>> mCallbacksByCode = new ArrayMap<>(); private final ArrayMap<Integer, Set<Callback>> mCallbacksByCode = new ArrayMap<>();
@@ -72,6 +76,9 @@ public class AppOpsControllerImpl implements AppOpsController,
private final List<AppOpItem> mActiveItems = new ArrayList<>(); private final List<AppOpItem> mActiveItems = new ArrayList<>();
@GuardedBy("mNotedItems") @GuardedBy("mNotedItems")
private final List<AppOpItem> mNotedItems = new ArrayList<>(); private final List<AppOpItem> mNotedItems = new ArrayList<>();
@GuardedBy("mActiveItems")
private final SparseArray<ArrayList<AudioRecordingConfiguration>> mRecordingsByUid =
new SparseArray<>();
protected static final int[] OPS = new int[] { protected static final int[] OPS = new int[] {
AppOpsManager.OP_CAMERA, AppOpsManager.OP_CAMERA,
@@ -86,7 +93,8 @@ public class AppOpsControllerImpl implements AppOpsController,
Context context, Context context,
@Background Looper bgLooper, @Background Looper bgLooper,
DumpManager dumpManager, DumpManager dumpManager,
PermissionFlagsCache cache PermissionFlagsCache cache,
AudioManager audioManager
) { ) {
mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
mFlagsCache = cache; mFlagsCache = cache;
@@ -95,6 +103,7 @@ public class AppOpsControllerImpl implements AppOpsController,
for (int i = 0; i < numOps; i++) { for (int i = 0; i < numOps; i++) {
mCallbacksByCode.put(OPS[i], new ArraySet<>()); mCallbacksByCode.put(OPS[i], new ArraySet<>());
} }
mAudioManager = audioManager;
dumpManager.registerDumpable(TAG, this); dumpManager.registerDumpable(TAG, this);
} }
@@ -109,12 +118,19 @@ public class AppOpsControllerImpl implements AppOpsController,
if (listening) { if (listening) {
mAppOps.startWatchingActive(OPS, this); mAppOps.startWatchingActive(OPS, this);
mAppOps.startWatchingNoted(OPS, this); mAppOps.startWatchingNoted(OPS, this);
mAudioManager.registerAudioRecordingCallback(mAudioRecordingCallback, mBGHandler);
mBGHandler.post(() -> mAudioRecordingCallback.onRecordingConfigChanged(
mAudioManager.getActiveRecordingConfigurations()));
} else { } else {
mAppOps.stopWatchingActive(this); mAppOps.stopWatchingActive(this);
mAppOps.stopWatchingNoted(this); mAppOps.stopWatchingNoted(this);
mAudioManager.unregisterAudioRecordingCallback(mAudioRecordingCallback);
mBGHandler.removeCallbacksAndMessages(null); // null removes all mBGHandler.removeCallbacksAndMessages(null); // null removes all
synchronized (mActiveItems) { synchronized (mActiveItems) {
mActiveItems.clear(); mActiveItems.clear();
mRecordingsByUid.clear();
} }
synchronized (mNotedItems) { synchronized (mNotedItems) {
mNotedItems.clear(); mNotedItems.clear();
@@ -187,9 +203,12 @@ public class AppOpsControllerImpl implements AppOpsController,
AppOpItem item = getAppOpItemLocked(mActiveItems, code, uid, packageName); AppOpItem item = getAppOpItemLocked(mActiveItems, code, uid, packageName);
if (item == null && active) { if (item == null && active) {
item = new AppOpItem(code, uid, packageName, System.currentTimeMillis()); item = new AppOpItem(code, uid, packageName, System.currentTimeMillis());
if (code == AppOpsManager.OP_RECORD_AUDIO) {
item.setSilenced(isAnyRecordingPausedLocked(uid));
}
mActiveItems.add(item); mActiveItems.add(item);
if (DEBUG) Log.w(TAG, "Added item: " + item.toString()); if (DEBUG) Log.w(TAG, "Added item: " + item.toString());
return true; return !item.isSilenced();
} else if (item != null && !active) { } else if (item != null && !active) {
mActiveItems.remove(item); mActiveItems.remove(item);
if (DEBUG) Log.w(TAG, "Removed item: " + item.toString()); if (DEBUG) Log.w(TAG, "Removed item: " + item.toString());
@@ -213,7 +232,7 @@ public class AppOpsControllerImpl implements AppOpsController,
active = getAppOpItemLocked(mActiveItems, code, uid, packageName) != null; active = getAppOpItemLocked(mActiveItems, code, uid, packageName) != null;
} }
if (!active) { if (!active) {
notifySuscribers(code, uid, packageName, false); notifySuscribersWorker(code, uid, packageName, false);
} }
} }
@@ -321,7 +340,7 @@ public class AppOpsControllerImpl implements AppOpsController,
AppOpItem item = mActiveItems.get(i); AppOpItem item = mActiveItems.get(i);
if ((userId == UserHandle.USER_ALL if ((userId == UserHandle.USER_ALL
|| UserHandle.getUserId(item.getUid()) == userId) || UserHandle.getUserId(item.getUid()) == userId)
&& isUserVisible(item)) { && isUserVisible(item) && !item.isSilenced()) {
list.add(item); list.add(item);
} }
} }
@@ -340,6 +359,10 @@ public class AppOpsControllerImpl implements AppOpsController,
return list; return list;
} }
private void notifySuscribers(int code, int uid, String packageName, boolean active) {
mBGHandler.post(() -> notifySuscribersWorker(code, uid, packageName, active));
}
@Override @Override
public void onOpActiveChanged(int code, int uid, String packageName, boolean active) { public void onOpActiveChanged(int code, int uid, String packageName, boolean active) {
if (DEBUG) { if (DEBUG) {
@@ -357,7 +380,7 @@ public class AppOpsControllerImpl implements AppOpsController,
// If active is false, we only send the update if the op is not actively noted (prevent // If active is false, we only send the update if the op is not actively noted (prevent
// early removal) // early removal)
if (!alsoNoted) { if (!alsoNoted) {
mBGHandler.post(() -> notifySuscribers(code, uid, packageName, active)); notifySuscribers(code, uid, packageName, active);
} }
} }
@@ -375,11 +398,11 @@ public class AppOpsControllerImpl implements AppOpsController,
alsoActive = getAppOpItemLocked(mActiveItems, code, uid, packageName) != null; alsoActive = getAppOpItemLocked(mActiveItems, code, uid, packageName) != null;
} }
if (!alsoActive) { if (!alsoActive) {
mBGHandler.post(() -> notifySuscribers(code, uid, packageName, true)); notifySuscribers(code, uid, packageName, true);
} }
} }
private void notifySuscribers(int code, int uid, String packageName, boolean active) { private void notifySuscribersWorker(int code, int uid, String packageName, boolean active) {
if (mCallbacksByCode.containsKey(code) && isUserVisible(code, uid, packageName)) { if (mCallbacksByCode.containsKey(code) && isUserVisible(code, uid, packageName)) {
if (DEBUG) Log.d(TAG, "Notifying of change in package " + packageName); if (DEBUG) Log.d(TAG, "Notifying of change in package " + packageName);
for (Callback cb: mCallbacksByCode.get(code)) { for (Callback cb: mCallbacksByCode.get(code)) {
@@ -405,6 +428,61 @@ public class AppOpsControllerImpl implements AppOpsController,
} }
private boolean isAnyRecordingPausedLocked(int uid) {
List<AudioRecordingConfiguration> configs = mRecordingsByUid.get(uid);
if (configs == null) return false;
int configsNum = configs.size();
for (int i = 0; i < configsNum; i++) {
AudioRecordingConfiguration config = configs.get(i);
if (config.isClientSilenced()) return true;
}
return false;
}
private void updateRecordingPausedStatus() {
synchronized (mActiveItems) {
int size = mActiveItems.size();
for (int i = 0; i < size; i++) {
AppOpItem item = mActiveItems.get(i);
if (item.getCode() == AppOpsManager.OP_RECORD_AUDIO) {
boolean paused = isAnyRecordingPausedLocked(item.getUid());
if (item.isSilenced() != paused) {
item.setSilenced(paused);
notifySuscribers(
item.getCode(),
item.getUid(),
item.getPackageName(),
!item.isSilenced()
);
}
}
}
}
}
private AudioManager.AudioRecordingCallback mAudioRecordingCallback =
new AudioManager.AudioRecordingCallback() {
@Override
public void onRecordingConfigChanged(List<AudioRecordingConfiguration> configs) {
synchronized (mActiveItems) {
mRecordingsByUid.clear();
final int recordingsCount = configs.size();
for (int i = 0; i < recordingsCount; i++) {
AudioRecordingConfiguration recording = configs.get(i);
ArrayList<AudioRecordingConfiguration> recordings = mRecordingsByUid.get(
recording.getClientUid());
if (recordings == null) {
recordings = new ArrayList<>();
mRecordingsByUid.put(recording.getClientUid(), recordings);
}
recordings.add(recording);
}
}
updateRecordingPausedStatus();
}
};
protected class H extends Handler { protected class H extends Handler {
H(Looper looper) { H(Looper looper) {
super(looper); super(looper);

View File

@@ -27,6 +27,9 @@ import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@@ -34,6 +37,8 @@ import static org.mockito.Mockito.when;
import android.app.AppOpsManager; import android.app.AppOpsManager;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.media.AudioManager;
import android.media.AudioRecordingConfiguration;
import android.os.Looper; import android.os.Looper;
import android.os.UserHandle; import android.os.UserHandle;
import android.testing.AndroidTestingRunner; import android.testing.AndroidTestingRunner;
@@ -47,9 +52,11 @@ import com.android.systemui.dump.DumpManager;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import java.util.Collections;
import java.util.List; import java.util.List;
@SmallTest @SmallTest
@@ -73,6 +80,12 @@ public class AppOpsControllerTest extends SysuiTestCase {
private PermissionFlagsCache mFlagsCache; private PermissionFlagsCache mFlagsCache;
@Mock @Mock
private PackageManager mPackageManager; private PackageManager mPackageManager;
@Mock(stubOnly = true)
private AudioManager mAudioManager;
@Mock(stubOnly = true)
private AudioManager.AudioRecordingCallback mRecordingCallback;
@Mock(stubOnly = true)
private AudioRecordingConfiguration mPausedMockRecording;
private AppOpsControllerImpl mController; private AppOpsControllerImpl mController;
private TestableLooper mTestableLooper; private TestableLooper mTestableLooper;
@@ -94,11 +107,20 @@ public class AppOpsControllerTest extends SysuiTestCase {
when(mFlagsCache.getPermissionFlags(anyString(), anyString(), when(mFlagsCache.getPermissionFlags(anyString(), anyString(),
eq(TEST_UID_NON_USER_SENSITIVE))).thenReturn(0); eq(TEST_UID_NON_USER_SENSITIVE))).thenReturn(0);
doAnswer((invocation) -> mRecordingCallback = invocation.getArgument(0))
.when(mAudioManager).registerAudioRecordingCallback(any(), any());
when(mPausedMockRecording.getClientUid()).thenReturn(TEST_UID);
when(mPausedMockRecording.isClientSilenced()).thenReturn(true);
when(mAudioManager.getActiveRecordingConfigurations())
.thenReturn(List.of(mPausedMockRecording));
mController = new AppOpsControllerImpl( mController = new AppOpsControllerImpl(
mContext, mContext,
mTestableLooper.getLooper(), mTestableLooper.getLooper(),
mDumpManager, mDumpManager,
mFlagsCache mFlagsCache,
mAudioManager
); );
} }
@@ -363,6 +385,89 @@ public class AppOpsControllerTest extends SysuiTestCase {
AppOpsManager.OP_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, true); AppOpsManager.OP_FINE_LOCATION, TEST_UID, TEST_PACKAGE_NAME, true);
} }
@Test
public void testPausedRecordingIsRetrievedOnCreation() {
mController.addCallback(new int[]{AppOpsManager.OP_RECORD_AUDIO}, mCallback);
mTestableLooper.processAllMessages();
mController.onOpActiveChanged(
AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
mTestableLooper.processAllMessages();
verify(mCallback, never())
.onActiveStateChanged(anyInt(), anyInt(), anyString(), anyBoolean());
}
@Test
public void testPausedRecordingFilteredOut() {
mController.addCallback(new int[]{AppOpsManager.OP_RECORD_AUDIO}, mCallback);
mTestableLooper.processAllMessages();
mController.onOpActiveChanged(
AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
mTestableLooper.processAllMessages();
assertTrue(mController.getActiveAppOps().isEmpty());
}
@Test
public void testOnlyRecordAudioPaused() {
mController.addCallback(new int[]{
AppOpsManager.OP_RECORD_AUDIO,
AppOpsManager.OP_CAMERA
}, mCallback);
mTestableLooper.processAllMessages();
mController.onOpActiveChanged(
AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, true);
mTestableLooper.processAllMessages();
verify(mCallback).onActiveStateChanged(
AppOpsManager.OP_CAMERA, TEST_UID, TEST_PACKAGE_NAME, true);
List<AppOpItem> list = mController.getActiveAppOps();
assertEquals(1, list.size());
assertEquals(AppOpsManager.OP_CAMERA, list.get(0).getCode());
}
@Test
public void testUnpausedRecordingSentActive() {
mController.addCallback(new int[]{AppOpsManager.OP_RECORD_AUDIO}, mCallback);
mTestableLooper.processAllMessages();
mController.onOpActiveChanged(
AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
mTestableLooper.processAllMessages();
mRecordingCallback.onRecordingConfigChanged(Collections.emptyList());
mTestableLooper.processAllMessages();
verify(mCallback).onActiveStateChanged(
AppOpsManager.OP_RECORD_AUDIO, TEST_UID, TEST_PACKAGE_NAME, true);
}
@Test
public void testAudioPausedSentInactive() {
mController.addCallback(new int[]{AppOpsManager.OP_RECORD_AUDIO}, mCallback);
mTestableLooper.processAllMessages();
mController.onOpActiveChanged(
AppOpsManager.OP_RECORD_AUDIO, TEST_UID_OTHER, TEST_PACKAGE_NAME, true);
mTestableLooper.processAllMessages();
AudioRecordingConfiguration mockARC = mock(AudioRecordingConfiguration.class);
when(mockARC.getClientUid()).thenReturn(TEST_UID_OTHER);
when(mockARC.isClientSilenced()).thenReturn(true);
mRecordingCallback.onRecordingConfigChanged(List.of(mockARC));
mTestableLooper.processAllMessages();
InOrder inOrder = inOrder(mCallback);
inOrder.verify(mCallback).onActiveStateChanged(
AppOpsManager.OP_RECORD_AUDIO, TEST_UID_OTHER, TEST_PACKAGE_NAME, true);
inOrder.verify(mCallback).onActiveStateChanged(
AppOpsManager.OP_RECORD_AUDIO, TEST_UID_OTHER, TEST_PACKAGE_NAME, false);
}
private class TestHandler extends AppOpsControllerImpl.H { private class TestHandler extends AppOpsControllerImpl.H {
TestHandler(Looper looper) { TestHandler(Looper looper) {
mController.super(looper); mController.super(looper);