AudioService: test mic muting API behavior with errors
Implement unit test for AudioService's mic muting API.
This requires being able to inject existing
AudioSystemAdapter in AudioService class, and creating an adapter
for operations that require running in system_server.
Bug: 153103117
Test: atest AudioServiceTest
Change-Id: I92538f585c5567718a4bad5666b2efafd446f4d3
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index c7fac44..1a84291 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -186,6 +186,9 @@
private static final String TAG = "AS.AudioService";
+ private final AudioSystemAdapter mAudioSystem;
+ private final SystemServerAdapter mSystemServer;
+
/** Debug audio mode */
protected static final boolean DEBUG_MODE = false;
@@ -650,10 +653,19 @@
/** @hide */
public AudioService(Context context) {
+ this(context, AudioSystemAdapter.getDefaultAdapter(),
+ SystemServerAdapter.getDefaultAdapter(context));
+ }
+
+ public AudioService(Context context, AudioSystemAdapter audioSystem,
+ SystemServerAdapter systemServer) {
mContext = context;
mContentResolver = context.getContentResolver();
mAppOps = (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE);
+ mAudioSystem = audioSystem;
+ mSystemServer = systemServer;
+
mPlatformType = AudioSystem.getPlatformType(context);
mIsSingleVolume = AudioSystem.isSingleVolume(context);
@@ -843,11 +855,13 @@
context.registerReceiverAsUser(mReceiver, UserHandle.ALL, intentFilter, null, null);
- LocalServices.addService(AudioManagerInternal.class, new AudioServiceInternal());
+ if (mSystemServer.isPrivileged()) {
+ LocalServices.addService(AudioManagerInternal.class, new AudioServiceInternal());
- mUserManagerInternal.addUserRestrictionsListener(mUserRestrictionsListener);
+ mUserManagerInternal.addUserRestrictionsListener(mUserRestrictionsListener);
- mRecordMonitor.initMonitor();
+ mRecordMonitor.initMonitor();
+ }
final float[] preScale = new float[3];
preScale[0] = mContext.getResources().getFraction(
@@ -936,7 +950,7 @@
onIndicateSystemReady();
- mMicMuteFromSystemCached = AudioSystem.isMicrophoneMuted();
+ mMicMuteFromSystemCached = mAudioSystem.isMicrophoneMuted();
setMicMuteFromSwitchInput();
}
@@ -1637,12 +1651,15 @@
}
if (currentImeUid != mCurrentImeUid || forceUpdate) {
- AudioSystem.setCurrentImeUid(currentImeUid);
+ mAudioSystem.setCurrentImeUid(currentImeUid);
mCurrentImeUid = currentImeUid;
}
}
private void readPersistedSettings() {
+ if (!mSystemServer.isPrivileged()) {
+ return;
+ }
final ContentResolver cr = mContentResolver;
int ringerModeFromSettings =
@@ -1713,6 +1730,9 @@
}
private void readUserRestrictions() {
+ if (!mSystemServer.isPrivileged()) {
+ return;
+ }
final int currentUser = getCurrentUserId();
// Check the current user restriction.
@@ -2817,6 +2837,9 @@
}
private void sendBroadcastToAll(Intent intent) {
+ if (!mSystemServer.isPrivileged()) {
+ return;
+ }
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
final long ident = Binder.clearCallingIdentity();
@@ -3246,9 +3269,9 @@
}
// only mute for the current user
if (getCurrentUserId() == userId || userId == android.os.Process.SYSTEM_UID) {
- final boolean currentMute = AudioSystem.isMicrophoneMuted();
+ final boolean currentMute = mAudioSystem.isMicrophoneMuted();
final long identity = Binder.clearCallingIdentity();
- final int ret = AudioSystem.muteMicrophone(muted);
+ final int ret = mAudioSystem.muteMicrophone(muted);
new MediaMetrics.Item(mAnalyticsId + "setMicrophoneMuteNoCallerCheck")
.setUid(userId)
@@ -3258,7 +3281,7 @@
.record();
// update cache with the real state independently from what was set
- mMicMuteFromSystemCached = AudioSystem.isMicrophoneMuted();
+ mMicMuteFromSystemCached = mAudioSystem.isMicrophoneMuted();
if (ret != AudioSystem.AUDIO_STATUS_OK) {
Log.e(TAG, "Error changing mic mute state to " + muted + " current:"
+ mMicMuteFromSystemCached);
@@ -4670,6 +4693,9 @@
}
private void broadcastRingerMode(String action, int ringerMode) {
+ if (!mSystemServer.isPrivileged()) {
+ return;
+ }
// Send sticky broadcast
Intent broadcast = new Intent(action);
broadcast.putExtra(AudioManager.EXTRA_RINGER_MODE, ringerMode);
@@ -4679,6 +4705,9 @@
}
private void broadcastVibrateSetting(int vibrateType) {
+ if (!mSystemServer.isPrivileged()) {
+ return;
+ }
// Send broadcast
if (mActivityManagerInternal.isSystemReady()) {
Intent broadcast = new Intent(AudioManager.VIBRATE_SETTING_CHANGED_ACTION);
@@ -5417,6 +5446,9 @@
}
public int observeDevicesForStream_syncVSS(boolean checkOthers) {
+ if (!mSystemServer.isPrivileged()) {
+ return AudioSystem.DEVICE_NONE;
+ }
final int devices = AudioSystem.getDevicesForStream(mStreamType);
if (devices == mObservedDevices) {
return devices;
@@ -6157,10 +6189,7 @@
break;
case MSG_BROADCAST_MICROPHONE_MUTE:
- mContext.sendBroadcastAsUser(
- new Intent(AudioManager.ACTION_MICROPHONE_MUTE_CHANGED)
- .setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY),
- UserHandle.ALL);
+ mSystemServer.sendMicrophoneMuteChangedIntent();
break;
}
}
diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
index 9f8f9f8..40c1390 100644
--- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java
+++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
@@ -40,10 +40,11 @@
/**
* Create an adapter for AudioSystem that always succeeds, and does nothing.
- * @return a no-op AudioSystem adapter
+ * Overridden methods can be configured
+ * @return a no-op AudioSystem adapter with configurable adapter
*/
- static final @NonNull AudioSystemAdapter getAlwaysOkAdapter() {
- return new AudioSystemOkAdapter();
+ static final @NonNull AudioSystemAdapter getConfigurableAdapter() {
+ return new AudioSystemConfigurableAdapter();
}
/**
@@ -113,10 +114,51 @@
return AudioSystem.setParameters(keyValuePairs);
}
- //--------------------------------------------------------------------
- protected static class AudioSystemOkAdapter extends AudioSystemAdapter {
- private static final String TAG = "ASA";
+ /**
+ * Same as {@link AudioSystem#isMicrophoneMuted()}}
+ * Checks whether the microphone mute is on or off.
+ * @return true if microphone is muted, false if it's not
+ */
+ public boolean isMicrophoneMuted() {
+ return AudioSystem.isMicrophoneMuted();
+ }
+ /**
+ * Same as {@link AudioSystem#muteMicrophone(boolean)}
+ * Sets the microphone mute on or off.
+ *
+ * @param on set <var>true</var> to mute the microphone;
+ * <var>false</var> to turn mute off
+ * @return command completion status see AUDIO_STATUS_OK, see AUDIO_STATUS_ERROR
+ */
+ public int muteMicrophone(boolean on) {
+ return AudioSystem.muteMicrophone(on);
+ }
+
+ /**
+ * Same as {@link AudioSystem#setCurrentImeUid(int)}
+ * Communicate UID of current InputMethodService to audio policy service.
+ */
+ public int setCurrentImeUid(int uid) {
+ return AudioSystem.setCurrentImeUid(uid);
+ }
+
+ //--------------------------------------------------------------------
+ protected static class AudioSystemConfigurableAdapter extends AudioSystemAdapter {
+ private static final String TAG = "ASA";
+ private boolean mIsMicMuted = false;
+ private boolean mMuteMicrophoneFails = false;
+
+ public void configureIsMicrophoneMuted(boolean muted) {
+ mIsMicMuted = muted;
+ }
+
+ public void configureMuteMicrophoneToFail(boolean fail) {
+ mMuteMicrophoneFails = fail;
+ }
+
+ //-----------------------------------------------------------------
+ // Overrides of AudioSystemAdapter
@Override
public int setDeviceConnectionState(int device, int state, String deviceAddress,
String deviceName, int codecFormat) {
@@ -152,5 +194,24 @@
public int setParameters(String keyValuePairs) {
return AudioSystem.AUDIO_STATUS_OK;
}
+
+ @Override
+ public boolean isMicrophoneMuted() {
+ return mIsMicMuted;
+ }
+
+ @Override
+ public int muteMicrophone(boolean on) {
+ if (mMuteMicrophoneFails) {
+ return AudioSystem.AUDIO_STATUS_ERROR;
+ }
+ mIsMicMuted = on;
+ return AudioSystem.AUDIO_STATUS_OK;
+ }
+
+ @Override
+ public int setCurrentImeUid(int uid) {
+ return AudioSystem.AUDIO_STATUS_OK;
+ }
}
}
diff --git a/services/core/java/com/android/server/audio/SystemServerAdapter.java b/services/core/java/com/android/server/audio/SystemServerAdapter.java
new file mode 100644
index 0000000..509f6be
--- /dev/null
+++ b/services/core/java/com/android/server/audio/SystemServerAdapter.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 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.audio;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.os.UserHandle;
+
+/**
+ * Provides an adapter to access functionality reserved to components running in system_server
+ * Functionality such as sending privileged broadcasts is to be accessed through the default
+ * adapter, whereas tests can inject a no-op adapter.
+ */
+public class SystemServerAdapter {
+
+ protected final Context mContext;
+
+ private SystemServerAdapter(@Nullable Context context) {
+ mContext = context;
+ }
+ /**
+ * Create a wrapper around privileged functionality.
+ * @return the adapter
+ */
+ static final @NonNull SystemServerAdapter getDefaultAdapter(Context context) {
+ return new SystemServerAdapter(context);
+ }
+
+ /**
+ * Create an adapter that does nothing.
+ * Use for running non-privileged tests, such as unit tests
+ * @return a no-op adapter
+ */
+ static final @NonNull SystemServerAdapter getNoOpAdapter() {
+ return new NoOpSystemServerAdapter();
+ }
+
+ /**
+ * @return true if this is supposed to be run in system_server, false otherwise (e.g. for a
+ * unit test)
+ */
+ public boolean isPrivileged() {
+ return true;
+ }
+
+ /**
+ * Broadcast ACTION_MICROPHONE_MUTE_CHANGED
+ */
+ public void sendMicrophoneMuteChangedIntent() {
+ mContext.sendBroadcastAsUser(
+ new Intent(AudioManager.ACTION_MICROPHONE_MUTE_CHANGED)
+ .setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY),
+ UserHandle.ALL);
+ }
+
+ //--------------------------------------------------------------------
+ protected static class NoOpSystemServerAdapter extends SystemServerAdapter {
+
+ NoOpSystemServerAdapter() {
+ super(null);
+ }
+
+ @Override
+ public boolean isPrivileged() {
+ return false;
+ }
+
+ @Override
+ public void sendMicrophoneMuteChangedIntent() {
+ // no-op
+ }
+ }
+}
diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml
index ef9224f..8d182cb 100644
--- a/services/tests/servicestests/AndroidManifest.xml
+++ b/services/tests/servicestests/AndroidManifest.xml
@@ -78,6 +78,7 @@
<uses-permission android:name="android.permission.DUMP"/>
<uses-permission android:name="android.permission.READ_DREAM_STATE"/>
<uses-permission android:name="android.permission.WRITE_DREAM_STATE"/>
+ <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<!-- Uses API introduced in O (26) -->
<uses-sdk android:minSdkVersion="1"
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
index 73a191d..22f8b9c 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceBrokerTest.java
@@ -66,7 +66,7 @@
mContext = InstrumentationRegistry.getTargetContext();
mMockAudioService = mock(AudioService.class);
- mSpyAudioSystem = spy(AudioSystemAdapter.getAlwaysOkAdapter());
+ mSpyAudioSystem = spy(AudioSystemAdapter.getConfigurableAdapter());
mSpyDevInventory = spy(new AudioDeviceInventory(mSpyAudioSystem));
mAudioDeviceBroker = new AudioDeviceBroker(mContext, mMockAudioService, mSpyDevInventory);
mSpyDevInventory.setDeviceBroker(mAudioDeviceBroker);
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
new file mode 100644
index 0000000..6185ae6
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 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.audio;
+
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.MediumTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Spy;
+
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class AudioServiceTest {
+ private static final String TAG = "AudioServiceTest";
+
+ private static final int MAX_MESSAGE_HANDLING_DELAY_MS = 100;
+
+ private Context mContext;
+ private AudioSystemAdapter mAudioSystem;
+ @Spy private SystemServerAdapter mSpySystemServer;
+ // the class being unit-tested here
+ private AudioService mAudioService;
+
+ private static boolean sLooperPrepared = false;
+
+ @Before
+ public void setUp() throws Exception {
+ if (!sLooperPrepared) {
+ Looper.prepare();
+ sLooperPrepared = true;
+ }
+ mContext = InstrumentationRegistry.getTargetContext();
+ mAudioSystem = AudioSystemAdapter.getConfigurableAdapter();
+ mSpySystemServer = spy(SystemServerAdapter.getNoOpAdapter());
+ mAudioService = new AudioService(mContext, mAudioSystem, mSpySystemServer);
+ }
+
+ /**
+ * Test muting the mic reports the expected value, and the corresponding intent was fired
+ * @throws Exception
+ */
+ @Test
+ public void testMuteMicrophone() throws Exception {
+ Log.i(TAG, "running testMuteMicrophone");
+ Assert.assertNotNull(mAudioService);
+ final AudioSystemAdapter.AudioSystemConfigurableAdapter testAudioSystem =
+ (AudioSystemAdapter.AudioSystemConfigurableAdapter) mAudioSystem;
+ testAudioSystem.configureMuteMicrophoneToFail(false);
+ for (boolean muted : new boolean[] { true, false}) {
+ testAudioSystem.configureIsMicrophoneMuted(!muted);
+ mAudioService.setMicrophoneMute(muted, mContext.getOpPackageName(),
+ UserHandle.getCallingUserId());
+ Assert.assertEquals("mic mute reporting wrong value",
+ muted, mAudioService.isMicrophoneMuted());
+ // verify the intent for mic mute changed is supposed to be fired
+ Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS);
+ verify(mSpySystemServer, times(1))
+ .sendMicrophoneMuteChangedIntent();
+ reset(mSpySystemServer);
+ }
+ }
+
+ /**
+ * Test muting the mic with simulated failure reports the expected value, and the corresponding
+ * intent was fired
+ * @throws Exception
+ */
+ @Test
+ public void testMuteMicrophoneWhenFail() throws Exception {
+ Log.i(TAG, "running testMuteMicrophoneWhenFail");
+ Assert.assertNotNull(mAudioService);
+ final AudioSystemAdapter.AudioSystemConfigurableAdapter testAudioSystem =
+ (AudioSystemAdapter.AudioSystemConfigurableAdapter) mAudioSystem;
+ testAudioSystem.configureMuteMicrophoneToFail(true);
+ for (boolean muted : new boolean[] { true, false}) {
+ testAudioSystem.configureIsMicrophoneMuted(!muted);
+ mAudioService.setMicrophoneMute(muted, mContext.getOpPackageName(),
+ UserHandle.getCallingUserId());
+ Assert.assertEquals("mic mute reporting wrong value",
+ !muted, mAudioService.isMicrophoneMuted());
+ // verify the intent for mic mute changed is supposed to be fired
+ Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS);
+ verify(mSpySystemServer, times(1))
+ .sendMicrophoneMuteChangedIntent();
+ reset(mSpySystemServer);
+ }
+ }
+}