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

Commit ba966c14 authored by Ahaan Ugale's avatar Ahaan Ugale
Browse files

HotwordDetectionSrvc: Handle SoundTrigger permissions in VIMS

This allows properly checking/noting against the voice interactor or
HotwordDetectionService as needed. Otherwise,
SoundTriggerMiddlewarePermission notes ops for data delivery to the
interactor, even if the data only reaches the HotwordDetectionService.

This is accomplished with a decorator for permission checks, that wraps
the real implementation. A proxy that serves as the remote Binder object
is also needed, to allow this decoration pattern.

The list of sessions stored in VIMS is removed for simplicity. It
currently serves no purpose (used only in dump() but doesn't implement
it so it's a no-op there).

DataDelivery checks will be addressed in a followup change.

Bug: 186164881
Test: manual - permissions are checked appropriately
Test: atest CtsVoiceInteractionTestCases
Change-Id: I80dabaf6ae0e781028dde16ead3321fbff319542
parent ba23b846
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -79,7 +79,7 @@ import java.util.function.Function;
final class HotwordDetectionConnection {
    private static final String TAG = "HotwordDetectionConnection";
    // TODO (b/177502877): Set the Debug flag to false before shipping.
    private static final boolean DEBUG = true;
    static final boolean DEBUG = true;

    // TODO: These constants need to be refined.
    private static final long VALIDATION_TIMEOUT_MILLIS = 3000;
+72 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.voiceinteraction;

import android.hardware.soundtrigger.SoundTrigger;
import android.os.RemoteException;

import com.android.internal.app.IHotwordRecognitionStatusCallback;
import com.android.internal.app.IVoiceInteractionSoundTriggerSession;

/**
 * A remote object that simply proxies calls to a real {@link IVoiceInteractionSoundTriggerSession}
 * implementation. This design pattern allows us to add decorators to the core implementation
 * (simply wrapping a binder object does not work).
 */
final class SoundTriggerSessionBinderProxy extends IVoiceInteractionSoundTriggerSession.Stub {

    private final IVoiceInteractionSoundTriggerSession mDelegate;

    SoundTriggerSessionBinderProxy(IVoiceInteractionSoundTriggerSession delegate) {
        mDelegate = delegate;
    }

    @Override
    public SoundTrigger.ModuleProperties getDspModuleProperties() throws RemoteException {
        return mDelegate.getDspModuleProperties();
    }

    @Override
    public int startRecognition(int i, String s,
            IHotwordRecognitionStatusCallback iHotwordRecognitionStatusCallback,
            SoundTrigger.RecognitionConfig recognitionConfig, boolean b) throws RemoteException {
        return mDelegate.startRecognition(i, s, iHotwordRecognitionStatusCallback,
                recognitionConfig, b);
    }

    @Override
    public int stopRecognition(int i,
            IHotwordRecognitionStatusCallback iHotwordRecognitionStatusCallback)
            throws RemoteException {
        return mDelegate.stopRecognition(i, iHotwordRecognitionStatusCallback);
    }

    @Override
    public int setParameter(int i, int i1, int i2) throws RemoteException {
        return mDelegate.setParameter(i, i1, i2);
    }

    @Override
    public int getParameter(int i, int i1) throws RemoteException {
        return mDelegate.getParameter(i, i1);
    }

    @Override
    public SoundTrigger.ModelParamRange queryParameter(int i, int i1) throws RemoteException {
        return mDelegate.queryParameter(i, i1);
    }
}
+158 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.voiceinteraction;

import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD;
import static android.Manifest.permission.RECORD_AUDIO;

import static com.android.server.voiceinteraction.HotwordDetectionConnection.DEBUG;

import android.annotation.NonNull;
import android.content.Context;
import android.content.PermissionChecker;
import android.hardware.soundtrigger.SoundTrigger;
import android.media.permission.Identity;
import android.media.permission.PermissionUtil;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.text.TextUtils;
import android.util.Slog;

import com.android.internal.app.IHotwordRecognitionStatusCallback;
import com.android.internal.app.IVoiceInteractionSoundTriggerSession;

/**
 * Decorates {@link IVoiceInteractionSoundTriggerSession} with permission checks for {@link
 * android.Manifest.permission#RECORD_AUDIO} and
 * {@link android.Manifest.permission#CAPTURE_AUDIO_HOTWORD}.
 * <p>
 * Does not implement {@link #asBinder()} as it's intended to be wrapped by an
 * {@link IVoiceInteractionSoundTriggerSession.Stub} object.
 */
final class SoundTriggerSessionPermissionsDecorator implements
        IVoiceInteractionSoundTriggerSession {
    static final String TAG = "SoundTriggerSessionPermissionsDecorator";

    private final IVoiceInteractionSoundTriggerSession mDelegate;
    private final Context mContext;
    private final Identity mOriginatorIdentity;

    SoundTriggerSessionPermissionsDecorator(IVoiceInteractionSoundTriggerSession delegate,
            Context context, Identity originatorIdentity) {
        mDelegate = delegate;
        mContext = context;
        mOriginatorIdentity = originatorIdentity;
    }

    @Override
    public SoundTrigger.ModuleProperties getDspModuleProperties() throws RemoteException {
        // No permission needed.
        return mDelegate.getDspModuleProperties();
    }

    @Override
    public int startRecognition(int i, String s,
            IHotwordRecognitionStatusCallback iHotwordRecognitionStatusCallback,
            SoundTrigger.RecognitionConfig recognitionConfig, boolean b) throws RemoteException {
        if (DEBUG) {
            Slog.d(TAG, "startRecognition");
        }
        enforcePermissions();
        return mDelegate.startRecognition(i, s, iHotwordRecognitionStatusCallback,
                recognitionConfig, b);
    }

    @Override
    public int stopRecognition(int i,
            IHotwordRecognitionStatusCallback iHotwordRecognitionStatusCallback)
            throws RemoteException {
        enforcePermissions();
        return mDelegate.stopRecognition(i, iHotwordRecognitionStatusCallback);
    }

    @Override
    public int setParameter(int i, int i1, int i2) throws RemoteException {
        enforcePermissions();
        return mDelegate.setParameter(i, i1, i2);
    }

    @Override
    public int getParameter(int i, int i1) throws RemoteException {
        enforcePermissions();
        return mDelegate.getParameter(i, i1);
    }

    @Override
    public SoundTrigger.ModelParamRange queryParameter(int i, int i1) throws RemoteException {
        enforcePermissions();
        return mDelegate.queryParameter(i, i1);
    }

    @Override
    public IBinder asBinder() {
        throw new UnsupportedOperationException(
                "This object isn't intended to be used as a Binder.");
    }

    // TODO: Share this code with SoundTriggerMiddlewarePermission.
    private void enforcePermissions() {
        enforcePermissionForPreflight(mContext, mOriginatorIdentity, RECORD_AUDIO);
        enforcePermissionForPreflight(mContext, mOriginatorIdentity, CAPTURE_AUDIO_HOTWORD);
    }

    /**
     * Throws a {@link SecurityException} if originator permanently doesn't have the given
     * permission, or a {@link ServiceSpecificException} with a {@link
     * #TEMPORARY_PERMISSION_DENIED} if caller originator doesn't have the given permission.
     *
     * @param context    A {@link Context}, used for permission checks.
     * @param identity   The identity to check.
     * @param permission The identifier of the permission we want to check.
     */
    private static void enforcePermissionForPreflight(@NonNull Context context,
            @NonNull Identity identity, @NonNull String permission) {
        final int status = PermissionUtil.checkPermissionForPreflight(context, identity,
                permission);
        switch (status) {
            case PermissionChecker.PERMISSION_GRANTED:
                return;
            case PermissionChecker.PERMISSION_HARD_DENIED:
                throw new SecurityException(
                        TextUtils.formatSimple("Failed to obtain permission %s for identity %s",
                                permission, toString(identity)));
            case PermissionChecker.PERMISSION_SOFT_DENIED:
                throw new ServiceSpecificException(TEMPORARY_PERMISSION_DENIED,
                        TextUtils.formatSimple("Failed to obtain permission %s for identity %s",
                                permission, toString(identity)));
            default:
                throw new RuntimeException("Unexpected permission check result.");
        }
    }

    private static String toString(Identity identity) {
        return "{uid=" + identity.uid
                + " pid=" + identity.pid
                + " packageName=" + identity.packageName
                + " attributionTag=" + identity.attributionTag
                + "}";
    }

    // Temporary hack for using the same status code as SoundTrigger, so we don't change behavior.
    // TODO: Reuse SoundTrigger code so we don't need to do this.
    private static final int TEMPORARY_PERMISSION_DENIED = 3;
}
+55 −28
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.ActivityThread;
import android.app.AppGlobals;
import android.app.role.OnRoleHoldersChangedListener;
import android.app.role.RoleManager;
@@ -50,6 +51,7 @@ import android.hardware.soundtrigger.SoundTrigger.ModuleProperties;
import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
import android.media.AudioFormat;
import android.media.permission.Identity;
import android.media.permission.IdentityContext;
import android.media.permission.PermissionUtil;
import android.media.permission.SafeCloseable;
import android.os.Binder;
@@ -59,6 +61,7 @@ import android.os.IBinder;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
import android.os.Process;
import android.os.RemoteCallback;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
@@ -104,11 +107,8 @@ import com.android.server.wm.ActivityTaskManagerInternal;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.Executor;
@@ -288,7 +288,6 @@ public class VoiceInteractionManagerService extends SystemService {
        private boolean mTemporarilyDisabled;

        private final boolean mEnableService;
        private final List<WeakReference<SoundTriggerSession>> mSessions = new LinkedList<>();

        VoiceInteractionManagerServiceStub() {
            mEnableService = shouldEnableService(mContext);
@@ -299,15 +298,49 @@ public class VoiceInteractionManagerService extends SystemService {
        public @NonNull IVoiceInteractionSoundTriggerSession createSoundTriggerSessionAsOriginator(
                @NonNull Identity originatorIdentity, IBinder client) {
            Objects.requireNonNull(originatorIdentity);
            boolean forHotwordDetectionService;
            synchronized (VoiceInteractionManagerServiceStub.this) {
                enforceIsCurrentVoiceInteractionService();
                forHotwordDetectionService =
                        mImpl != null && mImpl.mHotwordDetectionConnection != null;
            }
            IVoiceInteractionSoundTriggerSession session;
            if (forHotwordDetectionService) {
                // Use our own identity and handle the permission checks ourselves. This allows
                // properly checking/noting against the voice interactor or hotword detection
                // service as needed.
                if (HotwordDetectionConnection.DEBUG) {
                    Slog.d(TAG, "Creating a SoundTriggerSession for a HotwordDetectionService");
                }
                originatorIdentity.uid = Binder.getCallingUid();
                originatorIdentity.pid = Binder.getCallingPid();
                session = new SoundTriggerSessionPermissionsDecorator(
                        createSoundTriggerSessionForSelfIdentity(client),
                        mContext,
                        originatorIdentity);
            } else {
                if (HotwordDetectionConnection.DEBUG) {
                    Slog.d(TAG, "Creating a SoundTriggerSession");
                }
                try (SafeCloseable ignored = PermissionUtil.establishIdentityDirect(
                        originatorIdentity)) {
                SoundTriggerSession session = new SoundTriggerSession(
                        mSoundTriggerInternal.attach(client));
                synchronized (mSessions) {
                    mSessions.add(new WeakReference<>(session));
                    session = new SoundTriggerSession(mSoundTriggerInternal.attach(client));
                }
            }
                return session;
            return new SoundTriggerSessionBinderProxy(session);
        }

        private IVoiceInteractionSoundTriggerSession createSoundTriggerSessionForSelfIdentity(
                IBinder client) {
            Identity identity = new Identity();
            identity.uid = Process.myUid();
            identity.pid = Process.myPid();
            identity.packageName = ActivityThread.currentOpPackageName();
            return Binder.withCleanCallingIdentity(() -> {
                try (SafeCloseable ignored = IdentityContext.create(identity)) {
                    return new SoundTriggerSession(mSoundTriggerInternal.attach(client));
                }
            });
        }

        // TODO: VI Make sure the caller is the current user or profile
@@ -1334,7 +1367,11 @@ public class VoiceInteractionManagerService extends SystemService {
            return null;
        }

        class SoundTriggerSession extends IVoiceInteractionSoundTriggerSession.Stub {
        /**
         * Implementation of SoundTriggerSession. Does not implement {@link #asBinder()} as it's
         * intended to be wrapped by an {@link IVoiceInteractionSoundTriggerSession.Stub} object.
         */
        private class SoundTriggerSession implements IVoiceInteractionSoundTriggerSession {
            final SoundTriggerInternal.Session mSession;
            private IHotwordRecognitionStatusCallback mSessionExternalCallback;
            private IRecognitionStatusCallback mSessionInternalCallback;
@@ -1481,6 +1518,12 @@ public class VoiceInteractionManagerService extends SystemService {
                }
            }

            @Override
            public IBinder asBinder() {
                throw new UnsupportedOperationException(
                        "This object isn't intended to be used as a Binder.");
            }

            private int unloadKeyphraseModel(int keyphraseId) {
                final long caller = Binder.clearCallingIdentity();
                try {
@@ -1709,22 +1752,6 @@ public class VoiceInteractionManagerService extends SystemService {
            }

            mSoundTriggerInternal.dump(fd, pw, args);

            // Dump all sessions.
            synchronized (mSessions) {
                ListIterator<WeakReference<SoundTriggerSession>> iter =
                        mSessions.listIterator();
                while (iter.hasNext()) {
                    SoundTriggerSession session = iter.next().get();
                    if (session != null) {
                        session.dump(fd, args);
                    } else {
                        // Session is obsolete, now is a good chance to remove it from the list.
                        iter.remove();
                    }
                }
            }

        }

        @Override