Home | History | Annotate | Download | only in soundtrigger
      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 
    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
    125     public void onStart() {
    126         publishBinderService(Context.SOUND_TRIGGER_SERVICE, mServiceStub);
    127         publishLocalService(SoundTriggerInternal.class, mLocalSoundTriggerService);
    128     }
    129 
    130     @Override
    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
    141     public void onStartUser(int userHandle) {
    142     }
    143 
    144     @Override
    145     public void onSwitchUser(int userHandle) {
    146     }
    147 
    148     private synchronized void initSoundTriggerHelper() {
    149         if (mSoundTriggerHelper == null) {
    150             mSoundTriggerHelper = new SoundTriggerHelper(mContext);
    151         }
    152     }
    153 
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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 
    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
    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
    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
    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 
    470         public LocalSoundTriggerRecognitionStatusIntentCallback(UUID modelUuid,
    471                 PendingIntent callbackIntent,
    472                 RecognitionConfig config) {
    473             mUuid = modelUuid;
    474             mCallbackIntent = callbackIntent;
    475             mRecognitionConfig = config;
    476         }
    477 
    478         @Override
    479         public boolean pingBinder() {
    480             return mCallbackIntent != null;
    481         }
    482 
    483         @Override
    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
    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
    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
    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
    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 
    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          */
    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          */
    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          */
    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 {
    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 
    675         private Operation(@Nullable Runnable setupOp, @NonNull ExecuteOp executeOp,
    676                 @Nullable Runnable cancelOp) {
    677             mSetupOp = setupOp;
    678             mExecuteOp = executeOp;
    679             mDropOp = cancelOp;
    680         }
    681 
    682         private void setup() {
    683             if (mSetupOp != null) {
    684                 mSetupOp.run();
    685             }
    686         }
    687 
    688         void run(int opId, @NonNull ISoundTriggerDetectionService service) throws RemoteException {
    689             setup();
    690             mExecuteOp.run(opId, service);
    691         }
    692 
    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          */
    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
    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          */
    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          */
    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          */
    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          */
    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          */
    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
   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          */
   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
   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
   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
   1111         public void onRecognitionPaused() {
   1112             Slog.i(TAG, mPuuid + "->" + mServiceName + ": IGNORED onRecognitionPaused");
   1113         }
   1114 
   1115         @Override
   1116         public void onRecognitionResumed() {
   1117             Slog.i(TAG, mPuuid + "->" + mServiceName + ": IGNORED onRecognitionResumed");
   1118         }
   1119 
   1120         @Override
   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
   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
   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
   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 
   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 
   1193         LocalSoundTriggerService(Context context) {
   1194             mContext = context;
   1195         }
   1196 
   1197         synchronized void setSoundTriggerHelper(SoundTriggerHelper helper) {
   1198             mSoundTriggerHelper = helper;
   1199         }
   1200 
   1201         @Override
   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
   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
   1216         public ModuleProperties getModuleProperties() {
   1217             if (!isInitialized()) return null;
   1218             return mSoundTriggerHelper.getModuleProperties();
   1219         }
   1220 
   1221         @Override
   1222         public int unloadKeyphraseModel(int keyphraseId) {
   1223             if (!isInitialized()) return STATUS_ERROR;
   1224             return mSoundTriggerHelper.unloadKeyphraseSoundModel(keyphraseId);
   1225         }
   1226 
   1227         @Override
   1228         public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
   1229             if (!isInitialized()) return;
   1230             mSoundTriggerHelper.dump(fd, pw, args);
   1231         }
   1232 
   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 
   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