diff options
12 files changed, 236 insertions, 24 deletions
diff --git a/Android.bp b/Android.bp index 2740ccc9a803..d03128418eb4 100644 --- a/Android.bp +++ b/Android.bp @@ -404,7 +404,7 @@ java_defaults { "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 1317292fcd3b..85007de478ba 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -6596,6 +6596,7 @@ package android.media { 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 fdd623300cdb..e94f5175206b 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -7440,6 +7440,27 @@ public class AudioManager { /** * @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 0f63cc40ca36..ecea50cb7365 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -164,6 +164,9 @@ interface IAudioService { @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 5c00452396dc..4d53b487bf0a 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -173,7 +173,6 @@ java_library_static { "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 000000000000..02e80d611f3f --- /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 b505396000dd..e650875bd2ce 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.HwBinder; 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 @@ public class AudioService extends IAudioService.Stub 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 class AudioService extends IAudioService.Stub 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 @@ public class AudioService extends IAudioService.Stub // Construction /////////////////////////////////////////////////////////////////////////// - /** @hide */ - public AudioService(Context context) { - this(context, - AudioSystemAdapter.getDefaultAdapter(), - SystemServerAdapter.getDefaultAdapter(context), - SettingsAdapter.getDefaultAdapter(), - null); - } /** * @param context @@ -993,9 +993,11 @@ public class AudioService extends IAudioService.Stub * {@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 @@ public class AudioService extends IAudioService.Stub */ @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 @@ public class AudioService extends IAudioService.Stub mAudioSystem = audioSystem; mSystemServer = systemServer; mSettings = settings; - + mAudioPolicy = audioPolicy; mPlatformType = AudioSystem.getPlatformType(context); mIsSingleVolume = AudioSystem.isSingleVolume(context); @@ -3854,6 +3858,20 @@ public class AudioService extends IAudioService.Stub 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 000000000000..37b812685a3d --- /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 ad2e7e4586ba..38093dec6d9e 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 @@ public class AbsoluteVolumeBehaviorTest { 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 @@ public class AbsoluteVolumeBehaviorTest { 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 7f54b63bfe4b..4e9ac7ced327 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 @@ public class AudioDeviceVolumeManagerTest { private SystemServerAdapter mSystemServer; private SettingsAdapter mSettingsAdapter; private TestLooper mTestLooper; + private AudioPolicyFacade mAudioPolicyMock = mock(AudioPolicyFacade.class); private AudioService mAudioService; @@ -59,7 +61,7 @@ public class AudioDeviceVolumeManagerTest { 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 adcbe6bb12c0..88d57ac1ab88 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.app.AppOpsManager; 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 androidx.test.runner.AndroidJUnit4; 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 @@ public class AudioServiceTest { 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 @@ public class AudioServiceTest { } 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 @@ public class AudioServiceTest { 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 d89c6d5c3c78..77a62869e715 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 @@ package com.android.server.audio; 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 @@ public class DeviceVolumeBehaviorTest { private SystemServerAdapter mSystemServer; private SettingsAdapter mSettingsAdapter; private TestLooper mTestLooper; + private AudioPolicyFacade mAudioPolicyMock = mock(AudioPolicyFacade.class); private AudioService mAudioService; @@ -67,7 +69,7 @@ public class DeviceVolumeBehaviorTest { mSystemServer = new NoOpSystemServerAdapter(); mSettingsAdapter = new NoOpSettingsAdapter(); mAudioService = new AudioService(mContext, mAudioSystem, mSystemServer, - mSettingsAdapter, mTestLooper.getLooper()); + mSettingsAdapter, mAudioPolicyMock, mTestLooper.getLooper()); mTestLooper.dispatchAll(); } |