2 * Copyright (C) 2014 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.server.soundtrigger;
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;
30 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
32 import android.Manifest;
33 import android.annotation.NonNull;
34 import android.annotation.Nullable;
35 import android.content.ComponentName;
36 import android.content.Context;
37 import android.content.Intent;
38 import android.content.ServiceConnection;
39 import android.content.pm.PackageManager;
40 import android.content.pm.ResolveInfo;
41 import android.hardware.soundtrigger.IRecognitionStatusCallback;
42 import android.hardware.soundtrigger.SoundTrigger;
43 import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
44 import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel;
45 import android.hardware.soundtrigger.SoundTrigger.ModuleProperties;
46 import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
47 import android.hardware.soundtrigger.SoundTrigger.SoundModel;
48 import android.media.AudioAttributes;
49 import android.media.AudioFormat;
50 import android.media.AudioRecord;
51 import android.media.MediaRecorder;
52 import android.media.soundtrigger.ISoundTriggerDetectionService;
53 import android.media.soundtrigger.ISoundTriggerDetectionServiceClient;
54 import android.media.soundtrigger.SoundTriggerDetectionService;
55 import android.os.Binder;
56 import android.os.Bundle;
57 import android.os.Handler;
58 import android.os.IBinder;
59 import android.os.Looper;
60 import android.os.Parcel;
61 import android.os.ParcelUuid;
62 import android.os.PowerManager;
63 import android.os.RemoteException;
64 import android.os.SystemClock;
65 import android.os.UserHandle;
66 import android.provider.Settings;
67 import android.util.ArrayMap;
68 import android.util.ArraySet;
69 import android.util.Slog;
71 import com.android.internal.annotations.GuardedBy;
72 import com.android.internal.app.ISoundTriggerService;
73 import com.android.internal.util.Preconditions;
74 import com.android.server.SystemService;
76 import java.io.FileDescriptor;
77 import java.io.PrintWriter;
78 import java.util.ArrayList;
80 import java.util.TreeMap;
81 import java.util.UUID;
82 import java.util.concurrent.TimeUnit;
85 * A single SystemService to manage all sound/voice-based sound models on the DSP.
86 * This services provides apis to manage sound trigger-based sound models via
87 * the ISoundTriggerService interface. This class also publishes a local interface encapsulating
88 * the functionality provided by {@link SoundTriggerHelper} for use by
89 * {@link VoiceInteractionManagerService}.
93 public class SoundTriggerService extends SystemService {
94 private static final String TAG = "SoundTriggerService";
95 private static final boolean DEBUG = true;
97 final Context mContext;
99 private final SoundTriggerServiceStub mServiceStub;
100 private final LocalSoundTriggerService mLocalSoundTriggerService;
101 private SoundTriggerDbHelper mDbHelper;
102 private SoundTriggerHelper mSoundTriggerHelper;
103 private final TreeMap<UUID, SoundModel> mLoadedModels;
104 private Object mCallbacksLock;
105 private final TreeMap<UUID, IRecognitionStatusCallback> mCallbacks;
107 class SoundModelStatTracker {
108 private class SoundModelStat {
112 mLastStartTimestampMsec = 0;
113 mLastStopTimestampMsec = 0;
116 long mStartCount; // Number of times that given model started
117 long mTotalTimeMsec; // Total time (msec) that given model was running since boot
118 long mLastStartTimestampMsec; // SystemClock.elapsedRealtime model was last started
119 long mLastStopTimestampMsec; // SystemClock.elapsedRealtime model was last stopped
120 boolean mIsStarted; // true if model is currently running
122 private final TreeMap<UUID, SoundModelStat> mModelStats;
124 SoundModelStatTracker() {
125 mModelStats = new TreeMap<UUID, SoundModelStat>();
128 public synchronized void onStart(UUID id) {
129 SoundModelStat stat = mModelStats.get(id);
131 stat = new SoundModelStat();
132 mModelStats.put(id, stat);
135 if (stat.mIsStarted) {
136 Slog.e(TAG, "error onStart(): Model " + id + " already started");
141 stat.mLastStartTimestampMsec = SystemClock.elapsedRealtime();
142 stat.mIsStarted = true;
145 public synchronized void onStop(UUID id) {
146 SoundModelStat stat = mModelStats.get(id);
148 Slog.e(TAG, "error onStop(): Model " + id + " has no stats available");
152 if (!stat.mIsStarted) {
153 Slog.e(TAG, "error onStop(): Model " + id + " already stopped");
157 stat.mLastStopTimestampMsec = SystemClock.elapsedRealtime();
158 stat.mTotalTimeMsec += stat.mLastStopTimestampMsec - stat.mLastStartTimestampMsec;
159 stat.mIsStarted = false;
162 public synchronized void dump(PrintWriter pw) {
163 long curTime = SystemClock.elapsedRealtime();
164 pw.println("Model Stats:");
165 for (Map.Entry<UUID, SoundModelStat> entry : mModelStats.entrySet()) {
166 UUID uuid = entry.getKey();
167 SoundModelStat stat = entry.getValue();
168 long totalTimeMsec = stat.mTotalTimeMsec;
169 if (stat.mIsStarted) {
170 totalTimeMsec += curTime - stat.mLastStartTimestampMsec;
172 pw.println(uuid + ", total_time(msec)=" + totalTimeMsec
173 + ", total_count=" + stat.mStartCount
174 + ", last_start=" + stat.mLastStartTimestampMsec
175 + ", last_stop=" + stat.mLastStopTimestampMsec);
180 private final SoundModelStatTracker mSoundModelStatTracker;
181 /** Number of ops run by the {@link RemoteSoundTriggerDetectionService} per package name */
183 private final ArrayMap<String, NumOps> mNumOpsPerPackage = new ArrayMap<>();
185 public SoundTriggerService(Context context) {
188 mServiceStub = new SoundTriggerServiceStub();
189 mLocalSoundTriggerService = new LocalSoundTriggerService(context);
190 mLoadedModels = new TreeMap<UUID, SoundModel>();
191 mCallbacksLock = new Object();
192 mCallbacks = new TreeMap<>();
193 mLock = new Object();
194 mSoundModelStatTracker = new SoundModelStatTracker();
198 public void onStart() {
199 publishBinderService(Context.SOUND_TRIGGER_SERVICE, mServiceStub);
200 publishLocalService(SoundTriggerInternal.class, mLocalSoundTriggerService);
204 public void onBootPhase(int phase) {
205 if (PHASE_SYSTEM_SERVICES_READY == phase) {
206 initSoundTriggerHelper();
207 mLocalSoundTriggerService.setSoundTriggerHelper(mSoundTriggerHelper);
208 } else if (PHASE_THIRD_PARTY_APPS_CAN_START == phase) {
209 mDbHelper = new SoundTriggerDbHelper(mContext);
214 public void onStartUser(int userHandle) {
218 public void onSwitchUser(int userHandle) {
221 private synchronized void initSoundTriggerHelper() {
222 if (mSoundTriggerHelper == null) {
223 mSoundTriggerHelper = new SoundTriggerHelper(mContext);
227 private synchronized boolean isInitialized() {
228 if (mSoundTriggerHelper == null ) {
229 Slog.e(TAG, "SoundTriggerHelper not initialized.");
235 class SoundTriggerServiceStub extends ISoundTriggerService.Stub {
237 public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
238 throws RemoteException {
240 return super.onTransact(code, data, reply, flags);
241 } catch (RuntimeException e) {
242 // The activity manager only throws security exceptions, so let's
244 if (!(e instanceof SecurityException)) {
245 Slog.wtf(TAG, "SoundTriggerService Crash", e);
252 public int startRecognition(ParcelUuid parcelUuid, IRecognitionStatusCallback callback,
253 RecognitionConfig config) {
254 enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
255 if (!isInitialized()) return STATUS_ERROR;
257 Slog.i(TAG, "startRecognition(): Uuid : " + parcelUuid);
260 sEventLogger.log(new SoundTriggerLogger.StringEvent("startRecognition(): Uuid : "
263 GenericSoundModel model = getSoundModel(parcelUuid);
265 Slog.e(TAG, "Null model in database for id: " + parcelUuid);
267 sEventLogger.log(new SoundTriggerLogger.StringEvent(
268 "startRecognition(): Null model in database for id: " + parcelUuid));
273 int ret = mSoundTriggerHelper.startGenericRecognition(parcelUuid.getUuid(), model,
275 if (ret == STATUS_OK) {
276 mSoundModelStatTracker.onStart(parcelUuid.getUuid());
282 public int stopRecognition(ParcelUuid parcelUuid, IRecognitionStatusCallback callback) {
283 enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
285 Slog.i(TAG, "stopRecognition(): Uuid : " + parcelUuid);
288 sEventLogger.log(new SoundTriggerLogger.StringEvent("stopRecognition(): Uuid : "
291 if (!isInitialized()) return STATUS_ERROR;
293 int ret = mSoundTriggerHelper.stopGenericRecognition(parcelUuid.getUuid(), callback);
294 if (ret == STATUS_OK) {
295 mSoundModelStatTracker.onStop(parcelUuid.getUuid());
301 public SoundTrigger.GenericSoundModel getSoundModel(ParcelUuid soundModelId) {
302 enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
304 Slog.i(TAG, "getSoundModel(): id = " + soundModelId);
307 sEventLogger.log(new SoundTriggerLogger.StringEvent("getSoundModel(): id = "
310 SoundTrigger.GenericSoundModel model = mDbHelper.getGenericSoundModel(
311 soundModelId.getUuid());
316 public void updateSoundModel(SoundTrigger.GenericSoundModel soundModel) {
317 enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
319 Slog.i(TAG, "updateSoundModel(): model = " + soundModel);
322 sEventLogger.log(new SoundTriggerLogger.StringEvent("updateSoundModel(): model = "
325 mDbHelper.updateGenericSoundModel(soundModel);
329 public void deleteSoundModel(ParcelUuid soundModelId) {
330 enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
332 Slog.i(TAG, "deleteSoundModel(): id = " + soundModelId);
335 sEventLogger.log(new SoundTriggerLogger.StringEvent("deleteSoundModel(): id = "
338 // Unload the model if it is loaded.
339 mSoundTriggerHelper.unloadGenericSoundModel(soundModelId.getUuid());
340 mDbHelper.deleteGenericSoundModel(soundModelId.getUuid());
342 // Stop recognition if it is started.
343 mSoundModelStatTracker.onStop(soundModelId.getUuid());
347 public int loadGenericSoundModel(GenericSoundModel soundModel) {
348 enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
349 if (!isInitialized()) return STATUS_ERROR;
350 if (soundModel == null || soundModel.uuid == null) {
351 Slog.e(TAG, "Invalid sound model");
353 sEventLogger.log(new SoundTriggerLogger.StringEvent(
354 "loadGenericSoundModel(): Invalid sound model"));
359 Slog.i(TAG, "loadGenericSoundModel(): id = " + soundModel.uuid);
362 sEventLogger.log(new SoundTriggerLogger.StringEvent("loadGenericSoundModel(): id = "
365 synchronized (mLock) {
366 SoundModel oldModel = mLoadedModels.get(soundModel.uuid);
367 // If the model we're loading is actually different than what we had loaded, we
368 // should unload that other model now. We don't care about return codes since we
369 // don't know if the other model is loaded.
370 if (oldModel != null && !oldModel.equals(soundModel)) {
371 mSoundTriggerHelper.unloadGenericSoundModel(soundModel.uuid);
372 synchronized (mCallbacksLock) {
373 mCallbacks.remove(soundModel.uuid);
376 mLoadedModels.put(soundModel.uuid, soundModel);
382 public int loadKeyphraseSoundModel(KeyphraseSoundModel soundModel) {
383 enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
384 if (!isInitialized()) return STATUS_ERROR;
385 if (soundModel == null || soundModel.uuid == null) {
386 Slog.e(TAG, "Invalid sound model");
388 sEventLogger.log(new SoundTriggerLogger.StringEvent(
389 "loadKeyphraseSoundModel(): Invalid sound model"));
393 if (soundModel.keyphrases == null || soundModel.keyphrases.length != 1) {
394 Slog.e(TAG, "Only one keyphrase per model is currently supported.");
396 sEventLogger.log(new SoundTriggerLogger.StringEvent(
397 "loadKeyphraseSoundModel(): Only one keyphrase per model"
398 + " is currently supported."));
403 Slog.i(TAG, "loadKeyphraseSoundModel(): id = " + soundModel.uuid);
406 sEventLogger.log(new SoundTriggerLogger.StringEvent("loadKeyphraseSoundModel(): id = "
409 synchronized (mLock) {
410 SoundModel oldModel = mLoadedModels.get(soundModel.uuid);
411 // If the model we're loading is actually different than what we had loaded, we
412 // should unload that other model now. We don't care about return codes since we
413 // don't know if the other model is loaded.
414 if (oldModel != null && !oldModel.equals(soundModel)) {
415 mSoundTriggerHelper.unloadKeyphraseSoundModel(soundModel.keyphrases[0].id);
416 synchronized (mCallbacksLock) {
417 mCallbacks.remove(soundModel.uuid);
420 mLoadedModels.put(soundModel.uuid, soundModel);
426 public int startRecognitionForService(ParcelUuid soundModelId, Bundle params,
427 ComponentName detectionService, SoundTrigger.RecognitionConfig config) {
428 Preconditions.checkNotNull(soundModelId);
429 Preconditions.checkNotNull(detectionService);
430 Preconditions.checkNotNull(config);
432 enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
434 if (!isInitialized()) return STATUS_ERROR;
436 Slog.i(TAG, "startRecognition(): id = " + soundModelId);
439 sEventLogger.log(new SoundTriggerLogger.StringEvent(
440 "startRecognitionForService(): id = " + soundModelId));
442 IRecognitionStatusCallback callback =
443 new RemoteSoundTriggerDetectionService(soundModelId.getUuid(), params,
444 detectionService, Binder.getCallingUserHandle(), config);
446 synchronized (mLock) {
447 SoundModel soundModel = mLoadedModels.get(soundModelId.getUuid());
448 if (soundModel == null) {
449 Slog.e(TAG, soundModelId + " is not loaded");
451 sEventLogger.log(new SoundTriggerLogger.StringEvent(
452 "startRecognitionForService():" + soundModelId + " is not loaded"));
456 IRecognitionStatusCallback existingCallback = null;
457 synchronized (mCallbacksLock) {
458 existingCallback = mCallbacks.get(soundModelId.getUuid());
460 if (existingCallback != null) {
461 Slog.e(TAG, soundModelId + " is already running");
463 sEventLogger.log(new SoundTriggerLogger.StringEvent(
464 "startRecognitionForService():"
465 + soundModelId + " is already running"));
470 switch (soundModel.type) {
471 case SoundModel.TYPE_GENERIC_SOUND:
472 ret = mSoundTriggerHelper.startGenericRecognition(soundModel.uuid,
473 (GenericSoundModel) soundModel, callback, config);
476 Slog.e(TAG, "Unknown model type");
478 sEventLogger.log(new SoundTriggerLogger.StringEvent(
479 "startRecognitionForService(): Unknown model type"));
484 if (ret != STATUS_OK) {
485 Slog.e(TAG, "Failed to start model: " + ret);
487 sEventLogger.log(new SoundTriggerLogger.StringEvent(
488 "startRecognitionForService(): Failed to start model:"));
492 synchronized (mCallbacksLock) {
493 mCallbacks.put(soundModelId.getUuid(), callback);
496 mSoundModelStatTracker.onStart(soundModelId.getUuid());
502 public int stopRecognitionForService(ParcelUuid soundModelId) {
503 enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
504 if (!isInitialized()) return STATUS_ERROR;
506 Slog.i(TAG, "stopRecognition(): id = " + soundModelId);
509 sEventLogger.log(new SoundTriggerLogger.StringEvent(
510 "stopRecognitionForService(): id = " + soundModelId));
512 synchronized (mLock) {
513 SoundModel soundModel = mLoadedModels.get(soundModelId.getUuid());
514 if (soundModel == null) {
515 Slog.e(TAG, soundModelId + " is not loaded");
517 sEventLogger.log(new SoundTriggerLogger.StringEvent(
518 "stopRecognitionForService(): " + soundModelId
519 + " is not loaded"));
523 IRecognitionStatusCallback callback = null;
524 synchronized (mCallbacksLock) {
525 callback = mCallbacks.get(soundModelId.getUuid());
527 if (callback == null) {
528 Slog.e(TAG, soundModelId + " is not running");
530 sEventLogger.log(new SoundTriggerLogger.StringEvent(
531 "stopRecognitionForService(): " + soundModelId
532 + " is not running"));
537 switch (soundModel.type) {
538 case SoundModel.TYPE_GENERIC_SOUND:
539 ret = mSoundTriggerHelper.stopGenericRecognition(soundModel.uuid, callback);
542 Slog.e(TAG, "Unknown model type");
544 sEventLogger.log(new SoundTriggerLogger.StringEvent(
545 "stopRecognitionForService(): Unknown model type"));
550 if (ret != STATUS_OK) {
551 Slog.e(TAG, "Failed to stop model: " + ret);
553 sEventLogger.log(new SoundTriggerLogger.StringEvent(
554 "stopRecognitionForService(): Failed to stop model: " + ret));
558 synchronized (mCallbacksLock) {
559 mCallbacks.remove(soundModelId.getUuid());
562 mSoundModelStatTracker.onStop(soundModelId.getUuid());
568 public int unloadSoundModel(ParcelUuid soundModelId) {
569 enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
570 if (!isInitialized()) return STATUS_ERROR;
572 Slog.i(TAG, "unloadSoundModel(): id = " + soundModelId);
575 sEventLogger.log(new SoundTriggerLogger.StringEvent("unloadSoundModel(): id = "
578 synchronized (mLock) {
579 SoundModel soundModel = mLoadedModels.get(soundModelId.getUuid());
580 if (soundModel == null) {
581 Slog.e(TAG, soundModelId + " is not loaded");
583 sEventLogger.log(new SoundTriggerLogger.StringEvent(
584 "unloadSoundModel(): " + soundModelId + " is not loaded"));
589 switch (soundModel.type) {
590 case SoundModel.TYPE_KEYPHRASE:
591 ret = mSoundTriggerHelper.unloadKeyphraseSoundModel(
592 ((KeyphraseSoundModel)soundModel).keyphrases[0].id);
594 case SoundModel.TYPE_GENERIC_SOUND:
595 ret = mSoundTriggerHelper.unloadGenericSoundModel(soundModel.uuid);
598 Slog.e(TAG, "Unknown model type");
600 sEventLogger.log(new SoundTriggerLogger.StringEvent(
601 "unloadSoundModel(): Unknown model type"));
605 if (ret != STATUS_OK) {
606 Slog.e(TAG, "Failed to unload model");
608 sEventLogger.log(new SoundTriggerLogger.StringEvent(
609 "unloadSoundModel(): Failed to unload model"));
613 mLoadedModels.remove(soundModelId.getUuid());
619 public boolean isRecognitionActive(ParcelUuid parcelUuid) {
620 enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
621 if (!isInitialized()) return false;
622 synchronized (mCallbacksLock) {
623 IRecognitionStatusCallback callback = mCallbacks.get(parcelUuid.getUuid());
624 if (callback == null) {
628 return mSoundTriggerHelper.isRecognitionRequested(parcelUuid.getUuid());
632 public int getModelState(ParcelUuid soundModelId) {
633 enforceCallingPermission(Manifest.permission.MANAGE_SOUND_TRIGGER);
634 int ret = STATUS_ERROR;
635 if (!isInitialized()) return ret;
637 Slog.i(TAG, "getModelState(): id = " + soundModelId);
640 sEventLogger.log(new SoundTriggerLogger.StringEvent("getModelState(): id = "
643 synchronized (mLock) {
644 SoundModel soundModel = mLoadedModels.get(soundModelId.getUuid());
645 if (soundModel == null) {
646 Slog.e(TAG, soundModelId + " is not loaded");
648 sEventLogger.log(new SoundTriggerLogger.StringEvent("getModelState(): "
649 + soundModelId + " is not loaded"));
653 switch (soundModel.type) {
654 case SoundModel.TYPE_GENERIC_SOUND:
655 ret = mSoundTriggerHelper.getGenericModelState(soundModel.uuid);
658 // SoundModel.TYPE_KEYPHRASE is not supported to increase privacy.
659 Slog.e(TAG, "Unsupported model type, " + soundModel.type);
660 sEventLogger.log(new SoundTriggerLogger.StringEvent(
661 "getModelState(): Unsupported model type, "
672 * Counts the number of operations added in the last 24 hours.
674 private static class NumOps {
675 private final Object mLock = new Object();
678 private int[] mNumOps = new int[24];
680 private long mLastOpsHourSinceBoot;
683 * Clear buckets of new hours that have elapsed since last operation.
685 * <p>I.e. when the last operation was triggered at 1:40 and the current operation was
686 * triggered at 4:03, the buckets "2, 3, and 4" are cleared.
688 * @param currentTime Current elapsed time since boot in ns
690 void clearOldOps(long currentTime) {
691 synchronized (mLock) {
692 long numHoursSinceBoot = TimeUnit.HOURS.convert(currentTime, TimeUnit.NANOSECONDS);
694 // Clear buckets of new hours that have elapsed since last operation
695 // I.e. when the last operation was triggered at 1:40 and the current
696 // operation was triggered at 4:03, the bucket "2, 3, and 4" is cleared
697 if (mLastOpsHourSinceBoot != 0) {
698 for (long hour = mLastOpsHourSinceBoot + 1; hour <= numHoursSinceBoot; hour++) {
699 mNumOps[(int) (hour % 24)] = 0;
706 * Add a new operation.
708 * @param currentTime Current elapsed time since boot in ns
710 void addOp(long currentTime) {
711 synchronized (mLock) {
712 long numHoursSinceBoot = TimeUnit.HOURS.convert(currentTime, TimeUnit.NANOSECONDS);
714 mNumOps[(int) (numHoursSinceBoot % 24)]++;
715 mLastOpsHourSinceBoot = numHoursSinceBoot;
720 * Get the total operations added in the last 24 hours.
722 * @return The total number of operations added in the last 24 hours
725 synchronized (mLock) {
726 int totalOperationsInLastDay = 0;
727 for (int i = 0; i < 24; i++) {
728 totalOperationsInLastDay += mNumOps[i];
731 return totalOperationsInLastDay;
737 * A single operation run in a {@link RemoteSoundTriggerDetectionService}.
739 * <p>Once the remote service is connected either setup + execute or setup + stop is executed.
741 private static class Operation {
742 private interface ExecuteOp {
743 void run(int opId, ISoundTriggerDetectionService service) throws RemoteException;
746 private final @Nullable Runnable mSetupOp;
747 private final @NonNull ExecuteOp mExecuteOp;
748 private final @Nullable Runnable mDropOp;
750 private Operation(@Nullable Runnable setupOp, @NonNull ExecuteOp executeOp,
751 @Nullable Runnable cancelOp) {
753 mExecuteOp = executeOp;
757 private void setup() {
758 if (mSetupOp != null) {
763 void run(int opId, @NonNull ISoundTriggerDetectionService service) throws RemoteException {
765 mExecuteOp.run(opId, service);
771 if (mDropOp != null) {
778 * Local end for a {@link SoundTriggerDetectionService}. Operations are queued up and executed
779 * when the service connects.
781 * <p>If operations take too long they are forcefully aborted.
783 * <p>This also limits the amount of operations in 24 hours.
785 private class RemoteSoundTriggerDetectionService
786 extends IRecognitionStatusCallback.Stub implements ServiceConnection {
787 private static final int MSG_STOP_ALL_PENDING_OPERATIONS = 1;
789 private final Object mRemoteServiceLock = new Object();
791 /** UUID of the model the service is started for */
792 private final @NonNull ParcelUuid mPuuid;
793 /** Params passed into the start method for the service */
794 private final @Nullable Bundle mParams;
795 /** Component name passed when starting the service */
796 private final @NonNull ComponentName mServiceName;
797 /** User that started the service */
798 private final @NonNull UserHandle mUser;
799 /** Configuration of the recognition the service is handling */
800 private final @NonNull RecognitionConfig mRecognitionConfig;
801 /** Wake lock keeping the remote service alive */
802 private final @NonNull PowerManager.WakeLock mRemoteServiceWakeLock;
804 private final @NonNull Handler mHandler;
806 /** Callbacks that are called by the service */
807 private final @NonNull ISoundTriggerDetectionServiceClient mClient;
809 /** Operations that are pending because the service is not yet connected */
810 @GuardedBy("mRemoteServiceLock")
811 private final ArrayList<Operation> mPendingOps = new ArrayList<>();
812 /** Operations that have been send to the service but have no yet finished */
813 @GuardedBy("mRemoteServiceLock")
814 private final ArraySet<Integer> mRunningOpIds = new ArraySet<>();
815 /** The number of operations executed in each of the last 24 hours */
816 private final NumOps mNumOps;
818 /** The service binder if connected */
819 @GuardedBy("mRemoteServiceLock")
820 private @Nullable ISoundTriggerDetectionService mService;
821 /** Whether the service has been bound */
822 @GuardedBy("mRemoteServiceLock")
823 private boolean mIsBound;
824 /** Whether the service has been destroyed */
825 @GuardedBy("mRemoteServiceLock")
826 private boolean mIsDestroyed;
828 * Set once a final op is scheduled. No further ops can be added and the service is
829 * destroyed once the op finishes.
831 @GuardedBy("mRemoteServiceLock")
832 private boolean mDestroyOnceRunningOpsDone;
834 /** Total number of operations performed by this service */
835 @GuardedBy("mRemoteServiceLock")
836 private int mNumTotalOpsPerformed;
839 * Create a new remote sound trigger detection service. This only binds to the service when
840 * operations are in flight. Each operation has a certain time it can run. Once no
841 * operations are allowed to run anymore, {@link #stopAllPendingOperations() all operations
842 * are aborted and stopped} and the service is disconnected.
844 * @param modelUuid The UUID of the model the recognition is for
845 * @param params The params passed to each method of the service
846 * @param serviceName The component name of the service
847 * @param user The user of the service
848 * @param config The configuration of the recognition
850 public RemoteSoundTriggerDetectionService(@NonNull UUID modelUuid,
851 @Nullable Bundle params, @NonNull ComponentName serviceName, @NonNull UserHandle user,
852 @NonNull RecognitionConfig config) {
853 mPuuid = new ParcelUuid(modelUuid);
855 mServiceName = serviceName;
857 mRecognitionConfig = config;
858 mHandler = new Handler(Looper.getMainLooper());
860 PowerManager pm = ((PowerManager) mContext.getSystemService(Context.POWER_SERVICE));
861 mRemoteServiceWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
862 "RemoteSoundTriggerDetectionService " + mServiceName.getPackageName() + ":"
863 + mServiceName.getClassName());
865 synchronized (mLock) {
866 NumOps numOps = mNumOpsPerPackage.get(mServiceName.getPackageName());
867 if (numOps == null) {
868 numOps = new NumOps();
869 mNumOpsPerPackage.put(mServiceName.getPackageName(), numOps);
874 mClient = new ISoundTriggerDetectionServiceClient.Stub() {
876 public void onOpFinished(int opId) {
877 long token = Binder.clearCallingIdentity();
879 synchronized (mRemoteServiceLock) {
880 mRunningOpIds.remove(opId);
882 if (mRunningOpIds.isEmpty() && mPendingOps.isEmpty()) {
883 if (mDestroyOnceRunningOpsDone) {
891 Binder.restoreCallingIdentity(token);
898 public boolean pingBinder() {
899 return !(mIsDestroyed || mDestroyOnceRunningOpsDone);
903 * Disconnect from the service, but allow to re-connect when new operations are triggered.
905 @GuardedBy("mRemoteServiceLock")
906 private void disconnectLocked() {
907 if (mService != null) {
909 mService.removeClient(mPuuid);
910 } catch (Exception e) {
911 Slog.e(TAG, mPuuid + ": Cannot remove client", e);
913 sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
914 + ": Cannot remove client"));
922 mContext.unbindService(RemoteSoundTriggerDetectionService.this);
925 synchronized (mCallbacksLock) {
926 mRemoteServiceWakeLock.release();
932 * Disconnect, do not allow to reconnect to the service. All further operations will be
935 private void destroy() {
936 if (DEBUG) Slog.v(TAG, mPuuid + ": destroy");
938 sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid + ": destroy"));
940 synchronized (mRemoteServiceLock) {
946 // The callback is removed before the flag is set
947 if (!mDestroyOnceRunningOpsDone) {
948 synchronized (mCallbacksLock) {
949 mCallbacks.remove(mPuuid.getUuid());
955 * Stop all pending operations and then disconnect for the service.
957 private void stopAllPendingOperations() {
958 synchronized (mRemoteServiceLock) {
963 if (mService != null) {
964 int numOps = mRunningOpIds.size();
965 for (int i = 0; i < numOps; i++) {
967 mService.onStopOperation(mPuuid, mRunningOpIds.valueAt(i));
968 } catch (Exception e) {
969 Slog.e(TAG, mPuuid + ": Could not stop operation "
970 + mRunningOpIds.valueAt(i), e);
972 sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
973 + ": Could not stop operation " + mRunningOpIds.valueAt(i)));
978 mRunningOpIds.clear();
986 * Verify that the service has the expected properties and then bind to the service
988 private void bind() {
989 long token = Binder.clearCallingIdentity();
991 Intent i = new Intent();
992 i.setComponent(mServiceName);
994 ResolveInfo ri = mContext.getPackageManager().resolveServiceAsUser(i,
995 GET_SERVICES | GET_META_DATA | MATCH_DEBUG_TRIAGED_MISSING,
996 mUser.getIdentifier());
999 Slog.w(TAG, mPuuid + ": " + mServiceName + " not found");
1001 sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
1002 + ": " + mServiceName + " not found"));
1007 if (!BIND_SOUND_TRIGGER_DETECTION_SERVICE
1008 .equals(ri.serviceInfo.permission)) {
1009 Slog.w(TAG, mPuuid + ": " + mServiceName + " does not require "
1010 + BIND_SOUND_TRIGGER_DETECTION_SERVICE);
1012 sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
1013 + ": " + mServiceName + " does not require "
1014 + BIND_SOUND_TRIGGER_DETECTION_SERVICE));
1019 mIsBound = mContext.bindServiceAsUser(i, this,
1020 BIND_AUTO_CREATE | BIND_FOREGROUND_SERVICE, mUser);
1023 mRemoteServiceWakeLock.acquire();
1025 Slog.w(TAG, mPuuid + ": Could not bind to " + mServiceName);
1027 sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
1028 + ": Could not bind to " + mServiceName));
1032 Binder.restoreCallingIdentity(token);
1037 * Run an operation (i.e. send it do the service). If the service is not connected, this
1038 * binds the service and then runs the operation once connected.
1040 * @param op The operation to run
1042 private void runOrAddOperation(Operation op) {
1043 synchronized (mRemoteServiceLock) {
1044 if (mIsDestroyed || mDestroyOnceRunningOpsDone) {
1045 Slog.w(TAG, mPuuid + ": Dropped operation as already destroyed or marked for "
1048 sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
1049 + ":Dropped operation as already destroyed or marked for destruction"));
1055 if (mService == null) {
1056 mPendingOps.add(op);
1062 long currentTime = System.nanoTime();
1063 mNumOps.clearOldOps(currentTime);
1065 // Drop operation if too many were executed in the last 24 hours.
1066 int opsAllowed = Settings.Global.getInt(mContext.getContentResolver(),
1067 MAX_SOUND_TRIGGER_DETECTION_SERVICE_OPS_PER_DAY,
1070 // As we currently cannot dropping an op safely, disable throttling
1071 int opsAdded = mNumOps.getOpsAdded();
1072 if (false && mNumOps.getOpsAdded() >= opsAllowed) {
1074 if (DEBUG || opsAllowed + 10 > opsAdded) {
1075 Slog.w(TAG, mPuuid + ": Dropped operation as too many operations "
1076 + "were run in last 24 hours");
1078 sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
1079 + ": Dropped operation as too many operations "
1080 + "were run in last 24 hours"));
1085 } catch (Exception e) {
1086 Slog.e(TAG, mPuuid + ": Could not drop operation", e);
1088 sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
1089 + ": Could not drop operation"));
1093 mNumOps.addOp(currentTime);
1096 int opId = mNumTotalOpsPerformed;
1098 mNumTotalOpsPerformed++;
1099 } while (mRunningOpIds.contains(opId));
1103 if (DEBUG) Slog.v(TAG, mPuuid + ": runOp " + opId);
1105 sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
1106 + ": runOp " + opId));
1108 op.run(opId, mService);
1109 mRunningOpIds.add(opId);
1110 } catch (Exception e) {
1111 Slog.e(TAG, mPuuid + ": Could not run operation " + opId, e);
1113 sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
1114 + ": Could not run operation " + opId));
1119 // Unbind from service if no operations are left (i.e. if the operation failed)
1120 if (mPendingOps.isEmpty() && mRunningOpIds.isEmpty()) {
1121 if (mDestroyOnceRunningOpsDone) {
1127 mHandler.removeMessages(MSG_STOP_ALL_PENDING_OPERATIONS);
1128 mHandler.sendMessageDelayed(obtainMessage(
1129 RemoteSoundTriggerDetectionService::stopAllPendingOperations, this)
1130 .setWhat(MSG_STOP_ALL_PENDING_OPERATIONS),
1131 Settings.Global.getLong(mContext.getContentResolver(),
1132 SOUND_TRIGGER_DETECTION_SERVICE_OP_TIMEOUT,
1140 public void onKeyphraseDetected(SoundTrigger.KeyphraseRecognitionEvent event) {
1141 Slog.w(TAG, mPuuid + "->" + mServiceName + ": IGNORED onKeyphraseDetected(" + event
1144 sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid + "->" + mServiceName
1145 + ": IGNORED onKeyphraseDetected(" + event + ")"));
1150 * Create an AudioRecord enough for starting and releasing the data buffered for the event.
1152 * @param event The event that was received
1153 * @return The initialized AudioRecord
1155 private @NonNull AudioRecord createAudioRecordForEvent(
1156 @NonNull SoundTrigger.GenericRecognitionEvent event) {
1157 AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder();
1158 attributesBuilder.setInternalCapturePreset(MediaRecorder.AudioSource.HOTWORD);
1159 AudioAttributes attributes = attributesBuilder.build();
1161 // Use same AudioFormat processing as in RecognitionEvent.fromParcel
1162 AudioFormat originalFormat = event.getCaptureFormat();
1163 AudioFormat captureFormat = (new AudioFormat.Builder())
1164 .setChannelMask(originalFormat.getChannelMask())
1165 .setEncoding(originalFormat.getEncoding())
1166 .setSampleRate(originalFormat.getSampleRate())
1169 int bufferSize = AudioRecord.getMinBufferSize(
1170 captureFormat.getSampleRate() == AudioFormat.SAMPLE_RATE_UNSPECIFIED
1171 ? AudioFormat.SAMPLE_RATE_HZ_MAX
1172 : captureFormat.getSampleRate(),
1173 captureFormat.getChannelCount() == 2
1174 ? AudioFormat.CHANNEL_IN_STEREO
1175 : AudioFormat.CHANNEL_IN_MONO,
1176 captureFormat.getEncoding());
1178 sEventLogger.log(new SoundTriggerLogger.StringEvent("createAudioRecordForEvent"));
1180 return new AudioRecord(attributes, captureFormat, bufferSize,
1181 event.getCaptureSession());
1185 public void onGenericSoundTriggerDetected(SoundTrigger.GenericRecognitionEvent event) {
1186 if (DEBUG) Slog.v(TAG, mPuuid + ": Generic sound trigger event: " + event);
1188 sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
1189 + ": Generic sound trigger event: " + event));
1191 runOrAddOperation(new Operation(
1194 if (!mRecognitionConfig.allowMultipleTriggers) {
1195 // Unregister this remoteService once op is done
1196 synchronized (mCallbacksLock) {
1197 mCallbacks.remove(mPuuid.getUuid());
1199 mDestroyOnceRunningOpsDone = true;
1202 // execute if not throttled:
1203 (opId, service) -> service.onGenericRecognitionEvent(mPuuid, opId, event),
1204 // execute if throttled:
1206 if (event.isCaptureAvailable()) {
1207 AudioRecord capturedData = createAudioRecordForEvent(event);
1209 // Currently we need to start and release the audio record to reset
1210 // the DSP even if we don't want to process the event
1211 capturedData.startRecording();
1212 capturedData.release();
1218 public void onError(int status) {
1219 if (DEBUG) Slog.v(TAG, mPuuid + ": onError: " + status);
1221 sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
1222 + ": onError: " + status));
1228 // Unregister this remoteService once op is done
1229 synchronized (mCallbacksLock) {
1230 mCallbacks.remove(mPuuid.getUuid());
1232 mDestroyOnceRunningOpsDone = true;
1234 // execute if not throttled:
1235 (opId, service) -> service.onError(mPuuid, opId, status),
1236 // nothing to do if throttled
1241 public void onRecognitionPaused() {
1242 Slog.i(TAG, mPuuid + "->" + mServiceName + ": IGNORED onRecognitionPaused");
1244 sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
1245 + "->" + mServiceName + ": IGNORED onRecognitionPaused"));
1250 public void onRecognitionResumed() {
1251 Slog.i(TAG, mPuuid + "->" + mServiceName + ": IGNORED onRecognitionResumed");
1253 sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
1254 + "->" + mServiceName + ": IGNORED onRecognitionResumed"));
1259 public void onServiceConnected(ComponentName name, IBinder service) {
1260 if (DEBUG) Slog.v(TAG, mPuuid + ": onServiceConnected(" + service + ")");
1262 sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
1263 + ": onServiceConnected(" + service + ")"));
1265 synchronized (mRemoteServiceLock) {
1266 mService = ISoundTriggerDetectionService.Stub.asInterface(service);
1269 mService.setClient(mPuuid, mParams, mClient);
1270 } catch (Exception e) {
1271 Slog.e(TAG, mPuuid + ": Could not init " + mServiceName, e);
1275 while (!mPendingOps.isEmpty()) {
1276 runOrAddOperation(mPendingOps.remove(0));
1282 public void onServiceDisconnected(ComponentName name) {
1283 if (DEBUG) Slog.v(TAG, mPuuid + ": onServiceDisconnected");
1285 sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
1286 + ": onServiceDisconnected"));
1288 synchronized (mRemoteServiceLock) {
1294 public void onBindingDied(ComponentName name) {
1295 if (DEBUG) Slog.v(TAG, mPuuid + ": onBindingDied");
1297 sEventLogger.log(new SoundTriggerLogger.StringEvent(mPuuid
1298 + ": onBindingDied"));
1300 synchronized (mRemoteServiceLock) {
1306 public void onNullBinding(ComponentName name) {
1307 Slog.w(TAG, name + " for model " + mPuuid + " returned a null binding");
1309 sEventLogger.log(new SoundTriggerLogger.StringEvent(name + " for model "
1310 + mPuuid + " returned a null binding"));
1312 synchronized (mRemoteServiceLock) {
1318 public final class LocalSoundTriggerService extends SoundTriggerInternal {
1319 private final Context mContext;
1320 private SoundTriggerHelper mSoundTriggerHelper;
1322 LocalSoundTriggerService(Context context) {
1326 synchronized void setSoundTriggerHelper(SoundTriggerHelper helper) {
1327 mSoundTriggerHelper = helper;
1331 public int startRecognition(int keyphraseId, KeyphraseSoundModel soundModel,
1332 IRecognitionStatusCallback listener, RecognitionConfig recognitionConfig) {
1333 if (!isInitialized()) return STATUS_ERROR;
1334 return mSoundTriggerHelper.startKeyphraseRecognition(keyphraseId, soundModel, listener,
1339 public synchronized int stopRecognition(int keyphraseId, IRecognitionStatusCallback listener) {
1340 if (!isInitialized()) return STATUS_ERROR;
1341 return mSoundTriggerHelper.stopKeyphraseRecognition(keyphraseId, listener);
1345 public ModuleProperties getModuleProperties() {
1346 if (!isInitialized()) return null;
1347 return mSoundTriggerHelper.getModuleProperties();
1351 public int unloadKeyphraseModel(int keyphraseId) {
1352 if (!isInitialized()) return STATUS_ERROR;
1353 return mSoundTriggerHelper.unloadKeyphraseSoundModel(keyphraseId);
1357 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
1358 if (!isInitialized()) return;
1359 mSoundTriggerHelper.dump(fd, pw, args);
1361 sEventLogger.dump(pw);
1364 mSoundModelStatTracker.dump(pw);
1367 private synchronized boolean isInitialized() {
1368 if (mSoundTriggerHelper == null ) {
1369 Slog.e(TAG, "SoundTriggerHelper not initialized.");
1371 sEventLogger.log(new SoundTriggerLogger.StringEvent(
1372 "SoundTriggerHelper not initialized."));
1380 private void enforceCallingPermission(String permission) {
1381 if (mContext.checkCallingOrSelfPermission(permission)
1382 != PackageManager.PERMISSION_GRANTED) {
1383 throw new SecurityException("Caller does not hold the permission " + permission);
1387 //=================================================================
1390 private static final SoundTriggerLogger sEventLogger = new SoundTriggerLogger(200,
1391 "SoundTrigger activity");