1 /* 2 * Copyright (C) 2023 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.service.voice; 18 19 import static android.Manifest.permission.CAMERA; 20 import static android.Manifest.permission.RECORD_AUDIO; 21 22 import android.annotation.CallbackExecutor; 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.annotation.RequiresPermission; 26 import android.annotation.SuppressLint; 27 import android.annotation.SystemApi; 28 import android.content.Context; 29 import android.hardware.soundtrigger.SoundTrigger; 30 import android.media.AudioFormat; 31 import android.os.Binder; 32 import android.os.ParcelFileDescriptor; 33 import android.os.PersistableBundle; 34 import android.os.RemoteException; 35 import android.os.SharedMemory; 36 import android.text.TextUtils; 37 import android.util.Slog; 38 39 import com.android.internal.app.IHotwordRecognitionStatusCallback; 40 import com.android.internal.app.IVoiceInteractionManagerService; 41 import com.android.internal.infra.AndroidFuture; 42 43 import java.io.File; 44 import java.io.FileNotFoundException; 45 import java.io.PrintWriter; 46 import java.util.concurrent.Executor; 47 import java.util.function.Consumer; 48 49 /** 50 * Manages VisualQueryDetectionService. 51 * 52 * This detector provides necessary functionalities to initialize, start, update and destroy a 53 * {@link VisualQueryDetectionService}. 54 * 55 * @hide 56 **/ 57 @SystemApi 58 @SuppressLint("NotCloseable") 59 public class VisualQueryDetector { 60 private static final String TAG = VisualQueryDetector.class.getSimpleName(); 61 private static final boolean DEBUG = false; 62 63 private final Callback mCallback; 64 private final Executor mExecutor; 65 private final Context mContext; 66 private final IVoiceInteractionManagerService mManagerService; 67 private final VisualQueryDetectorInitializationDelegate mInitializationDelegate; 68 VisualQueryDetector( IVoiceInteractionManagerService managerService, @NonNull @CallbackExecutor Executor executor, Callback callback, Context context)69 VisualQueryDetector( 70 IVoiceInteractionManagerService managerService, 71 @NonNull @CallbackExecutor Executor executor, Callback callback, Context context) { 72 mManagerService = managerService; 73 mCallback = callback; 74 mExecutor = executor; 75 mInitializationDelegate = new VisualQueryDetectorInitializationDelegate(); 76 mContext = context; 77 } 78 79 /** 80 * Initialize the {@link VisualQueryDetectionService} by passing configurations and read-only 81 * data. 82 */ initialize(@ullable PersistableBundle options, @Nullable SharedMemory sharedMemory)83 void initialize(@Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory) { 84 mInitializationDelegate.initialize(options, sharedMemory); 85 } 86 87 /** 88 * Set configuration and pass read-only data to {@link VisualQueryDetectionService}. 89 * 90 * @see HotwordDetector#updateState(PersistableBundle, SharedMemory) 91 */ updateState(@ullable PersistableBundle options, @Nullable SharedMemory sharedMemory)92 public void updateState(@Nullable PersistableBundle options, 93 @Nullable SharedMemory sharedMemory) { 94 mInitializationDelegate.updateState(options, sharedMemory); 95 } 96 97 98 /** 99 * On calling this method, {@link VisualQueryDetectionService 100 * #onStartDetection(VisualQueryDetectionService.Callback)} will be called to start using 101 * visual signals such as camera frames and microphone audio to perform detection. When user 102 * attention is captured and the {@link VisualQueryDetectionService} streams queries, 103 * {@link VisualQueryDetector.Callback#onQueryDetected(String)} is called to control the 104 * behavior of handling {@code transcribedText}. When the query streaming is finished, 105 * {@link VisualQueryDetector.Callback#onQueryFinished()} is called. If the current streamed 106 * query is invalid, {@link VisualQueryDetector.Callback#onQueryRejected()} is called to abandon 107 * the streamed query. 108 * 109 * @see HotwordDetector#startRecognition() 110 */ 111 @RequiresPermission(allOf = {CAMERA, RECORD_AUDIO}) startRecognition()112 public boolean startRecognition() { 113 if (DEBUG) { 114 Slog.i(TAG, "#startRecognition"); 115 } 116 // check if the detector is active with the initialization delegate 117 mInitializationDelegate.startRecognition(); 118 119 try { 120 mManagerService.startPerceiving(new BinderCallback(mExecutor, mCallback)); 121 } catch (SecurityException e) { 122 Slog.e(TAG, "startRecognition failed: " + e); 123 return false; 124 } catch (RemoteException e) { 125 e.rethrowFromSystemServer(); 126 } 127 return true; 128 } 129 130 /** 131 * Stops visual query detection recognition. 132 * 133 * @see HotwordDetector#stopRecognition() 134 */ 135 @RequiresPermission(allOf = {CAMERA, RECORD_AUDIO}) stopRecognition()136 public boolean stopRecognition() { 137 if (DEBUG) { 138 Slog.i(TAG, "#stopRecognition"); 139 } 140 // check if the detector is active with the initialization delegate 141 mInitializationDelegate.startRecognition(); 142 143 try { 144 mManagerService.stopPerceiving(); 145 } catch (RemoteException e) { 146 e.rethrowFromSystemServer(); 147 } 148 return true; 149 } 150 151 /** 152 * Destroy the current detector. 153 * 154 * @see HotwordDetector#destroy() 155 */ destroy()156 public void destroy() { 157 if (DEBUG) { 158 Slog.i(TAG, "#destroy"); 159 } 160 mInitializationDelegate.destroy(); 161 } 162 163 /** @hide */ dump(String prefix, PrintWriter pw)164 public void dump(String prefix, PrintWriter pw) { 165 // TODO: implement this 166 } 167 168 /** @hide */ getInitializationDelegate()169 public HotwordDetector getInitializationDelegate() { 170 return mInitializationDelegate; 171 } 172 173 /** @hide */ registerOnDestroyListener(Consumer<AbstractDetector> onDestroyListener)174 void registerOnDestroyListener(Consumer<AbstractDetector> onDestroyListener) { 175 mInitializationDelegate.registerOnDestroyListener(onDestroyListener); 176 } 177 178 /** 179 * A class that lets a VoiceInteractionService implementation interact with 180 * visual query detection APIs. 181 */ 182 public interface Callback { 183 184 /** 185 * Called when the {@link VisualQueryDetectionService} starts to stream partial queries 186 * with {@link VisualQueryDetectionService#streamQuery(String)}. 187 * 188 * @param partialQuery The partial query in a text form being streamed. 189 */ onQueryDetected(@onNull String partialQuery)190 void onQueryDetected(@NonNull String partialQuery); 191 192 /** 193 * Called when the {@link VisualQueryDetectionService} decides to abandon the streamed 194 * partial queries with {@link VisualQueryDetectionService#rejectQuery()}. 195 */ onQueryRejected()196 void onQueryRejected(); 197 198 /** 199 * Called when the {@link VisualQueryDetectionService} finishes streaming partial queries 200 * with {@link VisualQueryDetectionService#finishQuery()}. 201 */ onQueryFinished()202 void onQueryFinished(); 203 204 /** 205 * Called when the {@link VisualQueryDetectionService} is created by the system and given a 206 * short amount of time to report its initialization state. 207 * 208 * @param status Info about initialization state of {@link VisualQueryDetectionService}; the 209 * allowed values are 210 * {@link SandboxedDetectionInitializer#INITIALIZATION_STATUS_SUCCESS}, 211 * 1<->{@link SandboxedDetectionInitializer#getMaxCustomInitializationStatus()}, 212 * {@link SandboxedDetectionInitializer#INITIALIZATION_STATUS_UNKNOWN}. 213 */ onVisualQueryDetectionServiceInitialized(int status)214 void onVisualQueryDetectionServiceInitialized(int status); 215 216 /** 217 * Called with the {@link VisualQueryDetectionService} is restarted. 218 * 219 * Clients are expected to call {@link HotwordDetector#updateState} to share the state with 220 * the newly created service. 221 */ onVisualQueryDetectionServiceRestarted()222 void onVisualQueryDetectionServiceRestarted(); 223 224 /** 225 * Called when the detection fails due to an error occurs in the 226 * {@link VisualQueryDetectionService}, {@link VisualQueryDetectionServiceFailure} will be 227 * reported to the detector. 228 * 229 * @param visualQueryDetectionServiceFailure It provides the error code, error message and 230 * suggested action. 231 */ onFailure( @onNull VisualQueryDetectionServiceFailure visualQueryDetectionServiceFailure)232 void onFailure( 233 @NonNull VisualQueryDetectionServiceFailure visualQueryDetectionServiceFailure); 234 235 /** 236 * Called when the detection fails due to an unknown error occurs, an error message 237 * will be reported to the detector. 238 * 239 * @param errorMessage It provides the error message. 240 */ onUnknownFailure(@onNull String errorMessage)241 void onUnknownFailure(@NonNull String errorMessage); 242 } 243 244 private class VisualQueryDetectorInitializationDelegate extends AbstractDetector { 245 VisualQueryDetectorInitializationDelegate()246 VisualQueryDetectorInitializationDelegate() { 247 super(mManagerService, mExecutor, /* callback= */ null); 248 } 249 250 @Override initialize(@ullable PersistableBundle options, @Nullable SharedMemory sharedMemory)251 void initialize(@Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory) { 252 initAndVerifyDetector(options, sharedMemory, 253 new InitializationStateListener(mExecutor, mCallback, mContext), 254 DETECTOR_TYPE_VISUAL_QUERY_DETECTOR); 255 } 256 257 @Override stopRecognition()258 public boolean stopRecognition() { 259 throwIfDetectorIsNoLongerActive(); 260 return true; 261 } 262 263 @Override startRecognition()264 public boolean startRecognition() { 265 throwIfDetectorIsNoLongerActive(); 266 return true; 267 } 268 269 @Override startRecognition( @onNull ParcelFileDescriptor audioStream, @NonNull AudioFormat audioFormat, @Nullable PersistableBundle options)270 public final boolean startRecognition( 271 @NonNull ParcelFileDescriptor audioStream, 272 @NonNull AudioFormat audioFormat, 273 @Nullable PersistableBundle options) { 274 //No-op, not supported by VisualQueryDetector as it should be trusted. 275 return false; 276 } 277 278 @Override isUsingSandboxedDetectionService()279 public boolean isUsingSandboxedDetectionService() { 280 return true; 281 } 282 } 283 284 private static class BinderCallback 285 extends IVisualQueryDetectionVoiceInteractionCallback.Stub { 286 private final Executor mExecutor; 287 private final VisualQueryDetector.Callback mCallback; 288 BinderCallback(Executor executor, VisualQueryDetector.Callback callback)289 BinderCallback(Executor executor, VisualQueryDetector.Callback callback) { 290 this.mExecutor = executor; 291 this.mCallback = callback; 292 } 293 294 /** Called when the detected result is valid. */ 295 @Override onQueryDetected(@onNull String partialQuery)296 public void onQueryDetected(@NonNull String partialQuery) { 297 Slog.v(TAG, "BinderCallback#onQueryDetected"); 298 Binder.withCleanCallingIdentity(() -> mExecutor.execute( 299 () -> mCallback.onQueryDetected(partialQuery))); 300 } 301 302 @Override onQueryFinished()303 public void onQueryFinished() { 304 Slog.v(TAG, "BinderCallback#onQueryFinished"); 305 Binder.withCleanCallingIdentity(() -> mExecutor.execute( 306 () -> mCallback.onQueryFinished())); 307 } 308 309 @Override onQueryRejected()310 public void onQueryRejected() { 311 Slog.v(TAG, "BinderCallback#onQueryRejected"); 312 Binder.withCleanCallingIdentity(() -> mExecutor.execute( 313 () -> mCallback.onQueryRejected())); 314 } 315 316 /** Called when the detection fails due to an error. */ 317 @Override onVisualQueryDetectionServiceFailure( VisualQueryDetectionServiceFailure visualQueryDetectionServiceFailure)318 public void onVisualQueryDetectionServiceFailure( 319 VisualQueryDetectionServiceFailure visualQueryDetectionServiceFailure) { 320 Slog.v(TAG, "BinderCallback#onVisualQueryDetectionServiceFailure: " 321 + visualQueryDetectionServiceFailure); 322 Binder.withCleanCallingIdentity(() -> mExecutor.execute(() -> { 323 if (visualQueryDetectionServiceFailure != null) { 324 mCallback.onFailure(visualQueryDetectionServiceFailure); 325 } else { 326 mCallback.onUnknownFailure("Error data is null"); 327 } 328 })); 329 } 330 } 331 332 333 private static class InitializationStateListener 334 extends IHotwordRecognitionStatusCallback.Stub { 335 private final Executor mExecutor; 336 private final Callback mCallback; 337 338 private final Context mContext; 339 InitializationStateListener(Executor executor, Callback callback, Context context)340 InitializationStateListener(Executor executor, Callback callback, Context context) { 341 this.mExecutor = executor; 342 this.mCallback = callback; 343 this.mContext = context; 344 } 345 346 @Override onKeyphraseDetected( SoundTrigger.KeyphraseRecognitionEvent recognitionEvent, HotwordDetectedResult result)347 public void onKeyphraseDetected( 348 SoundTrigger.KeyphraseRecognitionEvent recognitionEvent, 349 HotwordDetectedResult result) { 350 if (DEBUG) { 351 Slog.i(TAG, "Ignored #onKeyphraseDetected event"); 352 } 353 } 354 355 @Override onGenericSoundTriggerDetected( SoundTrigger.GenericRecognitionEvent recognitionEvent)356 public void onGenericSoundTriggerDetected( 357 SoundTrigger.GenericRecognitionEvent recognitionEvent) throws RemoteException { 358 if (DEBUG) { 359 Slog.i(TAG, "Ignored #onGenericSoundTriggerDetected event"); 360 } 361 } 362 363 @Override onRejected(HotwordRejectedResult result)364 public void onRejected(HotwordRejectedResult result) throws RemoteException { 365 if (DEBUG) { 366 Slog.i(TAG, "Ignored #onRejected event"); 367 } 368 } 369 370 @Override onRecognitionPaused()371 public void onRecognitionPaused() throws RemoteException { 372 if (DEBUG) { 373 Slog.i(TAG, "Ignored #onRecognitionPaused event"); 374 } 375 } 376 377 @Override onRecognitionResumed()378 public void onRecognitionResumed() throws RemoteException { 379 if (DEBUG) { 380 Slog.i(TAG, "Ignored #onRecognitionResumed event"); 381 } 382 } 383 384 @Override onStatusReported(int status)385 public void onStatusReported(int status) { 386 Slog.v(TAG, "onStatusReported" + (DEBUG ? "(" + status + ")" : "")); 387 //TODO: rename the target callback with a more general term 388 Binder.withCleanCallingIdentity(() -> mExecutor.execute( 389 () -> mCallback.onVisualQueryDetectionServiceInitialized(status))); 390 391 } 392 393 @Override onProcessRestarted()394 public void onProcessRestarted() throws RemoteException { 395 Slog.v(TAG, "onProcessRestarted()"); 396 //TODO: rename the target callback with a more general term 397 Binder.withCleanCallingIdentity(() -> mExecutor.execute( 398 () -> mCallback.onVisualQueryDetectionServiceRestarted())); 399 } 400 401 @Override onHotwordDetectionServiceFailure( HotwordDetectionServiceFailure hotwordDetectionServiceFailure)402 public void onHotwordDetectionServiceFailure( 403 HotwordDetectionServiceFailure hotwordDetectionServiceFailure) 404 throws RemoteException { 405 // It should never be called here. 406 Slog.w(TAG, "onHotwordDetectionServiceFailure: " + hotwordDetectionServiceFailure); 407 } 408 409 @Override onVisualQueryDetectionServiceFailure( VisualQueryDetectionServiceFailure visualQueryDetectionServiceFailure)410 public void onVisualQueryDetectionServiceFailure( 411 VisualQueryDetectionServiceFailure visualQueryDetectionServiceFailure) 412 throws RemoteException { 413 Slog.v(TAG, "onVisualQueryDetectionServiceFailure: " 414 + visualQueryDetectionServiceFailure); 415 Binder.withCleanCallingIdentity(() -> mExecutor.execute(() -> { 416 if (visualQueryDetectionServiceFailure != null) { 417 mCallback.onFailure(visualQueryDetectionServiceFailure); 418 } else { 419 mCallback.onUnknownFailure("Error data is null"); 420 } 421 })); 422 } 423 424 @Override onSoundTriggerFailure(SoundTriggerFailure soundTriggerFailure)425 public void onSoundTriggerFailure(SoundTriggerFailure soundTriggerFailure) { 426 Slog.wtf(TAG, "Unexpected STFailure in VisualQueryDetector" + soundTriggerFailure); 427 } 428 429 @Override onUnknownFailure(String errorMessage)430 public void onUnknownFailure(String errorMessage) throws RemoteException { 431 Slog.v(TAG, "onUnknownFailure: " + errorMessage); 432 Binder.withCleanCallingIdentity(() -> mExecutor.execute(() -> { 433 mCallback.onUnknownFailure( 434 !TextUtils.isEmpty(errorMessage) ? errorMessage : "Error data is null"); 435 })); 436 } 437 @Override onOpenFile(String filename, AndroidFuture future)438 public void onOpenFile(String filename, AndroidFuture future) throws RemoteException { 439 Slog.v(TAG, "BinderCallback#onOpenFile " + filename); 440 Binder.withCleanCallingIdentity(() -> mExecutor.execute(() -> { 441 Slog.v(TAG, "onOpenFile: " + filename); 442 File f = new File(mContext.getFilesDir(), filename); 443 ParcelFileDescriptor pfd = null; 444 try { 445 Slog.d(TAG, "opened a file with ParcelFileDescriptor."); 446 pfd = ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY); 447 } catch (FileNotFoundException e) { 448 Slog.e(TAG, "Cannot open file. No ParcelFileDescriptor returned."); 449 } finally { 450 future.complete(pfd); 451 } 452 })); 453 } 454 } 455 } 456