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.annotation.NonNull; 20 import android.annotation.SdkConstant; 21 import android.app.Service; 22 import android.compat.annotation.UnsupportedAppUsage; 23 import android.content.ComponentName; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.hardware.soundtrigger.KeyphraseEnrollmentInfo; 27 import android.os.Bundle; 28 import android.os.Handler; 29 import android.os.IBinder; 30 import android.os.RemoteException; 31 import android.os.ServiceManager; 32 import android.provider.Settings; 33 import android.util.ArraySet; 34 35 import com.android.internal.annotations.VisibleForTesting; 36 import com.android.internal.app.IVoiceActionCheckCallback; 37 import com.android.internal.app.IVoiceInteractionManagerService; 38 import com.android.internal.util.function.pooled.PooledLambda; 39 40 import java.io.FileDescriptor; 41 import java.io.PrintWriter; 42 import java.util.ArrayList; 43 import java.util.Collections; 44 import java.util.List; 45 import java.util.Locale; 46 import java.util.Set; 47 48 /** 49 * Top-level service of the current global voice interactor, which is providing 50 * support for hotwording, the back-end of a {@link android.app.VoiceInteractor}, etc. 51 * The current VoiceInteractionService that has been selected by the user is kept 52 * always running by the system, to allow it to do things like listen for hotwords 53 * in the background to instigate voice interactions. 54 * 55 * <p>Because this service is always running, it should be kept as lightweight as 56 * possible. Heavy-weight operations (including showing UI) should be implemented 57 * in the associated {@link android.service.voice.VoiceInteractionSessionService} when 58 * an actual voice interaction is taking place, and that service should run in a 59 * separate process from this one. 60 */ 61 public class VoiceInteractionService extends Service { 62 /** 63 * The {@link Intent} that must be declared as handled by the service. 64 * To be supported, the service must also require the 65 * {@link android.Manifest.permission#BIND_VOICE_INTERACTION} permission so 66 * that other applications can not abuse it. 67 */ 68 @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) 69 public static final String SERVICE_INTERFACE = 70 "android.service.voice.VoiceInteractionService"; 71 72 /** 73 * Name under which a VoiceInteractionService component publishes information about itself. 74 * This meta-data should reference an XML resource containing a 75 * <code><{@link 76 * android.R.styleable#VoiceInteractionService voice-interaction-service}></code> tag. 77 */ 78 public static final String SERVICE_META_DATA = "android.voice_interaction"; 79 80 IVoiceInteractionService mInterface = new IVoiceInteractionService.Stub() { 81 @Override 82 public void ready() { 83 Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage( 84 VoiceInteractionService::onReady, VoiceInteractionService.this)); 85 } 86 87 @Override 88 public void shutdown() { 89 Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage( 90 VoiceInteractionService::onShutdownInternal, VoiceInteractionService.this)); 91 } 92 93 @Override 94 public void soundModelsChanged() { 95 Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage( 96 VoiceInteractionService::onSoundModelsChangedInternal, 97 VoiceInteractionService.this)); 98 } 99 100 @Override 101 public void launchVoiceAssistFromKeyguard() { 102 Handler.getMain().executeOrSendMessage(PooledLambda.obtainMessage( 103 VoiceInteractionService::onLaunchVoiceAssistFromKeyguard, 104 VoiceInteractionService.this)); 105 } 106 107 @Override 108 public void getActiveServiceSupportedActions(List<String> voiceActions, 109 IVoiceActionCheckCallback callback) { 110 Handler.getMain().executeOrSendMessage( 111 PooledLambda.obtainMessage(VoiceInteractionService::onHandleVoiceActionCheck, 112 VoiceInteractionService.this, 113 voiceActions, 114 callback)); 115 } 116 }; 117 118 IVoiceInteractionManagerService mSystemService; 119 120 private final Object mLock = new Object(); 121 122 private KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo; 123 124 private AlwaysOnHotwordDetector mHotwordDetector; 125 126 /** 127 * Called when a user has activated an affordance to launch voice assist from the Keyguard. 128 * 129 * <p>This method will only be called if the VoiceInteractionService has set 130 * {@link android.R.attr#supportsLaunchVoiceAssistFromKeyguard} and the Keyguard is showing.</p> 131 * 132 * <p>A valid implementation must start a new activity that should use {@link 133 * android.view.WindowManager.LayoutParams#FLAG_SHOW_WHEN_LOCKED} to display 134 * on top of the lock screen.</p> 135 */ onLaunchVoiceAssistFromKeyguard()136 public void onLaunchVoiceAssistFromKeyguard() { 137 } 138 139 /** 140 * Check whether the given service component is the currently active 141 * VoiceInteractionService. 142 */ isActiveService(Context context, ComponentName service)143 public static boolean isActiveService(Context context, ComponentName service) { 144 String cur = Settings.Secure.getString(context.getContentResolver(), 145 Settings.Secure.VOICE_INTERACTION_SERVICE); 146 if (cur == null || cur.isEmpty()) { 147 return false; 148 } 149 ComponentName curComp = ComponentName.unflattenFromString(cur); 150 if (curComp == null) { 151 return false; 152 } 153 return curComp.equals(service); 154 } 155 156 /** 157 * Set contextual options you would always like to have disabled when a session 158 * is shown. The flags may be any combination of 159 * {@link VoiceInteractionSession#SHOW_WITH_ASSIST VoiceInteractionSession.SHOW_WITH_ASSIST} and 160 * {@link VoiceInteractionSession#SHOW_WITH_SCREENSHOT 161 * VoiceInteractionSession.SHOW_WITH_SCREENSHOT}. 162 */ setDisabledShowContext(int flags)163 public void setDisabledShowContext(int flags) { 164 try { 165 mSystemService.setDisabledShowContext(flags); 166 } catch (RemoteException e) { 167 } 168 } 169 170 /** 171 * Return the value set by {@link #setDisabledShowContext}. 172 */ getDisabledShowContext()173 public int getDisabledShowContext() { 174 try { 175 return mSystemService.getDisabledShowContext(); 176 } catch (RemoteException e) { 177 return 0; 178 } 179 } 180 181 /** 182 * Request that the associated {@link android.service.voice.VoiceInteractionSession} be 183 * shown to the user, starting it if necessary. 184 * @param args Arbitrary arguments that will be propagated to the session. 185 * @param flags Indicates additional optional behavior that should be performed. May 186 * be any combination of 187 * {@link VoiceInteractionSession#SHOW_WITH_ASSIST VoiceInteractionSession.SHOW_WITH_ASSIST} and 188 * {@link VoiceInteractionSession#SHOW_WITH_SCREENSHOT 189 * VoiceInteractionSession.SHOW_WITH_SCREENSHOT} 190 * to request that the system generate and deliver assist data on the current foreground 191 * app as part of showing the session UI. 192 */ showSession(Bundle args, int flags)193 public void showSession(Bundle args, int flags) { 194 if (mSystemService == null) { 195 throw new IllegalStateException("Not available until onReady() is called"); 196 } 197 try { 198 mSystemService.showSession(mInterface, args, flags); 199 } catch (RemoteException e) { 200 } 201 } 202 203 /** 204 * Request to query for what extended voice actions this service supports. This method will 205 * be called when the system checks the supported actions of this 206 * {@link VoiceInteractionService}. Supported actions may be delivered to 207 * {@link VoiceInteractionSession} later to request a session to perform an action. 208 * 209 * <p>Voice actions are defined in support libraries and could vary based on platform context. 210 * For example, car related voice actions will be defined in car support libraries. 211 * 212 * @param voiceActions A set of checked voice actions. 213 * @return Returns a subset of checked voice actions. Additional voice actions in the 214 * returned set will be ignored. Returns empty set if no actions are supported. 215 */ 216 @NonNull onGetSupportedVoiceActions(@onNull Set<String> voiceActions)217 public Set<String> onGetSupportedVoiceActions(@NonNull Set<String> voiceActions) { 218 return Collections.emptySet(); 219 } 220 221 @Override onBind(Intent intent)222 public IBinder onBind(Intent intent) { 223 if (SERVICE_INTERFACE.equals(intent.getAction())) { 224 return mInterface.asBinder(); 225 } 226 return null; 227 } 228 229 /** 230 * Called during service initialization to tell you when the system is ready 231 * to receive interaction from it. You should generally do initialization here 232 * rather than in {@link #onCreate}. Methods such as {@link #showSession} and 233 * {@link #createAlwaysOnHotwordDetector} 234 * will not be operational until this point. 235 */ onReady()236 public void onReady() { 237 mSystemService = IVoiceInteractionManagerService.Stub.asInterface( 238 ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE)); 239 mKeyphraseEnrollmentInfo = new KeyphraseEnrollmentInfo(getPackageManager()); 240 } 241 onShutdownInternal()242 private void onShutdownInternal() { 243 onShutdown(); 244 // Stop any active recognitions when shutting down. 245 // This ensures that if implementations forget to stop any active recognition, 246 // It's still guaranteed to have been stopped. 247 // This helps with cases where the voice interaction implementation is changed 248 // by the user. 249 safelyShutdownHotwordDetector(); 250 } 251 252 /** 253 * Called during service de-initialization to tell you when the system is shutting the 254 * service down. 255 * At this point this service may no longer be the active {@link VoiceInteractionService}. 256 */ onShutdown()257 public void onShutdown() { 258 } 259 onSoundModelsChangedInternal()260 private void onSoundModelsChangedInternal() { 261 synchronized (this) { 262 if (mHotwordDetector != null) { 263 // TODO: Stop recognition if a sound model that was being recognized gets deleted. 264 mHotwordDetector.onSoundModelsChanged(); 265 } 266 } 267 } 268 onHandleVoiceActionCheck(List<String> voiceActions, IVoiceActionCheckCallback callback)269 private void onHandleVoiceActionCheck(List<String> voiceActions, 270 IVoiceActionCheckCallback callback) { 271 if (callback != null) { 272 try { 273 Set<String> voiceActionsSet = new ArraySet<>(voiceActions); 274 Set<String> resultSet = onGetSupportedVoiceActions(voiceActionsSet); 275 callback.onComplete(new ArrayList<>(resultSet)); 276 } catch (RemoteException e) { 277 } 278 } 279 } 280 281 /** 282 * Creates an {@link AlwaysOnHotwordDetector} for the given keyphrase and locale. 283 * This instance must be retained and used by the client. 284 * Calling this a second time invalidates the previously created hotword detector 285 * which can no longer be used to manage recognition. 286 * 287 * @param keyphrase The keyphrase that's being used, for example "Hello Android". 288 * @param locale The locale for which the enrollment needs to be performed. 289 * @param callback The callback to notify of detection events. 290 * @return An always-on hotword detector for the given keyphrase and locale. 291 */ createAlwaysOnHotwordDetector( String keyphrase, Locale locale, AlwaysOnHotwordDetector.Callback callback)292 public final AlwaysOnHotwordDetector createAlwaysOnHotwordDetector( 293 String keyphrase, Locale locale, AlwaysOnHotwordDetector.Callback callback) { 294 if (mSystemService == null) { 295 throw new IllegalStateException("Not available until onReady() is called"); 296 } 297 synchronized (mLock) { 298 // Allow only one concurrent recognition via the APIs. 299 safelyShutdownHotwordDetector(); 300 mHotwordDetector = new AlwaysOnHotwordDetector(keyphrase, locale, callback, 301 mKeyphraseEnrollmentInfo, mInterface, mSystemService); 302 } 303 return mHotwordDetector; 304 } 305 306 /** 307 * @return Details of keyphrases available for enrollment. 308 * @hide 309 */ 310 @VisibleForTesting getKeyphraseEnrollmentInfo()311 protected final KeyphraseEnrollmentInfo getKeyphraseEnrollmentInfo() { 312 return mKeyphraseEnrollmentInfo; 313 } 314 315 /** 316 * Checks if a given keyphrase and locale are supported to create an 317 * {@link AlwaysOnHotwordDetector}. 318 * 319 * @return true if the keyphrase and locale combination is supported, false otherwise. 320 * @hide 321 */ 322 @UnsupportedAppUsage isKeyphraseAndLocaleSupportedForHotword(String keyphrase, Locale locale)323 public final boolean isKeyphraseAndLocaleSupportedForHotword(String keyphrase, Locale locale) { 324 if (mKeyphraseEnrollmentInfo == null) { 325 return false; 326 } 327 return mKeyphraseEnrollmentInfo.getKeyphraseMetadata(keyphrase, locale) != null; 328 } 329 safelyShutdownHotwordDetector()330 private void safelyShutdownHotwordDetector() { 331 try { 332 synchronized (mLock) { 333 if (mHotwordDetector != null) { 334 mHotwordDetector.stopRecognition(); 335 mHotwordDetector.invalidate(); 336 mHotwordDetector = null; 337 } 338 } 339 } catch (Exception ex) { 340 // Ignore. 341 } 342 } 343 344 /** 345 * Provide hints to be reflected in the system UI. 346 * 347 * @param hints Arguments used to show UI. 348 */ setUiHints(@onNull Bundle hints)349 public final void setUiHints(@NonNull Bundle hints) { 350 if (hints == null) { 351 throw new IllegalArgumentException("Hints must be non-null"); 352 } 353 354 try { 355 mSystemService.setUiHints(mInterface, hints); 356 } catch (RemoteException e) { 357 throw e.rethrowFromSystemServer(); 358 } 359 } 360 361 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)362 protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 363 pw.println("VOICE INTERACTION"); 364 synchronized (mLock) { 365 pw.println(" AlwaysOnHotwordDetector"); 366 if (mHotwordDetector == null) { 367 pw.println(" NULL"); 368 } else { 369 mHotwordDetector.dump(" ", pw); 370 } 371 } 372 } 373 } 374