Merge "Add query API for hotword flag support"

This commit is contained in:
Atneya Nair
2022-12-28 22:11:25 +00:00
committed by Android (Google) Code Review
12 changed files with 236 additions and 24 deletions

View File

@@ -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",
],
}

View File

@@ -6635,6 +6635,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;

View File

@@ -7526,6 +7526,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.

View File

@@ -165,6 +165,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);

View File

@@ -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",

View File

@@ -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);
}

View File

@@ -146,6 +146,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;
@@ -249,6 +250,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;
@@ -923,7 +925,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
@@ -976,14 +984,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
@@ -994,9 +994,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));
}
/**
@@ -1009,8 +1011,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();
@@ -1019,7 +1023,7 @@ public class AudioService extends IAudioService.Stub
mAudioSystem = audioSystem;
mSystemServer = systemServer;
mSettings = settings;
mAudioPolicy = audioPolicy;
mPlatformType = AudioSystem.getPlatformType(context);
mIsSingleVolume = AudioSystem.isSingleVolume(context);
@@ -3955,6 +3959,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(

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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));
}
}

View File

@@ -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();
}