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

Commit b0ef72e5 authored by Ahaan Ugale's avatar Ahaan Ugale Committed by Android (Google) Code Review
Browse files

Merge "HotwordDetectionSrvc: Handle SoundTrigger permissions in VIMS" into sc-dev

parents 06327bba ba966c14
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