Loading services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java +45 −23 Original line number Diff line number Diff line Loading @@ -411,7 +411,8 @@ abstract class DetectorSession { audioFormat, options, callback, /* shouldCloseAudioStreamWithDelayOnDetect= */ true); /* shouldCloseAudioStreamWithDelayOnDetect= */ true, /* shouldCheckPermissionsAndAppOpsOnDetected= */ true); } void startListeningFromWearableLocked( Loading Loading @@ -481,12 +482,29 @@ abstract class DetectorSession { return null; } }; /* * By setting shouldCheckPermissionsAndAppOpsOnDetected to false, when the audio * stream is sent from the sandboxed HotwordDetectionService to the non-sandboxed * VoiceInteractionService as a result of second-stage hotword detection, audio-related * permissions will not be checked against the VoiceInteractionService and the AppOpsManager * will not be notified of the data flow to the VoiceInteractionService. These checks are * not performed because the audio stream here originates from a remotely connected wearable * device. It does not originate from the microphone of the device where this code runs on, * or a microphone directly controlled by this system. Permission checks are expected to * happen on the remote wearable device. From the perspective of this system, the audio * stream is data received from an external source. * * Not notifying AppOpsManager allows this device's microphone indicator to remain off when * this data flow happens. It avoids confusion since the audio does not originate from * this device. The wearable is expected to turn on its own microphone indicator. */ handleExternalSourceHotwordDetectionLocked( audioStream, audioFormat, options, voiceInteractionCallback, /* shouldCloseAudioStreamWithDelayOnDetect= */ false); /* shouldCloseAudioStreamWithDelayOnDetect= */ false, /* shouldCheckPermissionsAndAppOpsOnDetected= */ false); } @SuppressWarnings("GuardedBy") Loading @@ -495,7 +513,8 @@ abstract class DetectorSession { AudioFormat audioFormat, @Nullable PersistableBundle options, IMicrophoneHotwordDetectionVoiceInteractionCallback callback, boolean shouldCloseAudioStreamWithDelayOnDetect) { boolean shouldCloseAudioStreamWithDelayOnDetect, boolean shouldCheckPermissionsAndAppOpsOnDetected) { if (DEBUG) { Slog.d(TAG, "#handleExternalSourceHotwordDetectionLocked"); } Loading Loading @@ -631,6 +650,7 @@ abstract class DetectorSession { EXTERNAL_HOTWORD_CLEANUP_MILLIS, TimeUnit.MILLISECONDS); } if (shouldCheckPermissionsAndAppOpsOnDetected) { try { enforcePermissionsForDataDelivery(); } catch (SecurityException e) { Loading @@ -655,12 +675,14 @@ abstract class DetectorSession { } return; } } HotwordDetectedResult newResult; try { newResult = mHotwordAudioStreamCopier .startCopyingAudioStreams( triggerResult); triggerResult, shouldCheckPermissionsAndAppOpsOnDetected); } catch (IOException e) { Slog.w( TAG, Loading services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordAudioStreamCopier.java +90 −58 Original line number Diff line number Diff line Loading @@ -86,19 +86,32 @@ final class HotwordAudioStreamCopier { mVoiceInteractorAttributionTag = voiceInteractorAttributionTag; } /** * Calls {@link #startCopyingAudioStreams(HotwordDetectedResult, boolean)} and notifies * AppOpsManager of the {@link AppOpsManager#OPSTR_RECORD_AUDIO_HOTWORD} operation. */ @NonNull public HotwordDetectedResult startCopyingAudioStreams(@NonNull HotwordDetectedResult result) throws IOException { return startCopyingAudioStreams(result, /* shouldNotifyAppOpsManager= */ true); } /** * Starts copying the audio streams in the given {@link HotwordDetectedResult}. * <p> * The returned {@link HotwordDetectedResult} is identical the one that was passed in, except * * <p>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 HotwordAudioStreamCopier}. The * returned value should be passed on to the client (i.e., the voice interactor). * </p> * * @param result The {@link HotwordDetectedResult} to copy from. * @param shouldNotifyAppOpsManager Whether the {@link AppOpsManager} should be notified of the * {@link AppOpsManager#OPSTR_RECORD_AUDIO_HOTWORD} operation during the copy. * @throws IOException If there was an error creating the managed pipe. */ @NonNull public HotwordDetectedResult startCopyingAudioStreams(@NonNull HotwordDetectedResult result) public HotwordDetectedResult startCopyingAudioStreams( @NonNull HotwordDetectedResult result, boolean shouldNotifyAppOpsManager) throws IOException { List<HotwordAudioStream> audioStreams = result.getAudioStreams(); if (audioStreams.isEmpty()) { Loading Loading @@ -154,8 +167,12 @@ final class HotwordAudioStreamCopier { String resultTaskId = TASK_ID_PREFIX + System.identityHashCode(result); mExecutorService.execute( new HotwordDetectedResultCopyTask(resultTaskId, copyTaskInfos, totalMetadataBundleSizeBytes, totalInitialAudioSizeBytes)); new HotwordDetectedResultCopyTask( resultTaskId, copyTaskInfos, totalMetadataBundleSizeBytes, totalInitialAudioSizeBytes, shouldNotifyAppOpsManager)); return result.buildUpon().setAudioStreams(newAudioStreams).build(); } Loading @@ -178,13 +195,19 @@ final class HotwordAudioStreamCopier { private final int mTotalMetadataSizeBytes; private final int mTotalInitialAudioSizeBytes; private final ExecutorService mExecutorService = Executors.newCachedThreadPool(); HotwordDetectedResultCopyTask(String resultTaskId, List<CopyTaskInfo> copyTaskInfos, int totalMetadataSizeBytes, int totalInitialAudioSizeBytes) { private final boolean mShouldNotifyAppOpsManager; HotwordDetectedResultCopyTask( String resultTaskId, List<CopyTaskInfo> copyTaskInfos, int totalMetadataSizeBytes, int totalInitialAudioSizeBytes, boolean shouldNotifyAppOpsManager) { mResultTaskId = resultTaskId; mCopyTaskInfos = copyTaskInfos; mTotalMetadataSizeBytes = totalMetadataSizeBytes; mTotalInitialAudioSizeBytes = totalInitialAudioSizeBytes; mShouldNotifyAppOpsManager = shouldNotifyAppOpsManager; } @Override Loading @@ -200,9 +223,25 @@ final class HotwordAudioStreamCopier { mVoiceInteractorUid)); } if (mAppOpsManager.startOpNoThrow(AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD, mVoiceInteractorUid, mVoiceInteractorPackageName, mVoiceInteractorAttributionTag, OP_MESSAGE) == MODE_ALLOWED) { if (mShouldNotifyAppOpsManager && mAppOpsManager.startOpNoThrow( AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD, mVoiceInteractorUid, mVoiceInteractorPackageName, mVoiceInteractorAttributionTag, OP_MESSAGE) != MODE_ALLOWED) { HotwordMetricsLogger.writeAudioEgressEvent(mDetectorType, HOTWORD_AUDIO_EGRESS_EVENT_REPORTED__EVENT__NO_PERMISSION, mVoiceInteractorUid, /* streamSizeBytes= */ 0, /* bundleSizeBytes= */ 0, size); bestEffortPropagateError( "Failed to obtain RECORD_AUDIO_HOTWORD permission for voice interactor with" + " uid=" + mVoiceInteractorUid + " packageName=" + mVoiceInteractorPackageName + " attributionTag=" + mVoiceInteractorAttributionTag); return; } try { HotwordMetricsLogger.writeAudioEgressEvent(mDetectorType, HOTWORD_AUDIO_EGRESS_EVENT_REPORTED__EVENT__STARTED, Loading Loading @@ -244,20 +283,13 @@ final class HotwordAudioStreamCopier { + mTotalMetadataSizeBytes); bestEffortPropagateError(e.getMessage()); } finally { mAppOpsManager.finishOp(AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD, mVoiceInteractorUid, mVoiceInteractorPackageName, if (mShouldNotifyAppOpsManager) { mAppOpsManager.finishOp( AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD, mVoiceInteractorUid, mVoiceInteractorPackageName, mVoiceInteractorAttributionTag); } } else { HotwordMetricsLogger.writeAudioEgressEvent(mDetectorType, HOTWORD_AUDIO_EGRESS_EVENT_REPORTED__EVENT__NO_PERMISSION, mVoiceInteractorUid, /* streamSizeBytes= */ 0, /* bundleSizeBytes= */ 0, size); bestEffortPropagateError( "Failed to obtain RECORD_AUDIO_HOTWORD permission for voice interactor with" + " uid=" + mVoiceInteractorUid + " packageName=" + mVoiceInteractorPackageName + " attributionTag=" + mVoiceInteractorAttributionTag); } } Loading Loading
services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java +45 −23 Original line number Diff line number Diff line Loading @@ -411,7 +411,8 @@ abstract class DetectorSession { audioFormat, options, callback, /* shouldCloseAudioStreamWithDelayOnDetect= */ true); /* shouldCloseAudioStreamWithDelayOnDetect= */ true, /* shouldCheckPermissionsAndAppOpsOnDetected= */ true); } void startListeningFromWearableLocked( Loading Loading @@ -481,12 +482,29 @@ abstract class DetectorSession { return null; } }; /* * By setting shouldCheckPermissionsAndAppOpsOnDetected to false, when the audio * stream is sent from the sandboxed HotwordDetectionService to the non-sandboxed * VoiceInteractionService as a result of second-stage hotword detection, audio-related * permissions will not be checked against the VoiceInteractionService and the AppOpsManager * will not be notified of the data flow to the VoiceInteractionService. These checks are * not performed because the audio stream here originates from a remotely connected wearable * device. It does not originate from the microphone of the device where this code runs on, * or a microphone directly controlled by this system. Permission checks are expected to * happen on the remote wearable device. From the perspective of this system, the audio * stream is data received from an external source. * * Not notifying AppOpsManager allows this device's microphone indicator to remain off when * this data flow happens. It avoids confusion since the audio does not originate from * this device. The wearable is expected to turn on its own microphone indicator. */ handleExternalSourceHotwordDetectionLocked( audioStream, audioFormat, options, voiceInteractionCallback, /* shouldCloseAudioStreamWithDelayOnDetect= */ false); /* shouldCloseAudioStreamWithDelayOnDetect= */ false, /* shouldCheckPermissionsAndAppOpsOnDetected= */ false); } @SuppressWarnings("GuardedBy") Loading @@ -495,7 +513,8 @@ abstract class DetectorSession { AudioFormat audioFormat, @Nullable PersistableBundle options, IMicrophoneHotwordDetectionVoiceInteractionCallback callback, boolean shouldCloseAudioStreamWithDelayOnDetect) { boolean shouldCloseAudioStreamWithDelayOnDetect, boolean shouldCheckPermissionsAndAppOpsOnDetected) { if (DEBUG) { Slog.d(TAG, "#handleExternalSourceHotwordDetectionLocked"); } Loading Loading @@ -631,6 +650,7 @@ abstract class DetectorSession { EXTERNAL_HOTWORD_CLEANUP_MILLIS, TimeUnit.MILLISECONDS); } if (shouldCheckPermissionsAndAppOpsOnDetected) { try { enforcePermissionsForDataDelivery(); } catch (SecurityException e) { Loading @@ -655,12 +675,14 @@ abstract class DetectorSession { } return; } } HotwordDetectedResult newResult; try { newResult = mHotwordAudioStreamCopier .startCopyingAudioStreams( triggerResult); triggerResult, shouldCheckPermissionsAndAppOpsOnDetected); } catch (IOException e) { Slog.w( TAG, Loading
services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordAudioStreamCopier.java +90 −58 Original line number Diff line number Diff line Loading @@ -86,19 +86,32 @@ final class HotwordAudioStreamCopier { mVoiceInteractorAttributionTag = voiceInteractorAttributionTag; } /** * Calls {@link #startCopyingAudioStreams(HotwordDetectedResult, boolean)} and notifies * AppOpsManager of the {@link AppOpsManager#OPSTR_RECORD_AUDIO_HOTWORD} operation. */ @NonNull public HotwordDetectedResult startCopyingAudioStreams(@NonNull HotwordDetectedResult result) throws IOException { return startCopyingAudioStreams(result, /* shouldNotifyAppOpsManager= */ true); } /** * Starts copying the audio streams in the given {@link HotwordDetectedResult}. * <p> * The returned {@link HotwordDetectedResult} is identical the one that was passed in, except * * <p>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 HotwordAudioStreamCopier}. The * returned value should be passed on to the client (i.e., the voice interactor). * </p> * * @param result The {@link HotwordDetectedResult} to copy from. * @param shouldNotifyAppOpsManager Whether the {@link AppOpsManager} should be notified of the * {@link AppOpsManager#OPSTR_RECORD_AUDIO_HOTWORD} operation during the copy. * @throws IOException If there was an error creating the managed pipe. */ @NonNull public HotwordDetectedResult startCopyingAudioStreams(@NonNull HotwordDetectedResult result) public HotwordDetectedResult startCopyingAudioStreams( @NonNull HotwordDetectedResult result, boolean shouldNotifyAppOpsManager) throws IOException { List<HotwordAudioStream> audioStreams = result.getAudioStreams(); if (audioStreams.isEmpty()) { Loading Loading @@ -154,8 +167,12 @@ final class HotwordAudioStreamCopier { String resultTaskId = TASK_ID_PREFIX + System.identityHashCode(result); mExecutorService.execute( new HotwordDetectedResultCopyTask(resultTaskId, copyTaskInfos, totalMetadataBundleSizeBytes, totalInitialAudioSizeBytes)); new HotwordDetectedResultCopyTask( resultTaskId, copyTaskInfos, totalMetadataBundleSizeBytes, totalInitialAudioSizeBytes, shouldNotifyAppOpsManager)); return result.buildUpon().setAudioStreams(newAudioStreams).build(); } Loading @@ -178,13 +195,19 @@ final class HotwordAudioStreamCopier { private final int mTotalMetadataSizeBytes; private final int mTotalInitialAudioSizeBytes; private final ExecutorService mExecutorService = Executors.newCachedThreadPool(); HotwordDetectedResultCopyTask(String resultTaskId, List<CopyTaskInfo> copyTaskInfos, int totalMetadataSizeBytes, int totalInitialAudioSizeBytes) { private final boolean mShouldNotifyAppOpsManager; HotwordDetectedResultCopyTask( String resultTaskId, List<CopyTaskInfo> copyTaskInfos, int totalMetadataSizeBytes, int totalInitialAudioSizeBytes, boolean shouldNotifyAppOpsManager) { mResultTaskId = resultTaskId; mCopyTaskInfos = copyTaskInfos; mTotalMetadataSizeBytes = totalMetadataSizeBytes; mTotalInitialAudioSizeBytes = totalInitialAudioSizeBytes; mShouldNotifyAppOpsManager = shouldNotifyAppOpsManager; } @Override Loading @@ -200,9 +223,25 @@ final class HotwordAudioStreamCopier { mVoiceInteractorUid)); } if (mAppOpsManager.startOpNoThrow(AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD, mVoiceInteractorUid, mVoiceInteractorPackageName, mVoiceInteractorAttributionTag, OP_MESSAGE) == MODE_ALLOWED) { if (mShouldNotifyAppOpsManager && mAppOpsManager.startOpNoThrow( AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD, mVoiceInteractorUid, mVoiceInteractorPackageName, mVoiceInteractorAttributionTag, OP_MESSAGE) != MODE_ALLOWED) { HotwordMetricsLogger.writeAudioEgressEvent(mDetectorType, HOTWORD_AUDIO_EGRESS_EVENT_REPORTED__EVENT__NO_PERMISSION, mVoiceInteractorUid, /* streamSizeBytes= */ 0, /* bundleSizeBytes= */ 0, size); bestEffortPropagateError( "Failed to obtain RECORD_AUDIO_HOTWORD permission for voice interactor with" + " uid=" + mVoiceInteractorUid + " packageName=" + mVoiceInteractorPackageName + " attributionTag=" + mVoiceInteractorAttributionTag); return; } try { HotwordMetricsLogger.writeAudioEgressEvent(mDetectorType, HOTWORD_AUDIO_EGRESS_EVENT_REPORTED__EVENT__STARTED, Loading Loading @@ -244,20 +283,13 @@ final class HotwordAudioStreamCopier { + mTotalMetadataSizeBytes); bestEffortPropagateError(e.getMessage()); } finally { mAppOpsManager.finishOp(AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD, mVoiceInteractorUid, mVoiceInteractorPackageName, if (mShouldNotifyAppOpsManager) { mAppOpsManager.finishOp( AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD, mVoiceInteractorUid, mVoiceInteractorPackageName, mVoiceInteractorAttributionTag); } } else { HotwordMetricsLogger.writeAudioEgressEvent(mDetectorType, HOTWORD_AUDIO_EGRESS_EVENT_REPORTED__EVENT__NO_PERMISSION, mVoiceInteractorUid, /* streamSizeBytes= */ 0, /* bundleSizeBytes= */ 0, size); bestEffortPropagateError( "Failed to obtain RECORD_AUDIO_HOTWORD permission for voice interactor with" + " uid=" + mVoiceInteractorUid + " packageName=" + mVoiceInteractorPackageName + " attributionTag=" + mVoiceInteractorAttributionTag); } } Loading