Add query API for hotword flag support
- Add API to query for device support for hotword record flags.
- Call through AudioService to enforce permissions.
- Clean up dependency injection, mocking in AudioService.
- Directly call APM from AudioService using AIDL, instead of JNI.
Test: atest AudioManagerTest, atest AudioRecordTest
Test: AudioService unit tests
Bug: 237449755
Change-Id: I42178b3895e4eb76dcabfc7b0259fc545223dcb6
diff --git a/Android.bp b/Android.bp
index 2740ccc..d031284 100644
--- a/Android.bp
+++ b/Android.bp
@@ -404,7 +404,7 @@
"modules-utils-uieventlogger-interface",
"framework-permission-aidl-java",
"spatializer-aidl-java",
- "audiopolicy-types-aidl-java",
+ "audiopolicy-aidl-java",
"sounddose-aidl-java",
],
}
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 1317292..85007de 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -6596,6 +6596,7 @@
method public boolean isAudioServerRunning();
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean isBluetoothVariableLatencyEnabled();
method public boolean isHdmiSystemAudioSupported();
+ method @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD) public boolean isHotwordStreamSupported(boolean);
method @RequiresPermission(android.Manifest.permission.CALL_AUDIO_INTERCEPTION) public boolean isPstnCallAudioInterceptable();
method @RequiresPermission(android.Manifest.permission.ACCESS_ULTRASOUND) public boolean isUltrasoundSupported();
method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void muteAwaitConnection(@NonNull int[], @NonNull android.media.AudioDeviceAttributes, long, @NonNull java.util.concurrent.TimeUnit) throws java.lang.IllegalStateException;
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index fdd6233..e94f517 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -7440,6 +7440,27 @@
/**
* @hide
+ * Indicates whether the platform supports capturing content from the hotword recognition
+ * pipeline. To capture content of this type, create an AudioRecord with
+ * {@link AudioRecord.Builder.setRequestHotwordStream(boolean, boolean)}.
+ * @param lookbackAudio Query if the hotword stream additionally supports providing buffered
+ * audio prior to stream open.
+ * @return True if the platform supports capturing hotword content, and if lookbackAudio
+ * is true, if it additionally supports capturing buffered hotword content prior to stream
+ * open. False otherwise.
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD)
+ public boolean isHotwordStreamSupported(boolean lookbackAudio) {
+ try {
+ return getService().isHotwordStreamSupported(lookbackAudio);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
+ /**
+ * @hide
* Introspection API to retrieve audio product strategies.
* When implementing {Car|Oem}AudioManager, use this method to retrieve the collection of
* audio product strategies, which is indexed by a weakly typed index in order to be extended
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index 0f63cc4..ecea50c 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -164,6 +164,9 @@
@EnforcePermission("ACCESS_ULTRASOUND")
boolean isUltrasoundSupported();
+ @EnforcePermission("CAPTURE_AUDIO_HOTWORD")
+ boolean isHotwordStreamSupported(boolean lookbackAudio);
+
void setMicrophoneMute(boolean on, String callingPackage, int userId, in String attributionTag);
oneway void setMicrophoneMuteFromSwitch(boolean on);
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 5c00452..4d53b48 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -173,7 +173,6 @@
"android.hardware.power.stats-V1-java",
"android.hardware.power-V4-java",
"android.hidl.manager-V1.2-java",
- "capture_state_listener-aidl-java",
"icu4j_calendar_astronomer",
"netd-client",
"overlayable_policy_aidl-java",
diff --git a/services/core/java/com/android/server/audio/AudioPolicyFacade.java b/services/core/java/com/android/server/audio/AudioPolicyFacade.java
new file mode 100644
index 0000000..02e80d6
--- /dev/null
+++ b/services/core/java/com/android/server/audio/AudioPolicyFacade.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2022 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;
+
+
+/**
+ * Facade to IAudioPolicyService which fulfills AudioService dependencies.
+ * See @link{IAudioPolicyService.aidl}
+ */
+public interface AudioPolicyFacade {
+
+ public boolean isHotwordStreamSupported(boolean lookbackAudio);
+}
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index b505396..e650875 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -145,6 +145,7 @@
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
+import android.os.PermissionEnforcer;
import android.os.PersistableBundle;
import android.os.PowerManager;
import android.os.Process;
@@ -248,6 +249,7 @@
private final AudioSystemAdapter mAudioSystem;
private final SystemServerAdapter mSystemServer;
private final SettingsAdapter mSettings;
+ private final AudioPolicyFacade mAudioPolicy;
/** Debug audio mode */
protected static final boolean DEBUG_MODE = false;
@@ -922,7 +924,13 @@
public Lifecycle(Context context) {
super(context);
- mService = new AudioService(context);
+ mService = new AudioService(context,
+ AudioSystemAdapter.getDefaultAdapter(),
+ SystemServerAdapter.getDefaultAdapter(context),
+ SettingsAdapter.getDefaultAdapter(),
+ new DefaultAudioPolicyFacade(),
+ null);
+
}
@Override
@@ -975,14 +983,6 @@
// Construction
///////////////////////////////////////////////////////////////////////////
- /** @hide */
- public AudioService(Context context) {
- this(context,
- AudioSystemAdapter.getDefaultAdapter(),
- SystemServerAdapter.getDefaultAdapter(context),
- SettingsAdapter.getDefaultAdapter(),
- null);
- }
/**
* @param context
@@ -993,9 +993,11 @@
* {@link AudioSystemThread} is created as the messaging thread instead.
*/
public AudioService(Context context, AudioSystemAdapter audioSystem,
- SystemServerAdapter systemServer, SettingsAdapter settings, @Nullable Looper looper) {
- this (context, audioSystem, systemServer, settings, looper,
- context.getSystemService(AppOpsManager.class));
+ SystemServerAdapter systemServer, SettingsAdapter settings,
+ AudioPolicyFacade audioPolicy, @Nullable Looper looper) {
+ this (context, audioSystem, systemServer, settings, audioPolicy, looper,
+ context.getSystemService(AppOpsManager.class),
+ PermissionEnforcer.fromContext(context));
}
/**
@@ -1008,8 +1010,10 @@
*/
@RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
public AudioService(Context context, AudioSystemAdapter audioSystem,
- SystemServerAdapter systemServer, SettingsAdapter settings, @Nullable Looper looper,
- AppOpsManager appOps) {
+ SystemServerAdapter systemServer, SettingsAdapter settings,
+ AudioPolicyFacade audioPolicy, @Nullable Looper looper, AppOpsManager appOps,
+ @NonNull PermissionEnforcer enforcer) {
+ super(enforcer);
sLifecycleLogger.enqueue(new EventLogger.StringEvent("AudioService()"));
mContext = context;
mContentResolver = context.getContentResolver();
@@ -1018,7 +1022,7 @@
mAudioSystem = audioSystem;
mSystemServer = systemServer;
mSettings = settings;
-
+ mAudioPolicy = audioPolicy;
mPlatformType = AudioSystem.getPlatformType(context);
mIsSingleVolume = AudioSystem.isSingleVolume(context);
@@ -3854,6 +3858,20 @@
return AudioSystem.isUltrasoundSupported();
}
+ /** @see AudioManager#isHotwordStreamSupported() */
+ @android.annotation.EnforcePermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD)
+ public boolean isHotwordStreamSupported(boolean lookbackAudio) {
+ super.isHotwordStreamSupported_enforcePermission();
+ try {
+ return mAudioPolicy.isHotwordStreamSupported(lookbackAudio);
+ } catch (IllegalStateException e) {
+ // Suppress connection failure to APM, since the method is purely informative
+ Log.e(TAG, "Suppressing exception calling into AudioPolicy", e);
+ return false;
+ }
+ }
+
+
private boolean canChangeAccessibilityVolume() {
synchronized (mAccessibilityServiceUidsLock) {
if (PackageManager.PERMISSION_GRANTED == mContext.checkCallingOrSelfPermission(
diff --git a/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java b/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java
new file mode 100644
index 0000000..37b8126
--- /dev/null
+++ b/services/core/java/com/android/server/audio/DefaultAudioPolicyFacade.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2022 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.media.IAudioPolicyService;
+import android.media.permission.ClearCallingIdentityContext;
+import android.media.permission.SafeCloseable;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+/**
+ * Default implementation of a facade to IAudioPolicyManager which fulfills AudioService
+ * dependencies. This forwards calls as-is to IAudioPolicyManager.
+ * Public methods throw IllegalStateException if AudioPolicy is not initialized/available
+ */
+public class DefaultAudioPolicyFacade implements AudioPolicyFacade, IBinder.DeathRecipient {
+
+ private static final String TAG = "DefaultAudioPolicyFacade";
+ private static final String AUDIO_POLICY_SERVICE_NAME = "media.audio_policy";
+
+ private final Object mServiceLock = new Object();
+ @GuardedBy("mServiceLock")
+ private IAudioPolicyService mAudioPolicy;
+
+ public DefaultAudioPolicyFacade() {
+ try {
+ getAudioPolicyOrInit();
+ } catch (IllegalStateException e) {
+ // Log and suppress this exception, we may be able to connect later
+ Log.e(TAG, "Failed to initialize APM connection", e);
+ }
+ }
+
+ @Override
+ public boolean isHotwordStreamSupported(boolean lookbackAudio) {
+ IAudioPolicyService ap = getAudioPolicyOrInit();
+ try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+ return ap.isHotwordStreamSupported(lookbackAudio);
+ } catch (RemoteException e) {
+ resetServiceConnection(ap.asBinder());
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @Override
+ public void binderDied() {
+ Log.wtf(TAG, "Unexpected binderDied without IBinder object");
+ }
+
+ @Override
+ public void binderDied(@NonNull IBinder who) {
+ resetServiceConnection(who);
+ }
+
+ private void resetServiceConnection(@Nullable IBinder deadAudioPolicy) {
+ synchronized (mServiceLock) {
+ if (mAudioPolicy != null && mAudioPolicy.asBinder().equals(deadAudioPolicy)) {
+ mAudioPolicy.asBinder().unlinkToDeath(this, 0);
+ mAudioPolicy = null;
+ }
+ }
+ }
+
+ private @Nullable IAudioPolicyService getAudioPolicy() {
+ synchronized (mServiceLock) {
+ return mAudioPolicy;
+ }
+ }
+
+ /*
+ * Does not block.
+ * @throws IllegalStateException for any failed connection
+ */
+ private @NonNull IAudioPolicyService getAudioPolicyOrInit() {
+ synchronized (mServiceLock) {
+ if (mAudioPolicy != null) {
+ return mAudioPolicy;
+ }
+ // Do not block while attempting to connect to APM. Defer to caller.
+ IAudioPolicyService ap = IAudioPolicyService.Stub.asInterface(
+ ServiceManager.checkService(AUDIO_POLICY_SERVICE_NAME));
+ if (ap == null) {
+ throw new IllegalStateException(TAG + ": Unable to connect to AudioPolicy");
+ }
+ try {
+ ap.asBinder().linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ throw new IllegalStateException(
+ TAG + ": Unable to link deathListener to AudioPolicy", e);
+ }
+ mAudioPolicy = ap;
+ return mAudioPolicy;
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java
index ad2e7e4..38093de 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AbsoluteVolumeBehaviorTest.java
@@ -64,6 +64,8 @@
private IAudioDeviceVolumeDispatcher.Stub mMockDispatcher =
mock(IAudioDeviceVolumeDispatcher.Stub.class);
+ private AudioPolicyFacade mMockAudioPolicy = mock(AudioPolicyFacade.class);
+
@Before
public void setUp() throws Exception {
mTestLooper = new TestLooper();
@@ -74,7 +76,7 @@
mSystemServer = new NoOpSystemServerAdapter();
mSettingsAdapter = new NoOpSettingsAdapter();
mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer,
- mSettingsAdapter, mTestLooper.getLooper()) {
+ mSettingsAdapter, mMockAudioPolicy, mTestLooper.getLooper()) {
@Override
public int getDeviceForStream(int stream) {
return AudioSystem.DEVICE_OUT_SPEAKER;
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
index 7f54b63..4e9ac7c 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioDeviceVolumeManagerTest.java
@@ -17,6 +17,7 @@
package com.android.server.audio;
import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
@@ -45,6 +46,7 @@
private SystemServerAdapter mSystemServer;
private SettingsAdapter mSettingsAdapter;
private TestLooper mTestLooper;
+ private AudioPolicyFacade mAudioPolicyMock = mock(AudioPolicyFacade.class);
private AudioService mAudioService;
@@ -59,7 +61,7 @@
mSystemServer = new NoOpSystemServerAdapter();
mSettingsAdapter = new NoOpSettingsAdapter();
mAudioService = new AudioService(mContext, mSpyAudioSystem, mSystemServer,
- mSettingsAdapter, mTestLooper.getLooper()) {
+ mSettingsAdapter, mAudioPolicyMock, mTestLooper.getLooper()) {
@Override
public int getDeviceForStream(int stream) {
return AudioSystem.DEVICE_OUT_SPEAKER;
diff --git a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
index adcbe6b..88d57ac 100644
--- a/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/AudioServiceTest.java
@@ -15,6 +15,7 @@
*/
package com.android.server.audio;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
@@ -28,6 +29,7 @@
import android.content.Context;
import android.media.AudioSystem;
import android.os.Looper;
+import android.os.PermissionEnforcer;
import android.os.UserHandle;
import android.util.Log;
@@ -37,8 +39,11 @@
import org.junit.Assert;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
import org.mockito.Mock;
import org.mockito.Spy;
@@ -49,11 +54,18 @@
private static final int MAX_MESSAGE_HANDLING_DELAY_MS = 100;
+ @Rule
+ public final MockitoRule mockito = MockitoJUnit.rule();
+
private Context mContext;
private AudioSystemAdapter mAudioSystem;
- @Spy private SystemServerAdapter mSpySystemServer;
private SettingsAdapter mSettingsAdapter;
+
+ @Spy private NoOpSystemServerAdapter mSpySystemServer;
@Mock private AppOpsManager mMockAppOpsManager;
+ @Mock private AudioPolicyFacade mMockAudioPolicy;
+ @Mock private PermissionEnforcer mMockPermissionEnforcer;
+
// the class being unit-tested here
private AudioService mAudioService;
@@ -67,13 +79,12 @@
}
mContext = InstrumentationRegistry.getTargetContext();
mAudioSystem = new NoOpAudioSystemAdapter();
- mSpySystemServer = spy(new NoOpSystemServerAdapter());
mSettingsAdapter = new NoOpSettingsAdapter();
- mMockAppOpsManager = mock(AppOpsManager.class);
when(mMockAppOpsManager.noteOp(anyInt(), anyInt(), anyString(), anyString(), anyString()))
.thenReturn(AppOpsManager.MODE_ALLOWED);
mAudioService = new AudioService(mContext, mAudioSystem, mSpySystemServer,
- mSettingsAdapter, null, mMockAppOpsManager);
+ mSettingsAdapter, mMockAudioPolicy, null, mMockAppOpsManager,
+ mMockPermissionEnforcer);
}
/**
@@ -153,4 +164,15 @@
Assert.assertEquals(ringMaxVol, mAudioService.getStreamVolume(
AudioSystem.STREAM_NOTIFICATION));
}
+
+ @Test
+ public void testAudioPolicyException() throws Exception {
+ Log.i(TAG, "running testAudioPolicyException");
+ Assert.assertNotNull(mAudioService);
+ // Ensure that AudioPolicy inavailability doesn't bring down SystemServer
+ when(mMockAudioPolicy.isHotwordStreamSupported(anyBoolean())).thenThrow(
+ new IllegalStateException(), new IllegalStateException());
+ Assert.assertEquals(false, mAudioService.isHotwordStreamSupported(false));
+ Assert.assertEquals(false, mAudioService.isHotwordStreamSupported(true));
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java
index d89c6d5..77a6286 100644
--- a/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/DeviceVolumeBehaviorTest.java
@@ -18,6 +18,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import static org.mockito.Mockito.mock;
import android.annotation.NonNull;
import android.content.Context;
@@ -47,6 +48,7 @@
private SystemServerAdapter mSystemServer;
private SettingsAdapter mSettingsAdapter;
private TestLooper mTestLooper;
+ private AudioPolicyFacade mAudioPolicyMock = mock(AudioPolicyFacade.class);
private AudioService mAudioService;
@@ -67,7 +69,7 @@
mSystemServer = new NoOpSystemServerAdapter();
mSettingsAdapter = new NoOpSettingsAdapter();
mAudioService = new AudioService(mContext, mAudioSystem, mSystemServer,
- mSettingsAdapter, mTestLooper.getLooper());
+ mSettingsAdapter, mAudioPolicyMock, mTestLooper.getLooper());
mTestLooper.dispatchAll();
}