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