Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ac81a572 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

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

parents 65be8ef9 a059a46d
Loading
Loading
Loading
Loading
+14 −5
Original line number Diff line number Diff line
@@ -25,7 +25,9 @@ public class AppOpItem {
    private int mUid;
    private String mPackageName;
    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) {
        this.mCode = code;
@@ -36,9 +38,8 @@ public class AppOpItem {
                .append("AppOpItem(")
                .append("Op code=").append(code).append(", ")
                .append("UID=").append(uid).append(", ")
                .append("Package name=").append(packageName)
                .append(")")
                .toString();
                .append("Package name=").append(packageName).append(", ")
                .append("Paused=");
    }

    public int getCode() {
@@ -57,8 +58,16 @@ public class AppOpItem {
        return mTimeStarted;
    }

    public void setSilenced(boolean silenced) {
        mSilenced = silenced;
    }

    public boolean isSilenced() {
        return mSilenced;
    }

    @Override
    public String toString() {
        return mState;
        return mState.append(mSilenced).append(")").toString();
    }
}
+85 −7
Original line number Diff line number Diff line
@@ -19,12 +19,15 @@ package com.android.systemui.appops;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioManager;
import android.media.AudioRecordingConfiguration;
import android.os.Handler;
import android.os.Looper;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;

import androidx.annotation.WorkerThread;

@@ -62,6 +65,7 @@ public class AppOpsControllerImpl implements AppOpsController,
    private static final boolean DEBUG = false;

    private final AppOpsManager mAppOps;
    private final AudioManager mAudioManager;
    private H mBGHandler;
    private final List<AppOpsController.Callback> mCallbacks = new ArrayList<>();
    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<>();
    @GuardedBy("mNotedItems")
    private final List<AppOpItem> mNotedItems = new ArrayList<>();
    @GuardedBy("mActiveItems")
    private final SparseArray<ArrayList<AudioRecordingConfiguration>> mRecordingsByUid =
            new SparseArray<>();

    protected static final int[] OPS = new int[] {
            AppOpsManager.OP_CAMERA,
@@ -86,7 +93,8 @@ public class AppOpsControllerImpl implements AppOpsController,
            Context context,
            @Background Looper bgLooper,
            DumpManager dumpManager,
            PermissionFlagsCache cache
            PermissionFlagsCache cache,
            AudioManager audioManager
    ) {
        mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
        mFlagsCache = cache;
@@ -95,6 +103,7 @@ public class AppOpsControllerImpl implements AppOpsController,
        for (int i = 0; i < numOps; i++) {
            mCallbacksByCode.put(OPS[i], new ArraySet<>());
        }
        mAudioManager = audioManager;
        dumpManager.registerDumpable(TAG, this);
    }

@@ -109,12 +118,19 @@ public class AppOpsControllerImpl implements AppOpsController,
        if (listening) {
            mAppOps.startWatchingActive(OPS, this);
            mAppOps.startWatchingNoted(OPS, this);
            mAudioManager.registerAudioRecordingCallback(mAudioRecordingCallback, mBGHandler);
            mBGHandler.post(() -> mAudioRecordingCallback.onRecordingConfigChanged(
                    mAudioManager.getActiveRecordingConfigurations()));

        } else {
            mAppOps.stopWatchingActive(this);
            mAppOps.stopWatchingNoted(this);
            mAudioManager.unregisterAudioRecordingCallback(mAudioRecordingCallback);

            mBGHandler.removeCallbacksAndMessages(null); // null removes all
            synchronized (mActiveItems) {
                mActiveItems.clear();
                mRecordingsByUid.clear();
            }
            synchronized (mNotedItems) {
                mNotedItems.clear();
@@ -187,9 +203,12 @@ public class AppOpsControllerImpl implements AppOpsController,
            AppOpItem item = getAppOpItemLocked(mActiveItems, code, uid, packageName);
            if (item == null && active) {
                item = new AppOpItem(code, uid, packageName, System.currentTimeMillis());
                if (code == AppOpsManager.OP_RECORD_AUDIO) {
                    item.setSilenced(isAnyRecordingPausedLocked(uid));
                }
                mActiveItems.add(item);
                if (DEBUG) Log.w(TAG, "Added item: " + item.toString());
                return true;
                return !item.isSilenced();
            } else if (item != null && !active) {
                mActiveItems.remove(item);
                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;
        }
        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);
                if ((userId == UserHandle.USER_ALL
                        || UserHandle.getUserId(item.getUid()) == userId)
                        && isUserVisible(item)) {
                        && isUserVisible(item) && !item.isSilenced()) {
                    list.add(item);
                }
            }
@@ -340,6 +359,10 @@ public class AppOpsControllerImpl implements AppOpsController,
        return list;
    }

    private void notifySuscribers(int code, int uid, String packageName, boolean active) {
        mBGHandler.post(() -> notifySuscribersWorker(code, uid, packageName, active));
    }

    @Override
    public void onOpActiveChanged(int code, int uid, String packageName, boolean active) {
        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
        // early removal)
        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;
        }
        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 (DEBUG) Log.d(TAG, "Notifying of change in package " + packageName);
            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 {
        H(Looper looper) {
            super(looper);
+106 −1
Original line number Diff line number Diff line
@@ -27,6 +27,9 @@ import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
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.times;
import static org.mockito.Mockito.verify;
@@ -34,6 +37,8 @@ import static org.mockito.Mockito.when;

import android.app.AppOpsManager;
import android.content.pm.PackageManager;
import android.media.AudioManager;
import android.media.AudioRecordingConfiguration;
import android.os.Looper;
import android.os.UserHandle;
import android.testing.AndroidTestingRunner;
@@ -47,9 +52,11 @@ import com.android.systemui.dump.DumpManager;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.Collections;
import java.util.List;

@SmallTest
@@ -73,6 +80,12 @@ public class AppOpsControllerTest extends SysuiTestCase {
    private PermissionFlagsCache mFlagsCache;
    @Mock
    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 TestableLooper mTestableLooper;
@@ -94,11 +107,20 @@ public class AppOpsControllerTest extends SysuiTestCase {
        when(mFlagsCache.getPermissionFlags(anyString(), anyString(),
                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(
                mContext,
                mTestableLooper.getLooper(),
                mDumpManager,
                mFlagsCache
                mFlagsCache,
                mAudioManager
        );
    }

@@ -363,6 +385,89 @@ public class AppOpsControllerTest extends SysuiTestCase {
                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 {
        TestHandler(Looper looper) {
            mController.super(looper);