Add SoundTriggerDetectionService

The service is meant to replace the PendingIntent based API. Once all
users of the PendingIntent based API switched the PendingIntent based API
will be removed.

To have as little as possible impact on the whole SoundTrigger framework
the RemoteSoundTriggerDetectionService class implements the same
interface as the PendingIntent based class. Hence the exising code has
very little change. Further once the old code can be removed the amount
of changed (and added) code is limited.

The RemoteSoundTriggerDetectionService -> SoundTriggerDetectionService
is a vanilla as possible service implementation. The special behaviors
are:
- The system holds a wakelock while service operations are in progress
and the service is bound as foreground. Hence the service can e.g.
listen to the microphone.
- Service operations have a certain amount of time they are allowed to
run. Once every operation is either finished or the the operation
exceeded the allotted time, the system calls onStopOperation for each
still pending operation. This is a similar model as for the commonly
used JobService.
  Please note that if the time allowed for an operation is 15s and
op1 was run as 0si, and op1 was run at 5s, the service is allowed to run
until 20s. Hence _both_ onStopOperations will happen at 20s. This is
done for ease of implementation but should not give the service more
power than calling onStopOperation exactly 15s after each operation is
triggered.
- If an operation is done before the allotted time is reached, the
service can declare the operation as finished manually by calling
onOperationFinished. This is a call back into the system, hence a
'client' binder is sent to the service. If the operation is finished
by calling this method onStopOperation will not be called.
- As the service instance might be killed and restored between
operations we add a opaque bundle 'params' to each operations. The users
of the API can use this to send data from the start command to the
operations. It can also just be set to null. The params are not meant to
store changing state in between operations. Such state needs to be
persisted using the regular methods (e.g. write it to disk)
- A service can be used for multiple recognition sessions. Each
recognition is uniquelity defined by its sound model UUID. Hence each
operation gets at least tree arguments: Operation ID, sound mode UUID, params
- As a small optimization the params are cached inside of the service
instance.

The time allowed for each operation is in a @SystemAPI global setting,
so the service can make sure it finishes the operations before they are
stopped. It might take some time to deliver the operations via the
binder, hence it is not recommended to try to use every last ms of
allotted time.

Test: atest SoundTriggerDetectionServiceTest (added in separate CL)
      atest android.provider.SettingsBackupTest
Change-Id: I47f813b7a5138a6f24732197813a605d29f85a93
Fixes: 73829108
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
index cd3fdee..f3ca6165 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
@@ -15,34 +15,64 @@
  */
 
 package com.android.server.soundtrigger;
+
+import static android.Manifest.permission.BIND_SOUND_TRIGGER_DETECTION_SERVICE;
+import static android.content.Context.BIND_AUTO_CREATE;
+import static android.content.Context.BIND_FOREGROUND_SERVICE;
+import static android.content.pm.PackageManager.GET_META_DATA;
+import static android.content.pm.PackageManager.GET_SERVICES;
+import static android.content.pm.PackageManager.MATCH_DEBUG_TRIAGED_MISSING;
 import static android.hardware.soundtrigger.SoundTrigger.STATUS_ERROR;
 import static android.hardware.soundtrigger.SoundTrigger.STATUS_OK;
+import static android.provider.Settings.Global.SOUND_TRIGGER_DETECTION_SERVICE_OP_TIMEOUT;
 
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.PendingIntent;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.ServiceConnection;
 import android.content.pm.PackageManager;
-import android.Manifest;
+import android.content.pm.ResolveInfo;
 import android.hardware.soundtrigger.IRecognitionStatusCallback;
 import android.hardware.soundtrigger.SoundTrigger;
-import android.hardware.soundtrigger.SoundTrigger.SoundModel;
 import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
 import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel;
 import android.hardware.soundtrigger.SoundTrigger.ModuleProperties;
 import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
+import android.hardware.soundtrigger.SoundTrigger.SoundModel;
+import android.media.soundtrigger.ISoundTriggerDetectionService;
+import android.media.soundtrigger.ISoundTriggerDetectionServiceClient;
+import android.media.soundtrigger.SoundTriggerDetectionService;
 import android.media.soundtrigger.SoundTriggerManager;
+import android.os.Binder;
 import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
 import android.os.Parcel;
 import android.os.ParcelUuid;
 import android.os.PowerManager;
 import android.os.RemoteException;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.ArraySet;
 import android.util.Slog;
 
-import com.android.server.SystemService;
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.app.ISoundTriggerService;
+import com.android.internal.util.DumpUtils;
+import com.android.internal.util.Preconditions;
+import com.android.server.SystemService;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.TreeMap;
 import java.util.UUID;
 
@@ -67,7 +97,7 @@
     private SoundTriggerHelper mSoundTriggerHelper;
     private final TreeMap<UUID, SoundModel> mLoadedModels;
     private Object mCallbacksLock;
-    private final TreeMap<UUID, LocalSoundTriggerRecognitionStatusCallback> mIntentCallbacks;
+    private final TreeMap<UUID, IRecognitionStatusCallback> mCallbacks;
     private PowerManager.WakeLock mWakelock;
 
     public SoundTriggerService(Context context) {
@@ -77,7 +107,7 @@
         mLocalSoundTriggerService = new LocalSoundTriggerService(context);
         mLoadedModels = new TreeMap<UUID, SoundModel>();
         mCallbacksLock = new Object();
-        mIntentCallbacks = new TreeMap<UUID, LocalSoundTriggerRecognitionStatusCallback>();
+        mCallbacks = new TreeMap<>();
         mLock = new Object();
     }
 
@@ -214,7 +244,7 @@
                 if (oldModel != null && !oldModel.equals(soundModel)) {
                     mSoundTriggerHelper.unloadGenericSoundModel(soundModel.uuid);
                     synchronized (mCallbacksLock) {
-                        mIntentCallbacks.remove(soundModel.uuid);
+                        mCallbacks.remove(soundModel.uuid);
                     }
                 }
                 mLoadedModels.put(soundModel.uuid, soundModel);
@@ -245,7 +275,7 @@
                 if (oldModel != null && !oldModel.equals(soundModel)) {
                     mSoundTriggerHelper.unloadKeyphraseSoundModel(soundModel.keyphrases[0].id);
                     synchronized (mCallbacksLock) {
-                        mIntentCallbacks.remove(soundModel.uuid);
+                        mCallbacks.remove(soundModel.uuid);
                     }
                 }
                 mLoadedModels.put(soundModel.uuid, soundModel);
@@ -254,8 +284,28 @@
         }
 
         @Override
+        public int startRecognitionForService(ParcelUuid soundModelId, Bundle params,
+            ComponentName detectionService, SoundTrigger.RecognitionConfig config) {
+            Preconditions.checkNotNull(soundModelId);
+            Preconditions.checkNotNull(detectionService);
+            Preconditions.checkNotNull(config);
+
+            return startRecognitionForInt(soundModelId,
+                new RemoteSoundTriggerDetectionService(soundModelId.getUuid(),
+                    params, detectionService, Binder.getCallingUserHandle(), config), config);
+
+        }
+
+        @Override
         public int startRecognitionForIntent(ParcelUuid soundModelId, PendingIntent callbackIntent,
                 SoundTrigger.RecognitionConfig config) {
+            return startRecognitionForInt(soundModelId,
+                new LocalSoundTriggerRecognitionStatusIntentCallback(soundModelId.getUuid(),
+                    callbackIntent, config), config);
+        }
+
+        private int startRecognitionForInt(ParcelUuid soundModelId,
+            IRecognitionStatusCallback callback, SoundTrigger.RecognitionConfig config) {
             enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
             if (!isInitialized()) return STATUS_ERROR;
             if (DEBUG) {
@@ -268,27 +318,25 @@
                     Slog.e(TAG, soundModelId + " is not loaded");
                     return STATUS_ERROR;
                 }
-                LocalSoundTriggerRecognitionStatusCallback callback = null;
+                IRecognitionStatusCallback existingCallback = null;
                 synchronized (mCallbacksLock) {
-                    callback = mIntentCallbacks.get(soundModelId.getUuid());
+                    existingCallback = mCallbacks.get(soundModelId.getUuid());
                 }
-                if (callback != null) {
+                if (existingCallback != null) {
                     Slog.e(TAG, soundModelId + " is already running");
                     return STATUS_ERROR;
                 }
-                callback = new LocalSoundTriggerRecognitionStatusCallback(soundModelId.getUuid(),
-                        callbackIntent, config);
                 int ret;
                 switch (soundModel.type) {
                     case SoundModel.TYPE_KEYPHRASE: {
                         KeyphraseSoundModel keyphraseSoundModel = (KeyphraseSoundModel) soundModel;
                         ret = mSoundTriggerHelper.startKeyphraseRecognition(
-                                keyphraseSoundModel.keyphrases[0].id, keyphraseSoundModel, callback,
-                                config);
+                            keyphraseSoundModel.keyphrases[0].id, keyphraseSoundModel, callback,
+                            config);
                     } break;
                     case SoundModel.TYPE_GENERIC_SOUND:
                         ret = mSoundTriggerHelper.startGenericRecognition(soundModel.uuid,
-                                (GenericSoundModel) soundModel, callback, config);
+                            (GenericSoundModel) soundModel, callback, config);
                         break;
                     default:
                         Slog.e(TAG, "Unknown model type");
@@ -300,7 +348,7 @@
                     return ret;
                 }
                 synchronized (mCallbacksLock) {
-                    mIntentCallbacks.put(soundModelId.getUuid(), callback);
+                    mCallbacks.put(soundModelId.getUuid(), callback);
                 }
             }
             return STATUS_OK;
@@ -320,9 +368,9 @@
                     Slog.e(TAG, soundModelId + " is not loaded");
                     return STATUS_ERROR;
                 }
-                LocalSoundTriggerRecognitionStatusCallback callback = null;
+                IRecognitionStatusCallback callback = null;
                 synchronized (mCallbacksLock) {
-                     callback = mIntentCallbacks.get(soundModelId.getUuid());
+                     callback = mCallbacks.get(soundModelId.getUuid());
                 }
                 if (callback == null) {
                     Slog.e(TAG, soundModelId + " is not running");
@@ -347,7 +395,7 @@
                     return ret;
                 }
                 synchronized (mCallbacksLock) {
-                    mIntentCallbacks.remove(soundModelId.getUuid());
+                    mCallbacks.remove(soundModelId.getUuid());
                 }
             }
             return STATUS_OK;
@@ -394,8 +442,7 @@
             enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
             if (!isInitialized()) return false;
             synchronized (mCallbacksLock) {
-                LocalSoundTriggerRecognitionStatusCallback callback =
-                        mIntentCallbacks.get(parcelUuid.getUuid());
+                IRecognitionStatusCallback callback = mCallbacks.get(parcelUuid.getUuid());
                 if (callback == null) {
                     return false;
                 }
@@ -404,13 +451,13 @@
         }
     }
 
-    private final class LocalSoundTriggerRecognitionStatusCallback
+    private final class LocalSoundTriggerRecognitionStatusIntentCallback
             extends IRecognitionStatusCallback.Stub {
         private UUID mUuid;
         private PendingIntent mCallbackIntent;
         private RecognitionConfig mRecognitionConfig;
 
-        public LocalSoundTriggerRecognitionStatusCallback(UUID modelUuid,
+        public LocalSoundTriggerRecognitionStatusIntentCallback(UUID modelUuid,
                 PendingIntent callbackIntent,
                 RecognitionConfig config) {
             mUuid = modelUuid;
@@ -528,7 +575,7 @@
         private void removeCallback(boolean releaseWakeLock) {
             mCallbackIntent = null;
             synchronized (mCallbacksLock) {
-                mIntentCallbacks.remove(mUuid);
+                mCallbacks.remove(mUuid);
                 if (releaseWakeLock) {
                     mWakelock.release();
                 }
@@ -536,6 +583,383 @@
         }
     }
 
+    private interface Operation {
+        void run(int opId, ISoundTriggerDetectionService service) throws RemoteException;
+    }
+
+    /**
+     * Local end for a {@link SoundTriggerDetectionService}. Operations are queued up and executed
+     * when the service connects.
+     *
+     * <p>If operations take too long they are forcefully aborted.
+     *
+     * <p>This also limits the amount of operations in 24 hours.
+     */
+    private class RemoteSoundTriggerDetectionService
+        extends IRecognitionStatusCallback.Stub implements ServiceConnection {
+        private static final int MSG_STOP_ALL_PENDING_OPERATIONS = 1;
+
+        private final Object mRemoteServiceLock = new Object();
+
+        /** UUID of the model the service is started for */
+        private final @NonNull ParcelUuid mPuuid;
+        /** Params passed into the start method for the service */
+        private final @Nullable Bundle mParams;
+        /** Component name passed when starting the service */
+        private final @NonNull ComponentName mServiceName;
+        /** User that started the service */
+        private final @NonNull UserHandle mUser;
+        /** Configuration of the recognition the service is handling */
+        private final @NonNull RecognitionConfig mRecognitionConfig;
+        /** Wake lock keeping the remote service alive */
+        private final @NonNull PowerManager.WakeLock mRemoteServiceWakeLock;
+
+        private final @NonNull Handler mHandler;
+
+        /** Callbacks that are called by the service */
+        private final @NonNull ISoundTriggerDetectionServiceClient mClient;
+
+        /** Operations that are pending because the service is not yet connected */
+        @GuardedBy("mRemoteServiceLock")
+        private final ArrayList<Operation> mPendingOps = new ArrayList<>();
+        /** Operations that have been send to the service but have no yet finished */
+        @GuardedBy("mRemoteServiceLock")
+        private final ArraySet<Integer> mRunningOpIds = new ArraySet<>();
+
+        /** The service binder if connected */
+        @GuardedBy("mRemoteServiceLock")
+        private @Nullable ISoundTriggerDetectionService mService;
+        /** Whether the service has been bound */
+        @GuardedBy("mRemoteServiceLock")
+        private boolean mIsBound;
+        /** Whether the service has been destroyed */
+        @GuardedBy("mRemoteServiceLock")
+        private boolean mIsDestroyed;
+        /**
+         * Set once a final op is scheduled. No further ops can be added and the service is
+         * destroyed once the op finishes.
+         */
+        @GuardedBy("mRemoteServiceLock")
+        private boolean mDestroyOnceRunningOpsDone;
+
+        /** Total number of operations performed by this service */
+        @GuardedBy("mRemoteServiceLock")
+        private int mNumTotalOpsPerformed;
+
+        /**
+         * Create a new remote sound trigger detection service. This only binds to the service when
+         * operations are in flight. Each operation has a certain time it can run. Once no
+         * operations are allowed to run anymore, {@link #stopAllPendingOperations() all operations
+         * are aborted and stopped} and the service is disconnected.
+         *
+         * @param modelUuid The UUID of the model the recognition is for
+         * @param params The params passed to each method of the service
+         * @param serviceName The component name of the service
+         * @param user The user of the service
+         * @param config The configuration of the recognition
+         */
+        public RemoteSoundTriggerDetectionService(@NonNull UUID modelUuid,
+            @Nullable Bundle params, @NonNull ComponentName serviceName, @NonNull UserHandle user,
+            @NonNull RecognitionConfig config) {
+            mPuuid = new ParcelUuid(modelUuid);
+            mParams = params;
+            mServiceName = serviceName;
+            mUser = user;
+            mRecognitionConfig = config;
+            mHandler = new Handler(Looper.getMainLooper());
+
+            PowerManager pm = ((PowerManager) mContext.getSystemService(Context.POWER_SERVICE));
+            mRemoteServiceWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+                    "RemoteSoundTriggerDetectionService " + mServiceName.getPackageName() + ":"
+                            + mServiceName.getClassName());
+
+            mClient = new ISoundTriggerDetectionServiceClient.Stub() {
+                @Override
+                public void onOpFinished(int opId) {
+                    long token = Binder.clearCallingIdentity();
+                    try {
+                        synchronized (mRemoteServiceLock) {
+                            mRunningOpIds.remove(opId);
+
+                            if (mRunningOpIds.isEmpty() && mPendingOps.isEmpty()) {
+                                if (mDestroyOnceRunningOpsDone) {
+                                    destroy();
+                                } else {
+                                    disconnectLocked();
+                                }
+                            }
+                        }
+                    } finally {
+                        Binder.restoreCallingIdentity(token);
+                    }
+                }
+            };
+        }
+
+        @Override
+        public boolean pingBinder() {
+            return !(mIsDestroyed || mDestroyOnceRunningOpsDone);
+        }
+
+        /**
+         * Disconnect from the service, but allow to re-connect when new operations are triggered.
+         */
+        private void disconnectLocked() {
+            if (mService != null) {
+                try {
+                    mService.removeClient(mPuuid);
+                } catch (Exception e) {
+                    Slog.e(TAG, mPuuid + ": Cannot remove client", e);
+                }
+
+                mService = null;
+            }
+
+            if (mIsBound) {
+                mContext.unbindService(RemoteSoundTriggerDetectionService.this);
+                mIsBound = false;
+
+                synchronized (mCallbacksLock) {
+                    mRemoteServiceWakeLock.release();
+                }
+            }
+        }
+
+        /**
+         * Disconnect, do not allow to reconnect to the service. All further operations will be
+         * dropped.
+         */
+        private void destroy() {
+            if (DEBUG) Slog.v(TAG, mPuuid + ": destroy");
+
+            synchronized (mRemoteServiceLock) {
+                disconnectLocked();
+
+                mIsDestroyed = true;
+            }
+
+            // The callback is removed before the flag is set
+            if (!mDestroyOnceRunningOpsDone) {
+                synchronized (mCallbacksLock) {
+                    mCallbacks.remove(mPuuid.getUuid());
+                }
+            }
+        }
+
+        /**
+         * Stop all pending operations and then disconnect for the service.
+         */
+        private void stopAllPendingOperations() {
+            synchronized (mRemoteServiceLock) {
+                if (mIsDestroyed) {
+                    return;
+                }
+
+                if (mService != null) {
+                    int numOps = mRunningOpIds.size();
+                    for (int i = 0; i < numOps; i++) {
+                        try {
+                            mService.onStopOperation(mPuuid, mRunningOpIds.valueAt(i));
+                        } catch (Exception e) {
+                            Slog.e(TAG, mPuuid + ": Could not stop operation "
+                                    + mRunningOpIds.valueAt(i), e);
+                        }
+                    }
+
+                    mRunningOpIds.clear();
+                }
+
+                disconnectLocked();
+            }
+        }
+
+        /**
+         * Verify that the service has the expected properties and then bind to the service
+         */
+        private void bind() {
+            long token = Binder.clearCallingIdentity();
+            try {
+                Intent i = new Intent();
+                i.setComponent(mServiceName);
+
+                ResolveInfo ri = mContext.getPackageManager().resolveServiceAsUser(i,
+                        GET_SERVICES | GET_META_DATA | MATCH_DEBUG_TRIAGED_MISSING,
+                        mUser.getIdentifier());
+
+                if (ri == null) {
+                    Slog.w(TAG, mPuuid + ": " + mServiceName + " not found");
+                    return;
+                }
+
+                if (!BIND_SOUND_TRIGGER_DETECTION_SERVICE
+                        .equals(ri.serviceInfo.permission)) {
+                    Slog.w(TAG, mPuuid + ": " + mServiceName + " does not require "
+                            + BIND_SOUND_TRIGGER_DETECTION_SERVICE);
+                    return;
+                }
+
+                mIsBound = mContext.bindServiceAsUser(i, this,
+                        BIND_AUTO_CREATE | BIND_FOREGROUND_SERVICE, mUser);
+
+                if (mIsBound) {
+                    mRemoteServiceWakeLock.acquire();
+                } else {
+                    Slog.w(TAG, mPuuid + ": Could not bind to " + mServiceName);
+                }
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        /**
+         * Run an operation (i.e. send it do the service). If the service is not connected, this
+         * binds the service and then runs the operation once connected.
+         *
+         * @param op The operation to run
+         */
+        private void runOrAddOperation(Operation op) {
+            synchronized (mRemoteServiceLock) {
+                if (mIsDestroyed || mDestroyOnceRunningOpsDone) {
+                    return;
+                }
+
+                if (mService == null) {
+                    mPendingOps.add(op);
+
+                    if (!mIsBound) {
+                        bind();
+                    }
+                } else {
+                    // Find a free opID
+                    int opId = mNumTotalOpsPerformed;
+                    do {
+                        mNumTotalOpsPerformed++;
+                    } while (mRunningOpIds.contains(opId));
+
+                    // Run OP
+                    try {
+                        if (DEBUG) Slog.v(TAG, mPuuid + ": runOp " + opId);
+
+                        op.run(opId, mService);
+                        mRunningOpIds.add(opId);
+                    } catch (Exception e) {
+                        Slog.e(TAG, mPuuid + ": Could not run operation " + opId, e);
+                    }
+
+                    // Unbind from service if no operations are left (i.e. if the operation failed)
+                    if (mPendingOps.isEmpty() && mRunningOpIds.isEmpty()) {
+                        if (mDestroyOnceRunningOpsDone) {
+                            destroy();
+                        } else {
+                            disconnectLocked();
+                        }
+                    } else {
+                        mHandler.removeMessages(MSG_STOP_ALL_PENDING_OPERATIONS);
+                        mHandler.sendMessageDelayed(obtainMessage(
+                                RemoteSoundTriggerDetectionService::stopAllPendingOperations, this)
+                                        .setWhat(MSG_STOP_ALL_PENDING_OPERATIONS),
+                                Settings.Global.getLong(mContext.getContentResolver(),
+                                        SOUND_TRIGGER_DETECTION_SERVICE_OP_TIMEOUT,
+                                        Long.MAX_VALUE));
+                    }
+                }
+            }
+        }
+
+        @Override
+        public void onKeyphraseDetected(SoundTrigger.KeyphraseRecognitionEvent event) {
+            Slog.w(TAG, mPuuid + "->" + mServiceName + ": IGNORED onKeyphraseDetected(" + event
+                    + ")");
+        }
+
+        @Override
+        public void onGenericSoundTriggerDetected(SoundTrigger.GenericRecognitionEvent event) {
+            if (DEBUG) Slog.v(TAG, mPuuid + ": Generic sound trigger event: " + event);
+
+            runOrAddOperation((opId, service) -> {
+                if (!mRecognitionConfig.allowMultipleTriggers) {
+                    synchronized (mCallbacksLock) {
+                        mCallbacks.remove(mPuuid.getUuid());
+                    }
+                    mDestroyOnceRunningOpsDone = true;
+                }
+
+                service.onGenericRecognitionEvent(mPuuid, opId, event);
+            });
+        }
+
+        @Override
+        public void onError(int status) {
+            if (DEBUG) Slog.v(TAG, mPuuid + ": onError: " + status);
+
+            runOrAddOperation((opId, service) -> {
+                synchronized (mCallbacksLock) {
+                    mCallbacks.remove(mPuuid.getUuid());
+                }
+                mDestroyOnceRunningOpsDone = true;
+
+                service.onError(mPuuid, opId, status);
+            });
+        }
+
+        @Override
+        public void onRecognitionPaused() {
+            Slog.i(TAG, mPuuid + "->" + mServiceName + ": IGNORED onRecognitionPaused");
+        }
+
+        @Override
+        public void onRecognitionResumed() {
+            Slog.i(TAG, mPuuid + "->" + mServiceName + ": IGNORED onRecognitionResumed");
+        }
+
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            if (DEBUG) Slog.v(TAG, mPuuid + ": onServiceConnected(" + service + ")");
+
+            synchronized (mRemoteServiceLock) {
+                mService = ISoundTriggerDetectionService.Stub.asInterface(service);
+
+                try {
+                    mService.setClient(mPuuid, mParams, mClient);
+                } catch (Exception e) {
+                    Slog.e(TAG, mPuuid + ": Could not init " + mServiceName, e);
+                    return;
+                }
+
+                while (!mPendingOps.isEmpty()) {
+                    runOrAddOperation(mPendingOps.remove(0));
+                }
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            if (DEBUG) Slog.v(TAG, mPuuid + ": onServiceDisconnected");
+
+            synchronized (mRemoteServiceLock) {
+                mService = null;
+            }
+        }
+
+        @Override
+        public void onBindingDied(ComponentName name) {
+            if (DEBUG) Slog.v(TAG, mPuuid + ": onBindingDied");
+
+            synchronized (mRemoteServiceLock) {
+                destroy();
+            }
+        }
+
+        @Override
+        public void onNullBinding(ComponentName name) {
+            Slog.w(TAG, name + " for model " + mPuuid + " returned a null binding");
+
+            synchronized (mRemoteServiceLock) {
+                disconnectLocked();
+            }
+        }
+    }
+
     private void grabWakeLock() {
         synchronized (mCallbacksLock) {
             if (mWakelock == null) {