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