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

This commit is contained in:
Ahaan Ugale
2021-06-30 14:27:12 +00:00
committed by Android (Google) Code Review
4 changed files with 287 additions and 30 deletions

View File

@@ -79,7 +79,7 @@ import java.util.function.Function;
final class HotwordDetectionConnection { final class HotwordDetectionConnection {
private static final String TAG = "HotwordDetectionConnection"; private static final String TAG = "HotwordDetectionConnection";
// TODO (b/177502877): Set the Debug flag to false before shipping. // 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. // TODO: These constants need to be refined.
private static final long VALIDATION_TIMEOUT_MILLIS = 3000; private static final long VALIDATION_TIMEOUT_MILLIS = 3000;

View File

@@ -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);
}
}

View File

@@ -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}.
* <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;
}

View File

@@ -23,6 +23,7 @@ import android.annotation.Nullable;
import android.annotation.UserIdInt; import android.annotation.UserIdInt;
import android.app.ActivityManager; import android.app.ActivityManager;
import android.app.ActivityManagerInternal; import android.app.ActivityManagerInternal;
import android.app.ActivityThread;
import android.app.AppGlobals; import android.app.AppGlobals;
import android.app.role.OnRoleHoldersChangedListener; import android.app.role.OnRoleHoldersChangedListener;
import android.app.role.RoleManager; import android.app.role.RoleManager;
@@ -50,6 +51,7 @@ import android.hardware.soundtrigger.SoundTrigger.ModuleProperties;
import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig; import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
import android.media.AudioFormat; import android.media.AudioFormat;
import android.media.permission.Identity; import android.media.permission.Identity;
import android.media.permission.IdentityContext;
import android.media.permission.PermissionUtil; import android.media.permission.PermissionUtil;
import android.media.permission.SafeCloseable; import android.media.permission.SafeCloseable;
import android.os.Binder; import android.os.Binder;
@@ -59,6 +61,7 @@ import android.os.IBinder;
import android.os.Parcel; import android.os.Parcel;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle; import android.os.PersistableBundle;
import android.os.Process;
import android.os.RemoteCallback; import android.os.RemoteCallback;
import android.os.RemoteCallbackList; import android.os.RemoteCallbackList;
import android.os.RemoteException; import android.os.RemoteException;
@@ -104,11 +107,8 @@ import com.android.server.wm.ActivityTaskManagerInternal;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.ListIterator;
import java.util.Locale; import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
@@ -288,7 +288,6 @@ public class VoiceInteractionManagerService extends SystemService {
private boolean mTemporarilyDisabled; private boolean mTemporarilyDisabled;
private final boolean mEnableService; private final boolean mEnableService;
private final List<WeakReference<SoundTriggerSession>> mSessions = new LinkedList<>();
VoiceInteractionManagerServiceStub() { VoiceInteractionManagerServiceStub() {
mEnableService = shouldEnableService(mContext); mEnableService = shouldEnableService(mContext);
@@ -299,15 +298,49 @@ public class VoiceInteractionManagerService extends SystemService {
public @NonNull IVoiceInteractionSoundTriggerSession createSoundTriggerSessionAsOriginator( public @NonNull IVoiceInteractionSoundTriggerSession createSoundTriggerSessionAsOriginator(
@NonNull Identity originatorIdentity, IBinder client) { @NonNull Identity originatorIdentity, IBinder client) {
Objects.requireNonNull(originatorIdentity); 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( try (SafeCloseable ignored = PermissionUtil.establishIdentityDirect(
originatorIdentity)) { originatorIdentity)) {
SoundTriggerSession session = new SoundTriggerSession( session = new SoundTriggerSession(mSoundTriggerInternal.attach(client));
mSoundTriggerInternal.attach(client));
synchronized (mSessions) {
mSessions.add(new WeakReference<>(session));
} }
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 // TODO: VI Make sure the caller is the current user or profile
@@ -1334,7 +1367,11 @@ public class VoiceInteractionManagerService extends SystemService {
return null; 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; final SoundTriggerInternal.Session mSession;
private IHotwordRecognitionStatusCallback mSessionExternalCallback; private IHotwordRecognitionStatusCallback mSessionExternalCallback;
private IRecognitionStatusCallback mSessionInternalCallback; 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) { private int unloadKeyphraseModel(int keyphraseId) {
final long caller = Binder.clearCallingIdentity(); final long caller = Binder.clearCallingIdentity();
try { try {
@@ -1709,22 +1752,6 @@ public class VoiceInteractionManagerService extends SystemService {
} }
mSoundTriggerInternal.dump(fd, pw, args); 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 @Override