diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java index beaca68b9a37d..fc10fb1042623 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java @@ -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; diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionBinderProxy.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionBinderProxy.java new file mode 100644 index 0000000000000..dd9fee3887cb3 --- /dev/null +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionBinderProxy.java @@ -0,0 +1,72 @@ +/* + * 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); + } +} diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionPermissionsDecorator.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionPermissionsDecorator.java new file mode 100644 index 0000000000000..bb7ca168fcaaf --- /dev/null +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionPermissionsDecorator.java @@ -0,0 +1,158 @@ +/* + * 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}. + *

+ * 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; +} diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java index 162acba8002db..91d17f74c6768 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java @@ -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> 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); - try (SafeCloseable ignored = PermissionUtil.establishIdentityDirect( - originatorIdentity)) { - SoundTriggerSession session = new SoundTriggerSession( - mSoundTriggerInternal.attach(client)); - synchronized (mSessions) { - mSessions.add(new WeakReference<>(session)); - } - return session; + 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)) { + session = new SoundTriggerSession(mSoundTriggerInternal.attach(client)); + } + } + 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> 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