diff --git a/core/java/android/service/voice/HotwordAudioStream.java b/core/java/android/service/voice/HotwordAudioStream.java
index 5442860df007f..1dceb7a08c133 100644
--- a/core/java/android/service/voice/HotwordAudioStream.java
+++ b/core/java/android/service/voice/HotwordAudioStream.java
@@ -36,6 +36,21 @@ import java.util.Objects;
*/
public final class HotwordAudioStream implements Parcelable {
+ /**
+ * Key for int value to be read from {@link #getMetadata()}. The value is read by the system and
+ * is the length (in bytes) of the byte buffers created to copy bytes in the
+ * {@link #getAudioStreamParcelFileDescriptor()} written by the {@link HotwordDetectionService}.
+ * The buffer length should be chosen such that no additional latency is introduced. Typically,
+ * this should be at least the size of byte chunks written by the
+ * {@link HotwordDetectionService}.
+ *
+ *
If no value specified in the metadata for the buffer length, or if the value is less than
+ * 1, or if it is greater than 65,536, or if it is not an int, the default value of 2,560 will
+ * be used.
+ */
+ public static final String KEY_AUDIO_STREAM_COPY_BUFFER_LENGTH_BYTES =
+ "android.service.voice.key.AUDIO_STREAM_COPY_BUFFER_LENGTH_BYTES";
+
/**
* The {@link AudioFormat} of the audio stream.
*/
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordAudioStreamManager.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordAudioStreamCopier.java
similarity index 52%
rename from services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordAudioStreamManager.java
rename to services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordAudioStreamCopier.java
index d5eea1f3ff355..b9d2ae6b0d392 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordAudioStreamManager.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordAudioStreamCopier.java
@@ -17,16 +17,23 @@
package com.android.server.voiceinteraction;
import static android.app.AppOpsManager.MODE_ALLOWED;
+import static android.service.voice.HotwordAudioStream.KEY_AUDIO_STREAM_COPY_BUFFER_LENGTH_BYTES;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__AUDIO_EGRESS_CLOSE_ERROR_FROM_SYSTEM;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__AUDIO_EGRESS_EMPTY_AUDIO_STREAM_LIST;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__AUDIO_EGRESS_END;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__AUDIO_EGRESS_ILLEGAL_COPY_BUFFER_SIZE;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__AUDIO_EGRESS_INTERRUPTED_EXCEPTION;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__AUDIO_EGRESS_NO_PERMISSION;
+import static com.android.internal.util.FrameworkStatsLog.HOTWORD_DETECTOR_EVENTS__EVENT__AUDIO_EGRESS_START;
import static com.android.server.voiceinteraction.HotwordDetectionConnection.DEBUG;
import android.annotation.NonNull;
import android.app.AppOpsManager;
-import android.media.permission.Identity;
import android.os.ParcelFileDescriptor;
+import android.os.PersistableBundle;
import android.service.voice.HotwordAudioStream;
import android.service.voice.HotwordDetectedResult;
-import android.util.Pair;
import android.util.Slog;
import java.io.IOException;
@@ -39,21 +46,40 @@ import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
-final class HotwordAudioStreamManager {
+/**
+ * Copies the audio streams in {@link HotwordDetectedResult}s. This allows the system to manage the
+ * lifetime of the {@link ParcelFileDescriptor}s and ensures that the flow of data is in the right
+ * direction from the {@link android.service.voice.HotwordDetectionService} to the client (i.e., the
+ * voice interactor).
+ *
+ * @hide
+ */
+final class HotwordAudioStreamCopier {
- private static final String TAG = "HotwordAudioStreamManager";
+ private static final String TAG = "HotwordAudioStreamCopier";
private static final String OP_MESSAGE = "Streaming hotword audio to VoiceInteractionService";
private static final String TASK_ID_PREFIX = "HotwordDetectedResult@";
private static final String THREAD_NAME_PREFIX = "Copy-";
+ private static final int DEFAULT_COPY_BUFFER_LENGTH_BYTES = 2_560;
+
+ // Corresponds to the OS pipe capacity in bytes
+ private static final int MAX_COPY_BUFFER_LENGTH_BYTES = 65_536;
private final AppOpsManager mAppOpsManager;
- private final Identity mVoiceInteractorIdentity;
+ private final int mDetectorType;
+ private final int mVoiceInteractorUid;
+ private final String mVoiceInteractorPackageName;
+ private final String mVoiceInteractorAttributionTag;
private final ExecutorService mExecutorService = Executors.newCachedThreadPool();
- HotwordAudioStreamManager(@NonNull AppOpsManager appOpsManager,
- @NonNull Identity voiceInteractorIdentity) {
+ HotwordAudioStreamCopier(@NonNull AppOpsManager appOpsManager, int detectorType,
+ int voiceInteractorUid, @NonNull String voiceInteractorPackageName,
+ @NonNull String voiceInteractorAttributionTag) {
mAppOpsManager = appOpsManager;
- mVoiceInteractorIdentity = voiceInteractorIdentity;
+ mDetectorType = detectorType;
+ mVoiceInteractorUid = voiceInteractorUid;
+ mVoiceInteractorPackageName = voiceInteractorPackageName;
+ mVoiceInteractorAttributionTag = voiceInteractorAttributionTag;
}
/**
@@ -61,7 +87,7 @@ final class HotwordAudioStreamManager {
*
* The returned {@link HotwordDetectedResult} is identical the one that was passed in, except
* that the {@link ParcelFileDescriptor}s within {@link HotwordDetectedResult#getAudioStreams()}
- * are replaced with descriptors from pipes managed by {@link HotwordAudioStreamManager}. The
+ * are replaced with descriptors from pipes managed by {@link HotwordAudioStreamCopier}. The
* returned value should be passed on to the client (i.e., the voice interactor).
*
*
@@ -72,12 +98,14 @@ final class HotwordAudioStreamManager {
throws IOException {
List audioStreams = result.getAudioStreams();
if (audioStreams.isEmpty()) {
+ HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
+ HOTWORD_DETECTOR_EVENTS__EVENT__AUDIO_EGRESS_EMPTY_AUDIO_STREAM_LIST,
+ mVoiceInteractorUid);
return result;
}
List newAudioStreams = new ArrayList<>(audioStreams.size());
- List> sourcesAndSinks = new ArrayList<>(
- audioStreams.size());
+ List copyTaskInfos = new ArrayList<>(audioStreams.size());
for (HotwordAudioStream audioStream : audioStreams) {
ParcelFileDescriptor[] clientPipe = ParcelFileDescriptor.createReliablePipe();
ParcelFileDescriptor clientAudioSource = clientPipe[0];
@@ -87,74 +115,114 @@ final class HotwordAudioStreamManager {
clientAudioSource).build();
newAudioStreams.add(newAudioStream);
+ int copyBufferLength = DEFAULT_COPY_BUFFER_LENGTH_BYTES;
+ PersistableBundle metadata = audioStream.getMetadata();
+ if (metadata.containsKey(KEY_AUDIO_STREAM_COPY_BUFFER_LENGTH_BYTES)) {
+ copyBufferLength = metadata.getInt(KEY_AUDIO_STREAM_COPY_BUFFER_LENGTH_BYTES, -1);
+ if (copyBufferLength < 1 || copyBufferLength > MAX_COPY_BUFFER_LENGTH_BYTES) {
+ HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
+ HOTWORD_DETECTOR_EVENTS__EVENT__AUDIO_EGRESS_ILLEGAL_COPY_BUFFER_SIZE,
+ mVoiceInteractorUid);
+ Slog.w(TAG, "Attempted to set an invalid copy buffer length ("
+ + copyBufferLength + ") for: " + audioStream);
+ copyBufferLength = DEFAULT_COPY_BUFFER_LENGTH_BYTES;
+ } else if (DEBUG) {
+ Slog.i(TAG, "Copy buffer length set to " + copyBufferLength + " for: "
+ + audioStream);
+ }
+ }
+
ParcelFileDescriptor serviceAudioSource =
audioStream.getAudioStreamParcelFileDescriptor();
- sourcesAndSinks.add(new Pair<>(serviceAudioSource, clientAudioSink));
+ copyTaskInfos.add(new CopyTaskInfo(serviceAudioSource, clientAudioSink,
+ copyBufferLength));
}
String resultTaskId = TASK_ID_PREFIX + System.identityHashCode(result);
- mExecutorService.execute(new HotwordDetectedResultCopyTask(resultTaskId, sourcesAndSinks));
+ mExecutorService.execute(new HotwordDetectedResultCopyTask(resultTaskId, copyTaskInfos));
return result.buildUpon().setAudioStreams(newAudioStreams).build();
}
+ private static class CopyTaskInfo {
+ private final ParcelFileDescriptor mSource;
+ private final ParcelFileDescriptor mSink;
+ private final int mCopyBufferLength;
+
+ CopyTaskInfo(ParcelFileDescriptor source, ParcelFileDescriptor sink, int copyBufferLength) {
+ mSource = source;
+ mSink = sink;
+ mCopyBufferLength = copyBufferLength;
+ }
+ }
+
private class HotwordDetectedResultCopyTask implements Runnable {
private final String mResultTaskId;
- private final List> mSourcesAndSinks;
+ private final List mCopyTaskInfos;
private final ExecutorService mExecutorService = Executors.newCachedThreadPool();
- HotwordDetectedResultCopyTask(String resultTaskId,
- List> sourcesAndSinks) {
+ HotwordDetectedResultCopyTask(String resultTaskId, List copyTaskInfos) {
mResultTaskId = resultTaskId;
- mSourcesAndSinks = sourcesAndSinks;
+ mCopyTaskInfos = copyTaskInfos;
}
@Override
public void run() {
Thread.currentThread().setName(THREAD_NAME_PREFIX + mResultTaskId);
- int size = mSourcesAndSinks.size();
+ int size = mCopyTaskInfos.size();
List tasks = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
- Pair sourceAndSink =
- mSourcesAndSinks.get(i);
- ParcelFileDescriptor serviceAudioSource = sourceAndSink.first;
- ParcelFileDescriptor clientAudioSink = sourceAndSink.second;
+ CopyTaskInfo copyTaskInfo = mCopyTaskInfos.get(i);
String streamTaskId = mResultTaskId + "@" + i;
- tasks.add(new SingleAudioStreamCopyTask(streamTaskId, serviceAudioSource,
- clientAudioSink));
+ tasks.add(new SingleAudioStreamCopyTask(streamTaskId, copyTaskInfo.mSource,
+ copyTaskInfo.mSink, copyTaskInfo.mCopyBufferLength, mDetectorType,
+ mVoiceInteractorUid));
}
if (mAppOpsManager.startOpNoThrow(AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD,
- mVoiceInteractorIdentity.uid, mVoiceInteractorIdentity.packageName,
- mVoiceInteractorIdentity.attributionTag, OP_MESSAGE) == MODE_ALLOWED) {
+ mVoiceInteractorUid, mVoiceInteractorPackageName,
+ mVoiceInteractorAttributionTag, OP_MESSAGE) == MODE_ALLOWED) {
try {
+ HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
+ HOTWORD_DETECTOR_EVENTS__EVENT__AUDIO_EGRESS_START,
+ mVoiceInteractorUid);
// TODO(b/244599891): Set timeout, close after inactivity
mExecutorService.invokeAll(tasks);
+ HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
+ HOTWORD_DETECTOR_EVENTS__EVENT__AUDIO_EGRESS_END,
+ mVoiceInteractorUid);
} catch (InterruptedException e) {
+ HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
+ HOTWORD_DETECTOR_EVENTS__EVENT__AUDIO_EGRESS_INTERRUPTED_EXCEPTION,
+ mVoiceInteractorUid);
Slog.e(TAG, mResultTaskId + ": Task was interrupted", e);
bestEffortPropagateError(e.getMessage());
} finally {
mAppOpsManager.finishOp(AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD,
- mVoiceInteractorIdentity.uid, mVoiceInteractorIdentity.packageName,
- mVoiceInteractorIdentity.attributionTag);
+ mVoiceInteractorUid, mVoiceInteractorPackageName,
+ mVoiceInteractorAttributionTag);
}
} else {
+ HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
+ HOTWORD_DETECTOR_EVENTS__EVENT__AUDIO_EGRESS_NO_PERMISSION,
+ mVoiceInteractorUid);
bestEffortPropagateError(
- "Failed to obtain RECORD_AUDIO_HOTWORD permission for "
- + SoundTriggerSessionPermissionsDecorator.toString(
- mVoiceInteractorIdentity));
+ "Failed to obtain RECORD_AUDIO_HOTWORD permission for voice interactor with"
+ + " uid=" + mVoiceInteractorUid
+ + " packageName=" + mVoiceInteractorPackageName
+ + " attributionTag=" + mVoiceInteractorAttributionTag);
}
}
private void bestEffortPropagateError(@NonNull String errorMessage) {
try {
- for (Pair sourceAndSink :
- mSourcesAndSinks) {
- ParcelFileDescriptor serviceAudioSource = sourceAndSink.first;
- ParcelFileDescriptor clientAudioSink = sourceAndSink.second;
- serviceAudioSource.closeWithError(errorMessage);
- clientAudioSink.closeWithError(errorMessage);
+ for (CopyTaskInfo copyTaskInfo : mCopyTaskInfos) {
+ copyTaskInfo.mSource.closeWithError(errorMessage);
+ copyTaskInfo.mSink.closeWithError(errorMessage);
}
+ HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
+ HOTWORD_DETECTOR_EVENTS__EVENT__AUDIO_EGRESS_CLOSE_ERROR_FROM_SYSTEM,
+ mVoiceInteractorUid);
} catch (IOException e) {
Slog.e(TAG, mResultTaskId + ": Failed to propagate error", e);
}
@@ -162,18 +230,21 @@ final class HotwordAudioStreamManager {
}
private static class SingleAudioStreamCopyTask implements Callable {
- // TODO: Make this buffer size customizable from updateState()
- private static final int COPY_BUFFER_LENGTH = 2_560;
-
private final String mStreamTaskId;
private final ParcelFileDescriptor mAudioSource;
private final ParcelFileDescriptor mAudioSink;
+ private final int mCopyBufferLength;
+ private final int mDetectorType;
+ private final int mUid;
SingleAudioStreamCopyTask(String streamTaskId, ParcelFileDescriptor audioSource,
- ParcelFileDescriptor audioSink) {
+ ParcelFileDescriptor audioSink, int copyBufferLength, int detectorType, int uid) {
mStreamTaskId = streamTaskId;
mAudioSource = audioSource;
mAudioSink = audioSink;
+ mCopyBufferLength = copyBufferLength;
+ mDetectorType = detectorType;
+ mUid = uid;
}
@Override
@@ -189,7 +260,7 @@ final class HotwordAudioStreamManager {
try {
fis = new ParcelFileDescriptor.AutoCloseInputStream(mAudioSource);
fos = new ParcelFileDescriptor.AutoCloseOutputStream(mAudioSink);
- byte[] buffer = new byte[COPY_BUFFER_LENGTH];
+ byte[] buffer = new byte[mCopyBufferLength];
while (true) {
if (Thread.interrupted()) {
Slog.e(TAG,
@@ -217,6 +288,8 @@ final class HotwordAudioStreamManager {
mAudioSource.closeWithError(e.getMessage());
mAudioSink.closeWithError(e.getMessage());
Slog.e(TAG, mStreamTaskId + ": Failed to copy audio stream", e);
+ HotwordMetricsLogger.writeDetectorEvent(mDetectorType,
+ HOTWORD_DETECTOR_EVENTS__EVENT__AUDIO_EGRESS_CLOSE_ERROR_FROM_SYSTEM, mUid);
} finally {
if (fis != null) {
fis.close();
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index 6f7d80caa1471..7074b19cecd07 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
@@ -170,7 +170,7 @@ final class HotwordDetectionConnection {
private final ScheduledExecutorService mScheduledExecutorService =
Executors.newSingleThreadScheduledExecutor();
private final AppOpsManager mAppOpsManager;
- private final HotwordAudioStreamManager mHotwordAudioStreamManager;
+ private final HotwordAudioStreamCopier mHotwordAudioStreamCopier;
@Nullable private final ScheduledFuture> mCancellationTaskFuture;
private final AtomicBoolean mUpdateStateAfterStartFinished = new AtomicBoolean(false);
private final IBinder.DeathRecipient mAudioServerDeathRecipient = this::audioServerDied;
@@ -232,8 +232,9 @@ final class HotwordDetectionConnection {
mVoiceInteractionServiceUid = voiceInteractionServiceUid;
mVoiceInteractorIdentity = voiceInteractorIdentity;
mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
- mHotwordAudioStreamManager = new HotwordAudioStreamManager(mAppOpsManager,
- mVoiceInteractorIdentity);
+ mHotwordAudioStreamCopier = new HotwordAudioStreamCopier(mAppOpsManager, detectorType,
+ mVoiceInteractorIdentity.uid, mVoiceInteractorIdentity.packageName,
+ mVoiceInteractorIdentity.attributionTag);
mDetectionComponentName = serviceName;
mUser = userId;
mCallback = callback;
@@ -490,7 +491,7 @@ final class HotwordDetectionConnection {
saveProximityValueToBundle(result);
HotwordDetectedResult newResult;
try {
- newResult = mHotwordAudioStreamManager.startCopyingAudioStreams(result);
+ newResult = mHotwordAudioStreamCopier.startCopyingAudioStreams(result);
} catch (IOException e) {
// TODO: Write event
mSoftwareCallback.onError();
@@ -682,7 +683,7 @@ final class HotwordDetectionConnection {
saveProximityValueToBundle(result);
HotwordDetectedResult newResult;
try {
- newResult = mHotwordAudioStreamManager.startCopyingAudioStreams(result);
+ newResult = mHotwordAudioStreamCopier.startCopyingAudioStreams(result);
} catch (IOException e) {
// TODO: Write event
externalCallback.onError(CALLBACK_ONDETECTED_STREAM_COPY_ERROR);
@@ -1014,7 +1015,7 @@ final class HotwordDetectionConnection {
HotwordDetectedResult newResult;
try {
newResult =
- mHotwordAudioStreamManager.startCopyingAudioStreams(
+ mHotwordAudioStreamCopier.startCopyingAudioStreams(
triggerResult);
} catch (IOException e) {
// TODO: Write event