Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit cc21837b authored by Tom Chan's avatar Tom Chan
Browse files

Remove permission and AppOpsManager checks for wearable hotword.

Specifically, the checks removed are the ones performed before the
sandboxed HotwordDetectionService sends the audio to the non-sandboxed
VoiceInteractionService. See the code comment in DetectorSession about
why this is needed.

Bug: 310055381
Test: Tried on device and verified that the mic indicator does not show
up when HotwordDetectionService sends wearable hotword audio to
VoiceInteractionService

Change-Id: Id4e0df1cbe74a4d46481009f43104b5369085039
parent 3092a907
Loading
Loading
Loading
Loading
+45 −23
Original line number Diff line number Diff line
@@ -409,7 +409,8 @@ abstract class DetectorSession {
                audioFormat,
                options,
                callback,
                /* shouldCloseAudioStreamWithDelayOnDetect= */ true);
                /* shouldCloseAudioStreamWithDelayOnDetect= */ true,
                /* shouldCheckPermissionsAndAppOpsOnDetected= */ true);
    }

    void startListeningFromWearableLocked(
@@ -479,12 +480,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")
@@ -493,7 +511,8 @@ abstract class DetectorSession {
            AudioFormat audioFormat,
            @Nullable PersistableBundle options,
            IMicrophoneHotwordDetectionVoiceInteractionCallback callback,
            boolean shouldCloseAudioStreamWithDelayOnDetect) {
            boolean shouldCloseAudioStreamWithDelayOnDetect,
            boolean shouldCheckPermissionsAndAppOpsOnDetected) {
        if (DEBUG) {
            Slog.d(TAG, "#handleExternalSourceHotwordDetectionLocked");
        }
@@ -629,6 +648,7 @@ abstract class DetectorSession {
                                                    EXTERNAL_HOTWORD_CLEANUP_MILLIS,
                                                    TimeUnit.MILLISECONDS);
                                        }
                                        if (shouldCheckPermissionsAndAppOpsOnDetected) {
                                            try {
                                                enforcePermissionsForDataDelivery();
                                            } catch (SecurityException e) {
@@ -653,12 +673,14 @@ abstract class DetectorSession {
                                                }
                                                return;
                                            }
                                        }
                                        HotwordDetectedResult newResult;
                                        try {
                                            newResult =
                                                mHotwordAudioStreamCopier
                                                    .startCopyingAudioStreams(
                                                                    triggerResult);
                                                        triggerResult,
                                                        shouldCheckPermissionsAndAppOpsOnDetected);
                                        } catch (IOException e) {
                                            Slog.w(
                                                    TAG,
+90 −58
Original line number Diff line number Diff line
@@ -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()) {
@@ -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();
    }
@@ -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
@@ -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,
@@ -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);
            }
        }