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