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