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