• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.soundtrigger;
18 
19 import static android.Manifest.permission.BIND_SOUND_TRIGGER_DETECTION_SERVICE;
20 import static android.content.Context.BIND_AUTO_CREATE;
21 import static android.content.Context.BIND_FOREGROUND_SERVICE;
22 import static android.content.pm.PackageManager.GET_META_DATA;
23 import static android.content.pm.PackageManager.GET_SERVICES;
24 import static android.content.pm.PackageManager.MATCH_DEBUG_TRIAGED_MISSING;
25 import static android.hardware.soundtrigger.SoundTrigger.STATUS_ERROR;
26 import static android.hardware.soundtrigger.SoundTrigger.STATUS_OK;
27 import static android.provider.Settings.Global.MAX_SOUND_TRIGGER_DETECTION_SERVICE_OPS_PER_DAY;
28 import static android.provider.Settings.Global.SOUND_TRIGGER_DETECTION_SERVICE_OP_TIMEOUT;
29 
30 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
31 
32 import android.Manifest;
33 import android.annotation.NonNull;
34 import android.annotation.Nullable;
35 import android.app.PendingIntent;
36 import android.content.ComponentName;
37 import android.content.Context;
38 import android.content.Intent;
39 import android.content.ServiceConnection;
40 import android.content.pm.PackageManager;
41 import android.content.pm.ResolveInfo;
42 import android.hardware.soundtrigger.IRecognitionStatusCallback;
43 import android.hardware.soundtrigger.SoundTrigger;
44 import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
45 import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel;
46 import android.hardware.soundtrigger.SoundTrigger.ModuleProperties;
47 import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
48 import android.hardware.soundtrigger.SoundTrigger.SoundModel;
49 import android.media.AudioAttributes;
50 import android.media.AudioFormat;
51 import android.media.AudioRecord;
52 import android.media.MediaRecorder;
53 import android.media.soundtrigger.ISoundTriggerDetectionService;
54 import android.media.soundtrigger.ISoundTriggerDetectionServiceClient;
55 import android.media.soundtrigger.SoundTriggerDetectionService;
56 import android.media.soundtrigger.SoundTriggerManager;
57 import android.os.Binder;
58 import android.os.Bundle;
59 import android.os.Handler;
60 import android.os.IBinder;
61 import android.os.Looper;
62 import android.os.Parcel;
63 import android.os.ParcelUuid;
64 import android.os.PowerManager;
65 import android.os.RemoteException;
66 import android.os.UserHandle;
67 import android.provider.Settings;
68 import android.util.ArrayMap;
69 import android.util.ArraySet;
70 import android.util.Slog;
71 
72 import com.android.internal.annotations.GuardedBy;
73 import com.android.internal.app.ISoundTriggerService;
74 import com.android.internal.util.DumpUtils;
75 import com.android.internal.util.Preconditions;
76 import com.android.server.SystemService;
77 
78 import java.io.FileDescriptor;
79 import java.io.PrintWriter;
80 import java.util.ArrayList;
81 import java.util.TreeMap;
82 import java.util.UUID;
83 import java.util.concurrent.TimeUnit;
84 
85 /**
86  * A single SystemService to manage all sound/voice-based sound models on the DSP.
87  * This services provides apis to manage sound trigger-based sound models via
88  * the ISoundTriggerService interface. This class also publishes a local interface encapsulating
89  * the functionality provided by {@link SoundTriggerHelper} for use by
90  * {@link VoiceInteractionManagerService}.
91  *
92  * @hide
93  */
94 public class SoundTriggerService extends SystemService {
95     private static final String TAG = "SoundTriggerService";
96     private static final boolean DEBUG = true;
97 
98     final Context mContext;
99     private Object mLock;
100     private final SoundTriggerServiceStub mServiceStub;
101     private final LocalSoundTriggerService mLocalSoundTriggerService;
102     private SoundTriggerDbHelper mDbHelper;
103     private SoundTriggerHelper mSoundTriggerHelper;
104     private final TreeMap<UUID, SoundModel> mLoadedModels;
105     private Object mCallbacksLock;
106     private final TreeMap<UUID, IRecognitionStatusCallback> mCallbacks;
107     private PowerManager.WakeLock mWakelock;
108 
109     /** Number of ops run by the {@link RemoteSoundTriggerDetectionService} per package name */
110     @GuardedBy("mLock")
111     private final ArrayMap<String, NumOps> mNumOpsPerPackage = new ArrayMap<>();
112 
SoundTriggerService(Context context)113     public SoundTriggerService(Context context) {
114         super(context);
115         mContext = context;
116         mServiceStub = new SoundTriggerServiceStub();
117         mLocalSoundTriggerService = new LocalSoundTriggerService(context);
118         mLoadedModels = new TreeMap<UUID, SoundModel>();
119         mCallbacksLock = new Object();
120         mCallbacks = new TreeMap<>();
121         mLock = new Object();
122     }
123 
124     @Override
onStart()125     public void onStart() {
126         publishBinderService(Context.SOUND_TRIGGER_SERVICE, mServiceStub);
127         publishLocalService(SoundTriggerInternal.class, mLocalSoundTriggerService);
128     }
129 
130     @Override
onBootPhase(int phase)131     public void onBootPhase(int phase) {
132         if (PHASE_SYSTEM_SERVICES_READY == phase) {
133             initSoundTriggerHelper();
134             mLocalSoundTriggerService.setSoundTriggerHelper(mSoundTriggerHelper);
135         } else if (PHASE_THIRD_PARTY_APPS_CAN_START == phase) {
136             mDbHelper = new SoundTriggerDbHelper(mContext);
137         }
138     }
139 
140     @Override
onStartUser(int userHandle)141     public void onStartUser(int userHandle) {
142     }
143 
144     @Override
onSwitchUser(int userHandle)145     public void onSwitchUser(int userHandle) {
146     }
147 
initSoundTriggerHelper()148     private synchronized void initSoundTriggerHelper() {
149         if (mSoundTriggerHelper == null) {
150             mSoundTriggerHelper = new SoundTriggerHelper(mContext);
151         }
152     }
153 
isInitialized()154     private synchronized boolean isInitialized() {
155         if (mSoundTriggerHelper == null ) {
156             Slog.e(TAG, "SoundTriggerHelper not initialized.");
157             return false;
158         }
159         return true;
160     }
161 
162     class SoundTriggerServiceStub extends ISoundTriggerService.Stub {
163         @Override
onTransact(int code, Parcel data, Parcel reply, int flags)164         public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
165                 throws RemoteException {
166             try {
167                 return super.onTransact(code, data, reply, flags);
168             } catch (RuntimeException e) {
169                 // The activity manager only throws security exceptions, so let's
170                 // log all others.
171                 if (!(e instanceof SecurityException)) {
172                     Slog.wtf(TAG, "SoundTriggerService Crash", e);
173                 }
174                 throw e;
175             }
176         }
177 
178         @Override
startRecognition(ParcelUuid parcelUuid, IRecognitionStatusCallback callback, RecognitionConfig config)179         public int startRecognition(ParcelUuid parcelUuid, IRecognitionStatusCallback callback,
180                 RecognitionConfig config) {
181             enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
182             if (!isInitialized()) return STATUS_ERROR;
183             if (DEBUG) {
184                 Slog.i(TAG, "startRecognition(): Uuid : " + parcelUuid);
185             }
186 
187             GenericSoundModel model = getSoundModel(parcelUuid);
188             if (model == null) {
189                 Slog.e(TAG, "Null model in database for id: " + parcelUuid);
190                 return STATUS_ERROR;
191             }
192 
193             return mSoundTriggerHelper.startGenericRecognition(parcelUuid.getUuid(), model,
194                     callback, config);
195         }
196 
197         @Override
stopRecognition(ParcelUuid parcelUuid, IRecognitionStatusCallback callback)198         public int stopRecognition(ParcelUuid parcelUuid, IRecognitionStatusCallback callback) {
199             enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
200             if (DEBUG) {
201                 Slog.i(TAG, "stopRecognition(): Uuid : " + parcelUuid);
202             }
203             if (!isInitialized()) return STATUS_ERROR;
204             return mSoundTriggerHelper.stopGenericRecognition(parcelUuid.getUuid(), callback);
205         }
206 
207         @Override
getSoundModel(ParcelUuid soundModelId)208         public SoundTrigger.GenericSoundModel getSoundModel(ParcelUuid soundModelId) {
209             enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
210             if (DEBUG) {
211                 Slog.i(TAG, "getSoundModel(): id = " + soundModelId);
212             }
213             SoundTrigger.GenericSoundModel model = mDbHelper.getGenericSoundModel(
214                     soundModelId.getUuid());
215             return model;
216         }
217 
218         @Override
updateSoundModel(SoundTrigger.GenericSoundModel soundModel)219         public void updateSoundModel(SoundTrigger.GenericSoundModel soundModel) {
220             enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
221             if (DEBUG) {
222                 Slog.i(TAG, "updateSoundModel(): model = " + soundModel);
223             }
224             mDbHelper.updateGenericSoundModel(soundModel);
225         }
226 
227         @Override
deleteSoundModel(ParcelUuid soundModelId)228         public void deleteSoundModel(ParcelUuid soundModelId) {
229             enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
230             if (DEBUG) {
231                 Slog.i(TAG, "deleteSoundModel(): id = " + soundModelId);
232             }
233             // Unload the model if it is loaded.
234             mSoundTriggerHelper.unloadGenericSoundModel(soundModelId.getUuid());
235             mDbHelper.deleteGenericSoundModel(soundModelId.getUuid());
236         }
237 
238         @Override
loadGenericSoundModel(GenericSoundModel soundModel)239         public int loadGenericSoundModel(GenericSoundModel soundModel) {
240             enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
241             if (!isInitialized()) return STATUS_ERROR;
242             if (soundModel == null || soundModel.uuid == null) {
243                 Slog.e(TAG, "Invalid sound model");
244                 return STATUS_ERROR;
245             }
246             if (DEBUG) {
247                 Slog.i(TAG, "loadGenericSoundModel(): id = " + soundModel.uuid);
248             }
249             synchronized (mLock) {
250                 SoundModel oldModel = mLoadedModels.get(soundModel.uuid);
251                 // If the model we're loading is actually different than what we had loaded, we
252                 // should unload that other model now. We don't care about return codes since we
253                 // don't know if the other model is loaded.
254                 if (oldModel != null && !oldModel.equals(soundModel)) {
255                     mSoundTriggerHelper.unloadGenericSoundModel(soundModel.uuid);
256                     synchronized (mCallbacksLock) {
257                         mCallbacks.remove(soundModel.uuid);
258                     }
259                 }
260                 mLoadedModels.put(soundModel.uuid, soundModel);
261             }
262             return STATUS_OK;
263         }
264 
265         @Override
loadKeyphraseSoundModel(KeyphraseSoundModel soundModel)266         public int loadKeyphraseSoundModel(KeyphraseSoundModel soundModel) {
267             enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
268             if (!isInitialized()) return STATUS_ERROR;
269             if (soundModel == null || soundModel.uuid == null) {
270                 Slog.e(TAG, "Invalid sound model");
271                 return STATUS_ERROR;
272             }
273             if (soundModel.keyphrases == null || soundModel.keyphrases.length != 1) {
274                 Slog.e(TAG, "Only one keyphrase per model is currently supported.");
275                 return STATUS_ERROR;
276             }
277             if (DEBUG) {
278                 Slog.i(TAG, "loadKeyphraseSoundModel(): id = " + soundModel.uuid);
279             }
280             synchronized (mLock) {
281                 SoundModel oldModel = mLoadedModels.get(soundModel.uuid);
282                 // If the model we're loading is actually different than what we had loaded, we
283                 // should unload that other model now. We don't care about return codes since we
284                 // don't know if the other model is loaded.
285                 if (oldModel != null && !oldModel.equals(soundModel)) {
286                     mSoundTriggerHelper.unloadKeyphraseSoundModel(soundModel.keyphrases[0].id);
287                     synchronized (mCallbacksLock) {
288                         mCallbacks.remove(soundModel.uuid);
289                     }
290                 }
291                 mLoadedModels.put(soundModel.uuid, soundModel);
292             }
293             return STATUS_OK;
294         }
295 
296         @Override
startRecognitionForService(ParcelUuid soundModelId, Bundle params, ComponentName detectionService, SoundTrigger.RecognitionConfig config)297         public int startRecognitionForService(ParcelUuid soundModelId, Bundle params,
298             ComponentName detectionService, SoundTrigger.RecognitionConfig config) {
299             Preconditions.checkNotNull(soundModelId);
300             Preconditions.checkNotNull(detectionService);
301             Preconditions.checkNotNull(config);
302 
303             return startRecognitionForInt(soundModelId,
304                 new RemoteSoundTriggerDetectionService(soundModelId.getUuid(),
305                     params, detectionService, Binder.getCallingUserHandle(), config), config);
306 
307         }
308 
309         @Override
startRecognitionForIntent(ParcelUuid soundModelId, PendingIntent callbackIntent, SoundTrigger.RecognitionConfig config)310         public int startRecognitionForIntent(ParcelUuid soundModelId, PendingIntent callbackIntent,
311                 SoundTrigger.RecognitionConfig config) {
312             return startRecognitionForInt(soundModelId,
313                 new LocalSoundTriggerRecognitionStatusIntentCallback(soundModelId.getUuid(),
314                     callbackIntent, config), config);
315         }
316 
startRecognitionForInt(ParcelUuid soundModelId, IRecognitionStatusCallback callback, SoundTrigger.RecognitionConfig config)317         private int startRecognitionForInt(ParcelUuid soundModelId,
318             IRecognitionStatusCallback callback, SoundTrigger.RecognitionConfig config) {
319             enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
320             if (!isInitialized()) return STATUS_ERROR;
321             if (DEBUG) {
322                 Slog.i(TAG, "startRecognition(): id = " + soundModelId);
323             }
324 
325             synchronized (mLock) {
326                 SoundModel soundModel = mLoadedModels.get(soundModelId.getUuid());
327                 if (soundModel == null) {
328                     Slog.e(TAG, soundModelId + " is not loaded");
329                     return STATUS_ERROR;
330                 }
331                 IRecognitionStatusCallback existingCallback = null;
332                 synchronized (mCallbacksLock) {
333                     existingCallback = mCallbacks.get(soundModelId.getUuid());
334                 }
335                 if (existingCallback != null) {
336                     Slog.e(TAG, soundModelId + " is already running");
337                     return STATUS_ERROR;
338                 }
339                 int ret;
340                 switch (soundModel.type) {
341                     case SoundModel.TYPE_KEYPHRASE: {
342                         KeyphraseSoundModel keyphraseSoundModel = (KeyphraseSoundModel) soundModel;
343                         ret = mSoundTriggerHelper.startKeyphraseRecognition(
344                             keyphraseSoundModel.keyphrases[0].id, keyphraseSoundModel, callback,
345                             config);
346                     } break;
347                     case SoundModel.TYPE_GENERIC_SOUND:
348                         ret = mSoundTriggerHelper.startGenericRecognition(soundModel.uuid,
349                             (GenericSoundModel) soundModel, callback, config);
350                         break;
351                     default:
352                         Slog.e(TAG, "Unknown model type");
353                         return STATUS_ERROR;
354                 }
355 
356                 if (ret != STATUS_OK) {
357                     Slog.e(TAG, "Failed to start model: " + ret);
358                     return ret;
359                 }
360                 synchronized (mCallbacksLock) {
361                     mCallbacks.put(soundModelId.getUuid(), callback);
362                 }
363             }
364             return STATUS_OK;
365         }
366 
367         @Override
stopRecognitionForIntent(ParcelUuid soundModelId)368         public int stopRecognitionForIntent(ParcelUuid soundModelId) {
369             enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
370             if (!isInitialized()) return STATUS_ERROR;
371             if (DEBUG) {
372                 Slog.i(TAG, "stopRecognition(): id = " + soundModelId);
373             }
374 
375             synchronized (mLock) {
376                 SoundModel soundModel = mLoadedModels.get(soundModelId.getUuid());
377                 if (soundModel == null) {
378                     Slog.e(TAG, soundModelId + " is not loaded");
379                     return STATUS_ERROR;
380                 }
381                 IRecognitionStatusCallback callback = null;
382                 synchronized (mCallbacksLock) {
383                      callback = mCallbacks.get(soundModelId.getUuid());
384                 }
385                 if (callback == null) {
386                     Slog.e(TAG, soundModelId + " is not running");
387                     return STATUS_ERROR;
388                 }
389                 int ret;
390                 switch (soundModel.type) {
391                     case SoundModel.TYPE_KEYPHRASE:
392                         ret = mSoundTriggerHelper.stopKeyphraseRecognition(
393                                 ((KeyphraseSoundModel)soundModel).keyphrases[0].id, callback);
394                         break;
395                     case SoundModel.TYPE_GENERIC_SOUND:
396                         ret = mSoundTriggerHelper.stopGenericRecognition(soundModel.uuid, callback);
397                         break;
398                     default:
399                         Slog.e(TAG, "Unknown model type");
400                         return STATUS_ERROR;
401                 }
402 
403                 if (ret != STATUS_OK) {
404                     Slog.e(TAG, "Failed to stop model: " + ret);
405                     return ret;
406                 }
407                 synchronized (mCallbacksLock) {
408                     mCallbacks.remove(soundModelId.getUuid());
409                 }
410             }
411             return STATUS_OK;
412         }
413 
414         @Override
unloadSoundModel(ParcelUuid soundModelId)415         public int unloadSoundModel(ParcelUuid soundModelId) {
416             enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
417             if (!isInitialized()) return STATUS_ERROR;
418             if (DEBUG) {
419                 Slog.i(TAG, "unloadSoundModel(): id = " + soundModelId);
420             }
421 
422             synchronized (mLock) {
423                 SoundModel soundModel = mLoadedModels.get(soundModelId.getUuid());
424                 if (soundModel == null) {
425                     Slog.e(TAG, soundModelId + " is not loaded");
426                     return STATUS_ERROR;
427                 }
428                 int ret;
429                 switch (soundModel.type) {
430                     case SoundModel.TYPE_KEYPHRASE:
431                         ret = mSoundTriggerHelper.unloadKeyphraseSoundModel(
432                                 ((KeyphraseSoundModel)soundModel).keyphrases[0].id);
433                         break;
434                     case SoundModel.TYPE_GENERIC_SOUND:
435                         ret = mSoundTriggerHelper.unloadGenericSoundModel(soundModel.uuid);
436                         break;
437                     default:
438                         Slog.e(TAG, "Unknown model type");
439                         return STATUS_ERROR;
440                 }
441                 if (ret != STATUS_OK) {
442                     Slog.e(TAG, "Failed to unload model");
443                     return ret;
444                 }
445                 mLoadedModels.remove(soundModelId.getUuid());
446                 return STATUS_OK;
447             }
448         }
449 
450         @Override
isRecognitionActive(ParcelUuid parcelUuid)451         public boolean isRecognitionActive(ParcelUuid parcelUuid) {
452             enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
453             if (!isInitialized()) return false;
454             synchronized (mCallbacksLock) {
455                 IRecognitionStatusCallback callback = mCallbacks.get(parcelUuid.getUuid());
456                 if (callback == null) {
457                     return false;
458                 }
459             }
460             return mSoundTriggerHelper.isRecognitionRequested(parcelUuid.getUuid());
461         }
462     }
463 
464     private final class LocalSoundTriggerRecognitionStatusIntentCallback
465             extends IRecognitionStatusCallback.Stub {
466         private UUID mUuid;
467         private PendingIntent mCallbackIntent;
468         private RecognitionConfig mRecognitionConfig;
469 
LocalSoundTriggerRecognitionStatusIntentCallback(UUID modelUuid, PendingIntent callbackIntent, RecognitionConfig config)470         public LocalSoundTriggerRecognitionStatusIntentCallback(UUID modelUuid,
471                 PendingIntent callbackIntent,
472                 RecognitionConfig config) {
473             mUuid = modelUuid;
474             mCallbackIntent = callbackIntent;
475             mRecognitionConfig = config;
476         }
477 
478         @Override
pingBinder()479         public boolean pingBinder() {
480             return mCallbackIntent != null;
481         }
482 
483         @Override
onKeyphraseDetected(SoundTrigger.KeyphraseRecognitionEvent event)484         public void onKeyphraseDetected(SoundTrigger.KeyphraseRecognitionEvent event) {
485             if (mCallbackIntent == null) {
486                 return;
487             }
488             grabWakeLock();
489 
490             Slog.w(TAG, "Keyphrase sound trigger event: " + event);
491             Intent extras = new Intent();
492             extras.putExtra(SoundTriggerManager.EXTRA_MESSAGE_TYPE,
493                     SoundTriggerManager.FLAG_MESSAGE_TYPE_RECOGNITION_EVENT);
494             extras.putExtra(SoundTriggerManager.EXTRA_RECOGNITION_EVENT, event);
495             try {
496                 mCallbackIntent.send(mContext, 0, extras, mCallbackCompletedHandler, null);
497                 if (!mRecognitionConfig.allowMultipleTriggers) {
498                     removeCallback(/*releaseWakeLock=*/false);
499                 }
500             } catch (PendingIntent.CanceledException e) {
501                 removeCallback(/*releaseWakeLock=*/true);
502             }
503         }
504 
505         @Override
onGenericSoundTriggerDetected(SoundTrigger.GenericRecognitionEvent event)506         public void onGenericSoundTriggerDetected(SoundTrigger.GenericRecognitionEvent event) {
507             if (mCallbackIntent == null) {
508                 return;
509             }
510             grabWakeLock();
511 
512             Slog.w(TAG, "Generic sound trigger event: " + event);
513             Intent extras = new Intent();
514             extras.putExtra(SoundTriggerManager.EXTRA_MESSAGE_TYPE,
515                     SoundTriggerManager.FLAG_MESSAGE_TYPE_RECOGNITION_EVENT);
516             extras.putExtra(SoundTriggerManager.EXTRA_RECOGNITION_EVENT, event);
517             try {
518                 mCallbackIntent.send(mContext, 0, extras, mCallbackCompletedHandler, null);
519                 if (!mRecognitionConfig.allowMultipleTriggers) {
520                     removeCallback(/*releaseWakeLock=*/false);
521                 }
522             } catch (PendingIntent.CanceledException e) {
523                 removeCallback(/*releaseWakeLock=*/true);
524             }
525         }
526 
527         @Override
onError(int status)528         public void onError(int status) {
529             if (mCallbackIntent == null) {
530                 return;
531             }
532             grabWakeLock();
533 
534             Slog.i(TAG, "onError: " + status);
535             Intent extras = new Intent();
536             extras.putExtra(SoundTriggerManager.EXTRA_MESSAGE_TYPE,
537                     SoundTriggerManager.FLAG_MESSAGE_TYPE_RECOGNITION_ERROR);
538             extras.putExtra(SoundTriggerManager.EXTRA_STATUS, status);
539             try {
540                 mCallbackIntent.send(mContext, 0, extras, mCallbackCompletedHandler, null);
541                 // Remove the callback, but wait for the intent to finish before we let go of the
542                 // wake lock
543                 removeCallback(/*releaseWakeLock=*/false);
544             } catch (PendingIntent.CanceledException e) {
545                 removeCallback(/*releaseWakeLock=*/true);
546             }
547         }
548 
549         @Override
onRecognitionPaused()550         public void onRecognitionPaused() {
551             if (mCallbackIntent == null) {
552                 return;
553             }
554             grabWakeLock();
555 
556             Slog.i(TAG, "onRecognitionPaused");
557             Intent extras = new Intent();
558             extras.putExtra(SoundTriggerManager.EXTRA_MESSAGE_TYPE,
559                     SoundTriggerManager.FLAG_MESSAGE_TYPE_RECOGNITION_PAUSED);
560             try {
561                 mCallbackIntent.send(mContext, 0, extras, mCallbackCompletedHandler, null);
562             } catch (PendingIntent.CanceledException e) {
563                 removeCallback(/*releaseWakeLock=*/true);
564             }
565         }
566 
567         @Override
onRecognitionResumed()568         public void onRecognitionResumed() {
569             if (mCallbackIntent == null) {
570                 return;
571             }
572             grabWakeLock();
573 
574             Slog.i(TAG, "onRecognitionResumed");
575             Intent extras = new Intent();
576             extras.putExtra(SoundTriggerManager.EXTRA_MESSAGE_TYPE,
577                     SoundTriggerManager.FLAG_MESSAGE_TYPE_RECOGNITION_RESUMED);
578             try {
579                 mCallbackIntent.send(mContext, 0, extras, mCallbackCompletedHandler, null);
580             } catch (PendingIntent.CanceledException e) {
581                 removeCallback(/*releaseWakeLock=*/true);
582             }
583         }
584 
removeCallback(boolean releaseWakeLock)585         private void removeCallback(boolean releaseWakeLock) {
586             mCallbackIntent = null;
587             synchronized (mCallbacksLock) {
588                 mCallbacks.remove(mUuid);
589                 if (releaseWakeLock) {
590                     mWakelock.release();
591                 }
592             }
593         }
594     }
595 
596     /**
597      * Counts the number of operations added in the last 24 hours.
598      */
599     private static class NumOps {
600         private final Object mLock = new Object();
601 
602         @GuardedBy("mLock")
603         private int[] mNumOps = new int[24];
604         @GuardedBy("mLock")
605         private long mLastOpsHourSinceBoot;
606 
607         /**
608          * Clear buckets of new hours that have elapsed since last operation.
609          *
610          * <p>I.e. when the last operation was triggered at 1:40 and the current operation was
611          * triggered at 4:03, the buckets "2, 3, and 4" are cleared.
612          *
613          * @param currentTime Current elapsed time since boot in ns
614          */
clearOldOps(long currentTime)615         void clearOldOps(long currentTime) {
616             synchronized (mLock) {
617                 long numHoursSinceBoot = TimeUnit.HOURS.convert(currentTime, TimeUnit.NANOSECONDS);
618 
619                 // Clear buckets of new hours that have elapsed since last operation
620                 // I.e. when the last operation was triggered at 1:40 and the current
621                 // operation was triggered at 4:03, the bucket "2, 3, and 4" is cleared
622                 if (mLastOpsHourSinceBoot != 0) {
623                     for (long hour = mLastOpsHourSinceBoot + 1; hour <= numHoursSinceBoot; hour++) {
624                         mNumOps[(int) (hour % 24)] = 0;
625                     }
626                 }
627             }
628         }
629 
630         /**
631          * Add a new operation.
632          *
633          * @param currentTime Current elapsed time since boot in ns
634          */
addOp(long currentTime)635         void addOp(long currentTime) {
636             synchronized (mLock) {
637                 long numHoursSinceBoot = TimeUnit.HOURS.convert(currentTime, TimeUnit.NANOSECONDS);
638 
639                 mNumOps[(int) (numHoursSinceBoot % 24)]++;
640                 mLastOpsHourSinceBoot = numHoursSinceBoot;
641             }
642         }
643 
644         /**
645          * Get the total operations added in the last 24 hours.
646          *
647          * @return The total number of operations added in the last 24 hours
648          */
getOpsAdded()649         int getOpsAdded() {
650             synchronized (mLock) {
651                 int totalOperationsInLastDay = 0;
652                 for (int i = 0; i < 24; i++) {
653                     totalOperationsInLastDay += mNumOps[i];
654                 }
655 
656                 return totalOperationsInLastDay;
657             }
658         }
659     }
660 
661     /**
662      * A single operation run in a {@link RemoteSoundTriggerDetectionService}.
663      *
664      * <p>Once the remote service is connected either setup + execute or setup + stop is executed.
665      */
666     private static class Operation {
667         private interface ExecuteOp {
run(int opId, ISoundTriggerDetectionService service)668             void run(int opId, ISoundTriggerDetectionService service) throws RemoteException;
669         }
670 
671         private final @Nullable Runnable mSetupOp;
672         private final @NonNull ExecuteOp mExecuteOp;
673         private final @Nullable Runnable mDropOp;
674 
Operation(@ullable Runnable setupOp, @NonNull ExecuteOp executeOp, @Nullable Runnable cancelOp)675         private Operation(@Nullable Runnable setupOp, @NonNull ExecuteOp executeOp,
676                 @Nullable Runnable cancelOp) {
677             mSetupOp = setupOp;
678             mExecuteOp = executeOp;
679             mDropOp = cancelOp;
680         }
681 
setup()682         private void setup() {
683             if (mSetupOp != null) {
684                 mSetupOp.run();
685             }
686         }
687 
run(int opId, @NonNull ISoundTriggerDetectionService service)688         void run(int opId, @NonNull ISoundTriggerDetectionService service) throws RemoteException {
689             setup();
690             mExecuteOp.run(opId, service);
691         }
692 
drop()693         void drop() {
694             setup();
695 
696             if (mDropOp != null) {
697                 mDropOp.run();
698             }
699         }
700     }
701 
702     /**
703      * Local end for a {@link SoundTriggerDetectionService}. Operations are queued up and executed
704      * when the service connects.
705      *
706      * <p>If operations take too long they are forcefully aborted.
707      *
708      * <p>This also limits the amount of operations in 24 hours.
709      */
710     private class RemoteSoundTriggerDetectionService
711         extends IRecognitionStatusCallback.Stub implements ServiceConnection {
712         private static final int MSG_STOP_ALL_PENDING_OPERATIONS = 1;
713 
714         private final Object mRemoteServiceLock = new Object();
715 
716         /** UUID of the model the service is started for */
717         private final @NonNull ParcelUuid mPuuid;
718         /** Params passed into the start method for the service */
719         private final @Nullable Bundle mParams;
720         /** Component name passed when starting the service */
721         private final @NonNull ComponentName mServiceName;
722         /** User that started the service */
723         private final @NonNull UserHandle mUser;
724         /** Configuration of the recognition the service is handling */
725         private final @NonNull RecognitionConfig mRecognitionConfig;
726         /** Wake lock keeping the remote service alive */
727         private final @NonNull PowerManager.WakeLock mRemoteServiceWakeLock;
728 
729         private final @NonNull Handler mHandler;
730 
731         /** Callbacks that are called by the service */
732         private final @NonNull ISoundTriggerDetectionServiceClient mClient;
733 
734         /** Operations that are pending because the service is not yet connected */
735         @GuardedBy("mRemoteServiceLock")
736         private final ArrayList<Operation> mPendingOps = new ArrayList<>();
737         /** Operations that have been send to the service but have no yet finished */
738         @GuardedBy("mRemoteServiceLock")
739         private final ArraySet<Integer> mRunningOpIds = new ArraySet<>();
740         /** The number of operations executed in each of the last 24 hours */
741         private final NumOps mNumOps;
742 
743         /** The service binder if connected */
744         @GuardedBy("mRemoteServiceLock")
745         private @Nullable ISoundTriggerDetectionService mService;
746         /** Whether the service has been bound */
747         @GuardedBy("mRemoteServiceLock")
748         private boolean mIsBound;
749         /** Whether the service has been destroyed */
750         @GuardedBy("mRemoteServiceLock")
751         private boolean mIsDestroyed;
752         /**
753          * Set once a final op is scheduled. No further ops can be added and the service is
754          * destroyed once the op finishes.
755          */
756         @GuardedBy("mRemoteServiceLock")
757         private boolean mDestroyOnceRunningOpsDone;
758 
759         /** Total number of operations performed by this service */
760         @GuardedBy("mRemoteServiceLock")
761         private int mNumTotalOpsPerformed;
762 
763         /**
764          * Create a new remote sound trigger detection service. This only binds to the service when
765          * operations are in flight. Each operation has a certain time it can run. Once no
766          * operations are allowed to run anymore, {@link #stopAllPendingOperations() all operations
767          * are aborted and stopped} and the service is disconnected.
768          *
769          * @param modelUuid The UUID of the model the recognition is for
770          * @param params The params passed to each method of the service
771          * @param serviceName The component name of the service
772          * @param user The user of the service
773          * @param config The configuration of the recognition
774          */
RemoteSoundTriggerDetectionService(@onNull UUID modelUuid, @Nullable Bundle params, @NonNull ComponentName serviceName, @NonNull UserHandle user, @NonNull RecognitionConfig config)775         public RemoteSoundTriggerDetectionService(@NonNull UUID modelUuid,
776             @Nullable Bundle params, @NonNull ComponentName serviceName, @NonNull UserHandle user,
777             @NonNull RecognitionConfig config) {
778             mPuuid = new ParcelUuid(modelUuid);
779             mParams = params;
780             mServiceName = serviceName;
781             mUser = user;
782             mRecognitionConfig = config;
783             mHandler = new Handler(Looper.getMainLooper());
784 
785             PowerManager pm = ((PowerManager) mContext.getSystemService(Context.POWER_SERVICE));
786             mRemoteServiceWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
787                     "RemoteSoundTriggerDetectionService " + mServiceName.getPackageName() + ":"
788                             + mServiceName.getClassName());
789 
790             synchronized (mLock) {
791                 NumOps numOps = mNumOpsPerPackage.get(mServiceName.getPackageName());
792                 if (numOps == null) {
793                     numOps = new NumOps();
794                     mNumOpsPerPackage.put(mServiceName.getPackageName(), numOps);
795                 }
796                 mNumOps = numOps;
797             }
798 
799             mClient = new ISoundTriggerDetectionServiceClient.Stub() {
800                 @Override
801                 public void onOpFinished(int opId) {
802                     long token = Binder.clearCallingIdentity();
803                     try {
804                         synchronized (mRemoteServiceLock) {
805                             mRunningOpIds.remove(opId);
806 
807                             if (mRunningOpIds.isEmpty() && mPendingOps.isEmpty()) {
808                                 if (mDestroyOnceRunningOpsDone) {
809                                     destroy();
810                                 } else {
811                                     disconnectLocked();
812                                 }
813                             }
814                         }
815                     } finally {
816                         Binder.restoreCallingIdentity(token);
817                     }
818                 }
819             };
820         }
821 
822         @Override
pingBinder()823         public boolean pingBinder() {
824             return !(mIsDestroyed || mDestroyOnceRunningOpsDone);
825         }
826 
827         /**
828          * Disconnect from the service, but allow to re-connect when new operations are triggered.
829          */
disconnectLocked()830         private void disconnectLocked() {
831             if (mService != null) {
832                 try {
833                     mService.removeClient(mPuuid);
834                 } catch (Exception e) {
835                     Slog.e(TAG, mPuuid + ": Cannot remove client", e);
836                 }
837 
838                 mService = null;
839             }
840 
841             if (mIsBound) {
842                 mContext.unbindService(RemoteSoundTriggerDetectionService.this);
843                 mIsBound = false;
844 
845                 synchronized (mCallbacksLock) {
846                     mRemoteServiceWakeLock.release();
847                 }
848             }
849         }
850 
851         /**
852          * Disconnect, do not allow to reconnect to the service. All further operations will be
853          * dropped.
854          */
destroy()855         private void destroy() {
856             if (DEBUG) Slog.v(TAG, mPuuid + ": destroy");
857 
858             synchronized (mRemoteServiceLock) {
859                 disconnectLocked();
860 
861                 mIsDestroyed = true;
862             }
863 
864             // The callback is removed before the flag is set
865             if (!mDestroyOnceRunningOpsDone) {
866                 synchronized (mCallbacksLock) {
867                     mCallbacks.remove(mPuuid.getUuid());
868                 }
869             }
870         }
871 
872         /**
873          * Stop all pending operations and then disconnect for the service.
874          */
stopAllPendingOperations()875         private void stopAllPendingOperations() {
876             synchronized (mRemoteServiceLock) {
877                 if (mIsDestroyed) {
878                     return;
879                 }
880 
881                 if (mService != null) {
882                     int numOps = mRunningOpIds.size();
883                     for (int i = 0; i < numOps; i++) {
884                         try {
885                             mService.onStopOperation(mPuuid, mRunningOpIds.valueAt(i));
886                         } catch (Exception e) {
887                             Slog.e(TAG, mPuuid + ": Could not stop operation "
888                                     + mRunningOpIds.valueAt(i), e);
889                         }
890                     }
891 
892                     mRunningOpIds.clear();
893                 }
894 
895                 disconnectLocked();
896             }
897         }
898 
899         /**
900          * Verify that the service has the expected properties and then bind to the service
901          */
bind()902         private void bind() {
903             long token = Binder.clearCallingIdentity();
904             try {
905                 Intent i = new Intent();
906                 i.setComponent(mServiceName);
907 
908                 ResolveInfo ri = mContext.getPackageManager().resolveServiceAsUser(i,
909                         GET_SERVICES | GET_META_DATA | MATCH_DEBUG_TRIAGED_MISSING,
910                         mUser.getIdentifier());
911 
912                 if (ri == null) {
913                     Slog.w(TAG, mPuuid + ": " + mServiceName + " not found");
914                     return;
915                 }
916 
917                 if (!BIND_SOUND_TRIGGER_DETECTION_SERVICE
918                         .equals(ri.serviceInfo.permission)) {
919                     Slog.w(TAG, mPuuid + ": " + mServiceName + " does not require "
920                             + BIND_SOUND_TRIGGER_DETECTION_SERVICE);
921                     return;
922                 }
923 
924                 mIsBound = mContext.bindServiceAsUser(i, this,
925                         BIND_AUTO_CREATE | BIND_FOREGROUND_SERVICE, mUser);
926 
927                 if (mIsBound) {
928                     mRemoteServiceWakeLock.acquire();
929                 } else {
930                     Slog.w(TAG, mPuuid + ": Could not bind to " + mServiceName);
931                 }
932             } finally {
933                 Binder.restoreCallingIdentity(token);
934             }
935         }
936 
937         /**
938          * Run an operation (i.e. send it do the service). If the service is not connected, this
939          * binds the service and then runs the operation once connected.
940          *
941          * @param op The operation to run
942          */
runOrAddOperation(Operation op)943         private void runOrAddOperation(Operation op) {
944             synchronized (mRemoteServiceLock) {
945                 if (mIsDestroyed || mDestroyOnceRunningOpsDone) {
946                     Slog.w(TAG, mPuuid + ": Dropped operation as already destroyed or marked for "
947                             + "destruction");
948 
949                     op.drop();
950                     return;
951                 }
952 
953                 if (mService == null) {
954                     mPendingOps.add(op);
955 
956                     if (!mIsBound) {
957                         bind();
958                     }
959                 } else {
960                     long currentTime = System.nanoTime();
961                     mNumOps.clearOldOps(currentTime);
962 
963                     // Drop operation if too many were executed in the last 24 hours.
964                     int opsAllowed = Settings.Global.getInt(mContext.getContentResolver(),
965                             MAX_SOUND_TRIGGER_DETECTION_SERVICE_OPS_PER_DAY,
966                             Integer.MAX_VALUE);
967 
968                     // As we currently cannot dropping an op safely, disable throttling
969                     int opsAdded = mNumOps.getOpsAdded();
970                     if (false && mNumOps.getOpsAdded() >= opsAllowed) {
971                         try {
972                             if (DEBUG || opsAllowed + 10 > opsAdded) {
973                                 Slog.w(TAG, mPuuid + ": Dropped operation as too many operations "
974                                         + "were run in last 24 hours");
975                             }
976 
977                             op.drop();
978                         } catch (Exception e) {
979                             Slog.e(TAG, mPuuid + ": Could not drop operation", e);
980                         }
981                     } else {
982                         mNumOps.addOp(currentTime);
983 
984                         // Find a free opID
985                         int opId = mNumTotalOpsPerformed;
986                         do {
987                             mNumTotalOpsPerformed++;
988                         } while (mRunningOpIds.contains(opId));
989 
990                         // Run OP
991                         try {
992                             if (DEBUG) Slog.v(TAG, mPuuid + ": runOp " + opId);
993 
994                             op.run(opId, mService);
995                             mRunningOpIds.add(opId);
996                         } catch (Exception e) {
997                             Slog.e(TAG, mPuuid + ": Could not run operation " + opId, e);
998                         }
999                     }
1000 
1001                     // Unbind from service if no operations are left (i.e. if the operation failed)
1002                     if (mPendingOps.isEmpty() && mRunningOpIds.isEmpty()) {
1003                         if (mDestroyOnceRunningOpsDone) {
1004                             destroy();
1005                         } else {
1006                             disconnectLocked();
1007                         }
1008                     } else {
1009                         mHandler.removeMessages(MSG_STOP_ALL_PENDING_OPERATIONS);
1010                         mHandler.sendMessageDelayed(obtainMessage(
1011                                 RemoteSoundTriggerDetectionService::stopAllPendingOperations, this)
1012                                         .setWhat(MSG_STOP_ALL_PENDING_OPERATIONS),
1013                                 Settings.Global.getLong(mContext.getContentResolver(),
1014                                         SOUND_TRIGGER_DETECTION_SERVICE_OP_TIMEOUT,
1015                                         Long.MAX_VALUE));
1016                     }
1017                 }
1018             }
1019         }
1020 
1021         @Override
onKeyphraseDetected(SoundTrigger.KeyphraseRecognitionEvent event)1022         public void onKeyphraseDetected(SoundTrigger.KeyphraseRecognitionEvent event) {
1023             Slog.w(TAG, mPuuid + "->" + mServiceName + ": IGNORED onKeyphraseDetected(" + event
1024                     + ")");
1025         }
1026 
1027         /**
1028          * Create an AudioRecord enough for starting and releasing the data buffered for the event.
1029          *
1030          * @param event The event that was received
1031          * @return The initialized AudioRecord
1032          */
createAudioRecordForEvent( @onNull SoundTrigger.GenericRecognitionEvent event)1033         private @NonNull AudioRecord createAudioRecordForEvent(
1034                 @NonNull SoundTrigger.GenericRecognitionEvent event) {
1035             AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder();
1036             attributesBuilder.setInternalCapturePreset(MediaRecorder.AudioSource.HOTWORD);
1037             AudioAttributes attributes = attributesBuilder.build();
1038 
1039             // Use same AudioFormat processing as in RecognitionEvent.fromParcel
1040             AudioFormat originalFormat = event.getCaptureFormat();
1041             AudioFormat captureFormat = (new AudioFormat.Builder())
1042                     .setChannelMask(originalFormat.getChannelMask())
1043                     .setEncoding(originalFormat.getEncoding())
1044                     .setSampleRate(originalFormat.getSampleRate())
1045                     .build();
1046 
1047             int bufferSize = AudioRecord.getMinBufferSize(
1048                     captureFormat.getSampleRate() == AudioFormat.SAMPLE_RATE_UNSPECIFIED
1049                             ? AudioFormat.SAMPLE_RATE_HZ_MAX
1050                             : captureFormat.getSampleRate(),
1051                     captureFormat.getChannelCount() == 2
1052                             ? AudioFormat.CHANNEL_IN_STEREO
1053                             : AudioFormat.CHANNEL_IN_MONO,
1054                     captureFormat.getEncoding());
1055 
1056             return new AudioRecord(attributes, captureFormat, bufferSize,
1057                     event.getCaptureSession());
1058         }
1059 
1060         @Override
onGenericSoundTriggerDetected(SoundTrigger.GenericRecognitionEvent event)1061         public void onGenericSoundTriggerDetected(SoundTrigger.GenericRecognitionEvent event) {
1062             if (DEBUG) Slog.v(TAG, mPuuid + ": Generic sound trigger event: " + event);
1063 
1064             runOrAddOperation(new Operation(
1065                     // always execute:
1066                     () -> {
1067                         if (!mRecognitionConfig.allowMultipleTriggers) {
1068                             // Unregister this remoteService once op is done
1069                             synchronized (mCallbacksLock) {
1070                                 mCallbacks.remove(mPuuid.getUuid());
1071                             }
1072                             mDestroyOnceRunningOpsDone = true;
1073                         }
1074                     },
1075                     // execute if not throttled:
1076                     (opId, service) -> service.onGenericRecognitionEvent(mPuuid, opId, event),
1077                     // execute if throttled:
1078                     () -> {
1079                         if (event.isCaptureAvailable()) {
1080                             AudioRecord capturedData = createAudioRecordForEvent(event);
1081 
1082                             // Currently we need to start and release the audio record to reset
1083                             // the DSP even if we don't want to process the event
1084                             capturedData.startRecording();
1085                             capturedData.release();
1086                         }
1087                     }));
1088         }
1089 
1090         @Override
onError(int status)1091         public void onError(int status) {
1092             if (DEBUG) Slog.v(TAG, mPuuid + ": onError: " + status);
1093 
1094             runOrAddOperation(
1095                     new Operation(
1096                             // always execute:
1097                             () -> {
1098                                 // Unregister this remoteService once op is done
1099                                 synchronized (mCallbacksLock) {
1100                                     mCallbacks.remove(mPuuid.getUuid());
1101                                 }
1102                                 mDestroyOnceRunningOpsDone = true;
1103                             },
1104                             // execute if not throttled:
1105                             (opId, service) -> service.onError(mPuuid, opId, status),
1106                             // nothing to do if throttled
1107                             null));
1108         }
1109 
1110         @Override
onRecognitionPaused()1111         public void onRecognitionPaused() {
1112             Slog.i(TAG, mPuuid + "->" + mServiceName + ": IGNORED onRecognitionPaused");
1113         }
1114 
1115         @Override
onRecognitionResumed()1116         public void onRecognitionResumed() {
1117             Slog.i(TAG, mPuuid + "->" + mServiceName + ": IGNORED onRecognitionResumed");
1118         }
1119 
1120         @Override
onServiceConnected(ComponentName name, IBinder service)1121         public void onServiceConnected(ComponentName name, IBinder service) {
1122             if (DEBUG) Slog.v(TAG, mPuuid + ": onServiceConnected(" + service + ")");
1123 
1124             synchronized (mRemoteServiceLock) {
1125                 mService = ISoundTriggerDetectionService.Stub.asInterface(service);
1126 
1127                 try {
1128                     mService.setClient(mPuuid, mParams, mClient);
1129                 } catch (Exception e) {
1130                     Slog.e(TAG, mPuuid + ": Could not init " + mServiceName, e);
1131                     return;
1132                 }
1133 
1134                 while (!mPendingOps.isEmpty()) {
1135                     runOrAddOperation(mPendingOps.remove(0));
1136                 }
1137             }
1138         }
1139 
1140         @Override
onServiceDisconnected(ComponentName name)1141         public void onServiceDisconnected(ComponentName name) {
1142             if (DEBUG) Slog.v(TAG, mPuuid + ": onServiceDisconnected");
1143 
1144             synchronized (mRemoteServiceLock) {
1145                 mService = null;
1146             }
1147         }
1148 
1149         @Override
onBindingDied(ComponentName name)1150         public void onBindingDied(ComponentName name) {
1151             if (DEBUG) Slog.v(TAG, mPuuid + ": onBindingDied");
1152 
1153             synchronized (mRemoteServiceLock) {
1154                 destroy();
1155             }
1156         }
1157 
1158         @Override
onNullBinding(ComponentName name)1159         public void onNullBinding(ComponentName name) {
1160             Slog.w(TAG, name + " for model " + mPuuid + " returned a null binding");
1161 
1162             synchronized (mRemoteServiceLock) {
1163                 disconnectLocked();
1164             }
1165         }
1166     }
1167 
grabWakeLock()1168     private void grabWakeLock() {
1169         synchronized (mCallbacksLock) {
1170             if (mWakelock == null) {
1171                 PowerManager pm = ((PowerManager) mContext.getSystemService(Context.POWER_SERVICE));
1172                 mWakelock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
1173             }
1174             mWakelock.acquire();
1175         }
1176     }
1177 
1178     private PendingIntent.OnFinished mCallbackCompletedHandler = new PendingIntent.OnFinished() {
1179         @Override
1180         public void onSendFinished(PendingIntent pendingIntent, Intent intent, int resultCode,
1181                 String resultData, Bundle resultExtras) {
1182             // We're only ever invoked when the callback is done, so release the lock.
1183             synchronized (mCallbacksLock) {
1184                 mWakelock.release();
1185             }
1186         }
1187     };
1188 
1189     public final class LocalSoundTriggerService extends SoundTriggerInternal {
1190         private final Context mContext;
1191         private SoundTriggerHelper mSoundTriggerHelper;
1192 
LocalSoundTriggerService(Context context)1193         LocalSoundTriggerService(Context context) {
1194             mContext = context;
1195         }
1196 
setSoundTriggerHelper(SoundTriggerHelper helper)1197         synchronized void setSoundTriggerHelper(SoundTriggerHelper helper) {
1198             mSoundTriggerHelper = helper;
1199         }
1200 
1201         @Override
startRecognition(int keyphraseId, KeyphraseSoundModel soundModel, IRecognitionStatusCallback listener, RecognitionConfig recognitionConfig)1202         public int startRecognition(int keyphraseId, KeyphraseSoundModel soundModel,
1203                 IRecognitionStatusCallback listener, RecognitionConfig recognitionConfig) {
1204             if (!isInitialized()) return STATUS_ERROR;
1205             return mSoundTriggerHelper.startKeyphraseRecognition(keyphraseId, soundModel, listener,
1206                     recognitionConfig);
1207         }
1208 
1209         @Override
stopRecognition(int keyphraseId, IRecognitionStatusCallback listener)1210         public synchronized int stopRecognition(int keyphraseId, IRecognitionStatusCallback listener) {
1211             if (!isInitialized()) return STATUS_ERROR;
1212             return mSoundTriggerHelper.stopKeyphraseRecognition(keyphraseId, listener);
1213         }
1214 
1215         @Override
getModuleProperties()1216         public ModuleProperties getModuleProperties() {
1217             if (!isInitialized()) return null;
1218             return mSoundTriggerHelper.getModuleProperties();
1219         }
1220 
1221         @Override
unloadKeyphraseModel(int keyphraseId)1222         public int unloadKeyphraseModel(int keyphraseId) {
1223             if (!isInitialized()) return STATUS_ERROR;
1224             return mSoundTriggerHelper.unloadKeyphraseSoundModel(keyphraseId);
1225         }
1226 
1227         @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)1228         public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
1229             if (!isInitialized()) return;
1230             mSoundTriggerHelper.dump(fd, pw, args);
1231         }
1232 
isInitialized()1233         private synchronized boolean isInitialized() {
1234             if (mSoundTriggerHelper == null ) {
1235                 Slog.e(TAG, "SoundTriggerHelper not initialized.");
1236                 return false;
1237             }
1238             return true;
1239         }
1240     }
1241 
enforceCallingPermission(String permission)1242     private void enforceCallingPermission(String permission) {
1243         if (mContext.checkCallingOrSelfPermission(permission)
1244                 != PackageManager.PERMISSION_GRANTED) {
1245             throw new SecurityException("Caller does not hold the permission " + permission);
1246         }
1247     }
1248 }
1249