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