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.service.voice;
18 
19 import android.Manifest;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.RequiresPermission;
23 import android.annotation.SdkConstant;
24 import android.annotation.SuppressLint;
25 import android.annotation.SystemApi;
26 import android.app.Service;
27 import android.compat.annotation.UnsupportedAppUsage;
28 import android.content.ComponentName;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.hardware.soundtrigger.KeyphraseEnrollmentInfo;
32 import android.media.voice.KeyphraseModelManager;
33 import android.os.Bundle;
34 import android.os.Handler;
35 import android.os.IBinder;
36 import android.os.PersistableBundle;
37 import android.os.RemoteException;
38 import android.os.ServiceManager;
39 import android.os.SharedMemory;
40 import android.provider.Settings;
41 import android.util.ArraySet;
42 import android.util.Log;
43 
44 import com.android.internal.annotations.VisibleForTesting;
45 import com.android.internal.app.IVoiceActionCheckCallback;
46 import com.android.internal.app.IVoiceInteractionManagerService;
47 import com.android.internal.util.function.pooled.PooledLambda;
48 
49 import java.io.FileDescriptor;
50 import java.io.PrintWriter;
51 import java.util.ArrayList;
52 import java.util.Collections;
53 import java.util.List;
54 import java.util.Locale;
55 import java.util.Objects;
56 import java.util.Set;
57 
58 /**
59  * Top-level service of the current global voice interactor, which is providing
60  * support for hotwording, the back-end of a {@link android.app.VoiceInteractor}, etc.
61  * The current VoiceInteractionService that has been selected by the user is kept
62  * always running by the system, to allow it to do things like listen for hotwords
63  * in the background to instigate voice interactions.
64  *
65  * <p>Because this service is always running, it should be kept as lightweight as
66  * possible.  Heavy-weight operations (including showing UI) should be implemented
67  * in the associated {@link android.service.voice.VoiceInteractionSessionService} when
68  * an actual voice interaction is taking place, and that service should run in a
69  * separate process from this one.
70  */
71 public class VoiceInteractionService extends Service {
72     static final String TAG = VoiceInteractionService.class.getSimpleName();
73 
74     /**
75      * The {@link Intent} that must be declared as handled by the service.
76      * To be supported, the service must also require the
77      * {@link android.Manifest.permission#BIND_VOICE_INTERACTION} permission so
78      * that other applications can not abuse it.
79      */
80     @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
81     public static final String SERVICE_INTERFACE =
82             "android.service.voice.VoiceInteractionService";
83 
84     /**
85      * Name under which a VoiceInteractionService component publishes information about itself.
86      * This meta-data should reference an XML resource containing a
87      * <code>&lt;{@link
88      * android.R.styleable#VoiceInteractionService voice-interaction-service}&gt;</code> tag.
89      */
90     public static final String SERVICE_META_DATA = "android.voice_interaction";
91 
92     IVoiceInteractionService mInterface = new IVoiceInteractionService.Stub() {
93         @Override
94         public void ready() {
95             Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage(
96                     VoiceInteractionService::onReady, VoiceInteractionService.this));
97         }
98 
99         @Override
100         public void shutdown() {
101             Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage(
102                     VoiceInteractionService::onShutdownInternal, VoiceInteractionService.this));
103         }
104 
105         @Override
106         public void soundModelsChanged() {
107             Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage(
108                     VoiceInteractionService::onSoundModelsChangedInternal,
109                     VoiceInteractionService.this));
110         }
111 
112         @Override
113         public void launchVoiceAssistFromKeyguard() {
114             Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage(
115                     VoiceInteractionService::onLaunchVoiceAssistFromKeyguard,
116                     VoiceInteractionService.this));
117         }
118 
119         @Override
120         public void getActiveServiceSupportedActions(List<String> voiceActions,
121                 IVoiceActionCheckCallback callback) {
122             Handler.getMain().executeOrSendMessage(
123                     PooledLambda.obtainMessage(VoiceInteractionService::onHandleVoiceActionCheck,
124                             VoiceInteractionService.this,
125                             voiceActions,
126                             callback));
127         }
128     };
129 
130     IVoiceInteractionManagerService mSystemService;
131 
132     private final Object mLock = new Object();
133 
134     private KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo;
135 
136     private AlwaysOnHotwordDetector mHotwordDetector;
137     private SoftwareHotwordDetector mSoftwareHotwordDetector;
138 
139     /**
140      * Called when a user has activated an affordance to launch voice assist from the Keyguard.
141      *
142      * <p>This method will only be called if the VoiceInteractionService has set
143      * {@link android.R.attr#supportsLaunchVoiceAssistFromKeyguard} and the Keyguard is showing.</p>
144      *
145      * <p>A valid implementation must start a new activity that should use {@link
146      * android.view.WindowManager.LayoutParams#FLAG_SHOW_WHEN_LOCKED} to display
147      * on top of the lock screen.</p>
148      */
onLaunchVoiceAssistFromKeyguard()149     public void onLaunchVoiceAssistFromKeyguard() {
150     }
151 
152     /**
153      * Check whether the given service component is the currently active
154      * VoiceInteractionService.
155      */
isActiveService(Context context, ComponentName service)156     public static boolean isActiveService(Context context, ComponentName service) {
157         String cur = Settings.Secure.getString(context.getContentResolver(),
158                 Settings.Secure.VOICE_INTERACTION_SERVICE);
159         if (cur == null || cur.isEmpty()) {
160             return false;
161         }
162         ComponentName curComp = ComponentName.unflattenFromString(cur);
163         if (curComp == null) {
164             return false;
165         }
166         return curComp.equals(service);
167     }
168 
169     /**
170      * Set contextual options you would always like to have disabled when a session
171      * is shown.  The flags may be any combination of
172      * {@link VoiceInteractionSession#SHOW_WITH_ASSIST VoiceInteractionSession.SHOW_WITH_ASSIST} and
173      * {@link VoiceInteractionSession#SHOW_WITH_SCREENSHOT
174      * VoiceInteractionSession.SHOW_WITH_SCREENSHOT}.
175      */
setDisabledShowContext(int flags)176     public void setDisabledShowContext(int flags) {
177         try {
178             mSystemService.setDisabledShowContext(flags);
179         } catch (RemoteException e) {
180         }
181     }
182 
183     /**
184      * Return the value set by {@link #setDisabledShowContext}.
185      */
getDisabledShowContext()186     public int getDisabledShowContext() {
187         try {
188             return mSystemService.getDisabledShowContext();
189         } catch (RemoteException e) {
190             return 0;
191         }
192     }
193 
194     /**
195      * Request that the associated {@link android.service.voice.VoiceInteractionSession} be
196      * shown to the user, starting it if necessary.
197      * @param args Arbitrary arguments that will be propagated to the session.
198      * @param flags Indicates additional optional behavior that should be performed.  May
199      * be any combination of
200      * {@link VoiceInteractionSession#SHOW_WITH_ASSIST VoiceInteractionSession.SHOW_WITH_ASSIST} and
201      * {@link VoiceInteractionSession#SHOW_WITH_SCREENSHOT
202      * VoiceInteractionSession.SHOW_WITH_SCREENSHOT}
203      * to request that the system generate and deliver assist data on the current foreground
204      * app as part of showing the session UI.
205      */
showSession(Bundle args, int flags)206     public void showSession(Bundle args, int flags) {
207         if (mSystemService == null) {
208             throw new IllegalStateException("Not available until onReady() is called");
209         }
210         try {
211             mSystemService.showSession(args, flags);
212         } catch (RemoteException e) {
213         }
214     }
215 
216     /**
217      * Request to query for what extended voice actions this service supports. This method will
218      * be called when the system checks the supported actions of this
219      * {@link VoiceInteractionService}. Supported actions may be delivered to
220      * {@link VoiceInteractionSession} later to request a session to perform an action.
221      *
222      * <p>Voice actions are defined in support libraries and could vary based on platform context.
223      * For example, car related voice actions will be defined in car support libraries.
224      *
225      * @param voiceActions A set of checked voice actions.
226      * @return Returns a subset of checked voice actions. Additional voice actions in the
227      * returned set will be ignored. Returns empty set if no actions are supported.
228      */
229     @NonNull
onGetSupportedVoiceActions(@onNull Set<String> voiceActions)230     public Set<String> onGetSupportedVoiceActions(@NonNull Set<String> voiceActions) {
231         return Collections.emptySet();
232     }
233 
234     @Override
onBind(Intent intent)235     public IBinder onBind(Intent intent) {
236         if (SERVICE_INTERFACE.equals(intent.getAction())) {
237             return mInterface.asBinder();
238         }
239         return null;
240     }
241 
242     /**
243      * Called during service initialization to tell you when the system is ready
244      * to receive interaction from it. You should generally do initialization here
245      * rather than in {@link #onCreate}. Methods such as {@link #showSession} will
246      * not be operational until this point.
247      */
onReady()248     public void onReady() {
249         mSystemService = IVoiceInteractionManagerService.Stub.asInterface(
250                 ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE));
251         Objects.requireNonNull(mSystemService);
252         try {
253             mSystemService.asBinder().linkToDeath(mDeathRecipient, 0);
254         } catch (RemoteException e) {
255             Log.wtf(TAG, "unable to link to death with system service");
256         }
257         mKeyphraseEnrollmentInfo = new KeyphraseEnrollmentInfo(getPackageManager());
258     }
259 
260     private IBinder.DeathRecipient mDeathRecipient = () -> {
261         Log.e(TAG, "system service binder died shutting down");
262         Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage(
263                 VoiceInteractionService::onShutdownInternal, VoiceInteractionService.this));
264     };
265 
266 
onShutdownInternal()267     private void onShutdownInternal() {
268         onShutdown();
269         // Stop any active recognitions when shutting down.
270         // This ensures that if implementations forget to stop any active recognition,
271         // It's still guaranteed to have been stopped.
272         // This helps with cases where the voice interaction implementation is changed
273         // by the user.
274         safelyShutdownHotwordDetector();
275     }
276 
277     /**
278      * Called during service de-initialization to tell you when the system is shutting the
279      * service down.
280      * At this point this service may no longer be the active {@link VoiceInteractionService}.
281      */
onShutdown()282     public void onShutdown() {
283     }
284 
onSoundModelsChangedInternal()285     private void onSoundModelsChangedInternal() {
286         synchronized (this) {
287             if (mHotwordDetector != null) {
288                 // TODO: Stop recognition if a sound model that was being recognized gets deleted.
289                 mHotwordDetector.onSoundModelsChanged();
290             }
291         }
292     }
293 
onHandleVoiceActionCheck(List<String> voiceActions, IVoiceActionCheckCallback callback)294     private void onHandleVoiceActionCheck(List<String> voiceActions,
295             IVoiceActionCheckCallback callback) {
296         if (callback != null) {
297             try {
298                 Set<String> voiceActionsSet = new ArraySet<>(voiceActions);
299                 Set<String> resultSet = onGetSupportedVoiceActions(voiceActionsSet);
300                 callback.onComplete(new ArrayList<>(resultSet));
301             } catch (RemoteException e) {
302             }
303         }
304     }
305 
306     /**
307      * Creates an {@link AlwaysOnHotwordDetector} for the given keyphrase and locale.
308      * This instance must be retained and used by the client.
309      * Calling this a second time invalidates the previously created hotword detector
310      * which can no longer be used to manage recognition.
311      *
312      * @param keyphrase The keyphrase that's being used, for example "Hello Android".
313      * @param locale The locale for which the enrollment needs to be performed.
314      * @param callback The callback to notify of detection events.
315      * @return An always-on hotword detector for the given keyphrase and locale.
316      *
317      * @hide
318      */
319     @SystemApi
320     @NonNull
createAlwaysOnHotwordDetector( @uppressLintR) String keyphrase, @SuppressLint({R, R}) Locale locale, @SuppressLint(R) AlwaysOnHotwordDetector.Callback callback)321     public final AlwaysOnHotwordDetector createAlwaysOnHotwordDetector(
322             @SuppressLint("MissingNullability") String keyphrase,  // TODO: nullability properly
323             @SuppressLint({"MissingNullability", "UseIcu"}) Locale locale,
324             @SuppressLint("MissingNullability") AlwaysOnHotwordDetector.Callback callback) {
325         return createAlwaysOnHotwordDetectorInternal(keyphrase, locale,
326                 /* supportHotwordDetectionService= */ false, /* options= */ null,
327                 /* sharedMemory= */ null, callback);
328     }
329 
330     /**
331      * Create an {@link AlwaysOnHotwordDetector} and trigger a {@link HotwordDetectionService}
332      * service, then it will also pass the read-only data to hotword detection service.
333      *
334      * Like {@see #createAlwaysOnHotwordDetector(String, Locale, AlwaysOnHotwordDetector.Callback)
335      * }. Before calling this function, you should set a valid hotword detection service with
336      * android:hotwordDetectionService in an android.voice_interaction metadata file and set
337      * android:isolatedProcess="true" in the AndroidManifest.xml of hotword detection service.
338      * Otherwise it will throw IllegalStateException. After calling this function, the system will
339      * also trigger a hotword detection service and pass the read-only data back to it.
340      *
341      * <p>Note: The system will trigger hotword detection service after calling this function when
342      * all conditions meet the requirements.
343      *
344      * @param keyphrase The keyphrase that's being used, for example "Hello Android".
345      * @param locale The locale for which the enrollment needs to be performed.
346      * @param options Application configuration data provided by the
347      * {@link VoiceInteractionService}. PersistableBundle does not allow any remotable objects or
348      * other contents that can be used to communicate with other processes.
349      * @param sharedMemory The unrestricted data blob provided by the
350      * {@link VoiceInteractionService}. Use this to provide the hotword models data or other
351      * such data to the trusted process.
352      * @param callback The callback to notify of detection events.
353      * @return An always-on hotword detector for the given keyphrase and locale.
354      *
355      * @hide
356      */
357     @SystemApi
358     @RequiresPermission(Manifest.permission.MANAGE_HOTWORD_DETECTION)
359     @NonNull
createAlwaysOnHotwordDetector( @uppressLintR) String keyphrase, @SuppressLint({R, R}) Locale locale, @Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory, @SuppressLint(R) AlwaysOnHotwordDetector.Callback callback)360     public final AlwaysOnHotwordDetector createAlwaysOnHotwordDetector(
361             @SuppressLint("MissingNullability") String keyphrase,  // TODO: nullability properly
362             @SuppressLint({"MissingNullability", "UseIcu"}) Locale locale,
363             @Nullable PersistableBundle options,
364             @Nullable SharedMemory sharedMemory,
365             @SuppressLint("MissingNullability") AlwaysOnHotwordDetector.Callback callback) {
366         return createAlwaysOnHotwordDetectorInternal(keyphrase, locale,
367                 /* supportHotwordDetectionService= */ true, options,
368                 sharedMemory, callback);
369     }
370 
createAlwaysOnHotwordDetectorInternal( @uppressLintR) String keyphrase, @SuppressLint({R, R}) Locale locale, boolean supportHotwordDetectionService, @Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory, @SuppressLint(R) AlwaysOnHotwordDetector.Callback callback)371     private AlwaysOnHotwordDetector createAlwaysOnHotwordDetectorInternal(
372             @SuppressLint("MissingNullability") String keyphrase,  // TODO: nullability properly
373             @SuppressLint({"MissingNullability", "UseIcu"}) Locale locale,
374             boolean supportHotwordDetectionService,
375             @Nullable PersistableBundle options,
376             @Nullable SharedMemory sharedMemory,
377             @SuppressLint("MissingNullability") AlwaysOnHotwordDetector.Callback callback) {
378         if (mSystemService == null) {
379             throw new IllegalStateException("Not available until onReady() is called");
380         }
381         synchronized (mLock) {
382             // Allow only one concurrent recognition via the APIs.
383             safelyShutdownHotwordDetector();
384             mHotwordDetector = new AlwaysOnHotwordDetector(keyphrase, locale, callback,
385                     mKeyphraseEnrollmentInfo, mSystemService,
386                     getApplicationContext().getApplicationInfo().targetSdkVersion,
387                     supportHotwordDetectionService, options, sharedMemory);
388         }
389         return mHotwordDetector;
390     }
391 
392     /**
393      * Creates a {@link HotwordDetector} and initializes the application's
394      * {@link HotwordDetectionService} using {@code options} and {code sharedMemory}.
395      *
396      * <p>To be able to call this, you need to set android:hotwordDetectionService in the
397      * android.voice_interaction metadata file to a valid hotword detection service, and set
398      * android:isolatedProcess="true" in the hotword detection service's declaration. Otherwise,
399      * this throws an {@link IllegalStateException}.
400      *
401      * <p>This instance must be retained and used by the client.
402      * Calling this a second time invalidates the previously created hotword detector
403      * which can no longer be used to manage recognition.
404      *
405      * <p>Using this has a noticeable impact on battery, since the microphone is kept open
406      * for the lifetime of the recognition {@link HotwordDetector#startRecognition() session}. On
407      * devices where hardware filtering is available (such as through a DSP), it's highly
408      * recommended to use {@link #createAlwaysOnHotwordDetector} instead.
409      *
410      * @param options Application configuration data to be provided to the
411      * {@link HotwordDetectionService}. PersistableBundle does not allow any remotable objects or
412      * other contents that can be used to communicate with other processes.
413      * @param sharedMemory The unrestricted data blob to be provided to the
414      * {@link HotwordDetectionService}. Use this to provide hotword models or other such data to the
415      * sandboxed process.
416      * @param callback The callback to notify of detection events.
417      * @return A hotword detector for the given audio format.
418      *
419      * @see #createAlwaysOnHotwordDetector(String, Locale, PersistableBundle, SharedMemory,
420      * AlwaysOnHotwordDetector.Callback)
421      *
422      * @hide
423      */
424     @SystemApi
425     @RequiresPermission(Manifest.permission.MANAGE_HOTWORD_DETECTION)
426     @NonNull
createHotwordDetector( @ullable PersistableBundle options, @Nullable SharedMemory sharedMemory, @NonNull HotwordDetector.Callback callback)427     public final HotwordDetector createHotwordDetector(
428             @Nullable PersistableBundle options,
429             @Nullable SharedMemory sharedMemory,
430             @NonNull HotwordDetector.Callback callback) {
431         if (mSystemService == null) {
432             throw new IllegalStateException("Not available until onReady() is called");
433         }
434         synchronized (mLock) {
435             // Allow only one concurrent recognition via the APIs.
436             safelyShutdownHotwordDetector();
437             mSoftwareHotwordDetector =
438                     new SoftwareHotwordDetector(
439                             mSystemService, null, options, sharedMemory, callback);
440         }
441         return mSoftwareHotwordDetector;
442     }
443 
444     /**
445      * Creates an {@link KeyphraseModelManager} to use for enrolling voice models outside of the
446      * pre-bundled system voice models.
447      * @hide
448      */
449     @SystemApi
450     @RequiresPermission(Manifest.permission.MANAGE_VOICE_KEYPHRASES)
451     @NonNull
createKeyphraseModelManager()452     public final KeyphraseModelManager createKeyphraseModelManager() {
453         if (mSystemService == null) {
454             throw new IllegalStateException("Not available until onReady() is called");
455         }
456         synchronized (mLock) {
457             return new KeyphraseModelManager(mSystemService);
458         }
459     }
460 
461     /**
462      * @return Details of keyphrases available for enrollment.
463      * @hide
464      */
465     @VisibleForTesting
getKeyphraseEnrollmentInfo()466     protected final KeyphraseEnrollmentInfo getKeyphraseEnrollmentInfo() {
467         return mKeyphraseEnrollmentInfo;
468     }
469 
470     /**
471      * Checks if a given keyphrase and locale are supported to create an
472      * {@link AlwaysOnHotwordDetector}.
473      *
474      * @return true if the keyphrase and locale combination is supported, false otherwise.
475      * @hide
476      */
477     @UnsupportedAppUsage
isKeyphraseAndLocaleSupportedForHotword(String keyphrase, Locale locale)478     public final boolean isKeyphraseAndLocaleSupportedForHotword(String keyphrase, Locale locale) {
479         if (mKeyphraseEnrollmentInfo == null) {
480             return false;
481         }
482         return mKeyphraseEnrollmentInfo.getKeyphraseMetadata(keyphrase, locale) != null;
483     }
484 
safelyShutdownHotwordDetector()485     private void safelyShutdownHotwordDetector() {
486         synchronized (mLock) {
487             shutdownDspHotwordDetectorLocked();
488             shutdownMicrophoneHotwordDetectorLocked();
489         }
490     }
491 
shutdownDspHotwordDetectorLocked()492     private void shutdownDspHotwordDetectorLocked() {
493         if (mHotwordDetector == null) {
494             return;
495         }
496 
497         try {
498             mHotwordDetector.stopRecognition();
499         } catch (Exception ex) {
500             // Ignore.
501         }
502 
503         try {
504             mHotwordDetector.invalidate();
505         } catch (Exception ex) {
506             // Ignore.
507         }
508 
509         mHotwordDetector = null;
510     }
511 
shutdownMicrophoneHotwordDetectorLocked()512     private void shutdownMicrophoneHotwordDetectorLocked() {
513         if (mSoftwareHotwordDetector == null) {
514             return;
515         }
516 
517         try {
518             mSoftwareHotwordDetector.stopRecognition();
519         } catch (Exception ex) {
520             // Ignore.
521         }
522 
523         try {
524             mSystemService.shutdownHotwordDetectionService();
525         } catch (Exception ex) {
526             // Ignore.
527         }
528 
529         mSoftwareHotwordDetector = null;
530     }
531 
532     /**
533      * Provide hints to be reflected in the system UI.
534      *
535      * @param hints Arguments used to show UI.
536      */
setUiHints(@onNull Bundle hints)537     public final void setUiHints(@NonNull Bundle hints) {
538         if (hints == null) {
539             throw new IllegalArgumentException("Hints must be non-null");
540         }
541 
542         try {
543             mSystemService.setUiHints(hints);
544         } catch (RemoteException e) {
545             throw e.rethrowFromSystemServer();
546         }
547     }
548 
549     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)550     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
551         pw.println("VOICE INTERACTION");
552         synchronized (mLock) {
553             pw.println("  AlwaysOnHotwordDetector");
554             if (mHotwordDetector == null) {
555                 pw.println("    NULL");
556             } else {
557                 mHotwordDetector.dump("    ", pw);
558             }
559 
560             pw.println("  MicrophoneHotwordDetector");
561             if (mSoftwareHotwordDetector == null) {
562                 pw.println("    NULL");
563             } else {
564                 mSoftwareHotwordDetector.dump("    ", pw);
565             }
566         }
567     }
568 }
569