VoiceInteractionSession.java revision e30e02f5d9a9141c9ee70c712d4f9d52c88ea969
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 17package android.service.voice; 18 19import android.app.Dialog; 20import android.app.Instrumentation; 21import android.content.Context; 22import android.content.Intent; 23import android.content.res.Resources; 24import android.content.res.TypedArray; 25import android.graphics.Rect; 26import android.graphics.Region; 27import android.inputmethodservice.SoftInputWindow; 28import android.os.Binder; 29import android.os.Bundle; 30import android.os.Handler; 31import android.os.IBinder; 32import android.os.Message; 33import android.os.RemoteException; 34import android.util.ArrayMap; 35import android.util.Log; 36import android.view.Gravity; 37import android.view.KeyEvent; 38import android.view.LayoutInflater; 39import android.view.View; 40import android.view.ViewGroup; 41import android.view.ViewTreeObserver; 42import android.view.WindowManager; 43import android.widget.FrameLayout; 44import com.android.internal.app.IVoiceInteractionManagerService; 45import com.android.internal.app.IVoiceInteractor; 46import com.android.internal.app.IVoiceInteractorCallback; 47import com.android.internal.app.IVoiceInteractorRequest; 48import com.android.internal.os.HandlerCaller; 49import com.android.internal.os.SomeArgs; 50 51import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 52import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 53 54public abstract class VoiceInteractionSession implements KeyEvent.Callback { 55 static final String TAG = "VoiceInteractionSession"; 56 static final boolean DEBUG = true; 57 58 final Context mContext; 59 final HandlerCaller mHandlerCaller; 60 61 final KeyEvent.DispatcherState mDispatcherState = new KeyEvent.DispatcherState(); 62 63 IVoiceInteractionManagerService mSystemService; 64 IBinder mToken; 65 66 int mTheme = 0; 67 LayoutInflater mInflater; 68 TypedArray mThemeAttrs; 69 View mRootView; 70 FrameLayout mContentFrame; 71 SoftInputWindow mWindow; 72 73 boolean mInitialized; 74 boolean mWindowAdded; 75 boolean mWindowVisible; 76 boolean mWindowWasVisible; 77 boolean mInShowWindow; 78 79 final ArrayMap<IBinder, Request> mActiveRequests = new ArrayMap<IBinder, Request>(); 80 81 final Insets mTmpInsets = new Insets(); 82 final int[] mTmpLocation = new int[2]; 83 84 final IVoiceInteractor mInteractor = new IVoiceInteractor.Stub() { 85 @Override 86 public IVoiceInteractorRequest startConfirmation(String callingPackage, 87 IVoiceInteractorCallback callback, String prompt, Bundle extras) { 88 Request request = findRequest(callback, true); 89 mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOOOO(MSG_START_CONFIRMATION, 90 new Caller(callingPackage, Binder.getCallingUid()), request, 91 prompt, extras)); 92 return request.mInterface; 93 } 94 95 @Override 96 public IVoiceInteractorRequest startCommand(String callingPackage, 97 IVoiceInteractorCallback callback, String command, Bundle extras) { 98 Request request = findRequest(callback, true); 99 mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOOOO(MSG_START_COMMAND, 100 new Caller(callingPackage, Binder.getCallingUid()), request, 101 command, extras)); 102 return request.mInterface; 103 } 104 105 @Override 106 public boolean[] supportsCommands(String callingPackage, String[] commands) { 107 Message msg = mHandlerCaller.obtainMessageIOO(MSG_SUPPORTS_COMMANDS, 108 0, new Caller(callingPackage, Binder.getCallingUid()), commands); 109 SomeArgs args = mHandlerCaller.sendMessageAndWait(msg); 110 if (args != null) { 111 boolean[] res = (boolean[])args.arg1; 112 args.recycle(); 113 return res; 114 } 115 return new boolean[commands.length]; 116 } 117 }; 118 119 final IVoiceInteractionSession mSession = new IVoiceInteractionSession.Stub() { 120 @Override 121 public void taskStarted(Intent intent, int taskId) { 122 mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIO(MSG_TASK_STARTED, 123 taskId, intent)); 124 } 125 126 @Override 127 public void taskFinished(Intent intent, int taskId) { 128 mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIO(MSG_TASK_FINISHED, 129 taskId, intent)); 130 } 131 132 @Override 133 public void closeSystemDialogs() { 134 mHandlerCaller.sendMessage(mHandlerCaller.obtainMessage(MSG_CLOSE_SYSTEM_DIALOGS)); 135 } 136 137 @Override 138 public void destroy() { 139 mHandlerCaller.sendMessage(mHandlerCaller.obtainMessage(MSG_DESTROY)); 140 } 141 }; 142 143 public static class Request { 144 final IVoiceInteractorRequest mInterface = new IVoiceInteractorRequest.Stub() { 145 @Override 146 public void cancel() throws RemoteException { 147 mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageO(MSG_CANCEL, Request.this)); 148 } 149 }; 150 final IVoiceInteractorCallback mCallback; 151 final HandlerCaller mHandlerCaller; 152 Request(IVoiceInteractorCallback callback, HandlerCaller handlerCaller) { 153 mCallback = callback; 154 mHandlerCaller = handlerCaller; 155 } 156 157 public void sendConfirmResult(boolean confirmed, Bundle result) { 158 try { 159 if (DEBUG) Log.d(TAG, "sendConfirmResult: req=" + mInterface 160 + " confirmed=" + confirmed + " result=" + result); 161 mCallback.deliverConfirmationResult(mInterface, confirmed, result); 162 } catch (RemoteException e) { 163 } 164 } 165 166 public void sendCommandResult(boolean complete, Bundle result) { 167 try { 168 if (DEBUG) Log.d(TAG, "sendCommandResult: req=" + mInterface 169 + " result=" + result); 170 mCallback.deliverCommandResult(mInterface, complete, result); 171 } catch (RemoteException e) { 172 } 173 } 174 175 public void sendCancelResult() { 176 try { 177 if (DEBUG) Log.d(TAG, "sendCancelResult: req=" + mInterface); 178 mCallback.deliverCancel(mInterface); 179 } catch (RemoteException e) { 180 } 181 } 182 } 183 184 public static class Caller { 185 final String packageName; 186 final int uid; 187 188 Caller(String _packageName, int _uid) { 189 packageName = _packageName; 190 uid = _uid; 191 } 192 } 193 194 static final int MSG_START_CONFIRMATION = 1; 195 static final int MSG_START_COMMAND = 2; 196 static final int MSG_SUPPORTS_COMMANDS = 3; 197 static final int MSG_CANCEL = 4; 198 199 static final int MSG_TASK_STARTED = 100; 200 static final int MSG_TASK_FINISHED = 101; 201 static final int MSG_CLOSE_SYSTEM_DIALOGS = 102; 202 static final int MSG_DESTROY = 103; 203 204 class MyCallbacks implements HandlerCaller.Callback, SoftInputWindow.Callback { 205 @Override 206 public void executeMessage(Message msg) { 207 SomeArgs args; 208 switch (msg.what) { 209 case MSG_START_CONFIRMATION: 210 args = (SomeArgs)msg.obj; 211 if (DEBUG) Log.d(TAG, "onConfirm: req=" + ((Request) args.arg2).mInterface 212 + " prompt=" + args.arg3 + " extras=" + args.arg4); 213 onConfirm((Caller)args.arg1, (Request)args.arg2, (String)args.arg3, 214 (Bundle)args.arg4); 215 break; 216 case MSG_START_COMMAND: 217 args = (SomeArgs)msg.obj; 218 if (DEBUG) Log.d(TAG, "onCommand: req=" + ((Request) args.arg2).mInterface 219 + " command=" + args.arg3 + " extras=" + args.arg4); 220 onCommand((Caller) args.arg1, (Request) args.arg2, (String) args.arg3, 221 (Bundle) args.arg4); 222 break; 223 case MSG_SUPPORTS_COMMANDS: 224 args = (SomeArgs)msg.obj; 225 if (DEBUG) Log.d(TAG, "onGetSupportedCommands: cmds=" + args.arg2); 226 args.arg1 = onGetSupportedCommands((Caller) args.arg1, (String[]) args.arg2); 227 break; 228 case MSG_CANCEL: 229 args = (SomeArgs)msg.obj; 230 if (DEBUG) Log.d(TAG, "onCancel: req=" + ((Request) args.arg1).mInterface); 231 onCancel((Request)args.arg1); 232 break; 233 case MSG_TASK_STARTED: 234 if (DEBUG) Log.d(TAG, "onTaskStarted: intent=" + msg.obj 235 + " taskId=" + msg.arg1); 236 onTaskStarted((Intent) msg.obj, msg.arg1); 237 break; 238 case MSG_TASK_FINISHED: 239 if (DEBUG) Log.d(TAG, "onTaskFinished: intent=" + msg.obj 240 + " taskId=" + msg.arg1); 241 onTaskFinished((Intent) msg.obj, msg.arg1); 242 break; 243 case MSG_CLOSE_SYSTEM_DIALOGS: 244 if (DEBUG) Log.d(TAG, "onCloseSystemDialogs"); 245 onCloseSystemDialogs(); 246 break; 247 case MSG_DESTROY: 248 if (DEBUG) Log.d(TAG, "doDestroy"); 249 doDestroy(); 250 break; 251 } 252 } 253 254 @Override 255 public void onBackPressed() { 256 VoiceInteractionSession.this.onBackPressed(); 257 } 258 } 259 260 final MyCallbacks mCallbacks = new MyCallbacks(); 261 262 /** 263 * Information about where interesting parts of the input method UI appear. 264 */ 265 public static final class Insets { 266 /** 267 * This is the part of the UI that is the main content. It is 268 * used to determine the basic space needed, to resize/pan the 269 * application behind. It is assumed that this inset does not 270 * change very much, since any change will cause a full resize/pan 271 * of the application behind. This value is relative to the top edge 272 * of the input method window. 273 */ 274 public final Rect contentInsets = new Rect(); 275 276 /** 277 * This is the region of the UI that is touchable. It is used when 278 * {@link #touchableInsets} is set to {@link #TOUCHABLE_INSETS_REGION}. 279 * The region should be specified relative to the origin of the window frame. 280 */ 281 public final Region touchableRegion = new Region(); 282 283 /** 284 * Option for {@link #touchableInsets}: the entire window frame 285 * can be touched. 286 */ 287 public static final int TOUCHABLE_INSETS_FRAME 288 = ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME; 289 290 /** 291 * Option for {@link #touchableInsets}: the area inside of 292 * the content insets can be touched. 293 */ 294 public static final int TOUCHABLE_INSETS_CONTENT 295 = ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT; 296 297 /** 298 * Option for {@link #touchableInsets}: the region specified by 299 * {@link #touchableRegion} can be touched. 300 */ 301 public static final int TOUCHABLE_INSETS_REGION 302 = ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION; 303 304 /** 305 * Determine which area of the window is touchable by the user. May 306 * be one of: {@link #TOUCHABLE_INSETS_FRAME}, 307 * {@link #TOUCHABLE_INSETS_CONTENT}, or {@link #TOUCHABLE_INSETS_REGION}. 308 */ 309 public int touchableInsets; 310 } 311 312 final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer = 313 new ViewTreeObserver.OnComputeInternalInsetsListener() { 314 public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) { 315 onComputeInsets(mTmpInsets); 316 info.contentInsets.set(mTmpInsets.contentInsets); 317 info.visibleInsets.set(mTmpInsets.contentInsets); 318 info.touchableRegion.set(mTmpInsets.touchableRegion); 319 info.setTouchableInsets(mTmpInsets.touchableInsets); 320 } 321 }; 322 323 public VoiceInteractionSession(Context context) { 324 this(context, new Handler()); 325 } 326 327 public VoiceInteractionSession(Context context, Handler handler) { 328 mContext = context; 329 mHandlerCaller = new HandlerCaller(context, handler.getLooper(), 330 mCallbacks, true); 331 } 332 333 Request findRequest(IVoiceInteractorCallback callback, boolean newRequest) { 334 synchronized (this) { 335 Request req = mActiveRequests.get(callback.asBinder()); 336 if (req != null) { 337 if (newRequest) { 338 throw new IllegalArgumentException("Given request callback " + callback 339 + " is already active"); 340 } 341 return req; 342 } 343 req = new Request(callback, mHandlerCaller); 344 mActiveRequests.put(callback.asBinder(), req); 345 return req; 346 } 347 } 348 349 void doCreate(IVoiceInteractionManagerService service, IBinder token, Bundle args) { 350 mSystemService = service; 351 mToken = token; 352 onCreate(args); 353 } 354 355 void doDestroy() { 356 if (mInitialized) { 357 mRootView.getViewTreeObserver().removeOnComputeInternalInsetsListener( 358 mInsetsComputer); 359 if (mWindowAdded) { 360 mWindow.dismiss(); 361 mWindowAdded = false; 362 } 363 mInitialized = false; 364 } 365 } 366 367 void initViews() { 368 mInitialized = true; 369 370 mThemeAttrs = mContext.obtainStyledAttributes(android.R.styleable.VoiceInteractionSession); 371 mRootView = mInflater.inflate( 372 com.android.internal.R.layout.voice_interaction_session, null); 373 mRootView.setSystemUiVisibility( 374 View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); 375 mWindow.setContentView(mRootView); 376 mRootView.getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsComputer); 377 378 mContentFrame = (FrameLayout)mRootView.findViewById(android.R.id.content); 379 } 380 381 public void showWindow() { 382 if (DEBUG) Log.v(TAG, "Showing window: mWindowAdded=" + mWindowAdded 383 + " mWindowVisible=" + mWindowVisible); 384 385 if (mInShowWindow) { 386 Log.w(TAG, "Re-entrance in to showWindow"); 387 return; 388 } 389 390 try { 391 mInShowWindow = true; 392 if (!mWindowVisible) { 393 mWindowVisible = true; 394 if (!mWindowAdded) { 395 mWindowAdded = true; 396 View v = onCreateContentView(); 397 if (v != null) { 398 setContentView(v); 399 } 400 } 401 mWindow.show(); 402 } 403 } finally { 404 mWindowWasVisible = true; 405 mInShowWindow = false; 406 } 407 } 408 409 public void hideWindow() { 410 if (mWindowVisible) { 411 mWindow.hide(); 412 mWindowVisible = false; 413 } 414 } 415 416 /** 417 * You can call this to customize the theme used by your IME's window. 418 * This must be set before {@link #onCreate}, so you 419 * will typically call it in your constructor with the resource ID 420 * of your custom theme. 421 */ 422 public void setTheme(int theme) { 423 if (mWindow != null) { 424 throw new IllegalStateException("Must be called before onCreate()"); 425 } 426 mTheme = theme; 427 } 428 429 public void startVoiceActivity(Intent intent) { 430 if (mToken == null) { 431 throw new IllegalStateException("Can't call before onCreate()"); 432 } 433 try { 434 intent.migrateExtraStreamToClipData(); 435 intent.prepareToLeaveProcess(); 436 int res = mSystemService.startVoiceActivity(mToken, intent, 437 intent.resolveType(mContext.getContentResolver())); 438 Instrumentation.checkStartActivityResult(res, intent); 439 } catch (RemoteException e) { 440 } 441 } 442 443 public LayoutInflater getLayoutInflater() { 444 return mInflater; 445 } 446 447 public Dialog getWindow() { 448 return mWindow; 449 } 450 451 public void finish() { 452 if (mToken == null) { 453 throw new IllegalStateException("Can't call before onCreate()"); 454 } 455 hideWindow(); 456 try { 457 mSystemService.finish(mToken); 458 } catch (RemoteException e) { 459 } 460 } 461 462 public void onCreate(Bundle args) { 463 mTheme = mTheme != 0 ? mTheme 464 : com.android.internal.R.style.Theme_DeviceDefault_VoiceInteractionSession; 465 mInflater = (LayoutInflater)mContext.getSystemService( 466 Context.LAYOUT_INFLATER_SERVICE); 467 mWindow = new SoftInputWindow(mContext, "VoiceInteractionSession", mTheme, 468 mCallbacks, this, mDispatcherState, 469 WindowManager.LayoutParams.TYPE_VOICE_INTERACTION, Gravity.TOP, true); 470 mWindow.getWindow().addFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); 471 initViews(); 472 mWindow.getWindow().setLayout(MATCH_PARENT, WRAP_CONTENT); 473 mWindow.setToken(mToken); 474 } 475 476 public void onDestroy() { 477 } 478 479 public View onCreateContentView() { 480 return null; 481 } 482 483 public void setContentView(View view) { 484 mContentFrame.removeAllViews(); 485 mContentFrame.addView(view, new FrameLayout.LayoutParams( 486 ViewGroup.LayoutParams.MATCH_PARENT, 487 ViewGroup.LayoutParams.WRAP_CONTENT)); 488 489 } 490 491 public boolean onKeyDown(int keyCode, KeyEvent event) { 492 return false; 493 } 494 495 public boolean onKeyLongPress(int keyCode, KeyEvent event) { 496 return false; 497 } 498 499 public boolean onKeyUp(int keyCode, KeyEvent event) { 500 return false; 501 } 502 503 public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) { 504 return false; 505 } 506 507 public void onBackPressed() { 508 finish(); 509 } 510 511 public void onCloseSystemDialogs() { 512 finish(); 513 } 514 515 /** 516 * Compute the interesting insets into your UI. The default implementation 517 * uses the entire window frame as the insets. The default touchable 518 * insets are {@link Insets#TOUCHABLE_INSETS_FRAME}. 519 * 520 * @param outInsets Fill in with the current UI insets. 521 */ 522 public void onComputeInsets(Insets outInsets) { 523 int[] loc = mTmpLocation; 524 View decor = getWindow().getWindow().getDecorView(); 525 decor.getLocationInWindow(loc); 526 outInsets.contentInsets.top = 0; 527 outInsets.contentInsets.left = 0; 528 outInsets.contentInsets.right = 0; 529 outInsets.contentInsets.bottom = 0; 530 outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_FRAME; 531 outInsets.touchableRegion.setEmpty(); 532 } 533 534 public void onTaskStarted(Intent intent, int taskId) { 535 } 536 537 public void onTaskFinished(Intent intent, int taskId) { 538 finish(); 539 } 540 541 public abstract boolean[] onGetSupportedCommands(Caller caller, String[] commands); 542 public abstract void onConfirm(Caller caller, Request request, String prompt, Bundle extras); 543 public abstract void onCommand(Caller caller, Request request, String command, Bundle extras); 544 public abstract void onCancel(Request request); 545} 546