diff --git a/packages/SystemUI/src/com/android/systemui/appops/AppOpItem.java b/packages/SystemUI/src/com/android/systemui/appops/AppOpItem.java index 93a8df41c673e..cd3d6a84a352d 100644 --- a/packages/SystemUI/src/com/android/systemui/appops/AppOpItem.java +++ b/packages/SystemUI/src/com/android/systemui/appops/AppOpItem.java @@ -26,8 +26,7 @@ public class AppOpItem { private String mPackageName; private long mTimeStarted; private StringBuilder mState; - // This is only used for items with mCode == AppOpsManager.OP_RECORD_AUDIO - private boolean mSilenced; + private boolean mIsDisabled; public AppOpItem(int code, int uid, String packageName, long timeStarted) { this.mCode = code; @@ -58,16 +57,16 @@ public class AppOpItem { return mTimeStarted; } - public void setSilenced(boolean silenced) { - mSilenced = silenced; + public void setDisabled(boolean misDisabled) { + this.mIsDisabled = misDisabled; } - public boolean isSilenced() { - return mSilenced; + public boolean isDisabled() { + return mIsDisabled; } @Override public String toString() { - return mState.append(mSilenced).append(")").toString(); + return mState.append(mIsDisabled).append(")").toString(); } } diff --git a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java index 1036c9916c8b2..d8ca63960b30d 100644 --- a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java @@ -16,6 +16,8 @@ package com.android.systemui.appops; +import static android.hardware.SensorPrivacyManager.INDIVIDUAL_SENSOR_CAMERA; +import static android.hardware.SensorPrivacyManager.INDIVIDUAL_SENSOR_MICROPHONE; import static android.media.AudioManager.ACTION_MICROPHONE_MUTE_CHANGED; import android.Manifest; @@ -45,6 +47,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dump.DumpManager; +import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController; import com.android.systemui.util.Assert; import java.io.FileDescriptor; @@ -64,7 +67,8 @@ import javax.inject.Inject; @SysUISingleton public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsController, AppOpsManager.OnOpActiveChangedInternalListener, - AppOpsManager.OnOpNotedListener, Dumpable { + AppOpsManager.OnOpNotedListener, IndividualSensorPrivacyController.Callback, + Dumpable { // This is the minimum time that we will keep AppOps that are noted on record. If multiple // occurrences of the same (op, package, uid) happen in a shorter interval, they will not be @@ -77,8 +81,8 @@ public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsCon private final AppOpsManager mAppOps; private final AudioManager mAudioManager; private final LocationManager mLocationManager; - // TODO ntmyren: remove t private final PackageManager mPackageManager; + private final IndividualSensorPrivacyController mSensorPrivacyController; // mLocationProviderPackages are cached and updated only occasionally private static final long LOCATION_PROVIDER_UPDATE_FREQUENCY_MS = 30000; @@ -91,6 +95,7 @@ public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsCon private final PermissionFlagsCache mFlagsCache; private boolean mListening; private boolean mMicMuted; + private boolean mCameraDisabled; @GuardedBy("mActiveItems") private final List mActiveItems = new ArrayList<>(); @@ -118,6 +123,7 @@ public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsCon DumpManager dumpManager, PermissionFlagsCache cache, AudioManager audioManager, + IndividualSensorPrivacyController sensorPrivacyController, BroadcastDispatcher dispatcher ) { mDispatcher = dispatcher; @@ -129,7 +135,10 @@ public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsCon mCallbacksByCode.put(OPS[i], new ArraySet<>()); } mAudioManager = audioManager; - mMicMuted = audioManager.isMicrophoneMute(); + mSensorPrivacyController = sensorPrivacyController; + mMicMuted = audioManager.isMicrophoneMute() + || mSensorPrivacyController.isSensorBlocked(INDIVIDUAL_SENSOR_MICROPHONE); + mCameraDisabled = mSensorPrivacyController.isSensorBlocked(INDIVIDUAL_SENSOR_CAMERA); mLocationManager = context.getSystemService(LocationManager.class); mPackageManager = context.getPackageManager(); dumpManager.registerDumpable(TAG, this); @@ -147,6 +156,12 @@ public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsCon mAppOps.startWatchingActive(OPS, this); mAppOps.startWatchingNoted(OPS, this); mAudioManager.registerAudioRecordingCallback(mAudioRecordingCallback, mBGHandler); + mSensorPrivacyController.addCallback(this); + + mMicMuted = mAudioManager.isMicrophoneMute() + || mSensorPrivacyController.isSensorBlocked(INDIVIDUAL_SENSOR_MICROPHONE); + mCameraDisabled = mSensorPrivacyController.isSensorBlocked(INDIVIDUAL_SENSOR_CAMERA); + mBGHandler.post(() -> mAudioRecordingCallback.onRecordingConfigChanged( mAudioManager.getActiveRecordingConfigurations())); mDispatcher.registerReceiverWithHandler(this, @@ -156,6 +171,7 @@ public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsCon mAppOps.stopWatchingActive(this); mAppOps.stopWatchingNoted(this); mAudioManager.unregisterAudioRecordingCallback(mAudioRecordingCallback); + mSensorPrivacyController.removeCallback(this); mBGHandler.removeCallbacksAndMessages(null); // null removes all mDispatcher.unregisterReceiver(this); @@ -235,11 +251,13 @@ public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsCon if (item == null && active) { item = new AppOpItem(code, uid, packageName, System.currentTimeMillis()); if (code == AppOpsManager.OP_RECORD_AUDIO) { - item.setSilenced(isAnyRecordingPausedLocked(uid)); + item.setDisabled(isAnyRecordingPausedLocked(uid)); + } else if (code == AppOpsManager.OP_CAMERA) { + item.setDisabled(mCameraDisabled); } mActiveItems.add(item); if (DEBUG) Log.w(TAG, "Added item: " + item.toString()); - return !item.isSilenced(); + return !item.isDisabled(); } else if (item != null && !active) { mActiveItems.remove(item); if (DEBUG) Log.w(TAG, "Removed item: " + item.toString()); @@ -409,7 +427,7 @@ public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsCon AppOpItem item = mActiveItems.get(i); if ((userId == UserHandle.USER_ALL || UserHandle.getUserId(item.getUid()) == userId) - && isUserVisible(item) && !item.isSilenced()) { + && isUserVisible(item) && !item.isDisabled()) { list.add(item); } } @@ -512,22 +530,27 @@ public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsCon return false; } - private void updateRecordingPausedStatus() { + private void updateSensorDisabledStatus() { synchronized (mActiveItems) { int size = mActiveItems.size(); for (int i = 0; i < size; i++) { AppOpItem item = mActiveItems.get(i); + + boolean paused = false; 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() - ); - } + paused = isAnyRecordingPausedLocked(item.getUid()); + } else if (item.getCode() == AppOpsManager.OP_CAMERA) { + paused = mCameraDisabled; + } + + if (item.isDisabled() != paused) { + item.setDisabled(paused); + notifySuscribers( + item.getCode(), + item.getUid(), + item.getPackageName(), + !item.isDisabled() + ); } } } @@ -552,14 +575,27 @@ public class AppOpsControllerImpl extends BroadcastReceiver implements AppOpsCon recordings.add(recording); } } - updateRecordingPausedStatus(); + updateSensorDisabledStatus(); } }; @Override public void onReceive(Context context, Intent intent) { - mMicMuted = mAudioManager.isMicrophoneMute(); - updateRecordingPausedStatus(); + mMicMuted = mAudioManager.isMicrophoneMute() + || mSensorPrivacyController.isSensorBlocked(INDIVIDUAL_SENSOR_MICROPHONE); + updateSensorDisabledStatus(); + } + + @Override + public void onSensorBlockedChanged(int sensor, boolean blocked) { + mBGHandler.post(() -> { + if (sensor == INDIVIDUAL_SENSOR_CAMERA) { + mCameraDisabled = blocked; + } else if (sensor == INDIVIDUAL_SENSOR_MICROPHONE) { + mMicMuted = mAudioManager.isMicrophoneMute() || blocked; + } + updateSensorDisabledStatus(); + }); } protected class H extends Handler { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java index 231fe08e6a998..32d15ed41648b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/IndividualSensorPrivacyControllerImpl.java @@ -16,8 +16,8 @@ package com.android.systemui.statusbar.policy; -import static android.service.SensorPrivacyIndividualEnabledSensorProto.CAMERA; -import static android.service.SensorPrivacyIndividualEnabledSensorProto.MICROPHONE; +import static android.hardware.SensorPrivacyManager.INDIVIDUAL_SENSOR_CAMERA; +import static android.hardware.SensorPrivacyManager.INDIVIDUAL_SENSOR_MICROPHONE; import android.hardware.SensorPrivacyManager; import android.hardware.SensorPrivacyManager.IndividualSensor; @@ -30,7 +30,8 @@ import java.util.Set; public class IndividualSensorPrivacyControllerImpl implements IndividualSensorPrivacyController { - private static final int[] SENSORS = new int[] {CAMERA, MICROPHONE}; + private static final int[] SENSORS = new int[] {INDIVIDUAL_SENSOR_CAMERA, + INDIVIDUAL_SENSOR_MICROPHONE}; private final @NonNull SensorPrivacyManager mSensorPrivacyManager; private final SparseBooleanArray mState = new SparseBooleanArray(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java index 02143a750caec..bc322f7f18fcc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/appops/AppOpsControllerTest.java @@ -16,6 +16,9 @@ package com.android.systemui.appops; +import static android.hardware.SensorPrivacyManager.INDIVIDUAL_SENSOR_CAMERA; +import static android.hardware.SensorPrivacyManager.INDIVIDUAL_SENSOR_MICROPHONE; + import static junit.framework.TestCase.assertFalse; import static org.junit.Assert.assertEquals; @@ -49,6 +52,7 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; +import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController; import org.junit.Before; import org.junit.Test; @@ -81,6 +85,8 @@ public class AppOpsControllerTest extends SysuiTestCase { private PermissionFlagsCache mFlagsCache; @Mock private PackageManager mPackageManager; + @Mock + private IndividualSensorPrivacyController mSensorPrivacyController; @Mock(stubOnly = true) private AudioManager mAudioManager; @Mock() @@ -118,12 +124,18 @@ public class AppOpsControllerTest extends SysuiTestCase { when(mAudioManager.getActiveRecordingConfigurations()) .thenReturn(List.of(mPausedMockRecording)); + when(mSensorPrivacyController.isSensorBlocked(INDIVIDUAL_SENSOR_CAMERA)) + .thenReturn(false); + when(mSensorPrivacyController.isSensorBlocked(INDIVIDUAL_SENSOR_CAMERA)) + .thenReturn(false); + mController = new AppOpsControllerImpl( mContext, mTestableLooper.getLooper(), mDumpManager, mFlagsCache, mAudioManager, + mSensorPrivacyController, mDispatcher ); } @@ -133,6 +145,7 @@ public class AppOpsControllerTest extends SysuiTestCase { mController.setListening(true); verify(mAppOpsManager, times(1)).startWatchingActive(AppOpsControllerImpl.OPS, mController); verify(mDispatcher, times(1)).registerReceiverWithHandler(eq(mController), any(), any()); + verify(mSensorPrivacyController, times(1)).addCallback(mController); } @Test @@ -140,6 +153,7 @@ public class AppOpsControllerTest extends SysuiTestCase { mController.setListening(false); verify(mAppOpsManager, times(1)).stopWatchingActive(mController); verify(mDispatcher, times(1)).unregisterReceiver(mController); + verify(mSensorPrivacyController, times(1)).removeCallback(mController); } @Test @@ -476,6 +490,71 @@ public class AppOpsControllerTest extends SysuiTestCase { AppOpsManager.OP_RECORD_AUDIO, TEST_UID_OTHER, TEST_PACKAGE_NAME, false); } + @Test + public void testAudioFilteredWhenMicDisabled() { + mController.addCallback(new int[]{AppOpsManager.OP_RECORD_AUDIO, AppOpsManager.OP_CAMERA}, + mCallback); + mTestableLooper.processAllMessages(); + mController.onOpActiveChanged( + AppOpsManager.OP_RECORD_AUDIO, TEST_UID_OTHER, TEST_PACKAGE_NAME, true); + mTestableLooper.processAllMessages(); + List list = mController.getActiveAppOps(); + assertEquals(1, list.size()); + assertEquals(AppOpsManager.OP_RECORD_AUDIO, list.get(0).getCode()); + assertFalse(list.get(0).isDisabled()); + + // Add a camera op, and disable the microphone. The camera op should be the only op returned + mController.onSensorBlockedChanged(INDIVIDUAL_SENSOR_MICROPHONE, true); + mController.onOpActiveChanged( + AppOpsManager.OP_CAMERA, TEST_UID_OTHER, TEST_PACKAGE_NAME, true); + mTestableLooper.processAllMessages(); + list = mController.getActiveAppOps(); + assertEquals(1, list.size()); + assertEquals(AppOpsManager.OP_CAMERA, list.get(0).getCode()); + + + // Re enable the microphone, and verify the op returns + mController.onSensorBlockedChanged(INDIVIDUAL_SENSOR_MICROPHONE, false); + mTestableLooper.processAllMessages(); + + list = mController.getActiveAppOps(); + assertEquals(2, list.size()); + int micIdx = list.get(0).getCode() == AppOpsManager.OP_CAMERA ? 1 : 0; + assertEquals(AppOpsManager.OP_RECORD_AUDIO, list.get(micIdx).getCode()); + } + + @Test + public void testCameraFilteredWhenCameraDisabled() { + mController.addCallback(new int[]{AppOpsManager.OP_RECORD_AUDIO, AppOpsManager.OP_CAMERA}, + mCallback); + mTestableLooper.processAllMessages(); + mController.onOpActiveChanged( + AppOpsManager.OP_CAMERA, TEST_UID_OTHER, TEST_PACKAGE_NAME, true); + mTestableLooper.processAllMessages(); + List list = mController.getActiveAppOps(); + assertEquals(1, list.size()); + assertEquals(AppOpsManager.OP_CAMERA, list.get(0).getCode()); + assertFalse(list.get(0).isDisabled()); + + // Add an audio op, and disable the camera. The audio op should be the only op returned + mController.onSensorBlockedChanged(INDIVIDUAL_SENSOR_CAMERA, true); + mController.onOpActiveChanged( + AppOpsManager.OP_RECORD_AUDIO, TEST_UID_OTHER, TEST_PACKAGE_NAME, true); + mTestableLooper.processAllMessages(); + list = mController.getActiveAppOps(); + assertEquals(1, list.size()); + assertEquals(AppOpsManager.OP_RECORD_AUDIO, list.get(0).getCode()); + + // Re enable the camera, and verify the op returns + mController.onSensorBlockedChanged(INDIVIDUAL_SENSOR_CAMERA, false); + mTestableLooper.processAllMessages(); + + list = mController.getActiveAppOps(); + assertEquals(2, list.size()); + int cameraIdx = list.get(0).getCode() == AppOpsManager.OP_CAMERA ? 0 : 1; + assertEquals(AppOpsManager.OP_CAMERA, list.get(cameraIdx).getCode()); + } + private class TestHandler extends AppOpsControllerImpl.H { TestHandler(Looper looper) { mController.super(looper);