Associate an originator identity to sessions

This change formalizes the permission enforcement patterns in the
sound trigger middleware layer. Every sound trigger session is
associated with an originator identity, established during attachment.
This identity is the used to authorize any operations performed on
this session, including data deliver via callbacks.

Temporarily, for b/w compatibility the existing SoundTrigger.java API
is preserved. Follow up changes will use the newer API, which requires
an identity to be provided.

Logging / dumpsys aspects have been modified to include the originator
identity associated with every call / session.

Change-Id: If83b151bd182af5a0dd98ff23dce252018de936b
Bug: 163865561
This commit is contained in:
Ytai Ben-Tsvi
2020-04-29 15:36:36 -07:00
parent 4f6f70cdd1
commit 1a619f0389
16 changed files with 1289 additions and 302 deletions

View File

@@ -16,6 +16,9 @@
package android.hardware.soundtrigger;
import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD;
import static android.Manifest.permission.RECORD_AUDIO;
import static android.Manifest.permission.SOUNDTRIGGER_DELEGATE_IDENTITY;
import static android.system.OsConstants.EINVAL;
import static android.system.OsConstants.ENODEV;
import static android.system.OsConstants.ENOSYS;
@@ -27,6 +30,7 @@ import static java.util.Objects.requireNonNull;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.TestApi;
@@ -34,9 +38,13 @@ import android.app.ActivityThread;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.media.AudioFormat;
import android.media.permission.ClearCallingIdentityContext;
import android.media.permission.Identity;
import android.media.permission.SafeCloseable;
import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService;
import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor;
import android.media.soundtrigger_middleware.Status;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -1943,16 +1951,6 @@ public class SoundTrigger {
public static final int SERVICE_STATE_DISABLED = 1;
private static Object mServiceLock = new Object();
private static ISoundTriggerMiddlewareService mService;
/**
* @return returns current package name.
*/
static String getCurrentOpPackageName() {
String packageName = ActivityThread.currentOpPackageName();
if (packageName == null) {
return "";
}
return packageName;
}
/**
* Translate an exception thrown from interaction with the underlying service to an error code.
@@ -2005,23 +2003,103 @@ public class SoundTrigger {
* - {@link #STATUS_BAD_VALUE} if modules is null
* - {@link #STATUS_DEAD_OBJECT} if the binder transaction to the native service fails
*
* @deprecated Please use {@link #listModulesAsOriginator(ArrayList, Identity)} or
* {@link #listModulesAsMiddleman(ArrayList, Identity, Identity)}, based on whether the
* client is acting on behalf of its own identity or a separate identity.
* @hide
*/
@UnsupportedAppUsage
public static int listModules(@NonNull ArrayList<ModuleProperties> modules) {
// TODO(ytai): This is a temporary hack to retain prior behavior, which makes
// assumptions about process affinity and Binder context, namely that the binder calling ID
// reliably reflects the originator identity.
Identity middlemanIdentity = new Identity();
middlemanIdentity.packageName = ActivityThread.currentOpPackageName();
Identity originatorIdentity = new Identity();
originatorIdentity.pid = Binder.getCallingPid();
originatorIdentity.uid = Binder.getCallingUid();
try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
return listModulesAsMiddleman(modules, middlemanIdentity, originatorIdentity);
}
}
/**
* Returns a list of descriptors for all hardware modules loaded.
* This variant is intended for use when the caller itself is the originator of the operation.
* @param modules A ModuleProperties array where the list will be returned.
* @param originatorIdentity The identity of the originator, which will be used for permission
* purposes.
* @return - {@link #STATUS_OK} in case of success
* - {@link #STATUS_ERROR} in case of unspecified error
* - {@link #STATUS_PERMISSION_DENIED} if the caller does not have system permission
* - {@link #STATUS_NO_INIT} if the native service cannot be reached
* - {@link #STATUS_BAD_VALUE} if modules is null
* - {@link #STATUS_DEAD_OBJECT} if the binder transaction to the native service fails
*
* @hide
*/
@RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
public static int listModulesAsOriginator(@NonNull ArrayList<ModuleProperties> modules,
@NonNull Identity originatorIdentity) {
try {
SoundTriggerModuleDescriptor[] descs = getService().listModules();
modules.clear();
modules.ensureCapacity(descs.length);
for (SoundTriggerModuleDescriptor desc : descs) {
modules.add(ConversionUtil.aidl2apiModuleDescriptor(desc));
}
SoundTriggerModuleDescriptor[] descs = getService().listModulesAsOriginator(
originatorIdentity);
convertDescriptorsToModuleProperties(descs, modules);
return STATUS_OK;
} catch (Exception e) {
return handleException(e);
}
}
/**
* Returns a list of descriptors for all hardware modules loaded.
* This variant is intended for use when the caller is acting on behalf of a different identity
* for permission purposes.
* @param modules A ModuleProperties array where the list will be returned.
* @param middlemanIdentity The identity of the caller, acting as middleman.
* @param originatorIdentity The identity of the originator, which will be used for permission
* purposes.
* @return - {@link #STATUS_OK} in case of success
* - {@link #STATUS_ERROR} in case of unspecified error
* - {@link #STATUS_PERMISSION_DENIED} if the caller does not have system permission
* - {@link #STATUS_NO_INIT} if the native service cannot be reached
* - {@link #STATUS_BAD_VALUE} if modules is null
* - {@link #STATUS_DEAD_OBJECT} if the binder transaction to the native service fails
*
* @hide
*/
@RequiresPermission(SOUNDTRIGGER_DELEGATE_IDENTITY)
public static int listModulesAsMiddleman(@NonNull ArrayList<ModuleProperties> modules,
@NonNull Identity middlemanIdentity,
@NonNull Identity originatorIdentity) {
try {
SoundTriggerModuleDescriptor[] descs = getService().listModulesAsMiddleman(
middlemanIdentity, originatorIdentity);
convertDescriptorsToModuleProperties(descs, modules);
return STATUS_OK;
} catch (Exception e) {
return handleException(e);
}
}
/**
* Converts an array of SoundTriggerModuleDescriptor into an (existing) ArrayList of
* ModuleProperties.
* @param descsIn The input descriptors.
* @param modulesOut The output list.
*/
private static void convertDescriptorsToModuleProperties(
@NonNull SoundTriggerModuleDescriptor[] descsIn,
@NonNull ArrayList<ModuleProperties> modulesOut) {
modulesOut.clear();
modulesOut.ensureCapacity(descsIn.length);
for (SoundTriggerModuleDescriptor desc : descsIn) {
modulesOut.add(ConversionUtil.aidl2apiModuleDescriptor(desc));
}
}
/**
* Get an interface on a hardware module to control sound models and recognition on
* this module.
@@ -2031,15 +2109,85 @@ public class SoundTrigger {
* is OK.
* @return a valid sound module in case of success or null in case of error.
*
* @deprecated Please use
* {@link #attachModuleAsOriginator(int, StatusListener, Handler, Identity)} or
* {@link #attachModuleAsMiddleman(int, StatusListener, Handler, Identity, Identity)}, based
* on whether the client is acting on behalf of its own identity or a separate identity.
* @hide
*/
@UnsupportedAppUsage
public static @NonNull SoundTriggerModule attachModule(int moduleId,
public static SoundTriggerModule attachModule(int moduleId,
@NonNull StatusListener listener,
@Nullable Handler handler) {
// TODO(ytai): This is a temporary hack to retain prior behavior, which makes
// assumptions about process affinity and Binder context, namely that the binder calling ID
// reliably reflects the originator identity.
Identity middlemanIdentity = new Identity();
middlemanIdentity.packageName = ActivityThread.currentOpPackageName();
Identity originatorIdentity = new Identity();
originatorIdentity.pid = Binder.getCallingPid();
originatorIdentity.uid = Binder.getCallingUid();
try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
return attachModuleAsMiddleman(moduleId, listener, handler, middlemanIdentity,
originatorIdentity);
}
}
/**
* Get an interface on a hardware module to control sound models and recognition on
* this module.
* This variant is intended for use when the caller is acting on behalf of a different identity
* for permission purposes.
* @param moduleId Sound module system identifier {@link ModuleProperties#mId}. mandatory.
* @param listener {@link StatusListener} interface. Mandatory.
* @param handler the Handler that will receive the callabcks. Can be null if default handler
* is OK.
* @param middlemanIdentity The identity of the caller, acting as middleman.
* @param originatorIdentity The identity of the originator, which will be used for permission
* purposes.
* @return a valid sound module in case of success or null in case of error.
*
* @hide
*/
@RequiresPermission(SOUNDTRIGGER_DELEGATE_IDENTITY)
public static SoundTriggerModule attachModuleAsMiddleman(int moduleId,
@NonNull SoundTrigger.StatusListener listener,
@Nullable Handler handler, Identity middlemanIdentity,
Identity originatorIdentity) {
Looper looper = handler != null ? handler.getLooper() : Looper.getMainLooper();
try {
return new SoundTriggerModule(getService(), moduleId, listener, looper);
return new SoundTriggerModule(getService(), moduleId, listener, looper,
middlemanIdentity, originatorIdentity);
} catch (Exception e) {
Log.e(TAG, "", e);
return null;
}
}
/**
* Get an interface on a hardware module to control sound models and recognition on
* this module.
* This variant is intended for use when the caller itself is the originator of the operation.
* @param moduleId Sound module system identifier {@link ModuleProperties#mId}. mandatory.
* @param listener {@link StatusListener} interface. Mandatory.
* @param handler the Handler that will receive the callabcks. Can be null if default handler
* is OK.
* @param originatorIdentity The identity of the originator, which will be used for permission
* purposes.
* @return a valid sound module in case of success or null in case of error.
*
* @hide
*/
@RequiresPermission(allOf = {RECORD_AUDIO, CAPTURE_AUDIO_HOTWORD})
public static SoundTriggerModule attachModuleAsOriginator(int moduleId,
@NonNull SoundTrigger.StatusListener listener,
@Nullable Handler handler, @NonNull Identity originatorIdentity) {
Looper looper = handler != null ? handler.getLooper() : Looper.getMainLooper();
try {
return new SoundTriggerModule(getService(), moduleId, listener, looper,
originatorIdentity);
} catch (Exception e) {
Log.e(TAG, "", e);
return null;

View File

@@ -19,6 +19,9 @@ package android.hardware.soundtrigger;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.media.permission.ClearCallingIdentityContext;
import android.media.permission.Identity;
import android.media.permission.SafeCloseable;
import android.media.soundtrigger_middleware.ISoundTriggerCallback;
import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService;
import android.media.soundtrigger_middleware.ISoundTriggerModule;
@@ -50,12 +53,39 @@ public class SoundTriggerModule {
private EventHandlerDelegate mEventHandlerDelegate;
private ISoundTriggerModule mService;
/**
* This variant is intended for use when the caller is acting an originator, rather than on
* behalf of a different entity, as far as authorization goes.
*/
SoundTriggerModule(@NonNull ISoundTriggerMiddlewareService service,
int moduleId, @NonNull SoundTrigger.StatusListener listener, @NonNull Looper looper)
int moduleId, @NonNull SoundTrigger.StatusListener listener, @NonNull Looper looper,
@NonNull Identity originatorIdentity)
throws RemoteException {
mId = moduleId;
mEventHandlerDelegate = new EventHandlerDelegate(listener, looper);
mService = service.attach(moduleId, mEventHandlerDelegate);
try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
mService = service.attachAsOriginator(moduleId, originatorIdentity,
mEventHandlerDelegate);
}
mService.asBinder().linkToDeath(mEventHandlerDelegate, 0);
}
/**
* This variant is intended for use when the caller is acting as a middleman, i.e. on behalf of
* a different entity, as far as authorization goes.
*/
SoundTriggerModule(@NonNull ISoundTriggerMiddlewareService service,
int moduleId, @NonNull SoundTrigger.StatusListener listener, @NonNull Looper looper,
@NonNull Identity middlemanIdentity, @NonNull Identity originatorIdentity)
throws RemoteException {
mId = moduleId;
mEventHandlerDelegate = new EventHandlerDelegate(listener, looper);
try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
mService = service.attachAsMiddleman(moduleId, middlemanIdentity, originatorIdentity,
mEventHandlerDelegate);
}
mService.asBinder().linkToDeath(mEventHandlerDelegate, 0);
}

View File

@@ -3972,6 +3972,15 @@
<permission android:name="android.permission.CAPTURE_AUDIO_HOTWORD"
android:protectionLevel="signature|privileged" />
<!-- Puts an application in the chain of trust for sound trigger
operations. Being in the chain of trust allows an application to
delegate an identity of a separate entity to the sound trigger system
and vouch for the authenticity of this identity.
<p>Not for use by third-party applications.</p>
@hide -->
<permission android:name="android.permission.SOUNDTRIGGER_DELEGATE_IDENTITY"
android:protectionLevel="signature|privileged" />
<!-- @SystemApi Allows an application to modify audio routing and override policy decisions.
<p>Not for use by third-party applications.</p>
@hide -->

View File

@@ -22,6 +22,25 @@ aidl_interface {
},
}
aidl_interface {
name: "media_permission-aidl",
unstable: true,
local_include_dir: "java",
srcs: [
"java/android/media/permission/Identity.aidl",
],
backend:
{
cpp: {
enabled: true,
},
java: {
// Already generated as part of the entire media java library.
enabled: false,
},
},
}
aidl_interface {
name: "soundtrigger_middleware-aidl",
unstable: true,
@@ -61,5 +80,8 @@ aidl_interface {
enabled: false,
},
},
imports: [ "audio_common-aidl" ],
imports: [
"audio_common-aidl",
"media_permission-aidl",
],
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (C) 2020 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 android.media.permission;
import android.annotation.NonNull;
import android.os.Binder;
/**
* An RAII-style object, used to establish a scope in which the binder calling identity is cleared.
*
* <p>
* Intended usage:
* <pre>
* void caller() {
* try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
* // Within this scope the binder calling identity is cleared.
* ...
* }
* // Outside the scope the calling identity is restored to its prior state.
* </pre>
*
* @hide
*/
public class ClearCallingIdentityContext implements SafeCloseable {
private final long mRestoreKey;
/**
* Creates a new instance.
* @return A {@link SafeCloseable}, intended to be used in a try-with-resource block.
*/
public static @NonNull
SafeCloseable create() {
return new ClearCallingIdentityContext();
}
private ClearCallingIdentityContext() {
mRestoreKey = Binder.clearCallingIdentity();
}
@Override
public void close() {
Binder.restoreCallingIdentity(mRestoreKey);
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright (C) 2020 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 android.media.permission;
import android.annotation.NonNull;
/**
* A composite {@link SafeCloseable}. Will close its children in reverse order.
*
* @hide
*/
class CompositeSafeCloseable implements SafeCloseable {
private final @NonNull SafeCloseable[] mChildren;
CompositeSafeCloseable(@NonNull SafeCloseable... children) {
mChildren = children;
}
@Override
public void close() {
// Close in reverse order.
for (int i = mChildren.length - 1; i >= 0; --i) {
mChildren[i].close();
}
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (C) 2020 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 android.media.permission;
/**
* A collection of identity-related information, required for permission enforcement.
*
* {@hide}
*/
parcelable Identity {
/** Linux user ID. */
int uid;
/** Linux process ID. */
int pid;
/** Package name. If null, the first package owned by the given uid will be assumed. */
@nullable String packageName;
/** Attribution tag. Mostly used for diagnostic purposes. */
@nullable String attributionTag;
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright (C) 2020 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 android.media.permission;
import android.annotation.NonNull;
import android.annotation.Nullable;
/**
* An RAII-style object, used to establish a scope in which a single identity is part of the
* context. This is used in order to avoid having to explicitly pass identity information through
* deep call-stacks.
* <p>
* Intended usage:
* <pre>
* void caller() {
* Identity originator = ...;
* try (SafeCloseable ignored = IdentityContext.create(originator)) {
* // Within this scope the context is established.
* callee();
* }
* // Outside the scope the context is restored to its prior state.
*
* void callee() {
* // Here we can access the identity without having to explicitly take it as an argument.
* // This is true even if this were a deeply nested call.
* Identity originator = IdentityContext.getNonNull();
* ...
* }
* </pre>
*
* @hide
*/
public class IdentityContext implements SafeCloseable {
private static ThreadLocal<Identity> sThreadLocalIdentity = new ThreadLocal<>();
private @Nullable Identity mPrior = get();
/**
* Create a scoped identity context.
*
* @param identity The identity to establish with the scope.
* @return A {@link SafeCloseable}, to be used in a try-with-resources block to establish a
* scope.
*/
public static @NonNull
SafeCloseable create(@Nullable Identity identity) {
return new IdentityContext(identity);
}
/**
* Get the current identity context.
*
* @return The identity, or null if it has not been established.
*/
public static @Nullable
Identity get() {
return sThreadLocalIdentity.get();
}
/**
* Get the current identity context. Throws a {@link NullPointerException} if it has not been
* established.
*
* @return The identity.
*/
public static @NonNull
Identity getNonNull() {
Identity result = get();
if (result == null) {
throw new NullPointerException("Identity context is not set");
}
return result;
}
private IdentityContext(@Nullable Identity identity) {
set(identity);
}
@Override
public void close() {
set(mPrior);
}
private static void set(@Nullable Identity identity) {
sThreadLocalIdentity.set(identity);
}
}

View File

@@ -0,0 +1,250 @@
/*
* Copyright (C) 2020 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 android.media.permission;
import android.annotation.NonNull;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.PermissionChecker;
import android.content.pm.PackageManager;
import android.os.Binder;
import java.util.Objects;
/**
* This module provides some utility methods for facilitating our permission enforcement patterns.
* <p>
* <h1>Intended usage:</h1>
* Close to the client-facing edge of the server, first authenticate the client, using {@link
* #establishIdentityDirect(Identity)}, or {@link #establishIdentityIndirect(Context, String,
* Identity, Identity)}, depending on whether the client is trying to authenticate as the
* originator or a middleman. Those methods will establish a scope with the originator in the
* {@link android.media.permission.IdentityContext} and a cleared binder calling identity.
* Typically there would be two distinct API methods for the two different options, and typically
* those API methods would be used to establish a client session which is associated with the
* originator for the lifetime of the session.
* <p>
* When performing an operation that requires permissions, use {@link
* #checkPermissionForPreflight(Context, Identity, String)} or {@link
* #checkPermissionForDataDelivery(Context, Identity, String, String)} on the originator
* identity. Note that this won't typically be the identity pulled from the {@link
* android.media.permission.IdentityContext}, since we are working with a session-based approach,
* the originator identity will be established once upon creation of a session, and then all
* interactions with this session will using the identity attached to the session. This also covers
* performing checks prior to invoking client callbacks for data delivery.
*
* @hide
*/
public class PermissionUtil {
/**
* Authenticate an originator, where the binder call is coming from a middleman.
*
* The middleman is expected to hold a special permission to act as such, or else a
* {@link SecurityException} will be thrown. If the call succeeds:
* <ul>
* <li>The passed middlemanIdentity argument will have its uid/pid fields overridden with
* those provided by binder.
* <li>An {@link SafeCloseable} is returned, used to established a scope in which the
* originator identity is available via {@link android.media.permission.IdentityContext}
* and in which the binder
* calling ID is cleared.
* </ul>
* Example usage:
* <pre>
* try (SafeCloseable ignored = PermissionUtil.establishIdentityIndirect(...)) {
* // Within this scope we have the identity context established, and the binder calling
* // identity cleared.
* ...
* Identity originator = IdentityContext.getNonNull();
* ...
* }
* // outside the scope, everything is back to the prior state.
* </pre>
* <p>
* <b>Important note:</b> The binder calling ID will be used to securely establish the identity
* of the middleman. However, if the middleman is on the same process as the server,
* the middleman must remember to clear the binder calling identity, or else the binder calling
* ID will reflect the process calling into the middleman, not the middleman process itself. If
* the middleman itself is using this API, this is typically not an issue, since this method
* will take care of that.
*
* @param context A {@link Context}, used for permission checks.
* @param middlemanPermission The permission that will be checked in order to authorize the
* middleman to act as such (i.e. be trusted to convey the
* originator
* identity reliably).
* @param middlemanIdentity The identity of the middleman.
* @param originatorIdentity The identity of the originator.
* @return A {@link SafeCloseable}, used to establish a scope, as mentioned above.
*/
public static @NonNull
SafeCloseable establishIdentityIndirect(
@NonNull Context context,
@NonNull String middlemanPermission,
@NonNull Identity middlemanIdentity,
@NonNull Identity originatorIdentity) {
Objects.requireNonNull(context);
Objects.requireNonNull(middlemanPermission);
Objects.requireNonNull(middlemanIdentity);
Objects.requireNonNull(originatorIdentity);
// Override uid/pid with the secure values provided by binder.
middlemanIdentity.pid = Binder.getCallingPid();
middlemanIdentity.uid = Binder.getCallingUid();
// Authorize middleman to delegate identity.
context.enforcePermission(middlemanPermission, middlemanIdentity.pid,
middlemanIdentity.uid,
String.format("Middleman must have the %s permision.", middlemanPermission));
return new CompositeSafeCloseable(IdentityContext.create(originatorIdentity),
ClearCallingIdentityContext.create());
}
/**
* Authenticate an originator, where the binder call is coming directly from the originator.
*
* If the call succeeds:
* <ul>
* <li>The passed originatorIdentity argument will have its uid/pid fields overridden with
* those provided by binder.
* <li>A {@link SafeCloseable} is returned, used to established a scope in which the
* originator identity is available via {@link IdentityContext} and in which the binder
* calling ID is cleared.
* </ul>
* Example usage:
* <pre>
* try (AutoClosable ignored = PermissionUtil.establishIdentityDirect(...)) {
* // Within this scope we have the identity context established, and the binder calling
* // identity cleared.
* ...
* Identity originator = IdentityContext.getNonNull();
* ...
* }
* // outside the scope, everything is back to the prior state.
* </pre>
* <p>
* <b>Important note:</b> The binder calling ID will be used to securely establish the identity
* of the client. However, if the client is on the same process as the server, and is itself a
* binder server, it must remember to clear the binder calling identity, or else the binder
* calling ID will reflect the process calling into the client, not the client process itself.
* If the client itself is using this API, this is typically not an issue, since this method
* will take care of that.
*
* @param originatorIdentity The identity of the originator.
* @return A {@link SafeCloseable}, used to establish a scope, as mentioned above.
*/
public static @NonNull
SafeCloseable establishIdentityDirect(@NonNull Identity originatorIdentity) {
Objects.requireNonNull(originatorIdentity);
originatorIdentity.uid = Binder.getCallingUid();
originatorIdentity.pid = Binder.getCallingPid();
return new CompositeSafeCloseable(
IdentityContext.create(originatorIdentity),
ClearCallingIdentityContext.create());
}
/**
* Checks whether the given identity has the given permission to receive data.
*
* @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.
* @param reason The reason why we're requesting the permission, for auditing purposes.
* @return The permission check result which is either
* {@link PermissionChecker#PERMISSION_GRANTED}
* or {@link PermissionChecker#PERMISSION_SOFT_DENIED} or
* {@link PermissionChecker#PERMISSION_HARD_DENIED}.
*/
public static int checkPermissionForDataDelivery(@NonNull Context context,
@NonNull Identity identity,
@NonNull String permission,
@NonNull String reason) {
return PermissionChecker.checkPermissionForDataDelivery(context, permission,
identity.pid, identity.uid, identity.packageName, identity.attributionTag,
reason);
}
/**
* Checks whether the given identity has the given permission to receive data.
*
* This variant ignores the proc-state for the sake of the check, i.e. overrides any
* restrictions that apply specifically to apps running in the background.
*
* TODO(ytai): This is a temporary hack until we have permissions that are specifically intended
* for background microphone access.
*
* @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.
* @param reason The reason why we're requesting the permission, for auditing purposes.
* @return The permission check result which is either
* {@link PermissionChecker#PERMISSION_GRANTED}
* or {@link PermissionChecker#PERMISSION_SOFT_DENIED} or
* {@link PermissionChecker#PERMISSION_HARD_DENIED}.
*/
public static int checkPermissionForDataDeliveryIgnoreProcState(@NonNull Context context,
@NonNull Identity identity,
@NonNull String permission,
@NonNull String reason) {
if (context.checkPermission(permission, identity.pid, identity.uid)
!= PackageManager.PERMISSION_GRANTED) {
return PermissionChecker.PERMISSION_HARD_DENIED;
}
String appOpOfPermission = AppOpsManager.permissionToOp(permission);
if (appOpOfPermission == null) {
// not platform defined
return PermissionChecker.PERMISSION_GRANTED;
}
String packageName = identity.packageName;
if (packageName == null) {
String[] packageNames = context.getPackageManager().getPackagesForUid(identity.uid);
if (packageNames != null && packageNames.length > 0) {
packageName = packageNames[0];
}
}
final AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class);
int appOpMode = appOpsManager.unsafeCheckOpRawNoThrow(appOpOfPermission, identity.uid,
packageName);
if (appOpMode == AppOpsManager.MODE_ALLOWED) {
return PermissionChecker.PERMISSION_GRANTED;
}
return PermissionChecker.PERMISSION_SOFT_DENIED;
}
/**
* Checks whether the given identity has 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.
* @return The permission check result which is either
* {@link PermissionChecker#PERMISSION_GRANTED}
* or {@link PermissionChecker#PERMISSION_SOFT_DENIED} or
* {@link PermissionChecker#PERMISSION_HARD_DENIED}.
*/
public static int checkPermissionForPreflight(@NonNull Context context,
@NonNull Identity identity,
@NonNull String permission) {
return PermissionChecker.checkPermissionForPreflight(context, permission,
identity.pid, identity.uid, identity.packageName);
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright (C) 2020 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 android.media.permission;
/**
* An {@link AutoCloseable} that doesn't throw on {@link #close()}.
*
* @hide
*/
public interface SafeCloseable extends AutoCloseable {
@Override
void close();
}

View File

@@ -15,6 +15,7 @@
*/
package android.media.soundtrigger_middleware;
import android.media.permission.Identity;
import android.media.soundtrigger_middleware.ISoundTriggerModule;
import android.media.soundtrigger_middleware.ISoundTriggerCallback;
import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor;
@@ -30,13 +31,59 @@ import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor;
interface ISoundTriggerMiddlewareService {
/**
* Query the available modules and their capabilities.
*
* This variant is intended for use by the originator of the operations for permission
* enforcement purposes. The provided identity's uid/pid fields will be ignored and overridden
* by the ones provided by Binder.getCallingUid() / Binder.getCallingPid().
*/
SoundTriggerModuleDescriptor[] listModules();
SoundTriggerModuleDescriptor[] listModulesAsOriginator(in Identity identity);
/**
* Query the available modules and their capabilities.
*
* This variant is intended for use by a trusted "middleman", acting on behalf of some identity
* other than itself. The caller must provide:
* - Its own identity, which will be used to establish trust via the
* SOUNDTRIGGER_DELEGATE_IDENTITY permission. This identity's uid/pid fields will be ignored
* and overridden by the ones provided by Binder.getCallingUid() / Binder.getCallingPid().
* This implies that the caller must clear its caller identity to protect from the case where
* it resides in the same process as the callee.
* - The identity of the entity on behalf of which module operations are to be performed.
*/
SoundTriggerModuleDescriptor[] listModulesAsMiddleman(in Identity middlemanIdentity,
in Identity originatorIdentity);
/**
* Attach to one of the available modules.
*
* This variant is intended for use by the originator of the operations for permission
* enforcement purposes. The provided identity's uid/pid fields will be ignored and overridden
* by the ones provided by Binder.getCallingUid() / Binder.getCallingPid().
*
* listModules() must be called prior to calling this method and the provided handle must be
* one of the handles from the returned list.
*/
ISoundTriggerModule attach(int handle, ISoundTriggerCallback callback);
}
ISoundTriggerModule attachAsOriginator(int handle,
in Identity identity,
ISoundTriggerCallback callback);
/**
* Attach to one of the available modules.
*
* This variant is intended for use by a trusted "middleman", acting on behalf of some identity
* other than itself. The caller must provide:
* - Its own identity, which will be used to establish trust via the
* SOUNDTRIGGER_DELEGATE_IDENTITY permission. This identity's uid/pid fields will be ignored
* and overridden by the ones provided by Binder.getCallingUid() / Binder.getCallingPid().
* This implies that the caller must clear its caller identity to protect from the case where
* it resides in the same process as the callee.
* - The identity of the entity on behalf of which module operations are to be performed.
*
* listModules() must be called prior to calling this method and the provided handle must be
* one of the handles from the returned list.
*/
ISoundTriggerModule attachAsMiddleman(int handle,
in Identity middlemanIdentity,
in Identity originatorIdentity,
ISoundTriggerCallback callback);
}

View File

@@ -17,11 +17,28 @@
package com.android.server.soundtrigger_middleware;
import android.media.ICaptureStateListener;
import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService;
import android.media.soundtrigger_middleware.ISoundTriggerCallback;
import android.media.soundtrigger_middleware.ISoundTriggerModule;
import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor;
/**
* This interface unifies ISoundTriggerMiddlewareService with ICaptureStateListener.
* This interface unifies methods from ISoundTriggerMiddlewareService and ICaptureStateListener.
*
* The ISoundTriggerMiddlewareService have been modified to exclude identity information and the
* RemoteException signature, both of which are only relevant at the service boundary layer.
*/
public interface ISoundTriggerMiddlewareInternal extends ISoundTriggerMiddlewareService,
ICaptureStateListener {
public interface ISoundTriggerMiddlewareInternal extends ICaptureStateListener {
/**
* Query the available modules and their capabilities.
*/
public SoundTriggerModuleDescriptor[] listModules();
/**
* Attach to one of the available modules.
*
* listModules() must be called prior to calling this method and the provided handle must be
* one of the handles from the returned list.
*/
public ISoundTriggerModule attach(int handle,
ISoundTriggerCallback callback);
}

View File

@@ -18,8 +18,9 @@ package com.android.server.soundtrigger_middleware;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.media.permission.Identity;
import android.media.permission.IdentityContext;
import android.media.soundtrigger_middleware.ISoundTriggerCallback;
import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService;
import android.media.soundtrigger_middleware.ISoundTriggerModule;
import android.media.soundtrigger_middleware.ModelParameterRange;
import android.media.soundtrigger_middleware.PhraseRecognitionEvent;
@@ -28,17 +29,15 @@ import android.media.soundtrigger_middleware.RecognitionConfig;
import android.media.soundtrigger_middleware.RecognitionEvent;
import android.media.soundtrigger_middleware.SoundModel;
import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor;
import android.os.Binder;
import android.os.IBinder;
import android.os.Parcelable;
import android.os.RemoteException;
import android.util.Log;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedList;
import java.util.Objects;
/**
* An ISoundTriggerMiddlewareService decorator, which adds logging of all API calls (and
@@ -71,7 +70,8 @@ public class SoundTriggerMiddlewareLogging implements ISoundTriggerMiddlewareInt
}
@Override
public @NonNull SoundTriggerModuleDescriptor[] listModules() throws RemoteException {
public @NonNull
SoundTriggerModuleDescriptor[] listModules() {
try {
SoundTriggerModuleDescriptor[] result = mDelegate.listModules();
logReturn("listModules", result);
@@ -83,12 +83,13 @@ public class SoundTriggerMiddlewareLogging implements ISoundTriggerMiddlewareInt
}
@Override
public @NonNull ISoundTriggerModule attach(int handle, ISoundTriggerCallback callback)
throws RemoteException {
public @NonNull
ISoundTriggerModule attach(int handle, ISoundTriggerCallback callback) {
try {
ISoundTriggerModule result = mDelegate.attach(handle, new CallbackLogging(callback));
ModuleLogging result = new ModuleLogging(callback);
result.attach(mDelegate.attach(handle, result.getCallbackWrapper()));
logReturn("attach", result, handle, callback);
return new ModuleLogging(result);
return result;
} catch (Exception e) {
logException("attach", e, handle, callback);
throw e;
@@ -106,7 +107,8 @@ public class SoundTriggerMiddlewareLogging implements ISoundTriggerMiddlewareInt
}
}
@Override public IBinder asBinder() {
@Override
public IBinder asBinder() {
throw new UnsupportedOperationException(
"This implementation is not inteded to be used directly with Binder.");
}
@@ -118,96 +120,35 @@ public class SoundTriggerMiddlewareLogging implements ISoundTriggerMiddlewareInt
}
private void logException(String methodName, Exception ex, Object... args) {
logExceptionWithObject(this, methodName, ex, args);
logExceptionWithObject(this, IdentityContext.get(), methodName, ex, args);
}
private void logReturn(String methodName, Object retVal, Object... args) {
logReturnWithObject(this, methodName, retVal, args);
logReturnWithObject(this, IdentityContext.get(), methodName, retVal, args);
}
private void logVoidReturn(String methodName, Object... args) {
logVoidReturnWithObject(this, methodName, args);
}
private class CallbackLogging implements ISoundTriggerCallback {
private final ISoundTriggerCallback mDelegate;
private CallbackLogging(ISoundTriggerCallback delegate) {
mDelegate = delegate;
}
@Override
public void onRecognition(int modelHandle, RecognitionEvent event) throws RemoteException {
try {
mDelegate.onRecognition(modelHandle, event);
logVoidReturn("onRecognition", modelHandle, event);
} catch (Exception e) {
logException("onRecognition", e, modelHandle, event);
throw e;
}
}
@Override
public void onPhraseRecognition(int modelHandle, PhraseRecognitionEvent event)
throws RemoteException {
try {
mDelegate.onPhraseRecognition(modelHandle, event);
logVoidReturn("onPhraseRecognition", modelHandle, event);
} catch (Exception e) {
logException("onPhraseRecognition", e, modelHandle, event);
throw e;
}
}
@Override
public void onRecognitionAvailabilityChange(boolean available) throws RemoteException {
try {
mDelegate.onRecognitionAvailabilityChange(available);
logVoidReturn("onRecognitionAvailabilityChange", available);
} catch (Exception e) {
logException("onRecognitionAvailabilityChange", e, available);
throw e;
}
}
@Override
public void onModuleDied() throws RemoteException {
try {
mDelegate.onModuleDied();
logVoidReturn("onModuleDied");
} catch (Exception e) {
logException("onModuleDied", e);
throw e;
}
}
private void logException(String methodName, Exception ex, Object... args) {
logExceptionWithObject(this, methodName, ex, args);
}
private void logVoidReturn(String methodName, Object... args) {
logVoidReturnWithObject(this, methodName, args);
}
@Override
public IBinder asBinder() {
return mDelegate.asBinder();
}
// Override toString() in order to have the delegate's ID in it.
@Override
public String toString() {
return mDelegate.toString();
}
logVoidReturnWithObject(this, IdentityContext.get(), methodName, args);
}
private class ModuleLogging implements ISoundTriggerModule {
private final ISoundTriggerModule mDelegate;
private ISoundTriggerModule mDelegate;
private final @NonNull CallbackLogging mCallbackWrapper;
private final @NonNull Identity mOriginatorIdentity;
private ModuleLogging(ISoundTriggerModule delegate) {
ModuleLogging(@NonNull ISoundTriggerCallback callback) {
mCallbackWrapper = new CallbackLogging(callback);
mOriginatorIdentity = IdentityContext.getNonNull();
}
void attach(@NonNull ISoundTriggerModule delegate) {
mDelegate = delegate;
}
ISoundTriggerCallback getCallbackWrapper() {
return mCallbackWrapper;
}
@Override
public int loadModel(SoundModel model) throws RemoteException {
try {
@@ -334,19 +275,92 @@ public class SoundTriggerMiddlewareLogging implements ISoundTriggerMiddlewareInt
// Override toString() in order to have the delegate's ID in it.
@Override
public String toString() {
return mDelegate.toString();
return Objects.toString(mDelegate);
}
private void logException(String methodName, Exception ex, Object... args) {
logExceptionWithObject(this, methodName, ex, args);
logExceptionWithObject(this, mOriginatorIdentity, methodName, ex, args);
}
private void logReturn(String methodName, Object retVal, Object... args) {
logReturnWithObject(this, methodName, retVal, args);
logReturnWithObject(this, mOriginatorIdentity, methodName, retVal, args);
}
private void logVoidReturn(String methodName, Object... args) {
logVoidReturnWithObject(this, methodName, args);
logVoidReturnWithObject(this, mOriginatorIdentity, methodName, args);
}
private class CallbackLogging implements ISoundTriggerCallback {
private final ISoundTriggerCallback mCallbackDelegate;
private CallbackLogging(ISoundTriggerCallback delegate) {
mCallbackDelegate = delegate;
}
@Override
public void onRecognition(int modelHandle, RecognitionEvent event)
throws RemoteException {
try {
mCallbackDelegate.onRecognition(modelHandle, event);
logVoidReturn("onRecognition", modelHandle, event);
} catch (Exception e) {
logException("onRecognition", e, modelHandle, event);
throw e;
}
}
@Override
public void onPhraseRecognition(int modelHandle, PhraseRecognitionEvent event)
throws RemoteException {
try {
mCallbackDelegate.onPhraseRecognition(modelHandle, event);
logVoidReturn("onPhraseRecognition", modelHandle, event);
} catch (Exception e) {
logException("onPhraseRecognition", e, modelHandle, event);
throw e;
}
}
@Override
public void onRecognitionAvailabilityChange(boolean available) throws RemoteException {
try {
mCallbackDelegate.onRecognitionAvailabilityChange(available);
logVoidReturn("onRecognitionAvailabilityChange", available);
} catch (Exception e) {
logException("onRecognitionAvailabilityChange", e, available);
throw e;
}
}
@Override
public void onModuleDied() throws RemoteException {
try {
mCallbackDelegate.onModuleDied();
logVoidReturn("onModuleDied");
} catch (Exception e) {
logException("onModuleDied", e);
throw e;
}
}
private void logException(String methodName, Exception ex, Object... args) {
logExceptionWithObject(this, mOriginatorIdentity, methodName, ex, args);
}
private void logVoidReturn(String methodName, Object... args) {
logVoidReturnWithObject(this, mOriginatorIdentity, methodName, args);
}
@Override
public IBinder asBinder() {
return mCallbackDelegate.asBinder();
}
// Override toString() in order to have the delegate's ID in it.
@Override
public String toString() {
return Objects.toString(mCallbackDelegate);
}
}
}
@@ -386,34 +400,37 @@ public class SoundTriggerMiddlewareLogging implements ISoundTriggerMiddlewareInt
return builder.toString();
}
private void logReturnWithObject(@NonNull Object object, String methodName,
private void logReturnWithObject(@NonNull Object object, @Nullable Identity originatorIdentity,
String methodName,
@Nullable Object retVal,
@NonNull Object[] args) {
final String message = String.format("%s[this=%s, caller=%d/%d](%s) -> %s", methodName,
final String message = String.format("%s[this=%s, client=%s](%s) -> %s", methodName,
object,
Binder.getCallingUid(), Binder.getCallingPid(),
printObject(originatorIdentity),
printArgs(args),
printObject(retVal));
Log.i(TAG, message);
appendMessage(message);
}
private void logVoidReturnWithObject(@NonNull Object object, @NonNull String methodName,
private void logVoidReturnWithObject(@NonNull Object object,
@Nullable Identity originatorIdentity, @NonNull String methodName,
@NonNull Object[] args) {
final String message = String.format("%s[this=%s, caller=%d/%d](%s)", methodName,
final String message = String.format("%s[this=%s, client=%s](%s)", methodName,
object,
Binder.getCallingUid(), Binder.getCallingPid(),
printObject(originatorIdentity),
printArgs(args));
Log.i(TAG, message);
appendMessage(message);
}
private void logExceptionWithObject(@NonNull Object object, @NonNull String methodName,
private void logExceptionWithObject(@NonNull Object object,
@Nullable Identity originatorIdentity, @NonNull String methodName,
@NonNull Exception ex,
Object[] args) {
final String message = String.format("%s[this=%s, caller=%d/%d](%s) threw", methodName,
final String message = String.format("%s[this=%s, client=%s](%s) threw", methodName,
object,
Binder.getCallingUid(), Binder.getCallingPid(),
printObject(originatorIdentity),
printArgs(args));
Log.e(TAG, message, ex);
appendMessage(message + " " + ex.toString());
@@ -429,7 +446,8 @@ public class SoundTriggerMiddlewareLogging implements ISoundTriggerMiddlewareInt
}
}
@Override public void dump(PrintWriter pw) {
@Override
public void dump(PrintWriter pw) {
pw.println();
pw.println("=========================================");
pw.println("Last events");

View File

@@ -16,11 +16,16 @@
package com.android.server.soundtrigger_middleware;
import android.Manifest;
import static android.Manifest.permission.CAPTURE_AUDIO_HOTWORD;
import static android.Manifest.permission.RECORD_AUDIO;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.PermissionChecker;
import android.media.permission.Identity;
import android.media.permission.IdentityContext;
import android.media.permission.PermissionUtil;
import android.media.soundtrigger_middleware.ISoundTriggerCallback;
import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService;
import android.media.soundtrigger_middleware.ISoundTriggerModule;
@@ -42,12 +47,12 @@ import java.util.Objects;
/**
* This is a decorator of an {@link ISoundTriggerMiddlewareService}, which enforces permissions.
* <p>
* Every public method in this class, overriding an interface method, must follow the following
* Every public method in this class, overriding an interface method, must follow a similar
* pattern:
* <code><pre>
* @Override public T method(S arg) {
* // Permission check.
* checkPermissions();
* enforcePermissions*(...);
* return mDelegate.method(arg);
* }
* </pre></code>
@@ -68,18 +73,20 @@ public class SoundTriggerMiddlewarePermission implements ISoundTriggerMiddleware
@Override
public @NonNull
SoundTriggerModuleDescriptor[] listModules() throws RemoteException {
checkPermissions();
SoundTriggerModuleDescriptor[] listModules() {
Identity identity = getIdentity();
enforcePermissionsForPreflight(identity);
return mDelegate.listModules();
}
@Override
public @NonNull
ISoundTriggerModule attach(int handle,
@NonNull ISoundTriggerCallback callback) throws RemoteException {
checkPermissions();
return new ModuleWrapper(
mDelegate.attach(handle, new CallbackWrapper(callback)));
@NonNull ISoundTriggerCallback callback) {
Identity identity = getIdentity();
enforcePermissionsForPreflight(identity);
ModuleWrapper wrapper = new ModuleWrapper(identity, callback);
return wrapper.attach(mDelegate.attach(handle, wrapper.getCallbackWrapper()));
}
@Override
@@ -91,7 +98,7 @@ public class SoundTriggerMiddlewarePermission implements ISoundTriggerMiddleware
// Override toString() in order to have the delegate's ID in it.
@Override
public String toString() {
return mDelegate.toString();
return Objects.toString(mDelegate);
}
@Override
@@ -101,39 +108,89 @@ public class SoundTriggerMiddlewarePermission implements ISoundTriggerMiddleware
}
/**
* Throws a {@link SecurityException} if caller permanently doesn't have the given permission,
* or a {@link ServiceSpecificException} with a {@link Status#TEMPORARY_PERMISSION_DENIED} if
* caller temporarily doesn't have the right permissions to use this service.
* Get the identity context, or throws an InternalServerError if it has not been established.
*
* @return The identity.
*/
private void checkPermissions() {
enforcePermission(Manifest.permission.RECORD_AUDIO);
enforcePermission(Manifest.permission.CAPTURE_AUDIO_HOTWORD);
private static @NonNull
Identity getIdentity() {
return IdentityContext.getNonNull();
}
/**
* Throws a {@link SecurityException} if caller permanently doesn't have the given permission,
* Throws a {@link SecurityException} if originator permanently doesn't have the given
* permission,
* or a {@link ServiceSpecificException} with a {@link Status#TEMPORARY_PERMISSION_DENIED} if
* caller temporarily doesn't have the given permission.
*
* @param permission The permission to check.
* originator temporarily doesn't have the right permissions to use this service.
*/
private void enforcePermission(String permission) {
final int status = PermissionChecker.checkCallingOrSelfPermissionForPreflight(mContext,
private void enforcePermissionsForPreflight(@NonNull Identity identity) {
enforcePermissionForPreflight(mContext, identity, RECORD_AUDIO);
enforcePermissionForPreflight(mContext, identity, CAPTURE_AUDIO_HOTWORD);
}
/**
* Throws a {@link SecurityException} iff the originator has permission to receive data.
*/
void enforcePermissionsForDataDelivery(@NonNull Identity identity, @NonNull String reason) {
enforcePermissionForDataDelivery(mContext, identity, RECORD_AUDIO, reason);
enforcePermissionForDataDelivery(mContext, identity, CAPTURE_AUDIO_HOTWORD,
reason);
}
/**
* Throws a {@link SecurityException} iff the given identity has given permission to receive
* data.
*
* @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.
* @param reason The reason why we're requesting the permission, for auditing purposes.
*/
private static void enforcePermissionForDataDelivery(@NonNull Context context,
@NonNull Identity identity,
@NonNull String permission, @NonNull String reason) {
// TODO(ytai): We're temporarily ignoring proc state until we have a proper permission that
// represents being able to use the microphone in the background. Otherwise, some of our
// existing use-cases would break.
final int status = PermissionUtil.checkPermissionForDataDeliveryIgnoreProcState(context,
identity, permission, reason);
if (status != PermissionChecker.PERMISSION_GRANTED) {
throw new SecurityException(
String.format("Failed to obtain permission %s for identity %s", permission,
ObjectPrinter.print(identity, true, 16)));
}
}
/**
* Throws a {@link SecurityException} if originator permanently doesn't have the given
* permission, or a {@link ServiceSpecificException} with a {@link
* Status#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(
String.format("Caller must have the %s permission.", permission));
String.format("Failed to obtain permission %s for identity %s", permission,
ObjectPrinter.print(identity, true, 16)));
case PermissionChecker.PERMISSION_SOFT_DENIED:
throw new ServiceSpecificException(Status.TEMPORARY_PERMISSION_DENIED,
String.format("Caller must have the %s permission.", permission));
String.format("Failed to obtain permission %s for identity %s", permission,
ObjectPrinter.print(identity, true, 16)));
default:
throw new RuntimeException("Unexpected perimission check result.");
}
}
@Override
public void dump(PrintWriter pw) {
if (mDelegate instanceof Dumpable) {
@@ -146,27 +203,40 @@ public class SoundTriggerMiddlewarePermission implements ISoundTriggerMiddleware
* mentioned in {@link SoundTriggerModule} above. This class follows the same conventions.
*/
private class ModuleWrapper extends ISoundTriggerModule.Stub {
private final ISoundTriggerModule mDelegate;
private ISoundTriggerModule mDelegate;
private final @NonNull Identity mOriginatorIdentity;
private final @NonNull CallbackWrapper mCallbackWrapper;
ModuleWrapper(@NonNull ISoundTriggerModule delegate) {
ModuleWrapper(@NonNull Identity originatorIdentity,
@NonNull ISoundTriggerCallback callback) {
mOriginatorIdentity = originatorIdentity;
mCallbackWrapper = new CallbackWrapper(callback);
}
ModuleWrapper attach(@NonNull ISoundTriggerModule delegate) {
mDelegate = delegate;
return this;
}
ISoundTriggerCallback getCallbackWrapper() {
return mCallbackWrapper;
}
@Override
public int loadModel(@NonNull SoundModel model) throws RemoteException {
checkPermissions();
enforcePermissions();
return mDelegate.loadModel(model);
}
@Override
public int loadPhraseModel(@NonNull PhraseSoundModel model) throws RemoteException {
checkPermissions();
enforcePermissions();
return mDelegate.loadPhraseModel(model);
}
@Override
public void unloadModel(int modelHandle) throws RemoteException {
checkPermissions();
enforcePermissions();
mDelegate.unloadModel(modelHandle);
}
@@ -174,32 +244,32 @@ public class SoundTriggerMiddlewarePermission implements ISoundTriggerMiddleware
@Override
public void startRecognition(int modelHandle, @NonNull RecognitionConfig config)
throws RemoteException {
checkPermissions();
enforcePermissions();
mDelegate.startRecognition(modelHandle, config);
}
@Override
public void stopRecognition(int modelHandle) throws RemoteException {
checkPermissions();
enforcePermissions();
mDelegate.stopRecognition(modelHandle);
}
@Override
public void forceRecognitionEvent(int modelHandle) throws RemoteException {
checkPermissions();
enforcePermissions();
mDelegate.forceRecognitionEvent(modelHandle);
}
@Override
public void setModelParameter(int modelHandle, int modelParam, int value)
throws RemoteException {
checkPermissions();
enforcePermissions();
mDelegate.setModelParameter(modelHandle, modelParam, value);
}
@Override
public int getModelParameter(int modelHandle, int modelParam) throws RemoteException {
checkPermissions();
enforcePermissions();
return mDelegate.getModelParameter(modelHandle, modelParam);
}
@@ -207,14 +277,14 @@ public class SoundTriggerMiddlewarePermission implements ISoundTriggerMiddleware
@Nullable
public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam)
throws RemoteException {
checkPermissions();
enforcePermissions();
return mDelegate.queryModelParameterSupport(modelHandle,
modelParam);
}
@Override
public void detach() throws RemoteException {
checkPermissions();
enforcePermissions();
mDelegate.detach();
}
@@ -223,45 +293,56 @@ public class SoundTriggerMiddlewarePermission implements ISoundTriggerMiddleware
public String toString() {
return Objects.toString(mDelegate);
}
}
private class CallbackWrapper implements ISoundTriggerCallback {
private final ISoundTriggerCallback mDelegate;
private CallbackWrapper(ISoundTriggerCallback delegate) {
mDelegate = delegate;
private void enforcePermissions() {
enforcePermissionsForPreflight(mOriginatorIdentity);
}
@Override
public void onRecognition(int modelHandle, RecognitionEvent event) throws RemoteException {
mDelegate.onRecognition(modelHandle, event);
}
private class CallbackWrapper implements ISoundTriggerCallback {
private final ISoundTriggerCallback mDelegate;
@Override
public void onPhraseRecognition(int modelHandle, PhraseRecognitionEvent event)
throws RemoteException {
mDelegate.onPhraseRecognition(modelHandle, event);
}
private CallbackWrapper(ISoundTriggerCallback delegate) {
mDelegate = delegate;
}
@Override
public void onRecognitionAvailabilityChange(boolean available) throws RemoteException {
mDelegate.onRecognitionAvailabilityChange(available);
}
@Override
public void onRecognition(int modelHandle, RecognitionEvent event)
throws RemoteException {
enforcePermissions("Sound trigger recognition.");
mDelegate.onRecognition(modelHandle, event);
}
@Override
public void onModuleDied() throws RemoteException {
mDelegate.onModuleDied();
}
@Override
public void onPhraseRecognition(int modelHandle, PhraseRecognitionEvent event)
throws RemoteException {
enforcePermissions("Sound trigger phrase recognition.");
mDelegate.onPhraseRecognition(modelHandle, event);
}
@Override
public IBinder asBinder() {
return mDelegate.asBinder();
}
@Override
public void onRecognitionAvailabilityChange(boolean available) throws RemoteException {
mDelegate.onRecognitionAvailabilityChange(available);
}
// Override toString() in order to have the delegate's ID in it.
@Override
public String toString() {
return Objects.toString(mDelegate);
@Override
public void onModuleDied() throws RemoteException {
mDelegate.onModuleDied();
}
@Override
public IBinder asBinder() {
return mDelegate.asBinder();
}
// Override toString() in order to have the delegate's ID in it.
@Override
public String toString() {
return mDelegate.toString();
}
private void enforcePermissions(String reason) {
enforcePermissionsForDataDelivery(mOriginatorIdentity, reason);
}
}
}
}

View File

@@ -16,9 +16,15 @@
package com.android.server.soundtrigger_middleware;
import static android.Manifest.permission.SOUNDTRIGGER_DELEGATE_IDENTITY;
import android.annotation.NonNull;
import android.content.Context;
import android.hardware.soundtrigger.V2_0.ISoundTriggerHw;
import android.media.permission.ClearCallingIdentityContext;
import android.media.permission.Identity;
import android.media.permission.PermissionUtil;
import android.media.permission.SafeCloseable;
import android.media.soundtrigger_middleware.ISoundTriggerCallback;
import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService;
import android.media.soundtrigger_middleware.ISoundTriggerModule;
@@ -62,15 +68,17 @@ import java.util.Objects;
public class SoundTriggerMiddlewareService extends ISoundTriggerMiddlewareService.Stub {
static private final String TAG = "SoundTriggerMiddlewareService";
@NonNull
private final ISoundTriggerMiddlewareInternal mDelegate;
private final @NonNull ISoundTriggerMiddlewareInternal mDelegate;
private final @NonNull Context mContext;
/**
* Constructor for internal use only. Could be exposed for testing purposes in the future.
* Users should access this class via {@link Lifecycle}.
*/
private SoundTriggerMiddlewareService(@NonNull ISoundTriggerMiddlewareInternal delegate) {
private SoundTriggerMiddlewareService(@NonNull ISoundTriggerMiddlewareInternal delegate,
@NonNull Context context) {
mDelegate = Objects.requireNonNull(delegate);
mContext = context;
new ExternalCaptureStateTracker(active -> {
try {
mDelegate.setCaptureState(active);
@@ -81,16 +89,37 @@ public class SoundTriggerMiddlewareService extends ISoundTriggerMiddlewareServic
}
@Override
public @NonNull
SoundTriggerModuleDescriptor[] listModules() throws RemoteException {
return mDelegate.listModules();
public SoundTriggerModuleDescriptor[] listModulesAsOriginator(Identity identity) {
try (SafeCloseable ignored = establishIdentityDirect(identity)) {
return mDelegate.listModules();
}
}
@Override
public @NonNull
ISoundTriggerModule attach(int handle, @NonNull ISoundTriggerCallback callback)
throws RemoteException {
return new ModuleService(mDelegate.attach(handle, callback));
public SoundTriggerModuleDescriptor[] listModulesAsMiddleman(Identity middlemanIdentity,
Identity originatorIdentity) {
try (SafeCloseable ignored = establishIdentityIndirect(middlemanIdentity,
originatorIdentity)) {
return mDelegate.listModules();
}
}
@Override
public ISoundTriggerModule attachAsOriginator(int handle, Identity identity,
ISoundTriggerCallback callback) {
try (SafeCloseable ignored = establishIdentityDirect(Objects.requireNonNull(identity))) {
return new ModuleService(mDelegate.attach(handle, callback));
}
}
@Override
public ISoundTriggerModule attachAsMiddleman(int handle, Identity middlemanIdentity,
Identity originatorIdentity, ISoundTriggerCallback callback) {
try (SafeCloseable ignored = establishIdentityIndirect(
Objects.requireNonNull(middlemanIdentity),
Objects.requireNonNull(originatorIdentity))) {
return new ModuleService(mDelegate.attach(handle, callback));
}
}
@Override
@@ -100,6 +129,18 @@ public class SoundTriggerMiddlewareService extends ISoundTriggerMiddlewareServic
}
}
private @NonNull
SafeCloseable establishIdentityIndirect(Identity middlemanIdentity,
Identity originatorIdentity) {
return PermissionUtil.establishIdentityIndirect(mContext, SOUNDTRIGGER_DELEGATE_IDENTITY,
middlemanIdentity, originatorIdentity);
}
private @NonNull
SafeCloseable establishIdentityDirect(Identity originatorIdentity) {
return PermissionUtil.establishIdentityDirect(originatorIdentity);
}
private final static class ModuleService extends ISoundTriggerModule.Stub {
private final ISoundTriggerModule mDelegate;
@@ -109,55 +150,75 @@ public class SoundTriggerMiddlewareService extends ISoundTriggerMiddlewareServic
@Override
public int loadModel(SoundModel model) throws RemoteException {
return mDelegate.loadModel(model);
try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
return mDelegate.loadModel(model);
}
}
@Override
public int loadPhraseModel(PhraseSoundModel model) throws RemoteException {
return mDelegate.loadPhraseModel(model);
try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
return mDelegate.loadPhraseModel(model);
}
}
@Override
public void unloadModel(int modelHandle) throws RemoteException {
mDelegate.unloadModel(modelHandle);
try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
mDelegate.unloadModel(modelHandle);
}
}
@Override
public void startRecognition(int modelHandle, RecognitionConfig config)
throws RemoteException {
mDelegate.startRecognition(modelHandle, config);
try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
mDelegate.startRecognition(modelHandle, config);
}
}
@Override
public void stopRecognition(int modelHandle) throws RemoteException {
mDelegate.stopRecognition(modelHandle);
try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
mDelegate.stopRecognition(modelHandle);
}
}
@Override
public void forceRecognitionEvent(int modelHandle) throws RemoteException {
mDelegate.forceRecognitionEvent(modelHandle);
try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
mDelegate.forceRecognitionEvent(modelHandle);
}
}
@Override
public void setModelParameter(int modelHandle, int modelParam, int value)
throws RemoteException {
mDelegate.setModelParameter(modelHandle, modelParam, value);
try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
mDelegate.setModelParameter(modelHandle, modelParam, value);
}
}
@Override
public int getModelParameter(int modelHandle, int modelParam) throws RemoteException {
return mDelegate.getModelParameter(modelHandle, modelParam);
try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
return mDelegate.getModelParameter(modelHandle, modelParam);
}
}
@Override
public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam)
throws RemoteException {
return mDelegate.queryModelParameterSupport(modelHandle, modelParam);
try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
return mDelegate.queryModelParameterSupport(modelHandle, modelParam);
}
}
@Override
public void detach() throws RemoteException {
mDelegate.detach();
try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
mDelegate.detach();
}
}
}
@@ -187,7 +248,7 @@ public class SoundTriggerMiddlewareService extends ISoundTriggerMiddlewareServic
new SoundTriggerMiddlewareValidation(
new SoundTriggerMiddlewareImpl(factories,
new AudioSessionProviderImpl())),
getContext()))));
getContext())), getContext()));
}
}
}

View File

@@ -16,11 +16,10 @@
package com.android.server.soundtrigger_middleware;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.PermissionChecker;
import android.media.permission.Identity;
import android.media.permission.IdentityContext;
import android.media.soundtrigger_middleware.ISoundTriggerCallback;
import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService;
import android.media.soundtrigger_middleware.ISoundTriggerModule;
@@ -113,11 +112,11 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
ALIVE,
DETACHED,
DEAD
};
}
private class ModuleState {
final @NonNull SoundTriggerModuleProperties properties;
Set<ModuleService> sessions = new HashSet<>();
Set<Session> sessions = new HashSet<>();
private ModuleState(@NonNull SoundTriggerModuleProperties properties) {
this.properties = properties;
@@ -202,10 +201,9 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
// From here on, every exception isn't client's fault.
try {
ModuleService moduleService =
new ModuleService(handle, callback);
moduleService.attach(mDelegate.attach(handle, moduleService));
return moduleService;
Session session = new Session(handle, callback);
session.attach(mDelegate.attach(handle, session.getCallbackWrapper()));
return session;
} catch (Exception e) {
throw handleException(e);
}
@@ -254,7 +252,7 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
pw.printf("Module %d\n%s\n", handle,
ObjectPrinter.print(module.properties, true, 16));
pw.println("=========================================");
for (ModuleService session : module.sessions) {
for (Session session : module.sessions) {
session.dump(pw);
}
}
@@ -284,7 +282,14 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
/** Model is loaded, recognition is inactive. */
LOADED,
/** Model is loaded, recognition is active. */
ACTIVE
ACTIVE,
/**
* Model is active as far as the client is concerned, but loaded as far as the
* layers are concerned. This condition occurs when a recognition event that indicates
* the recognition for this model arrived from the underlying layer, but had not been
* delivered to the caller (most commonly, for permission reasons).
*/
INTERCEPTED,
}
/** Activity state. */
@@ -357,9 +362,7 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
* A wrapper around an {@link ISoundTriggerModule} implementation, to address the same aspects
* mentioned in {@link SoundTriggerModule} above. This class follows the same conventions.
*/
private class ModuleService extends ISoundTriggerModule.Stub implements ISoundTriggerCallback,
IBinder.DeathRecipient {
private final ISoundTriggerCallback mCallback;
private class Session extends ISoundTriggerModule.Stub {
private ISoundTriggerModule mDelegate;
// While generally all the fields of this class must be changed under a lock, an exception
// is made for the specific case of changing a model state from ACTIVE to LOADED, which
@@ -370,15 +373,17 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
ConcurrentMap<Integer, ModelState> mLoadedModels = new ConcurrentHashMap<>();
private final int mHandle;
private ModuleStatus mState = ModuleStatus.ALIVE;
private final CallbackWrapper mCallbackWrapper;
private final Identity mOriginatorIdentity;
ModuleService(int handle, @NonNull ISoundTriggerCallback callback) {
mCallback = callback;
Session(int handle, @NonNull ISoundTriggerCallback callback) {
mCallbackWrapper = new CallbackWrapper(callback);
mHandle = handle;
try {
mCallback.asBinder().linkToDeath(this, 0);
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
mOriginatorIdentity = IdentityContext.get();
}
ISoundTriggerCallback getCallbackWrapper() {
return mCallbackWrapper;
}
void attach(@NonNull ISoundTriggerModule delegate) {
@@ -446,7 +451,7 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
}
if (modelState.getActivityState() != ModelState.Activity.LOADED) {
throw new IllegalStateException("Model with handle: " + modelHandle
+ " has invalid state for unloading: " + modelState.getActivityState());
+ " has invalid state for unloading");
}
// From here on, every exception isn't client's fault.
@@ -476,8 +481,7 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
}
if (modelState.getActivityState() != ModelState.Activity.LOADED) {
throw new IllegalStateException("Model with handle: " + modelHandle
+ " has invalid state for starting recognition: "
+ modelState.getActivityState());
+ " has invalid state for starting recognition");
}
// From here on, every exception isn't client's fault.
@@ -512,7 +516,12 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
// From here on, every exception isn't client's fault.
try {
mDelegate.stopRecognition(modelHandle);
// If the activity state is LOADED or INTERCEPTED, we skip delegating the
// command, but still consider the call valid. In either case, the resulting
// state is LOADED.
if (modelState.getActivityState() == ModelState.Activity.ACTIVE) {
mDelegate.stopRecognition(modelHandle);
}
modelState.setActivityState(ModelState.Activity.LOADED);
} catch (Exception e) {
throw handleException(e);
@@ -538,7 +547,11 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
// From here on, every exception isn't client's fault.
try {
mDelegate.forceRecognitionEvent(modelHandle);
// If the activity state is LOADED or INTERCEPTED, we skip delegating the
// command, but still consider the call valid.
if (modelState.getActivityState() == ModelState.Activity.ACTIVE) {
mDelegate.forceRecognitionEvent(modelHandle);
}
} catch (Exception e) {
throw handleException(e);
}
@@ -658,7 +671,7 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
try {
mDelegate.detach();
mState = ModuleStatus.DETACHED;
mCallback.asBinder().unlinkToDeath(this, 0);
mCallbackWrapper.detached();
mModules.get(mHandle).sessions.remove(this);
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
@@ -667,7 +680,9 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
void dump(PrintWriter pw) {
if (mState == ModuleStatus.ALIVE) {
pw.printf("Loaded models for session %s (handle, active)", toString());
pw.printf("Session %s, client: %s\n", toString(),
ObjectPrinter.print(mOriginatorIdentity, true, 16));
pw.printf("Loaded models (handle, active, description):", toString());
pw.println();
pw.println("-------------------------------");
for (Map.Entry<Integer, ModelState> entry : mLoadedModels.entrySet()) {
@@ -684,10 +699,24 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
}
}
////////////////////////////////////////////////////////////////////////////////////////////
// Callbacks
class CallbackWrapper implements ISoundTriggerCallback,
IBinder.DeathRecipient {
private final ISoundTriggerCallback mCallback;
@Override
CallbackWrapper(ISoundTriggerCallback callback) {
mCallback = callback;
try {
mCallback.asBinder().linkToDeath(this, 0);
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
}
void detached() {
mCallback.asBinder().unlinkToDeath(this, 0);
}
@Override
public void onRecognition(int modelHandle, @NonNull RecognitionEvent event) {
// We cannot obtain a lock on SoundTriggerMiddlewareValidation.this, since this call
// might be coming from the audio server (via setCaptureState()) while it is holding
@@ -696,18 +725,21 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
// To avoid this problem, we use an atomic model activity state. There is a risk of the
// model not being in the mLoadedModels map here, since it might have been stopped /
// unloaded while the event was in flight.
if (event.status != RecognitionStatus.FORCED) {
ModelState modelState = mLoadedModels.get(modelHandle);
if (modelState != null) {
ModelState modelState = mLoadedModels.get(modelHandle);
if (modelState != null) {
if (event.status != RecognitionStatus.FORCED) {
modelState.setActivityState(ModelState.Activity.LOADED);
}
}
try {
mCallback.onRecognition(modelHandle, event);
} catch (RemoteException e) {
// Dead client will be handled by binderDied() - no need to handle here.
// In any case, client callbacks are considered best effort.
Log.e(TAG, "Client callback exception.", e);
try {
mCallback.onRecognition(modelHandle, event);
} catch (Exception e) {
// Dead client will be handled by binderDied() - no need to handle here.
// In any case, client callbacks are considered best effort.
Log.e(TAG, "Client callback exception.", e);
if (event.status != RecognitionStatus.FORCED) {
modelState.setActivityState(ModelState.Activity.INTERCEPTED);
}
}
}
}
@@ -720,18 +752,21 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
// To avoid this problem, we use an atomic model activity state. There is a risk of the
// model not being in the mLoadedModels map here, since it might have been stopped /
// unloaded while the event was in flight.
if (event.common.status != RecognitionStatus.FORCED) {
ModelState modelState = mLoadedModels.get(modelHandle);
if (modelState != null) {
modelState.setActivityState(ModelState.Activity.LOADED);
ModelState modelState = mLoadedModels.get(modelHandle);
if (modelState != null) {
if (event.common.status != RecognitionStatus.FORCED) {
modelState.setActivityState(ModelState.Activity.LOADED);
}
try {
mCallback.onPhraseRecognition(modelHandle, event);
} catch (Exception e) {
// Dead client will be handled by binderDied() - no need to handle here.
// In any case, client callbacks are considered best effort.
Log.e(TAG, "Client callback exception.", e);
if (event.common.status != RecognitionStatus.FORCED) {
modelState.setActivityState(ModelState.Activity.INTERCEPTED);
}
}
}
try {
mCallback.onPhraseRecognition(modelHandle, event);
} catch (RemoteException e) {
// Dead client will be handled by binderDied() - no need to handle here.
// In any case, client callbacks are considered best effort.
Log.e(TAG, "Client callback exception.", e);
}
}
@@ -747,40 +782,52 @@ public class SoundTriggerMiddlewareValidation implements ISoundTriggerMiddleware
}
}
@Override
public void onModuleDied() {
synchronized (SoundTriggerMiddlewareValidation.this) {
mState = ModuleStatus.DEAD;
}
// Trigger the callback outside of the lock to avoid deadlocks.
try {
mCallback.onModuleDied();
} catch (RemoteException e) {
// Dead client will be handled by binderDied() - no need to handle here.
// In any case, client callbacks are considered best effort.
Log.e(TAG, "Client callback exception.", e);
}
}
@Override
public void binderDied() {
// This is called whenever our client process dies.
synchronized (SoundTriggerMiddlewareValidation.this) {
try {
// Gracefully stop all active recognitions and unload the models.
for (Map.Entry<Integer, ModelState> entry :
mLoadedModels.entrySet()) {
// Idempotent call, no harm in calling even for models that are already
// stopped.
mDelegate.stopRecognition(entry.getKey());
mDelegate.unloadModel(entry.getKey());
}
// Detach.
detachInternal();
} catch (Exception e) {
throw handleException(e);
@Override
public void onModuleDied() {
synchronized (SoundTriggerMiddlewareValidation.this) {
mState = ModuleStatus.DEAD;
}
// Trigger the callback outside of the lock to avoid deadlocks.
try {
mCallback.onModuleDied();
} catch (RemoteException e) {
// Dead client will be handled by binderDied() - no need to handle here.
// In any case, client callbacks are considered best effort.
Log.e(TAG, "Client callback exception.", e);
}
}
@Override
public void binderDied() {
// This is called whenever our client process dies.
synchronized (SoundTriggerMiddlewareValidation.this) {
try {
// Gracefully stop all active recognitions and unload the models.
for (Map.Entry<Integer, ModelState> entry :
mLoadedModels.entrySet()) {
if (entry.getValue().getActivityState()
== ModelState.Activity.ACTIVE) {
mDelegate.stopRecognition(entry.getKey());
}
mDelegate.unloadModel(entry.getKey());
}
// Detach.
detachInternal();
} catch (Exception e) {
throw handleException(e);
}
}
}
@Override
public IBinder asBinder() {
return mCallback.asBinder();
}
// Override toString() in order to have the delegate's ID in it.
@Override
public String toString() {
return Objects.toString(mDelegate);
}
}
}