Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2019 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | package com.android.server.soundtrigger_middleware; |
| 18 | |
| 19 | import android.annotation.NonNull; |
| 20 | import android.annotation.Nullable; |
| 21 | import android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback; |
| 22 | import android.hardware.soundtrigger.V2_2.ISoundTriggerHw; |
| 23 | import android.media.soundtrigger_middleware.ISoundTriggerCallback; |
| 24 | import android.media.soundtrigger_middleware.ISoundTriggerModule; |
| 25 | import android.media.soundtrigger_middleware.ModelParameterRange; |
Ytai Ben-Tsvi | 8ed1a8b | 2020-06-04 13:11:54 -0700 | [diff] [blame] | 26 | import android.media.soundtrigger_middleware.PhraseRecognitionEvent; |
| 27 | import android.media.soundtrigger_middleware.PhraseRecognitionExtra; |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 28 | import android.media.soundtrigger_middleware.PhraseSoundModel; |
| 29 | import android.media.soundtrigger_middleware.RecognitionConfig; |
Ytai Ben-Tsvi | 8ed1a8b | 2020-06-04 13:11:54 -0700 | [diff] [blame] | 30 | import android.media.soundtrigger_middleware.RecognitionEvent; |
Ytai Ben-Tsvi | 7caef40a | 2020-06-09 15:50:20 -0700 | [diff] [blame] | 31 | import android.media.soundtrigger_middleware.RecognitionStatus; |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 32 | import android.media.soundtrigger_middleware.SoundModel; |
| 33 | import android.media.soundtrigger_middleware.SoundModelType; |
| 34 | import android.media.soundtrigger_middleware.SoundTriggerModuleProperties; |
| 35 | import android.media.soundtrigger_middleware.Status; |
| 36 | import android.os.IBinder; |
Ytai Ben-Tsvi | c2327e7 | 2020-01-10 10:47:00 -0800 | [diff] [blame] | 37 | import android.os.IHwBinder; |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 38 | import android.os.RemoteException; |
Ytai Ben-Tsvi | c2327e7 | 2020-01-10 10:47:00 -0800 | [diff] [blame] | 39 | import android.os.ServiceSpecificException; |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 40 | import android.util.Log; |
| 41 | |
Ytai Ben-Tsvi | 77c195d | 2020-05-04 15:26:38 -0700 | [diff] [blame] | 42 | import java.util.ArrayList; |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 43 | import java.util.HashMap; |
| 44 | import java.util.HashSet; |
Ytai Ben-Tsvi | 21c886c | 2020-06-18 11:52:39 -0700 | [diff] [blame] | 45 | import java.util.LinkedList; |
Ytai Ben-Tsvi | 77c195d | 2020-05-04 15:26:38 -0700 | [diff] [blame] | 46 | import java.util.List; |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 47 | import java.util.Map; |
| 48 | import java.util.Set; |
| 49 | |
| 50 | /** |
| 51 | * This is an implementation of a single module of the ISoundTriggerMiddlewareService interface, |
| 52 | * exposing itself through the {@link ISoundTriggerModule} interface, possibly to multiple separate |
| 53 | * clients. |
| 54 | * <p> |
| 55 | * Typical usage is to query the module capabilities using {@link #getProperties()} and then to use |
| 56 | * the module through an {@link ISoundTriggerModule} instance, obtained via {@link |
| 57 | * #attach(ISoundTriggerCallback)}. Every such interface is its own session and state is not shared |
| 58 | * between sessions (i.e. cannot use a handle obtained from one session through another). |
| 59 | * <p> |
| 60 | * <b>Important conventions:</b> |
| 61 | * <ul> |
| 62 | * <li>Correct usage is assumed. This implementation does not attempt to gracefully handle |
| 63 | * invalid usage, and such usage will result in undefined behavior. If this service is to be |
| 64 | * offered to an untrusted client, it must be wrapped with input and state validation. |
| 65 | * <li>The underlying driver is assumed to be correct. This implementation does not attempt to |
| 66 | * gracefully handle driver malfunction and such behavior will result in undefined behavior. If this |
| 67 | * service is to used with an untrusted driver, the driver must be wrapped with validation / error |
| 68 | * recovery code. |
Ytai Ben-Tsvi | c2327e7 | 2020-01-10 10:47:00 -0800 | [diff] [blame] | 69 | * <li>Recovery from driver death is supported.</li> |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 70 | * <li>RemoteExceptions thrown by the driver are treated as RuntimeExceptions - they are not |
| 71 | * considered recoverable faults and should not occur in a properly functioning system. |
| 72 | * <li>There is no binder instance associated with this implementation. Do not call asBinder(). |
| 73 | * <li>The implementation may throw a {@link RecoverableException} to indicate non-fatal, |
| 74 | * recoverable faults. The error code would one of the |
| 75 | * {@link android.media.soundtrigger_middleware.Status} constants. Any other exception |
| 76 | * thrown should be regarded as a bug in the implementation or one of its dependencies |
| 77 | * (assuming correct usage). |
Ytai Ben-Tsvi | 7d383d1 | 2019-11-25 12:47:40 -0800 | [diff] [blame] | 78 | * <li>The implementation is designed for testability by featuring dependency injection (the |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 79 | * underlying HAL driver instances are passed to the ctor) and by minimizing dependencies |
| 80 | * on Android runtime. |
| 81 | * <li>The implementation is thread-safe. This is achieved by a simplistic model, where all entry- |
| 82 | * points (both client API and driver callbacks) obtain a lock on the SoundTriggerModule instance |
| 83 | * for their entire scope. Any other method can be assumed to be running with the lock already |
| 84 | * obtained, so no further locking should be done. While this is not necessarily the most efficient |
| 85 | * synchronization strategy, it is very easy to reason about and this code is likely not on any |
| 86 | * performance-critical |
| 87 | * path. |
| 88 | * </ul> |
| 89 | * |
| 90 | * @hide |
| 91 | */ |
Ytai Ben-Tsvi | c2327e7 | 2020-01-10 10:47:00 -0800 | [diff] [blame] | 92 | class SoundTriggerModule implements IHwBinder.DeathRecipient { |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 93 | static private final String TAG = "SoundTriggerModule"; |
Ytai Ben-Tsvi | c2327e7 | 2020-01-10 10:47:00 -0800 | [diff] [blame] | 94 | @NonNull private HalFactory mHalFactory; |
| 95 | @NonNull private ISoundTriggerHw2 mHalService; |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 96 | @NonNull private final SoundTriggerMiddlewareImpl.AudioSessionProvider mAudioSessionProvider; |
| 97 | private final Set<Session> mActiveSessions = new HashSet<>(); |
| 98 | private int mNumLoadedModels = 0; |
Ytai Ben-Tsvi | 6df1f3d | 2020-01-09 15:50:51 -0800 | [diff] [blame] | 99 | private SoundTriggerModuleProperties mProperties; |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 100 | private boolean mRecognitionAvailable; |
| 101 | |
| 102 | /** |
| 103 | * Ctor. |
| 104 | * |
Ytai Ben-Tsvi | c2327e7 | 2020-01-10 10:47:00 -0800 | [diff] [blame] | 105 | * @param halFactory A factory for the underlying HAL driver. |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 106 | */ |
Ytai Ben-Tsvi | c2327e7 | 2020-01-10 10:47:00 -0800 | [diff] [blame] | 107 | SoundTriggerModule(@NonNull HalFactory halFactory, |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 108 | @NonNull SoundTriggerMiddlewareImpl.AudioSessionProvider audioSessionProvider) { |
Ytai Ben-Tsvi | c2327e7 | 2020-01-10 10:47:00 -0800 | [diff] [blame] | 109 | assert halFactory != null; |
| 110 | mHalFactory = halFactory; |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 111 | mAudioSessionProvider = audioSessionProvider; |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 112 | |
Ytai Ben-Tsvi | c2327e7 | 2020-01-10 10:47:00 -0800 | [diff] [blame] | 113 | attachToHal(); |
| 114 | mProperties = ConversionUtil.hidl2aidlProperties(mHalService.getProperties()); |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 115 | // We conservatively assume that external capture is active until explicitly told otherwise. |
| 116 | mRecognitionAvailable = mProperties.concurrentCapture; |
| 117 | } |
| 118 | |
| 119 | /** |
| 120 | * Establish a client session with this module. |
| 121 | * |
| 122 | * This module may be shared by multiple clients, each will get its own session. While resources |
| 123 | * are shared between the clients, each session has its own state and data should not be shared |
| 124 | * across sessions. |
| 125 | * |
| 126 | * @param callback The client callback, which will be used for all messages. This is a oneway |
| 127 | * callback, so will never block, throw an unchecked exception or return a |
| 128 | * value. |
| 129 | * @return The interface through which this module can be controlled. |
| 130 | */ |
| 131 | synchronized @NonNull |
Ytai Ben-Tsvi | 6df1f3d | 2020-01-09 15:50:51 -0800 | [diff] [blame] | 132 | ISoundTriggerModule attach(@NonNull ISoundTriggerCallback callback) { |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 133 | Session session = new Session(callback); |
| 134 | mActiveSessions.add(session); |
| 135 | return session; |
| 136 | } |
| 137 | |
| 138 | /** |
| 139 | * Query the module's properties. |
| 140 | * |
| 141 | * @return The properties structure. |
| 142 | */ |
| 143 | synchronized @NonNull |
| 144 | SoundTriggerModuleProperties getProperties() { |
| 145 | return mProperties; |
| 146 | } |
| 147 | |
| 148 | /** |
| 149 | * Notify the module that external capture has started / finished, using the same input device |
| 150 | * used for recognition. |
| 151 | * If the underlying driver does not support recognition while capturing, capture will be |
| 152 | * aborted, and the recognition callback will receive and abort event. In addition, all active |
| 153 | * clients will be notified of the change in state. |
| 154 | * |
| 155 | * @param active true iff external capture is active. |
| 156 | */ |
Ytai Ben-Tsvi | 21c886c | 2020-06-18 11:52:39 -0700 | [diff] [blame] | 157 | void setExternalCaptureState(boolean active) { |
| 158 | // We should never invoke callbacks while holding the lock, since this may deadlock with |
| 159 | // forward calls. Thus, we first gather all the callbacks we need to invoke while holding |
| 160 | // the lock, but invoke them after releasing it. |
| 161 | List<Runnable> callbacks = new LinkedList<>(); |
| 162 | |
| 163 | synchronized (this) { |
| 164 | if (mProperties.concurrentCapture) { |
| 165 | // If we support concurrent capture, we don't care about any of this. |
| 166 | return; |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 167 | } |
Ytai Ben-Tsvi | 21c886c | 2020-06-18 11:52:39 -0700 | [diff] [blame] | 168 | mRecognitionAvailable = !active; |
| 169 | if (!mRecognitionAvailable) { |
| 170 | // Our module does not support recognition while a capture is active - |
| 171 | // need to abort all active recognitions. |
| 172 | for (Session session : mActiveSessions) { |
| 173 | session.abortActiveRecognitions(callbacks); |
| 174 | } |
| 175 | } |
| 176 | } |
| 177 | for (Runnable callback : callbacks) { |
| 178 | callback.run(); |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 179 | } |
| 180 | for (Session session : mActiveSessions) { |
| 181 | session.notifyRecognitionAvailability(); |
| 182 | } |
| 183 | } |
| 184 | |
Ytai Ben-Tsvi | c2327e7 | 2020-01-10 10:47:00 -0800 | [diff] [blame] | 185 | @Override |
Ytai Ben-Tsvi | 77c195d | 2020-05-04 15:26:38 -0700 | [diff] [blame] | 186 | public void serviceDied(long cookie) { |
Ytai Ben-Tsvi | c2327e7 | 2020-01-10 10:47:00 -0800 | [diff] [blame] | 187 | Log.w(TAG, String.format("Underlying HAL driver died.")); |
Ytai Ben-Tsvi | 77c195d | 2020-05-04 15:26:38 -0700 | [diff] [blame] | 188 | List<ISoundTriggerCallback> callbacks = new ArrayList<>(mActiveSessions.size()); |
| 189 | synchronized (this) { |
| 190 | for (Session session : mActiveSessions) { |
| 191 | callbacks.add(session.moduleDied()); |
| 192 | } |
| 193 | reset(); |
Ytai Ben-Tsvi | c2327e7 | 2020-01-10 10:47:00 -0800 | [diff] [blame] | 194 | } |
Ytai Ben-Tsvi | 77c195d | 2020-05-04 15:26:38 -0700 | [diff] [blame] | 195 | // Trigger the callbacks outside of the lock to avoid deadlocks. |
| 196 | for (ISoundTriggerCallback callback : callbacks) { |
| 197 | try { |
| 198 | callback.onModuleDied(); |
| 199 | } catch (RemoteException e) { |
| 200 | throw e.rethrowAsRuntimeException(); |
| 201 | } |
| 202 | } |
Ytai Ben-Tsvi | a23eaa7 | 2020-01-21 17:25:52 -0800 | [diff] [blame] | 203 | } |
| 204 | |
| 205 | /** |
| 206 | * Resets the transient state of this object. |
| 207 | */ |
| 208 | private void reset() { |
Ytai Ben-Tsvi | c2327e7 | 2020-01-10 10:47:00 -0800 | [diff] [blame] | 209 | attachToHal(); |
Ytai Ben-Tsvi | a23eaa7 | 2020-01-21 17:25:52 -0800 | [diff] [blame] | 210 | // We conservatively assume that external capture is active until explicitly told otherwise. |
| 211 | mRecognitionAvailable = mProperties.concurrentCapture; |
| 212 | mNumLoadedModels = 0; |
Ytai Ben-Tsvi | c2327e7 | 2020-01-10 10:47:00 -0800 | [diff] [blame] | 213 | } |
| 214 | |
| 215 | /** |
| 216 | * Attached to the HAL service via factory. |
| 217 | */ |
| 218 | private void attachToHal() { |
Ytai Ben-Tsvi | 56a4ee8 | 2020-02-14 16:00:54 -0800 | [diff] [blame] | 219 | mHalService = new SoundTriggerHw2Enforcer(new SoundTriggerHw2Compat(mHalFactory.create())); |
Ytai Ben-Tsvi | c2327e7 | 2020-01-10 10:47:00 -0800 | [diff] [blame] | 220 | mHalService.linkToDeath(this, 0); |
| 221 | } |
| 222 | |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 223 | /** |
| 224 | * Remove session from the list of active sessions. |
| 225 | * |
| 226 | * @param session The session to remove. |
| 227 | */ |
| 228 | private void removeSession(@NonNull Session session) { |
| 229 | mActiveSessions.remove(session); |
| 230 | } |
| 231 | |
| 232 | /** State of a single sound model. */ |
| 233 | private enum ModelState { |
| 234 | /** Initial state, until load() is called. */ |
| 235 | INIT, |
| 236 | /** Model is loaded, but recognition is not active. */ |
| 237 | LOADED, |
| 238 | /** Model is loaded and recognition is active. */ |
| 239 | ACTIVE |
| 240 | } |
| 241 | |
| 242 | /** |
| 243 | * A single client session with this module. |
| 244 | * |
| 245 | * This is the main interface used to interact with this module. |
| 246 | */ |
| 247 | private class Session implements ISoundTriggerModule { |
| 248 | private ISoundTriggerCallback mCallback; |
| 249 | private Map<Integer, Model> mLoadedModels = new HashMap<>(); |
| 250 | |
| 251 | /** |
| 252 | * Ctor. |
| 253 | * |
| 254 | * @param callback The client callback interface. |
| 255 | */ |
| 256 | private Session(@NonNull ISoundTriggerCallback callback) { |
| 257 | mCallback = callback; |
| 258 | notifyRecognitionAvailability(); |
| 259 | } |
| 260 | |
| 261 | @Override |
| 262 | public void detach() { |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 263 | synchronized (SoundTriggerModule.this) { |
Ytai Ben-Tsvi | c2327e7 | 2020-01-10 10:47:00 -0800 | [diff] [blame] | 264 | if (mCallback == null) { |
| 265 | return; |
| 266 | } |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 267 | removeSession(this); |
Ytai Ben-Tsvi | c2327e7 | 2020-01-10 10:47:00 -0800 | [diff] [blame] | 268 | mCallback = null; |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 269 | } |
| 270 | } |
| 271 | |
| 272 | @Override |
| 273 | public int loadModel(@NonNull SoundModel model) { |
Ytai Ben-Tsvi | 406619f4 | 2020-02-13 16:24:56 -0800 | [diff] [blame] | 274 | // We must do this outside the lock, to avoid possible deadlocks with the remote process |
| 275 | // that provides the audio sessions, which may also be calling into us. |
| 276 | SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession audioSession = |
| 277 | mAudioSessionProvider.acquireSession(); |
| 278 | |
| 279 | try { |
| 280 | synchronized (SoundTriggerModule.this) { |
| 281 | checkValid(); |
| 282 | if (mNumLoadedModels == mProperties.maxSoundModels) { |
| 283 | throw new RecoverableException(Status.RESOURCE_CONTENTION, |
| 284 | "Maximum number of models loaded."); |
| 285 | } |
| 286 | Model loadedModel = new Model(); |
| 287 | int result = loadedModel.load(model, audioSession); |
| 288 | ++mNumLoadedModels; |
| 289 | return result; |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 290 | } |
Ytai Ben-Tsvi | 406619f4 | 2020-02-13 16:24:56 -0800 | [diff] [blame] | 291 | } catch (Exception e) { |
| 292 | // We must do this outside the lock, to avoid possible deadlocks with the remote |
| 293 | // process that provides the audio sessions, which may also be calling into us. |
| 294 | mAudioSessionProvider.releaseSession(audioSession.mSessionHandle); |
| 295 | throw e; |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 296 | } |
| 297 | } |
| 298 | |
| 299 | @Override |
| 300 | public int loadPhraseModel(@NonNull PhraseSoundModel model) { |
Ytai Ben-Tsvi | 406619f4 | 2020-02-13 16:24:56 -0800 | [diff] [blame] | 301 | // We must do this outside the lock, to avoid possible deadlocks with the remote process |
| 302 | // that provides the audio sessions, which may also be calling into us. |
| 303 | SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession audioSession = |
| 304 | mAudioSessionProvider.acquireSession(); |
| 305 | |
| 306 | try { |
| 307 | synchronized (SoundTriggerModule.this) { |
| 308 | checkValid(); |
| 309 | if (mNumLoadedModels == mProperties.maxSoundModels) { |
| 310 | throw new RecoverableException(Status.RESOURCE_CONTENTION, |
| 311 | "Maximum number of models loaded."); |
| 312 | } |
| 313 | Model loadedModel = new Model(); |
| 314 | int result = loadedModel.load(model, audioSession); |
| 315 | ++mNumLoadedModels; |
| 316 | Log.d(TAG, String.format("loadPhraseModel()->%d", result)); |
| 317 | return result; |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 318 | } |
Ytai Ben-Tsvi | 406619f4 | 2020-02-13 16:24:56 -0800 | [diff] [blame] | 319 | } catch (Exception e) { |
| 320 | // We must do this outside the lock, to avoid possible deadlocks with the remote |
| 321 | // process that provides the audio sessions, which may also be calling into us. |
| 322 | mAudioSessionProvider.releaseSession(audioSession.mSessionHandle); |
| 323 | throw e; |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 324 | } |
| 325 | } |
| 326 | |
| 327 | @Override |
| 328 | public void unloadModel(int modelHandle) { |
Ytai Ben-Tsvi | 406619f4 | 2020-02-13 16:24:56 -0800 | [diff] [blame] | 329 | int sessionId; |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 330 | synchronized (SoundTriggerModule.this) { |
Ytai Ben-Tsvi | c2327e7 | 2020-01-10 10:47:00 -0800 | [diff] [blame] | 331 | checkValid(); |
Ytai Ben-Tsvi | 406619f4 | 2020-02-13 16:24:56 -0800 | [diff] [blame] | 332 | sessionId = mLoadedModels.get(modelHandle).unload(); |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 333 | --mNumLoadedModels; |
| 334 | } |
Ytai Ben-Tsvi | 406619f4 | 2020-02-13 16:24:56 -0800 | [diff] [blame] | 335 | |
| 336 | // We must do this outside the lock, to avoid possible deadlocks with the remote process |
| 337 | // that provides the audio sessions, which may also be calling into us. |
| 338 | mAudioSessionProvider.releaseSession(sessionId); |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 339 | } |
| 340 | |
| 341 | @Override |
| 342 | public void startRecognition(int modelHandle, @NonNull RecognitionConfig config) { |
Ytai Ben-Tsvi | 21c886c | 2020-06-18 11:52:39 -0700 | [diff] [blame] | 343 | // We should never invoke callbacks while holding the lock, since this may deadlock with |
| 344 | // forward calls. Thus, we first gather all the callbacks we need to invoke while holding |
| 345 | // the lock, but invoke them after releasing it. |
| 346 | List<Runnable> callbacks = new LinkedList<>(); |
| 347 | |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 348 | synchronized (SoundTriggerModule.this) { |
Ytai Ben-Tsvi | c2327e7 | 2020-01-10 10:47:00 -0800 | [diff] [blame] | 349 | checkValid(); |
Ytai Ben-Tsvi | 21c886c | 2020-06-18 11:52:39 -0700 | [diff] [blame] | 350 | mLoadedModels.get(modelHandle).startRecognition(config, callbacks); |
| 351 | } |
| 352 | |
| 353 | for (Runnable callback : callbacks) { |
| 354 | callback.run(); |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 355 | } |
| 356 | } |
| 357 | |
| 358 | @Override |
| 359 | public void stopRecognition(int modelHandle) { |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 360 | synchronized (SoundTriggerModule.this) { |
| 361 | mLoadedModels.get(modelHandle).stopRecognition(); |
| 362 | } |
| 363 | } |
| 364 | |
| 365 | @Override |
| 366 | public void forceRecognitionEvent(int modelHandle) { |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 367 | synchronized (SoundTriggerModule.this) { |
Ytai Ben-Tsvi | c2327e7 | 2020-01-10 10:47:00 -0800 | [diff] [blame] | 368 | checkValid(); |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 369 | mLoadedModels.get(modelHandle).forceRecognitionEvent(); |
| 370 | } |
| 371 | } |
| 372 | |
| 373 | @Override |
Ytai Ben-Tsvi | 6df1f3d | 2020-01-09 15:50:51 -0800 | [diff] [blame] | 374 | public void setModelParameter(int modelHandle, int modelParam, int value) { |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 375 | synchronized (SoundTriggerModule.this) { |
Ytai Ben-Tsvi | c2327e7 | 2020-01-10 10:47:00 -0800 | [diff] [blame] | 376 | checkValid(); |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 377 | mLoadedModels.get(modelHandle).setParameter(modelParam, value); |
| 378 | } |
| 379 | } |
| 380 | |
| 381 | @Override |
Ytai Ben-Tsvi | 6df1f3d | 2020-01-09 15:50:51 -0800 | [diff] [blame] | 382 | public int getModelParameter(int modelHandle, int modelParam) { |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 383 | synchronized (SoundTriggerModule.this) { |
Ytai Ben-Tsvi | c2327e7 | 2020-01-10 10:47:00 -0800 | [diff] [blame] | 384 | checkValid(); |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 385 | return mLoadedModels.get(modelHandle).getParameter(modelParam); |
| 386 | } |
| 387 | } |
| 388 | |
| 389 | @Override |
| 390 | @Nullable |
| 391 | public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam) { |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 392 | synchronized (SoundTriggerModule.this) { |
Ytai Ben-Tsvi | c2327e7 | 2020-01-10 10:47:00 -0800 | [diff] [blame] | 393 | checkValid(); |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 394 | return mLoadedModels.get(modelHandle).queryModelParameterSupport(modelParam); |
| 395 | } |
| 396 | } |
| 397 | |
| 398 | /** |
| 399 | * Abort all currently active recognitions. |
Ytai Ben-Tsvi | 21c886c | 2020-06-18 11:52:39 -0700 | [diff] [blame] | 400 | * @param callbacks Will be appended with a list of callbacks that need to be invoked |
| 401 | * after this method returns, without holding the module lock. |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 402 | */ |
Ytai Ben-Tsvi | 21c886c | 2020-06-18 11:52:39 -0700 | [diff] [blame] | 403 | private void abortActiveRecognitions(@NonNull List<Runnable> callbacks) { |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 404 | for (Model model : mLoadedModels.values()) { |
Ytai Ben-Tsvi | 21c886c | 2020-06-18 11:52:39 -0700 | [diff] [blame] | 405 | model.abortActiveRecognition(callbacks); |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 406 | } |
| 407 | } |
| 408 | |
| 409 | private void notifyRecognitionAvailability() { |
| 410 | try { |
| 411 | mCallback.onRecognitionAvailabilityChange(mRecognitionAvailable); |
| 412 | } catch (RemoteException e) { |
| 413 | // Dead client will be handled by binderDied() - no need to handle here. |
| 414 | // In any case, client callbacks are considered best effort. |
| 415 | Log.e(TAG, "Client callback execption.", e); |
| 416 | } |
| 417 | } |
| 418 | |
Ytai Ben-Tsvi | c2327e7 | 2020-01-10 10:47:00 -0800 | [diff] [blame] | 419 | /** |
| 420 | * The underlying module HAL is dead. |
Ytai Ben-Tsvi | 77c195d | 2020-05-04 15:26:38 -0700 | [diff] [blame] | 421 | * @return The client callback that needs to be invoked to notify the client. |
Ytai Ben-Tsvi | c2327e7 | 2020-01-10 10:47:00 -0800 | [diff] [blame] | 422 | */ |
Ytai Ben-Tsvi | 77c195d | 2020-05-04 15:26:38 -0700 | [diff] [blame] | 423 | private ISoundTriggerCallback moduleDied() { |
| 424 | ISoundTriggerCallback callback = mCallback; |
| 425 | removeSession(this); |
| 426 | mCallback = null; |
| 427 | return callback; |
Ytai Ben-Tsvi | c2327e7 | 2020-01-10 10:47:00 -0800 | [diff] [blame] | 428 | } |
| 429 | |
| 430 | private void checkValid() { |
| 431 | if (mCallback == null) { |
| 432 | throw new ServiceSpecificException(Status.DEAD_OBJECT); |
| 433 | } |
| 434 | } |
| 435 | |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 436 | @Override |
| 437 | public @NonNull |
| 438 | IBinder asBinder() { |
| 439 | throw new UnsupportedOperationException( |
| 440 | "This implementation is not intended to be used directly with Binder."); |
| 441 | } |
| 442 | |
| 443 | /** |
| 444 | * A single sound model in the system. |
| 445 | * |
| 446 | * All model-based operations are delegated to this class and implemented here. |
| 447 | */ |
| 448 | private class Model implements ISoundTriggerHw2.Callback { |
| 449 | public int mHandle; |
| 450 | private ModelState mState = ModelState.INIT; |
| 451 | private int mModelType = SoundModelType.UNKNOWN; |
| 452 | private SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession mSession; |
| 453 | |
| 454 | private @NonNull |
| 455 | ModelState getState() { |
| 456 | return mState; |
| 457 | } |
| 458 | |
| 459 | private void setState(@NonNull ModelState state) { |
| 460 | mState = state; |
| 461 | SoundTriggerModule.this.notifyAll(); |
| 462 | } |
| 463 | |
Ytai Ben-Tsvi | 406619f4 | 2020-02-13 16:24:56 -0800 | [diff] [blame] | 464 | private int load(@NonNull SoundModel model, |
| 465 | SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession audioSession) { |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 466 | mModelType = model.type; |
Ytai Ben-Tsvi | 406619f4 | 2020-02-13 16:24:56 -0800 | [diff] [blame] | 467 | mSession = audioSession; |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 468 | ISoundTriggerHw.SoundModel hidlModel = ConversionUtil.aidl2hidlSoundModel(model); |
| 469 | |
Ytai Ben-Tsvi | 406619f4 | 2020-02-13 16:24:56 -0800 | [diff] [blame] | 470 | mHandle = mHalService.loadSoundModel(hidlModel, this, 0); |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 471 | setState(ModelState.LOADED); |
| 472 | mLoadedModels.put(mHandle, this); |
| 473 | return mHandle; |
| 474 | } |
| 475 | |
Ytai Ben-Tsvi | 406619f4 | 2020-02-13 16:24:56 -0800 | [diff] [blame] | 476 | private int load(@NonNull PhraseSoundModel model, |
| 477 | SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession audioSession) { |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 478 | mModelType = model.common.type; |
Ytai Ben-Tsvi | 406619f4 | 2020-02-13 16:24:56 -0800 | [diff] [blame] | 479 | mSession = audioSession; |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 480 | ISoundTriggerHw.PhraseSoundModel hidlModel = |
| 481 | ConversionUtil.aidl2hidlPhraseSoundModel(model); |
| 482 | |
Ytai Ben-Tsvi | 406619f4 | 2020-02-13 16:24:56 -0800 | [diff] [blame] | 483 | mHandle = mHalService.loadPhraseSoundModel(hidlModel, this, 0); |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 484 | |
| 485 | setState(ModelState.LOADED); |
| 486 | mLoadedModels.put(mHandle, this); |
| 487 | return mHandle; |
| 488 | } |
| 489 | |
Ytai Ben-Tsvi | 406619f4 | 2020-02-13 16:24:56 -0800 | [diff] [blame] | 490 | /** |
| 491 | * Unloads the model. |
| 492 | * @return The audio session handle. |
| 493 | */ |
| 494 | private int unload() { |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 495 | mHalService.unloadSoundModel(mHandle); |
| 496 | mLoadedModels.remove(mHandle); |
Ytai Ben-Tsvi | 406619f4 | 2020-02-13 16:24:56 -0800 | [diff] [blame] | 497 | return mSession.mSessionHandle; |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 498 | } |
| 499 | |
Ytai Ben-Tsvi | 21c886c | 2020-06-18 11:52:39 -0700 | [diff] [blame] | 500 | private void startRecognition(@NonNull RecognitionConfig config, |
| 501 | @NonNull List<Runnable> callbacks) { |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 502 | if (!mRecognitionAvailable) { |
| 503 | // Recognition is unavailable - send an abort event immediately. |
Ytai Ben-Tsvi | 21c886c | 2020-06-18 11:52:39 -0700 | [diff] [blame] | 504 | callbacks.add(this::notifyAbort); |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 505 | return; |
| 506 | } |
Nicholas Ambur | 7092a56 | 2019-12-16 11:18:55 -0800 | [diff] [blame] | 507 | android.hardware.soundtrigger.V2_3.RecognitionConfig hidlConfig = |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 508 | ConversionUtil.aidl2hidlRecognitionConfig(config); |
Nicholas Ambur | 7092a56 | 2019-12-16 11:18:55 -0800 | [diff] [blame] | 509 | hidlConfig.base.header.captureDevice = mSession.mDeviceHandle; |
| 510 | hidlConfig.base.header.captureHandle = mSession.mIoHandle; |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 511 | mHalService.startRecognition(mHandle, hidlConfig, this, 0); |
| 512 | setState(ModelState.ACTIVE); |
| 513 | } |
| 514 | |
| 515 | private void stopRecognition() { |
| 516 | if (getState() == ModelState.LOADED) { |
| 517 | // This call is idempotent in order to avoid races. |
| 518 | return; |
| 519 | } |
| 520 | mHalService.stopRecognition(mHandle); |
| 521 | setState(ModelState.LOADED); |
| 522 | } |
| 523 | |
| 524 | /** Request a forced recognition event. Will do nothing if recognition is inactive. */ |
| 525 | private void forceRecognitionEvent() { |
| 526 | if (getState() != ModelState.ACTIVE) { |
| 527 | // This call is idempotent in order to avoid races. |
| 528 | return; |
| 529 | } |
| 530 | mHalService.getModelState(mHandle); |
| 531 | } |
| 532 | |
| 533 | |
| 534 | private void setParameter(int modelParam, int value) { |
| 535 | mHalService.setModelParameter(mHandle, |
| 536 | ConversionUtil.aidl2hidlModelParameter(modelParam), value); |
| 537 | } |
| 538 | |
| 539 | private int getParameter(int modelParam) { |
| 540 | return mHalService.getModelParameter(mHandle, |
| 541 | ConversionUtil.aidl2hidlModelParameter(modelParam)); |
| 542 | } |
| 543 | |
| 544 | @Nullable |
| 545 | private ModelParameterRange queryModelParameterSupport(int modelParam) { |
| 546 | return ConversionUtil.hidl2aidlModelParameterRange( |
| 547 | mHalService.queryParameter(mHandle, |
| 548 | ConversionUtil.aidl2hidlModelParameter(modelParam))); |
| 549 | } |
| 550 | |
Ytai Ben-Tsvi | 21c886c | 2020-06-18 11:52:39 -0700 | [diff] [blame] | 551 | /** |
| 552 | * Abort the recognition, if active. |
| 553 | * @param callbacks Will be appended with a list of callbacks that need to be invoked |
| 554 | * after this method returns, without holding the module lock. |
| 555 | */ |
| 556 | private void abortActiveRecognition(List<Runnable> callbacks) { |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 557 | // If we're inactive, do nothing. |
| 558 | if (getState() != ModelState.ACTIVE) { |
| 559 | return; |
| 560 | } |
| 561 | // Stop recognition. |
| 562 | stopRecognition(); |
| 563 | |
| 564 | // Notify the client that recognition has been aborted. |
Ytai Ben-Tsvi | 21c886c | 2020-06-18 11:52:39 -0700 | [diff] [blame] | 565 | callbacks.add(this::notifyAbort); |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 566 | } |
| 567 | |
| 568 | /** Notify the client that recognition has been aborted. */ |
| 569 | private void notifyAbort() { |
| 570 | try { |
| 571 | switch (mModelType) { |
| 572 | case SoundModelType.GENERIC: { |
| 573 | android.media.soundtrigger_middleware.RecognitionEvent event = |
Ytai Ben-Tsvi | 8ed1a8b | 2020-06-04 13:11:54 -0700 | [diff] [blame] | 574 | newEmptyRecognitionEvent(); |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 575 | event.status = |
| 576 | android.media.soundtrigger_middleware.RecognitionStatus.ABORTED; |
Ytai Ben-Tsvi | 8ed1a8b | 2020-06-04 13:11:54 -0700 | [diff] [blame] | 577 | event.type = SoundModelType.GENERIC; |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 578 | mCallback.onRecognition(mHandle, event); |
| 579 | } |
| 580 | break; |
| 581 | |
| 582 | case SoundModelType.KEYPHRASE: { |
| 583 | android.media.soundtrigger_middleware.PhraseRecognitionEvent event = |
Ytai Ben-Tsvi | 8ed1a8b | 2020-06-04 13:11:54 -0700 | [diff] [blame] | 584 | newEmptyPhraseRecognitionEvent(); |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 585 | event.common.status = |
| 586 | android.media.soundtrigger_middleware.RecognitionStatus.ABORTED; |
Ytai Ben-Tsvi | 8ed1a8b | 2020-06-04 13:11:54 -0700 | [diff] [blame] | 587 | event.common.type = SoundModelType.KEYPHRASE; |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 588 | mCallback.onPhraseRecognition(mHandle, event); |
| 589 | } |
| 590 | break; |
| 591 | |
| 592 | default: |
| 593 | Log.e(TAG, "Unknown model type: " + mModelType); |
| 594 | |
| 595 | } |
| 596 | } catch (RemoteException e) { |
| 597 | // Dead client will be handled by binderDied() - no need to handle here. |
| 598 | // In any case, client callbacks are considered best effort. |
| 599 | Log.e(TAG, "Client callback execption.", e); |
| 600 | } |
| 601 | } |
| 602 | |
| 603 | @Override |
| 604 | public void recognitionCallback( |
| 605 | @NonNull ISoundTriggerHwCallback.RecognitionEvent recognitionEvent, |
| 606 | int cookie) { |
Ytai Ben-Tsvi | 21c886c | 2020-06-18 11:52:39 -0700 | [diff] [blame] | 607 | RecognitionEvent aidlEvent = |
| 608 | ConversionUtil.hidl2aidlRecognitionEvent(recognitionEvent); |
| 609 | aidlEvent.captureSession = mSession.mSessionHandle; |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 610 | synchronized (SoundTriggerModule.this) { |
Ytai Ben-Tsvi | 7caef40a | 2020-06-09 15:50:20 -0700 | [diff] [blame] | 611 | if (aidlEvent.status != RecognitionStatus.FORCED) { |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 612 | setState(ModelState.LOADED); |
| 613 | } |
| 614 | } |
Ytai Ben-Tsvi | 21c886c | 2020-06-18 11:52:39 -0700 | [diff] [blame] | 615 | // The callback must be invoked outside of the lock. |
| 616 | try { |
| 617 | mCallback.onRecognition(mHandle, aidlEvent); |
| 618 | } catch (RemoteException e) { |
| 619 | // We're not expecting any exceptions here. |
| 620 | throw e.rethrowAsRuntimeException(); |
| 621 | } |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 622 | } |
| 623 | |
| 624 | @Override |
| 625 | public void phraseRecognitionCallback( |
| 626 | @NonNull ISoundTriggerHwCallback.PhraseRecognitionEvent phraseRecognitionEvent, |
| 627 | int cookie) { |
Ytai Ben-Tsvi | 21c886c | 2020-06-18 11:52:39 -0700 | [diff] [blame] | 628 | PhraseRecognitionEvent aidlEvent = |
| 629 | ConversionUtil.hidl2aidlPhraseRecognitionEvent(phraseRecognitionEvent); |
| 630 | aidlEvent.common.captureSession = mSession.mSessionHandle; |
| 631 | |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 632 | synchronized (SoundTriggerModule.this) { |
Ytai Ben-Tsvi | 7caef40a | 2020-06-09 15:50:20 -0700 | [diff] [blame] | 633 | if (aidlEvent.common.status != RecognitionStatus.FORCED) { |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 634 | setState(ModelState.LOADED); |
| 635 | } |
| 636 | } |
Ytai Ben-Tsvi | 21c886c | 2020-06-18 11:52:39 -0700 | [diff] [blame] | 637 | |
| 638 | // The callback must be invoked outside of the lock. |
| 639 | try { |
| 640 | mCallback.onPhraseRecognition(mHandle, aidlEvent); |
| 641 | } catch (RemoteException e) { |
| 642 | // We're not expecting any exceptions here. |
| 643 | throw e.rethrowAsRuntimeException(); |
| 644 | } |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 645 | } |
| 646 | } |
| 647 | } |
Ytai Ben-Tsvi | 8ed1a8b | 2020-06-04 13:11:54 -0700 | [diff] [blame] | 648 | |
| 649 | /** |
| 650 | * Creates a default-initialized recognition event. |
| 651 | * |
Ytai Ben-Tsvi | 7caef40a | 2020-06-09 15:50:20 -0700 | [diff] [blame] | 652 | * Non-nullable object fields are default constructed. |
| 653 | * Non-nullable array fields are initialized to 0 length. |
Ytai Ben-Tsvi | 8ed1a8b | 2020-06-04 13:11:54 -0700 | [diff] [blame] | 654 | * |
| 655 | * @return The event. |
| 656 | */ |
| 657 | private static RecognitionEvent newEmptyRecognitionEvent() { |
| 658 | RecognitionEvent result = new RecognitionEvent(); |
Ytai Ben-Tsvi | 8ed1a8b | 2020-06-04 13:11:54 -0700 | [diff] [blame] | 659 | result.data = new byte[0]; |
| 660 | return result; |
| 661 | } |
| 662 | |
| 663 | /** |
| 664 | * Creates a default-initialized phrase recognition event. |
| 665 | * |
Ytai Ben-Tsvi | 7caef40a | 2020-06-09 15:50:20 -0700 | [diff] [blame] | 666 | * Non-nullable object fields are default constructed. |
| 667 | * Non-nullable array fields are initialized to 0 length. |
Ytai Ben-Tsvi | 8ed1a8b | 2020-06-04 13:11:54 -0700 | [diff] [blame] | 668 | * |
| 669 | * @return The event. |
| 670 | */ |
| 671 | private static PhraseRecognitionEvent newEmptyPhraseRecognitionEvent() { |
| 672 | PhraseRecognitionEvent result = new PhraseRecognitionEvent(); |
| 673 | result.common = newEmptyRecognitionEvent(); |
| 674 | result.phraseExtras = new PhraseRecognitionExtra[0]; |
| 675 | return result; |
| 676 | } |
Ytai Ben-Tsvi | 93c117c86 | 2019-11-25 12:43:28 -0800 | [diff] [blame] | 677 | } |