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.compat.annotation.UnsupportedAppUsage;
27 import android.content.ComponentName;
28 import android.content.Context;
29 import android.hardware.soundtrigger.SoundTrigger;
30 import android.hardware.soundtrigger.SoundTrigger.GenericSoundModel;
31 import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel;
32 import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig;
33 import android.hardware.soundtrigger.SoundTrigger.SoundModel;
34 import android.os.Bundle;
35 import android.os.Handler;
36 import android.os.ParcelUuid;
37 import android.os.RemoteException;
38 import android.provider.Settings;
39 import android.util.Slog;
40 
41 import com.android.internal.app.ISoundTriggerService;
42 import com.android.internal.util.Preconditions;
43 
44 import java.util.HashMap;
45 import java.util.UUID;
46 
47 /**
48  * This class provides management of non-voice (general sound trigger) based sound recognition
49  * models. Usage of this class is restricted to system or signature applications only. This allows
50  * OEMs to write apps that can manage non-voice based sound trigger models.
51  *
52  * @hide
53  */
54 @SystemApi
55 @SystemService(Context.SOUND_TRIGGER_SERVICE)
56 public final class SoundTriggerManager {
57     private static final boolean DBG = false;
58     private static final String TAG = "SoundTriggerManager";
59 
60     private final Context mContext;
61     private final ISoundTriggerService mSoundTriggerService;
62 
63     // Stores a mapping from the sound model UUID to the SoundTriggerInstance created by
64     // the createSoundTriggerDetector() call.
65     private final HashMap<UUID, SoundTriggerDetector> mReceiverInstanceMap;
66 
67     /**
68      * @hide
69      */
SoundTriggerManager(Context context, ISoundTriggerService soundTriggerService )70     public SoundTriggerManager(Context context, ISoundTriggerService soundTriggerService ) {
71         if (DBG) {
72             Slog.i(TAG, "SoundTriggerManager created.");
73         }
74         mSoundTriggerService = soundTriggerService;
75         mContext = context;
76         mReceiverInstanceMap = new HashMap<UUID, SoundTriggerDetector>();
77     }
78 
79     /**
80      * Updates the given sound trigger model.
81      */
82     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
updateModel(Model model)83     public void updateModel(Model model) {
84         try {
85             mSoundTriggerService.updateSoundModel(model.getGenericSoundModel());
86         } catch (RemoteException e) {
87             throw e.rethrowFromSystemServer();
88         }
89     }
90 
91     /**
92      * Returns the sound trigger model represented by the given UUID. An instance of {@link Model}
93      * is returned.
94      */
95     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
getModel(UUID soundModelId)96     public Model getModel(UUID soundModelId) {
97         try {
98             return new Model(mSoundTriggerService.getSoundModel(
99                     new ParcelUuid(soundModelId)));
100         } catch (RemoteException e) {
101             throw e.rethrowFromSystemServer();
102         }
103     }
104 
105     /**
106      * Deletes the sound model represented by the provided UUID.
107      */
108     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
deleteModel(UUID soundModelId)109     public void deleteModel(UUID soundModelId) {
110         try {
111             mSoundTriggerService.deleteSoundModel(new ParcelUuid(soundModelId));
112         } catch (RemoteException e) {
113             throw e.rethrowFromSystemServer();
114         }
115     }
116 
117     /**
118      * Creates an instance of {@link SoundTriggerDetector} which can be used to start/stop
119      * recognition on the model and register for triggers from the model. Note that this call
120      * invalidates any previously returned instances for the same sound model Uuid.
121      *
122      * @param soundModelId UUID of the sound model to create the receiver object for.
123      * @param callback Instance of the {@link SoundTriggerDetector#Callback} object for the
124      * callbacks for the given sound model.
125      * @param handler The Handler to use for the callback operations. A null value will use the
126      * current thread's Looper.
127      * @return Instance of {@link SoundTriggerDetector} or null on error.
128      */
129     @Nullable
130     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
createSoundTriggerDetector(UUID soundModelId, @NonNull SoundTriggerDetector.Callback callback, @Nullable Handler handler)131     public SoundTriggerDetector createSoundTriggerDetector(UUID soundModelId,
132             @NonNull SoundTriggerDetector.Callback callback, @Nullable Handler handler) {
133         if (soundModelId == null) {
134             return null;
135         }
136 
137         SoundTriggerDetector oldInstance = mReceiverInstanceMap.get(soundModelId);
138         if (oldInstance != null) {
139             // Shutdown old instance.
140         }
141         SoundTriggerDetector newInstance = new SoundTriggerDetector(mSoundTriggerService,
142                 soundModelId, callback, handler);
143         mReceiverInstanceMap.put(soundModelId, newInstance);
144         return newInstance;
145     }
146 
147     /**
148      * Class captures the data and fields that represent a non-keyphrase sound model. Use the
149      * factory constructor {@link Model#create()} to create an instance.
150      */
151     // We use encapsulation to expose the SoundTrigger.GenericSoundModel as a SystemApi. This
152     // prevents us from exposing SoundTrigger.GenericSoundModel as an Api.
153     public static class Model {
154 
155         private SoundTrigger.GenericSoundModel mGenericSoundModel;
156 
157         /**
158          * @hide
159          */
Model(SoundTrigger.GenericSoundModel soundTriggerModel)160         Model(SoundTrigger.GenericSoundModel soundTriggerModel) {
161             mGenericSoundModel = soundTriggerModel;
162         }
163 
164         /**
165          * Factory constructor to create a SoundModel instance for use with methods in this
166          * class.
167          */
create(UUID modelUuid, UUID vendorUuid, byte[] data)168         public static Model create(UUID modelUuid, UUID vendorUuid, byte[] data) {
169             return new Model(new SoundTrigger.GenericSoundModel(modelUuid,
170                         vendorUuid, data));
171         }
172 
getModelUuid()173         public UUID getModelUuid() {
174             return mGenericSoundModel.uuid;
175         }
176 
getVendorUuid()177         public UUID getVendorUuid() {
178             return mGenericSoundModel.vendorUuid;
179         }
180 
getModelData()181         public byte[] getModelData() {
182             return mGenericSoundModel.data;
183         }
184 
185         /**
186          * @hide
187          */
getGenericSoundModel()188         SoundTrigger.GenericSoundModel getGenericSoundModel() {
189             return mGenericSoundModel;
190         }
191     }
192 
193 
194     /**
195      * Default message type.
196      * @hide
197      */
198     public static final int FLAG_MESSAGE_TYPE_UNKNOWN = -1;
199     /**
200      * Contents of EXTRA_MESSAGE_TYPE extra for a RecognitionEvent.
201      * @hide
202      */
203     public static final int FLAG_MESSAGE_TYPE_RECOGNITION_EVENT = 0;
204     /**
205      * Contents of EXTRA_MESSAGE_TYPE extra for recognition error events.
206      * @hide
207      */
208     public static final int FLAG_MESSAGE_TYPE_RECOGNITION_ERROR = 1;
209     /**
210      * Contents of EXTRA_MESSAGE_TYPE extra for a recognition paused events.
211      * @hide
212      */
213     public static final int FLAG_MESSAGE_TYPE_RECOGNITION_PAUSED = 2;
214     /**
215      * Contents of EXTRA_MESSAGE_TYPE extra for recognition resumed events.
216      * @hide
217      */
218     public static final int FLAG_MESSAGE_TYPE_RECOGNITION_RESUMED = 3;
219 
220     /**
221      * Extra key in the intent for the type of the message.
222      * @hide
223      */
224     public static final String EXTRA_MESSAGE_TYPE = "android.media.soundtrigger.MESSAGE_TYPE";
225     /**
226      * Extra key in the intent that holds the RecognitionEvent parcelable.
227      * @hide
228      */
229     public static final String EXTRA_RECOGNITION_EVENT = "android.media.soundtrigger.RECOGNITION_EVENT";
230     /**
231      * Extra key in the intent that holds the status in an error message.
232      * @hide
233      */
234     public static final String EXTRA_STATUS = "android.media.soundtrigger.STATUS";
235 
236     /**
237      * Loads a given sound model into the sound trigger. Note the model will be unloaded if there is
238      * an error/the system service is restarted.
239      * @hide
240      */
241     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
242     @UnsupportedAppUsage
loadSoundModel(SoundModel soundModel)243     public int loadSoundModel(SoundModel soundModel) {
244         if (soundModel == null) {
245             return STATUS_ERROR;
246         }
247 
248         try {
249             switch (soundModel.type) {
250                 case SoundModel.TYPE_GENERIC_SOUND:
251                     return mSoundTriggerService.loadGenericSoundModel(
252                             (GenericSoundModel) soundModel);
253                 case SoundModel.TYPE_KEYPHRASE:
254                     return mSoundTriggerService.loadKeyphraseSoundModel(
255                             (KeyphraseSoundModel) soundModel);
256                 default:
257                     Slog.e(TAG, "Unkown model type");
258                     return STATUS_ERROR;
259             }
260         } catch (RemoteException e) {
261             throw e.rethrowFromSystemServer();
262         }
263     }
264 
265     /**
266      * Starts recognition for the given model id. All events from the model will be sent to the
267      * service.
268      *
269      * <p>This only supports generic sound trigger events. For keyphrase events, please use
270      * {@link android.service.voice.VoiceInteractionService}.
271      *
272      * @param soundModelId Id of the sound model
273      * @param params Opaque data sent to each service call of the service as the {@code params}
274      *               argument
275      * @param detectionService The component name of the service that should receive the events.
276      *                         Needs to subclass {@link SoundTriggerDetectionService}
277      * @param config Configures the recognition
278      *
279      * @return {@link SoundTrigger#STATUS_OK} if the recognition could be started, error code
280      *         otherwise
281      *
282      * @hide
283      */
284     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
285     @UnsupportedAppUsage
startRecognition(@onNull UUID soundModelId, @Nullable Bundle params, @NonNull ComponentName detectionService, @NonNull RecognitionConfig config)286     public int startRecognition(@NonNull UUID soundModelId, @Nullable Bundle params,
287         @NonNull ComponentName detectionService, @NonNull RecognitionConfig config) {
288         Preconditions.checkNotNull(soundModelId);
289         Preconditions.checkNotNull(detectionService);
290         Preconditions.checkNotNull(config);
291 
292         try {
293             return mSoundTriggerService.startRecognitionForService(new ParcelUuid(soundModelId),
294                 params, detectionService, config);
295         } catch (RemoteException e) {
296             throw e.rethrowFromSystemServer();
297         }
298     }
299 
300     /**
301      * Stops the given model's recognition.
302      * @hide
303      */
304     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
305     @UnsupportedAppUsage
stopRecognition(UUID soundModelId)306     public int stopRecognition(UUID soundModelId) {
307         if (soundModelId == null) {
308             return STATUS_ERROR;
309         }
310         try {
311             return mSoundTriggerService.stopRecognitionForService(new ParcelUuid(soundModelId));
312         } catch (RemoteException e) {
313             throw e.rethrowFromSystemServer();
314         }
315     }
316 
317     /**
318      * Removes the given model from memory. Will also stop any pending recognitions.
319      * @hide
320      */
321     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
322     @UnsupportedAppUsage
unloadSoundModel(UUID soundModelId)323     public int unloadSoundModel(UUID soundModelId) {
324         if (soundModelId == null) {
325             return STATUS_ERROR;
326         }
327         try {
328             return mSoundTriggerService.unloadSoundModel(
329                     new ParcelUuid(soundModelId));
330         } catch (RemoteException e) {
331             throw e.rethrowFromSystemServer();
332         }
333     }
334 
335     /**
336      * Returns true if the given model has had detection started on it.
337      * @hide
338      */
339     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
340     @UnsupportedAppUsage
isRecognitionActive(UUID soundModelId)341     public boolean isRecognitionActive(UUID soundModelId) {
342         if (soundModelId == null) {
343             return false;
344         }
345         try {
346             return mSoundTriggerService.isRecognitionActive(
347                     new ParcelUuid(soundModelId));
348         } catch (RemoteException e) {
349             throw e.rethrowFromSystemServer();
350         }
351     }
352 
353     /**
354      * Get the amount of time (in milliseconds) an operation of the
355      * {@link ISoundTriggerDetectionService} is allowed to ask.
356      *
357      * @return The amount of time an sound trigger detection service operation is allowed to last
358      */
getDetectionServiceOperationsTimeout()359     public int getDetectionServiceOperationsTimeout() {
360         try {
361             return Settings.Global.getInt(mContext.getContentResolver(),
362                     Settings.Global.SOUND_TRIGGER_DETECTION_SERVICE_OP_TIMEOUT);
363         } catch (Settings.SettingNotFoundException e) {
364             return Integer.MAX_VALUE;
365         }
366     }
367 
368     /**
369      * Asynchronously get state of the indicated model.  The model state is returned as
370      * a recognition event in the callback that was registered in the startRecognition
371      * method.
372      * @hide
373      */
374     @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
375     @UnsupportedAppUsage
getModelState(UUID soundModelId)376     public int getModelState(UUID soundModelId) {
377         if (soundModelId == null) {
378             return STATUS_ERROR;
379         }
380         try {
381             return mSoundTriggerService.getModelState(new ParcelUuid(soundModelId));
382         } catch (RemoteException e) {
383             throw e.rethrowFromSystemServer();
384         }
385     }
386 }
387