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 android.media.soundtrigger;
18 
19 import static android.hardware.soundtrigger.SoundTrigger.STATUS_ERROR;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.annotation.RequiresPermission;
24 import android.annotation.SystemApi;
25 import android.annotation.SystemService;
26 import android.app.ActivityThread;
27 import android.compat.annotation.UnsupportedAppUsage;
28 import android.content.ComponentName;
29 import android.content.Context;
30 import android.hardware.soundtrigger.ModelParams;
31 import android.hardware.soundtrigger.SoundTrigger;
32 import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
33 import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel;
34 import android.hardware.soundtrigger.SoundTrigger.ModelParamRange;
35 import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
36 import android.hardware.soundtrigger.SoundTrigger.SoundModel;
37 import android.media.permission.ClearCallingIdentityContext;
38 import android.media.permission.Identity;
39 import android.media.permission.SafeCloseable;
40 import android.os.Binder;
41 import android.os.Build;
42 import android.os.Bundle;
43 import android.os.Handler;
44 import android.os.IBinder;
45 import android.os.ParcelUuid;
46 import android.os.RemoteException;
47 import android.provider.Settings;
48 import android.util.Slog;
49 
50 import com.android.internal.app.ISoundTriggerService;
51 import com.android.internal.app.ISoundTriggerSession;
52 import com.android.internal.util.Preconditions;
53 
54 import java.util.HashMap;
55 import java.util.Objects;
56 import java.util.UUID;
57 
58 /**
59  * This class provides management of non-voice (general sound trigger) based sound recognition
60  * models. Usage of this class is restricted to system or signature applications only. This allows
61  * OEMs to write apps that can manage non-voice based sound trigger models.
62  *
63  * @hide
64  */
65 @SystemApi
66 @SystemService(Context.SOUND_TRIGGER_SERVICE)
67 public final class SoundTriggerManager {
68     private static final boolean DBG = false;
69     private static final String TAG = "SoundTriggerManager";
70 
71     private final Context mContext;
72     private final ISoundTriggerSession mSoundTriggerSession;
73     private final IBinder mBinderToken = new Binder();
74 
75     // Stores a mapping from the sound model UUID to the SoundTriggerInstance created by
76     // the createSoundTriggerDetector() call.
77     private final HashMap<UUID, SoundTriggerDetector> mReceiverInstanceMap;
78 
79     /**
80      * @hide
81      */
SoundTriggerManager(Context context, ISoundTriggerService soundTriggerService)82     public SoundTriggerManager(Context context, ISoundTriggerService soundTriggerService) {
83         if (DBG) {
84             Slog.i(TAG, "SoundTriggerManager created.");
85         }
86         try {
87             // This assumes that whoever is calling this ctor is the originator of the operations,
88             // as opposed to a service acting on behalf of a separate identity.
89             // Services acting on behalf of some other identity should not be using this class at
90             // all, but rather directly connect to the server and attach with explicit credentials.
91             Identity originatorIdentity = new Identity();
92             originatorIdentity.packageName = ActivityThread.currentOpPackageName();
93 
94             try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
95                 mSoundTriggerSession = soundTriggerService.attachAsOriginator(originatorIdentity,
96                         mBinderToken);
97             }
98         } catch (RemoteException e) {
99             throw e.rethrowAsRuntimeException();
100         }
101         mContext = context;
102         mReceiverInstanceMap = new HashMap<UUID, SoundTriggerDetector>();
103     }
104 
105     /**
106      * Updates the given sound trigger model.
107      */
108     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
updateModel(Model model)109     public void updateModel(Model model) {
110         try {
111             mSoundTriggerSession.updateSoundModel(model.getGenericSoundModel());
112         } catch (RemoteException e) {
113             throw e.rethrowFromSystemServer();
114         }
115     }
116 
117     /**
118      * Get {@link SoundTriggerManager.Model} which is registered with the passed UUID
119      *
120      * @param soundModelId UUID associated with a loaded model
121      * @return {@link SoundTriggerManager.Model} associated with UUID soundModelId
122      */
123     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
124     @Nullable
getModel(UUID soundModelId)125     public Model getModel(UUID soundModelId) {
126         try {
127             GenericSoundModel model =
128                     mSoundTriggerSession.getSoundModel(new ParcelUuid(soundModelId));
129             if (model == null) {
130                 return null;
131             }
132 
133             return new Model(model);
134         } catch (RemoteException e) {
135             throw e.rethrowFromSystemServer();
136         }
137     }
138 
139     /**
140      * Deletes the sound model represented by the provided UUID.
141      */
142     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
deleteModel(UUID soundModelId)143     public void deleteModel(UUID soundModelId) {
144         try {
145             mSoundTriggerSession.deleteSoundModel(new ParcelUuid(soundModelId));
146         } catch (RemoteException e) {
147             throw e.rethrowFromSystemServer();
148         }
149     }
150 
151     /**
152      * Creates an instance of {@link SoundTriggerDetector} which can be used to start/stop
153      * recognition on the model and register for triggers from the model. Note that this call
154      * invalidates any previously returned instances for the same sound model Uuid.
155      *
156      * @param soundModelId UUID of the sound model to create the receiver object for.
157      * @param callback Instance of the {@link SoundTriggerDetector#Callback} object for the
158      * callbacks for the given sound model.
159      * @param handler The Handler to use for the callback operations. A null value will use the
160      * current thread's Looper.
161      * @return Instance of {@link SoundTriggerDetector} or null on error.
162      */
163     @Nullable
164     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
createSoundTriggerDetector(UUID soundModelId, @NonNull SoundTriggerDetector.Callback callback, @Nullable Handler handler)165     public SoundTriggerDetector createSoundTriggerDetector(UUID soundModelId,
166             @NonNull SoundTriggerDetector.Callback callback, @Nullable Handler handler) {
167         if (soundModelId == null) {
168             return null;
169         }
170 
171         SoundTriggerDetector oldInstance = mReceiverInstanceMap.get(soundModelId);
172         if (oldInstance != null) {
173             // Shutdown old instance.
174         }
175         SoundTriggerDetector newInstance = new SoundTriggerDetector(mSoundTriggerSession,
176                 soundModelId, callback, handler);
177         mReceiverInstanceMap.put(soundModelId, newInstance);
178         return newInstance;
179     }
180 
181     /**
182      * Class captures the data and fields that represent a non-keyphrase sound model. Use the
183      * factory constructor {@link Model#create()} to create an instance.
184      */
185     // We use encapsulation to expose the SoundTrigger.GenericSoundModel as a SystemApi. This
186     // prevents us from exposing SoundTrigger.GenericSoundModel as an Api.
187     public static class Model {
188 
189         private SoundTrigger.GenericSoundModel mGenericSoundModel;
190 
191         /**
192          * @hide
193          */
Model(SoundTrigger.GenericSoundModel soundTriggerModel)194         Model(SoundTrigger.GenericSoundModel soundTriggerModel) {
195             mGenericSoundModel = soundTriggerModel;
196         }
197 
198         /**
199          * Factory constructor to a voice model to be used with {@link SoundTriggerManager}
200          *
201          * @param modelUuid Unique identifier associated with the model.
202          * @param vendorUuid Unique identifier associated the calling vendor.
203          * @param data Model's data.
204          * @param version Version identifier for the model.
205          * @return Voice model
206          */
207         @NonNull
create(@onNull UUID modelUuid, @NonNull UUID vendorUuid, @Nullable byte[] data, int version)208         public static Model create(@NonNull UUID modelUuid, @NonNull UUID vendorUuid,
209                 @Nullable byte[] data, int version) {
210             Objects.requireNonNull(modelUuid);
211             Objects.requireNonNull(vendorUuid);
212             return new Model(new SoundTrigger.GenericSoundModel(modelUuid, vendorUuid, data,
213                     version));
214         }
215 
216         /**
217          * Factory constructor to a voice model to be used with {@link SoundTriggerManager}
218          *
219          * @param modelUuid Unique identifier associated with the model.
220          * @param vendorUuid Unique identifier associated the calling vendor.
221          * @param data Model's data.
222          * @return Voice model
223          */
224         @NonNull
create(@onNull UUID modelUuid, @NonNull UUID vendorUuid, @Nullable byte[] data)225         public static Model create(@NonNull UUID modelUuid, @NonNull UUID vendorUuid,
226                 @Nullable byte[] data) {
227             return create(modelUuid, vendorUuid, data, -1);
228         }
229 
230         /**
231          * Get the model's unique identifier
232          *
233          * @return UUID associated with the model
234          */
235         @NonNull
getModelUuid()236         public UUID getModelUuid() {
237             return mGenericSoundModel.getUuid();
238         }
239 
240         /**
241          * Get the model's vendor identifier
242          *
243          * @return UUID associated with the vendor of the model
244          */
245         @NonNull
getVendorUuid()246         public UUID getVendorUuid() {
247             return mGenericSoundModel.getVendorUuid();
248         }
249 
250         /**
251          * Get the model's version
252          *
253          * @return Version associated with the model
254          */
getVersion()255         public int getVersion() {
256             return mGenericSoundModel.getVersion();
257         }
258 
259         /**
260          * Get the underlying model data
261          *
262          * @return Backing data of the model
263          */
264         @Nullable
getModelData()265         public byte[] getModelData() {
266             return mGenericSoundModel.getData();
267         }
268 
269         /**
270          * @hide
271          */
getGenericSoundModel()272         SoundTrigger.GenericSoundModel getGenericSoundModel() {
273             return mGenericSoundModel;
274         }
275     }
276 
277 
278     /**
279      * Default message type.
280      * @hide
281      */
282     public static final int FLAG_MESSAGE_TYPE_UNKNOWN = -1;
283     /**
284      * Contents of EXTRA_MESSAGE_TYPE extra for a RecognitionEvent.
285      * @hide
286      */
287     public static final int FLAG_MESSAGE_TYPE_RECOGNITION_EVENT = 0;
288     /**
289      * Contents of EXTRA_MESSAGE_TYPE extra for recognition error events.
290      * @hide
291      */
292     public static final int FLAG_MESSAGE_TYPE_RECOGNITION_ERROR = 1;
293     /**
294      * Contents of EXTRA_MESSAGE_TYPE extra for a recognition paused events.
295      * @hide
296      */
297     public static final int FLAG_MESSAGE_TYPE_RECOGNITION_PAUSED = 2;
298     /**
299      * Contents of EXTRA_MESSAGE_TYPE extra for recognition resumed events.
300      * @hide
301      */
302     public static final int FLAG_MESSAGE_TYPE_RECOGNITION_RESUMED = 3;
303 
304     /**
305      * Extra key in the intent for the type of the message.
306      * @hide
307      */
308     public static final String EXTRA_MESSAGE_TYPE = "android.media.soundtrigger.MESSAGE_TYPE";
309     /**
310      * Extra key in the intent that holds the RecognitionEvent parcelable.
311      * @hide
312      */
313     public static final String EXTRA_RECOGNITION_EVENT = "android.media.soundtrigger.RECOGNITION_EVENT";
314     /**
315      * Extra key in the intent that holds the status in an error message.
316      * @hide
317      */
318     public static final String EXTRA_STATUS = "android.media.soundtrigger.STATUS";
319 
320     /**
321      * Loads a given sound model into the sound trigger. Note the model will be unloaded if there is
322      * an error/the system service is restarted.
323      * @hide
324      */
325     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
326     @UnsupportedAppUsage
loadSoundModel(SoundModel soundModel)327     public int loadSoundModel(SoundModel soundModel) {
328         if (soundModel == null) {
329             return STATUS_ERROR;
330         }
331 
332         try {
333             switch (soundModel.getType()) {
334                 case SoundModel.TYPE_GENERIC_SOUND:
335                     return mSoundTriggerSession.loadGenericSoundModel(
336                             (GenericSoundModel) soundModel);
337                 case SoundModel.TYPE_KEYPHRASE:
338                     return mSoundTriggerSession.loadKeyphraseSoundModel(
339                             (KeyphraseSoundModel) soundModel);
340                 default:
341                     Slog.e(TAG, "Unkown model type");
342                     return STATUS_ERROR;
343             }
344         } catch (RemoteException e) {
345             throw e.rethrowFromSystemServer();
346         }
347     }
348 
349     /**
350      * Starts recognition for the given model id. All events from the model will be sent to the
351      * service.
352      *
353      * <p>This only supports generic sound trigger events. For keyphrase events, please use
354      * {@link android.service.voice.VoiceInteractionService}.
355      *
356      * @param soundModelId Id of the sound model
357      * @param params Opaque data sent to each service call of the service as the {@code params}
358      *               argument
359      * @param detectionService The component name of the service that should receive the events.
360      *                         Needs to subclass {@link SoundTriggerDetectionService}
361      * @param config Configures the recognition
362      *
363      * @return {@link SoundTrigger#STATUS_OK} if the recognition could be started, error code
364      *         otherwise
365      *
366      * @hide
367      */
368     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
369     @UnsupportedAppUsage
startRecognition(@onNull UUID soundModelId, @Nullable Bundle params, @NonNull ComponentName detectionService, @NonNull RecognitionConfig config)370     public int startRecognition(@NonNull UUID soundModelId, @Nullable Bundle params,
371         @NonNull ComponentName detectionService, @NonNull RecognitionConfig config) {
372         Preconditions.checkNotNull(soundModelId);
373         Preconditions.checkNotNull(detectionService);
374         Preconditions.checkNotNull(config);
375 
376         try {
377             return mSoundTriggerSession.startRecognitionForService(new ParcelUuid(soundModelId),
378                 params, detectionService, config);
379         } catch (RemoteException e) {
380             throw e.rethrowFromSystemServer();
381         }
382     }
383 
384     /**
385      * Stops the given model's recognition.
386      * @hide
387      */
388     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
389     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
stopRecognition(UUID soundModelId)390     public int stopRecognition(UUID soundModelId) {
391         if (soundModelId == null) {
392             return STATUS_ERROR;
393         }
394         try {
395             return mSoundTriggerSession.stopRecognitionForService(new ParcelUuid(soundModelId));
396         } catch (RemoteException e) {
397             throw e.rethrowFromSystemServer();
398         }
399     }
400 
401     /**
402      * Removes the given model from memory. Will also stop any pending recognitions.
403      * @hide
404      */
405     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
406     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
unloadSoundModel(UUID soundModelId)407     public int unloadSoundModel(UUID soundModelId) {
408         if (soundModelId == null) {
409             return STATUS_ERROR;
410         }
411         try {
412             return mSoundTriggerSession.unloadSoundModel(
413                     new ParcelUuid(soundModelId));
414         } catch (RemoteException e) {
415             throw e.rethrowFromSystemServer();
416         }
417     }
418 
419     /**
420      * Returns true if the given model has had detection started on it.
421      * @hide
422      */
423     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
424     @UnsupportedAppUsage
isRecognitionActive(UUID soundModelId)425     public boolean isRecognitionActive(UUID soundModelId) {
426         if (soundModelId == null) {
427             return false;
428         }
429         try {
430             return mSoundTriggerSession.isRecognitionActive(
431                     new ParcelUuid(soundModelId));
432         } catch (RemoteException e) {
433             throw e.rethrowFromSystemServer();
434         }
435     }
436 
437     /**
438      * Get the amount of time (in milliseconds) an operation of the
439      * {@link ISoundTriggerDetectionService} is allowed to ask.
440      *
441      * @return The amount of time an sound trigger detection service operation is allowed to last
442      */
getDetectionServiceOperationsTimeout()443     public int getDetectionServiceOperationsTimeout() {
444         try {
445             return Settings.Global.getInt(mContext.getContentResolver(),
446                     Settings.Global.SOUND_TRIGGER_DETECTION_SERVICE_OP_TIMEOUT);
447         } catch (Settings.SettingNotFoundException e) {
448             return Integer.MAX_VALUE;
449         }
450     }
451 
452     /**
453      * Asynchronously get state of the indicated model.  The model state is returned as
454      * a recognition event in the callback that was registered in the startRecognition
455      * method.
456      * @hide
457      */
458     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
459     @UnsupportedAppUsage
getModelState(UUID soundModelId)460     public int getModelState(UUID soundModelId) {
461         if (soundModelId == null) {
462             return STATUS_ERROR;
463         }
464         try {
465             return mSoundTriggerSession.getModelState(new ParcelUuid(soundModelId));
466         } catch (RemoteException e) {
467             throw e.rethrowFromSystemServer();
468         }
469     }
470 
471     /**
472      * Get the hardware sound trigger module properties currently loaded.
473      *
474      * @return The properties currently loaded. Returns null if no supported hardware loaded.
475      */
476     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
477     @Nullable
getModuleProperties()478     public SoundTrigger.ModuleProperties getModuleProperties() {
479 
480         try {
481             return mSoundTriggerSession.getModuleProperties();
482         } catch (RemoteException e) {
483             throw e.rethrowFromSystemServer();
484         }
485     }
486 
487     /**
488      * Set a model specific {@link ModelParams} with the given value. This
489      * parameter will keep its value for the duration the model is loaded regardless of starting and
490      * stopping recognition. Once the model is unloaded, the value will be lost.
491      * {@link SoundTriggerManager#queryParameter} should be checked first before calling this
492      * method.
493      *
494      * @param soundModelId UUID of model to apply the parameter value to.
495      * @param modelParam   {@link ModelParams}
496      * @param value        Value to set
497      * @return - {@link SoundTrigger#STATUS_OK} in case of success
498      *         - {@link SoundTrigger#STATUS_NO_INIT} if the native service cannot be reached
499      *         - {@link SoundTrigger#STATUS_BAD_VALUE} invalid input parameter
500      *         - {@link SoundTrigger#STATUS_INVALID_OPERATION} if the call is out of sequence or
501      *           if API is not supported by HAL
502      */
503     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
setParameter(@ullable UUID soundModelId, @ModelParams int modelParam, int value)504     public int setParameter(@Nullable UUID soundModelId,
505             @ModelParams int modelParam, int value) {
506         try {
507             return mSoundTriggerSession.setParameter(new ParcelUuid(soundModelId), modelParam,
508                     value);
509         } catch (RemoteException e) {
510             throw e.rethrowFromSystemServer();
511         }
512     }
513 
514     /**
515      * Get a model specific {@link ModelParams}. This parameter will keep its value
516      * for the duration the model is loaded regardless of starting and stopping recognition.
517      * Once the model is unloaded, the value will be lost. If the value is not set, a default
518      * value is returned. See {@link ModelParams} for parameter default values.
519      * {@link SoundTriggerManager#queryParameter} should be checked first before
520      * calling this method. Otherwise, an exception can be thrown.
521      *
522      * @param soundModelId UUID of model to get parameter
523      * @param modelParam   {@link ModelParams}
524      * @return value of parameter
525      */
526     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
getParameter(@onNull UUID soundModelId, @ModelParams int modelParam)527     public int getParameter(@NonNull UUID soundModelId,
528             @ModelParams int modelParam) {
529         try {
530             return mSoundTriggerSession.getParameter(new ParcelUuid(soundModelId), modelParam);
531         } catch (RemoteException e) {
532             throw e.rethrowFromSystemServer();
533         }
534     }
535 
536     /**
537      * Determine if parameter control is supported for the given model handle.
538      * This method should be checked prior to calling {@link SoundTriggerManager#setParameter} or
539      * {@link SoundTriggerManager#getParameter}.
540      *
541      * @param soundModelId handle of model to get parameter
542      * @param modelParam {@link ModelParams}
543      * @return supported range of parameter, null if not supported
544      */
545     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
546     @Nullable
queryParameter(@ullable UUID soundModelId, @ModelParams int modelParam)547     public ModelParamRange queryParameter(@Nullable UUID soundModelId,
548             @ModelParams int modelParam) {
549         try {
550             return mSoundTriggerSession.queryParameter(new ParcelUuid(soundModelId), modelParam);
551         } catch (RemoteException e) {
552             throw e.rethrowFromSystemServer();
553         }
554     }
555 }
556