1 /* 2 * Copyright (C) 2017 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 com.googlecode.android_scripting.facade.ui; 18 19 import android.app.ProgressDialog; 20 import android.app.Service; 21 import android.util.AndroidRuntimeException; 22 import android.view.ContextMenu; 23 import android.view.ContextMenu.ContextMenuInfo; 24 import android.view.Menu; 25 import android.view.MenuItem; 26 import android.view.MotionEvent; 27 import android.view.View; 28 29 import com.googlecode.android_scripting.BaseApplication; 30 import com.googlecode.android_scripting.FutureActivityTaskExecutor; 31 import com.googlecode.android_scripting.Log; 32 import com.googlecode.android_scripting.facade.EventFacade; 33 import com.googlecode.android_scripting.facade.FacadeManager; 34 import com.googlecode.android_scripting.jsonrpc.RpcReceiver; 35 import com.googlecode.android_scripting.rpc.Rpc; 36 import com.googlecode.android_scripting.rpc.RpcDefault; 37 import com.googlecode.android_scripting.rpc.RpcOptional; 38 import com.googlecode.android_scripting.rpc.RpcParameter; 39 40 import java.util.ArrayList; 41 import java.util.Collections; 42 import java.util.List; 43 import java.util.Map; 44 import java.util.Set; 45 import java.util.concurrent.CopyOnWriteArrayList; 46 import java.util.concurrent.atomic.AtomicBoolean; 47 48 import org.json.JSONArray; 49 import org.json.JSONException; 50 51 /** 52 * User Interface Facade. <br> 53 * <br> 54 * <b>Usage Notes</b><br> 55 * <br> 56 * The UI facade provides access to a selection of dialog boxes for general user interaction, and 57 * also hosts the {@link #webViewShow} call which allows interactive use of html pages.<br> 58 * The general use of the dialog functions is as follows:<br> 59 * <ol> 60 * <li>Create a dialog using one of the following calls: 61 * <ul> 62 * <li>{@link #dialogCreateInput} 63 * <li>{@link #dialogCreateAlert} 64 * <li>{@link #dialogCreateDatePicker} 65 * <li>{@link #dialogCreateHorizontalProgress} 66 * <li>{@link #dialogCreatePassword} 67 * <li>{@link #dialogCreateSeekBar} 68 * <li>{@link #dialogCreateSpinnerProgress} 69 * </ul> 70 * <li>Set additional features to your dialog 71 * <ul> 72 * <li>{@link #dialogSetItems} Set a list of items. Used like a menu. 73 * <li>{@link #dialogSetMultiChoiceItems} Set a multichoice list of items. 74 * <li>{@link #dialogSetSingleChoiceItems} Set a single choice list of items. 75 * <li>{@link #dialogSetPositiveButtonText} 76 * <li>{@link #dialogSetNeutralButtonText} 77 * <li>{@link #dialogSetNegativeButtonText} 78 * <li>{@link #dialogSetMaxProgress} Set max progress for your progress bar. 79 * </ul> 80 * <li>Display the dialog using {@link #dialogShow} 81 * <li>Update dialog information if needed 82 * <ul> 83 * <li>{@link #dialogSetCurrentProgress} 84 * </ul> 85 * <li>Get the results 86 * <ul> 87 * <li>Using {@link #dialogGetResponse}, which will wait until the user performs an action to close 88 * the dialog box, or 89 * <li>Use eventPoll to wait for a "dialog" event. 90 * <li>You can find out which list items were selected using {@link #dialogGetSelectedItems}, which 91 * returns an array of numeric indices to your list. For a single choice list, there will only ever 92 * be one of these. 93 * </ul> 94 * <li>Once done, use {@link #dialogDismiss} to remove the dialog. 95 * </ol> 96 * <br> 97 * You can also manipulate menu options. The menu options are available for both {@link #dialogShow} 98 * and {@link #fullShow}. 99 * <ul> 100 * <li>{@link #clearOptionsMenu} 101 * <li>{@link #addOptionsMenuItem} 102 * </ul> 103 * <br> 104 * <b>Some notes:</b><br> 105 * Not every dialogSet function is relevant to every dialog type, ie, dialogSetMaxProgress obviously 106 * only applies to dialogs created with a progress bar. Also, an Alert Dialog may have a message or 107 * items, not both. If you set both, items will take priority.<br> 108 * In addition to the above functions, {@link #dialogGetInput} and {@link #dialogGetPassword} are 109 * convenience functions that create, display and return the relevant dialogs in one call.<br> 110 * There is only ever one instance of a dialog. Any dialogCreate call will cause the existing dialog 111 * to be destroyed. 112 * 113 */ 114 public class UiFacade extends RpcReceiver { 115 // This value should not be used for menu groups outside this class. 116 private static final int MENU_GROUP_ID = Integer.MAX_VALUE; 117 private static final String blankLayout = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" 118 + "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"" 119 + "android:id=\"@+id/background\" android:orientation=\"vertical\"" 120 + "android:layout_width=\"match_parent\" android:layout_height=\"match_parent\"" 121 + "android:background=\"#ff000000\"></LinearLayout>"; 122 123 private final Service mService; 124 private final FutureActivityTaskExecutor mTaskQueue; 125 private DialogTask mDialogTask; 126 private FullScreenTask mFullScreenTask; 127 128 private final List<UiMenuItem> mContextMenuItems; 129 private final List<UiMenuItem> mOptionsMenuItems; 130 private final AtomicBoolean mMenuUpdated; 131 132 private final EventFacade mEventFacade; 133 private List<Integer> mOverrideKeys = Collections.synchronizedList(new ArrayList<Integer>()); 134 135 private float mLastXPosition; 136 UiFacade(FacadeManager manager)137 public UiFacade(FacadeManager manager) { 138 super(manager); 139 mService = manager.getService(); 140 mTaskQueue = ((BaseApplication) mService.getApplication()).getTaskExecutor(); 141 mContextMenuItems = new CopyOnWriteArrayList<UiMenuItem>(); 142 mOptionsMenuItems = new CopyOnWriteArrayList<UiMenuItem>(); 143 mEventFacade = manager.getReceiver(EventFacade.class); 144 mMenuUpdated = new AtomicBoolean(false); 145 } 146 147 /** 148 * For inputType, see <a 149 * href="http://developer.android.com/reference/android/R.styleable.html#TextView_inputType" 150 * >InputTypes</a>. Some useful ones are text, number, and textUri. Multiple flags can be 151 * supplied, seperated by "|", ie: "textUri|textAutoComplete" 152 */ 153 @Rpc(description = "Create a text input dialog.") dialogCreateInput( @pcParametername = "title", description = "title of the input box") @pcDefault"Value") final String title, @RpcParameter(name = "message", description = "message to display above the input box") @RpcDefault("Please enter value:") final String message, @RpcParameter(name = "defaultText", description = "text to insert into the input box") @RpcOptional final String text, @RpcParameter(name = "inputType", description = "type of input data, ie number or text") @RpcOptional final String inputType)154 public void dialogCreateInput( 155 @RpcParameter(name = "title", description = "title of the input box") @RpcDefault("Value") final String title, 156 @RpcParameter(name = "message", description = "message to display above the input box") @RpcDefault("Please enter value:") final String message, 157 @RpcParameter(name = "defaultText", description = "text to insert into the input box") @RpcOptional final String text, 158 @RpcParameter(name = "inputType", description = "type of input data, ie number or text") @RpcOptional final String inputType) 159 throws InterruptedException { 160 dialogDismiss(); 161 mDialogTask = new AlertDialogTask(title, message); 162 ((AlertDialogTask) mDialogTask).setTextInput(text); 163 if (inputType != null) { 164 ((AlertDialogTask) mDialogTask).setEditInputType(inputType); 165 } 166 } 167 168 @Rpc(description = "Create a password input dialog.") dialogCreatePassword( @pcParametername = "title", description = "title of the input box") @pcDefault"Password") final String title, @RpcParameter(name = "message", description = "message to display above the input box") @RpcDefault("Please enter password:") final String message)169 public void dialogCreatePassword( 170 @RpcParameter(name = "title", description = "title of the input box") @RpcDefault("Password") final String title, 171 @RpcParameter(name = "message", description = "message to display above the input box") @RpcDefault("Please enter password:") final String message) { 172 dialogDismiss(); 173 mDialogTask = new AlertDialogTask(title, message); 174 ((AlertDialogTask) mDialogTask).setPasswordInput(); 175 } 176 177 /** 178 * The result is the user's input, or None (null) if cancel was hit. <br> 179 * Example (python) 180 * 181 * <pre> 182 * import android 183 * droid=android.Android() 184 * 185 * print droid.dialogGetInput("Title","Message","Default").result 186 * </pre> 187 * 188 */ 189 @SuppressWarnings("unchecked") 190 @Rpc(description = "Queries the user for a text input.") dialogGetInput( @pcParametername = "title", description = "title of the input box") @pcDefault"Value") final String title, @RpcParameter(name = "message", description = "message to display above the input box") @RpcDefault("Please enter value:") final String message, @RpcParameter(name = "defaultText", description = "text to insert into the input box") @RpcOptional final String text)191 public String dialogGetInput( 192 @RpcParameter(name = "title", description = "title of the input box") @RpcDefault("Value") final String title, 193 @RpcParameter(name = "message", description = "message to display above the input box") @RpcDefault("Please enter value:") final String message, 194 @RpcParameter(name = "defaultText", description = "text to insert into the input box") @RpcOptional final String text) 195 throws InterruptedException { 196 dialogCreateInput(title, message, text, "text"); 197 dialogSetNegativeButtonText("Cancel"); 198 dialogSetPositiveButtonText("Ok"); 199 dialogShow(); 200 Map<String, Object> response = (Map<String, Object>) dialogGetResponse(); 201 if ("positive".equals(response.get("which"))) { 202 return (String) response.get("value"); 203 } else { 204 return null; 205 } 206 } 207 208 @SuppressWarnings("unchecked") 209 @Rpc(description = "Queries the user for a password.") dialogGetPassword( @pcParametername = "title", description = "title of the password box") @pcDefault"Password") final String title, @RpcParameter(name = "message", description = "message to display above the input box") @RpcDefault("Please enter password:") final String message)210 public String dialogGetPassword( 211 @RpcParameter(name = "title", description = "title of the password box") @RpcDefault("Password") final String title, 212 @RpcParameter(name = "message", description = "message to display above the input box") @RpcDefault("Please enter password:") final String message) 213 throws InterruptedException { 214 dialogCreatePassword(title, message); 215 dialogSetNegativeButtonText("Cancel"); 216 dialogSetPositiveButtonText("Ok"); 217 dialogShow(); 218 Map<String, Object> response = (Map<String, Object>) dialogGetResponse(); 219 if ("positive".equals(response.get("which"))) { 220 return (String) response.get("value"); 221 } else { 222 return null; 223 } 224 } 225 226 @Rpc(description = "Create a spinner progress dialog.") dialogCreateSpinnerProgress(@pcParametername = "title") @pcOptional String title, @RpcParameter(name = "message") @RpcOptional String message, @RpcParameter(name = "maximum progress") @RpcDefault("100") Integer max)227 public void dialogCreateSpinnerProgress(@RpcParameter(name = "title") @RpcOptional String title, 228 @RpcParameter(name = "message") @RpcOptional String message, 229 @RpcParameter(name = "maximum progress") @RpcDefault("100") Integer max) { 230 dialogDismiss(); // Dismiss any existing dialog. 231 mDialogTask = new ProgressDialogTask(ProgressDialog.STYLE_SPINNER, max, title, message, true); 232 } 233 234 @Rpc(description = "Create a horizontal progress dialog.") dialogCreateHorizontalProgress( @pcParametername = "title") @pcOptional String title, @RpcParameter(name = "message") @RpcOptional String message, @RpcParameter(name = "maximum progress") @RpcDefault("100") Integer max)235 public void dialogCreateHorizontalProgress( 236 @RpcParameter(name = "title") @RpcOptional String title, 237 @RpcParameter(name = "message") @RpcOptional String message, 238 @RpcParameter(name = "maximum progress") @RpcDefault("100") Integer max) { 239 dialogDismiss(); // Dismiss any existing dialog. 240 mDialogTask = 241 new ProgressDialogTask(ProgressDialog.STYLE_HORIZONTAL, max, title, message, true); 242 } 243 244 /** 245 * <b>Example (python)</b> 246 * 247 * <pre> 248 * import android 249 * droid=android.Android() 250 * droid.dialogCreateAlert("I like swords.","Do you like swords?") 251 * droid.dialogSetPositiveButtonText("Yes") 252 * droid.dialogSetNegativeButtonText("No") 253 * droid.dialogShow() 254 * response=droid.dialogGetResponse().result 255 * droid.dialogDismiss() 256 * if response.has_key("which"): 257 * result=response["which"] 258 * if result=="positive": 259 * print "Yay! I like swords too!" 260 * elif result=="negative": 261 * print "Oh. How sad." 262 * elif response.has_key("canceled"): # Yes, I know it's mispelled. 263 * print "You can't even make up your mind?" 264 * else: 265 * print "Unknown response=",response 266 * 267 * print "Done" 268 * </pre> 269 */ 270 @Rpc(description = "Create alert dialog.") dialogCreateAlert(@pcParametername = "title") @pcOptional String title, @RpcParameter(name = "message") @RpcOptional String message)271 public void dialogCreateAlert(@RpcParameter(name = "title") @RpcOptional String title, 272 @RpcParameter(name = "message") @RpcOptional String message) { 273 dialogDismiss(); // Dismiss any existing dialog. 274 mDialogTask = new AlertDialogTask(title, message); 275 } 276 277 /** 278 * Will produce "dialog" events on change, containing: 279 * <ul> 280 * <li>"progress" - Position chosen, between 0 and max 281 * <li>"which" = "seekbar" 282 * <li>"fromuser" = true/false change is from user input 283 * </ul> 284 * Response will contain a "progress" element. 285 */ 286 @Rpc(description = "Create seek bar dialog.") dialogCreateSeekBar( @pcParametername = "starting value") @pcDefault"50") Integer progress, @RpcParameter(name = "maximum value") @RpcDefault("100") Integer max, @RpcParameter(name = "title") String title, @RpcParameter(name = "message") String message)287 public void dialogCreateSeekBar( 288 @RpcParameter(name = "starting value") @RpcDefault("50") Integer progress, 289 @RpcParameter(name = "maximum value") @RpcDefault("100") Integer max, 290 @RpcParameter(name = "title") String title, @RpcParameter(name = "message") String message) { 291 dialogDismiss(); // Dismiss any existing dialog. 292 mDialogTask = new SeekBarDialogTask(progress, max, title, message); 293 } 294 295 @Rpc(description = "Create time picker dialog.") dialogCreateTimePicker( @pcParametername = "hour") @pcDefault"0") Integer hour, @RpcParameter(name = "minute") @RpcDefault("0") Integer minute, @RpcParameter(name = "is24hour", description = "Use 24 hour clock") @RpcDefault("false") Boolean is24hour)296 public void dialogCreateTimePicker( 297 @RpcParameter(name = "hour") @RpcDefault("0") Integer hour, 298 @RpcParameter(name = "minute") @RpcDefault("0") Integer minute, 299 @RpcParameter(name = "is24hour", description = "Use 24 hour clock") @RpcDefault("false") Boolean is24hour) { 300 dialogDismiss(); // Dismiss any existing dialog. 301 mDialogTask = new TimePickerDialogTask(hour, minute, is24hour); 302 } 303 304 @Rpc(description = "Create date picker dialog.") dialogCreateDatePicker(@pcParametername = "year") @pcDefault"1970") Integer year, @RpcParameter(name = "month") @RpcDefault("1") Integer month, @RpcParameter(name = "day") @RpcDefault("1") Integer day)305 public void dialogCreateDatePicker(@RpcParameter(name = "year") @RpcDefault("1970") Integer year, 306 @RpcParameter(name = "month") @RpcDefault("1") Integer month, 307 @RpcParameter(name = "day") @RpcDefault("1") Integer day) { 308 dialogDismiss(); // Dismiss any existing dialog. 309 mDialogTask = new DatePickerDialogTask(year, month, day); 310 } 311 312 @Rpc(description = "Dismiss dialog.") dialogDismiss()313 public void dialogDismiss() { 314 if (mDialogTask != null) { 315 mDialogTask.dismissDialog(); 316 mDialogTask = null; 317 } 318 } 319 320 @Rpc(description = "Show dialog.") dialogShow()321 public void dialogShow() throws InterruptedException { 322 if (mDialogTask != null && mDialogTask.getDialog() == null) { 323 mDialogTask.setEventFacade(mEventFacade); 324 mTaskQueue.execute(mDialogTask); 325 mDialogTask.getShowLatch().await(); 326 } else { 327 throw new RuntimeException("No dialog to show."); 328 } 329 } 330 331 @Rpc(description = "Set progress dialog current value.") dialogSetCurrentProgress(@pcParametername = "current") Integer current)332 public void dialogSetCurrentProgress(@RpcParameter(name = "current") Integer current) { 333 if (mDialogTask != null && mDialogTask instanceof ProgressDialogTask) { 334 ((ProgressDialog) mDialogTask.getDialog()).setProgress(current); 335 } else { 336 throw new RuntimeException("No valid dialog to assign value to."); 337 } 338 } 339 340 @Rpc(description = "Set progress dialog maximum value.") dialogSetMaxProgress(@pcParametername = "max") Integer max)341 public void dialogSetMaxProgress(@RpcParameter(name = "max") Integer max) { 342 if (mDialogTask != null && mDialogTask instanceof ProgressDialogTask) { 343 ((ProgressDialog) mDialogTask.getDialog()).setMax(max); 344 } else { 345 throw new RuntimeException("No valid dialog to set maximum value of."); 346 } 347 } 348 349 @Rpc(description = "Set alert dialog positive button text.") dialogSetPositiveButtonText(@pcParametername = "text") String text)350 public void dialogSetPositiveButtonText(@RpcParameter(name = "text") String text) { 351 if (mDialogTask != null && mDialogTask instanceof AlertDialogTask) { 352 ((AlertDialogTask) mDialogTask).setPositiveButtonText(text); 353 } else if (mDialogTask != null && mDialogTask instanceof SeekBarDialogTask) { 354 ((SeekBarDialogTask) mDialogTask).setPositiveButtonText(text); 355 } else { 356 throw new AndroidRuntimeException("No dialog to add button to."); 357 } 358 } 359 360 @Rpc(description = "Set alert dialog button text.") dialogSetNegativeButtonText(@pcParametername = "text") String text)361 public void dialogSetNegativeButtonText(@RpcParameter(name = "text") String text) { 362 if (mDialogTask != null && mDialogTask instanceof AlertDialogTask) { 363 ((AlertDialogTask) mDialogTask).setNegativeButtonText(text); 364 } else if (mDialogTask != null && mDialogTask instanceof SeekBarDialogTask) { 365 ((SeekBarDialogTask) mDialogTask).setNegativeButtonText(text); 366 } else { 367 throw new AndroidRuntimeException("No dialog to add button to."); 368 } 369 } 370 371 @Rpc(description = "Set alert dialog button text.") dialogSetNeutralButtonText(@pcParametername = "text") String text)372 public void dialogSetNeutralButtonText(@RpcParameter(name = "text") String text) { 373 if (mDialogTask != null && mDialogTask instanceof AlertDialogTask) { 374 ((AlertDialogTask) mDialogTask).setNeutralButtonText(text); 375 } else { 376 throw new AndroidRuntimeException("No dialog to add button to."); 377 } 378 } 379 380 // TODO(damonkohler): Make RPC layer translate between JSONArray and List<Object>. 381 /** 382 * This effectively creates list of options. Clicking on an item will immediately return an "item" 383 * element, which is the index of the selected item. 384 */ 385 @Rpc(description = "Set alert dialog list items.") dialogSetItems(@pcParametername = "items") JSONArray items)386 public void dialogSetItems(@RpcParameter(name = "items") JSONArray items) { 387 if (mDialogTask != null && mDialogTask instanceof AlertDialogTask) { 388 ((AlertDialogTask) mDialogTask).setItems(items); 389 } else { 390 throw new AndroidRuntimeException("No dialog to add list to."); 391 } 392 } 393 394 /** 395 * This creates a list of radio buttons. You can select one item out of the list. A response will 396 * not be returned until the dialog is closed, either with the Cancel key or a button 397 * (positive/negative/neutral). Use {@link #dialogGetSelectedItems()} to find out what was 398 * selected. 399 */ 400 @Rpc(description = "Set dialog single choice items and selected item.") dialogSetSingleChoiceItems( @pcParametername = "items") JSONArray items, @RpcParameter(name = "selected", description = "selected item index") @RpcDefault("0") Integer selected)401 public void dialogSetSingleChoiceItems( 402 @RpcParameter(name = "items") JSONArray items, 403 @RpcParameter(name = "selected", description = "selected item index") @RpcDefault("0") Integer selected) { 404 if (mDialogTask != null && mDialogTask instanceof AlertDialogTask) { 405 ((AlertDialogTask) mDialogTask).setSingleChoiceItems(items, selected); 406 } else { 407 throw new AndroidRuntimeException("No dialog to add list to."); 408 } 409 } 410 411 /** 412 * This creates a list of check boxes. You can select multiple items out of the list. A response 413 * will not be returned until the dialog is closed, either with the Cancel key or a button 414 * (positive/negative/neutral). Use {@link #dialogGetSelectedItems()} to find out what was 415 * selected. 416 */ 417 418 @Rpc(description = "Set dialog multiple choice items and selection.") dialogSetMultiChoiceItems( @pcParametername = "items") JSONArray items, @RpcParameter(name = "selected", description = "list of selected items") @RpcOptional JSONArray selected)419 public void dialogSetMultiChoiceItems( 420 @RpcParameter(name = "items") JSONArray items, 421 @RpcParameter(name = "selected", description = "list of selected items") @RpcOptional JSONArray selected) 422 throws JSONException { 423 if (mDialogTask != null && mDialogTask instanceof AlertDialogTask) { 424 ((AlertDialogTask) mDialogTask).setMultiChoiceItems(items, selected); 425 } else { 426 throw new AndroidRuntimeException("No dialog to add list to."); 427 } 428 } 429 430 @Rpc(description = "Returns dialog response.") dialogGetResponse()431 public Object dialogGetResponse() { 432 try { 433 return mDialogTask.getResult(); 434 } catch (Exception e) { 435 throw new AndroidRuntimeException(e); 436 } 437 } 438 439 @Rpc(description = "This method provides list of items user selected.", returns = "Selected items") dialogGetSelectedItems()440 public Set<Integer> dialogGetSelectedItems() { 441 if (mDialogTask != null && mDialogTask instanceof AlertDialogTask) { 442 return ((AlertDialogTask) mDialogTask).getSelectedItems(); 443 } else { 444 throw new AndroidRuntimeException("No dialog to add list to."); 445 } 446 } 447 448 @Rpc(description = "Adds a new item to context menu.") addContextMenuItem( @pcParametername = "label", description = "label for this menu item") String label, @RpcParameter(name = "event", description = "event that will be generated on menu item click") String event, @RpcParameter(name = "eventData") @RpcOptional Object data)449 public void addContextMenuItem( 450 @RpcParameter(name = "label", description = "label for this menu item") String label, 451 @RpcParameter(name = "event", description = "event that will be generated on menu item click") String event, 452 @RpcParameter(name = "eventData") @RpcOptional Object data) { 453 mContextMenuItems.add(new UiMenuItem(label, event, data, null)); 454 } 455 456 /** 457 * <b>Example (python)</b> 458 * 459 * <pre> 460 * import android 461 * droid=android.Android() 462 * 463 * droid.addOptionsMenuItem("Silly","silly",None,"star_on") 464 * droid.addOptionsMenuItem("Sensible","sensible","I bet.","star_off") 465 * droid.addOptionsMenuItem("Off","off",None,"ic_menu_revert") 466 * 467 * print "Hit menu to see extra options." 468 * print "Will timeout in 10 seconds if you hit nothing." 469 * 470 * while True: # Wait for events from the menu. 471 * response=droid.eventWait(10000).result 472 * if response==None: 473 * break 474 * print response 475 * if response["name"]=="off": 476 * break 477 * print "And done." 478 * 479 * </pre> 480 */ 481 @Rpc(description = "Adds a new item to options menu.") addOptionsMenuItem( @pcParametername = "label", description = "label for this menu item") String label, @RpcParameter(name = "event", description = "event that will be generated on menu item click") String event, @RpcParameter(name = "eventData") @RpcOptional Object data, @RpcParameter(name = "iconName", description = "Android system menu icon, see http://developer.android.com/reference/android/R.drawable.html") @RpcOptional String iconName)482 public void addOptionsMenuItem( 483 @RpcParameter(name = "label", description = "label for this menu item") String label, 484 @RpcParameter(name = "event", description = "event that will be generated on menu item click") String event, 485 @RpcParameter(name = "eventData") @RpcOptional Object data, 486 @RpcParameter(name = "iconName", description = "Android system menu icon, see http://developer.android.com/reference/android/R.drawable.html") @RpcOptional String iconName) { 487 mOptionsMenuItems.add(new UiMenuItem(label, event, data, iconName)); 488 mMenuUpdated.set(true); 489 } 490 491 @Rpc(description = "Removes all items previously added to context menu.") clearContextMenu()492 public void clearContextMenu() { 493 mContextMenuItems.clear(); 494 } 495 496 @Rpc(description = "Removes all items previously added to options menu.") clearOptionsMenu()497 public void clearOptionsMenu() { 498 mOptionsMenuItems.clear(); 499 mMenuUpdated.set(true); 500 } 501 onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo)502 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { 503 for (UiMenuItem item : mContextMenuItems) { 504 MenuItem menuItem = menu.add(item.mmTitle); 505 menuItem.setOnMenuItemClickListener(item.mmListener); 506 } 507 } 508 onPrepareOptionsMenu(Menu menu)509 public boolean onPrepareOptionsMenu(Menu menu) { 510 if (mMenuUpdated.getAndSet(false)) { 511 menu.removeGroup(MENU_GROUP_ID); 512 for (UiMenuItem item : mOptionsMenuItems) { 513 MenuItem menuItem = menu.add(MENU_GROUP_ID, Menu.NONE, Menu.NONE, item.mmTitle); 514 if (item.mmIcon != null) { 515 menuItem.setIcon(mService.getResources() 516 .getIdentifier(item.mmIcon, "drawable", "android")); 517 } 518 menuItem.setOnMenuItemClickListener(item.mmListener); 519 } 520 return true; 521 } 522 return true; 523 } 524 525 /** 526 * See <a href=http://code.google.com/p/android-scripting/wiki/FullScreenUI>wiki page</a> for more 527 * detail. 528 */ 529 @Rpc(description = "Show Full Screen.") fullShow( @pcParametername = "layout", description = "String containing View layout") String layout, @RpcParameter(name = "title", description = "Activity Title") @RpcOptional String title)530 public List<String> fullShow( 531 @RpcParameter(name = "layout", description = "String containing View layout") String layout, 532 @RpcParameter(name = "title", description = "Activity Title") @RpcOptional String title) 533 throws InterruptedException { 534 if (mFullScreenTask != null) { 535 // fullDismiss(); 536 mFullScreenTask.setLayout(layout); 537 if (title != null) { 538 mFullScreenTask.setTitle(title); 539 } 540 } else { 541 mFullScreenTask = new FullScreenTask(layout, title); 542 mFullScreenTask.setEventFacade(mEventFacade); 543 mFullScreenTask.setUiFacade(this); 544 mFullScreenTask.setOverrideKeys(mOverrideKeys); 545 mTaskQueue.execute(mFullScreenTask); 546 mFullScreenTask.getShowLatch().await(); 547 } 548 return mFullScreenTask.mInflater.getErrors(); 549 } 550 551 @Rpc(description = "Dismiss Full Screen.") fullDismiss()552 public void fullDismiss() { 553 if (mFullScreenTask != null) { 554 mFullScreenTask.finish(); 555 mFullScreenTask = null; 556 } 557 } 558 559 class MouseMotionListener implements View.OnGenericMotionListener { 560 561 @Override onGenericMotion(View v, MotionEvent event)562 public boolean onGenericMotion(View v, MotionEvent event) { 563 Log.d("Generic motion triggered."); 564 if (event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) { 565 mLastXPosition = event.getAxisValue(MotionEvent.AXIS_X); 566 Log.d("New mouse x coord: " + mLastXPosition); 567 // Bundle msg = new Bundle(); 568 // msg.putFloat("value", mLastXPosition); 569 // mEventFacade.postEvent("MouseXPositionUpdate", msg); 570 return true; 571 } 572 return false; 573 } 574 } 575 576 @Rpc(description = "Get Fullscreen Properties") fullQuery()577 public Map<String, Map<String, String>> fullQuery() { 578 if (mFullScreenTask == null) { 579 throw new RuntimeException("No screen displayed."); 580 } 581 return mFullScreenTask.getViewAsMap(); 582 } 583 584 @Rpc(description = "Get fullscreen properties for a specific widget") fullQueryDetail( @pcParametername = "id", description = "id of layout widget") String id)585 public Map<String, String> fullQueryDetail( 586 @RpcParameter(name = "id", description = "id of layout widget") String id) { 587 if (mFullScreenTask == null) { 588 throw new RuntimeException("No screen displayed."); 589 } 590 return mFullScreenTask.getViewDetail(id); 591 } 592 593 @Rpc(description = "Set fullscreen widget property") fullSetProperty( @pcParametername = "id", description = "id of layout widget") String id, @RpcParameter(name = "property", description = "name of property to set") String property, @RpcParameter(name = "value", description = "value to set property to") String value)594 public String fullSetProperty( 595 @RpcParameter(name = "id", description = "id of layout widget") String id, 596 @RpcParameter(name = "property", description = "name of property to set") String property, 597 @RpcParameter(name = "value", description = "value to set property to") String value) { 598 if (mFullScreenTask == null) { 599 throw new RuntimeException("No screen displayed."); 600 } 601 return mFullScreenTask.setViewProperty(id, property, value); 602 } 603 604 @Rpc(description = "Attach a list to a fullscreen widget") fullSetList( @pcParametername = "id", description = "id of layout widget") String id, @RpcParameter(name = "list", description = "List to set") JSONArray items)605 public String fullSetList( 606 @RpcParameter(name = "id", description = "id of layout widget") String id, 607 @RpcParameter(name = "list", description = "List to set") JSONArray items) { 608 if (mFullScreenTask == null) { 609 throw new RuntimeException("No screen displayed."); 610 } 611 return mFullScreenTask.setList(id, items); 612 } 613 614 @Rpc(description = "Set the Full Screen Activity Title") fullSetTitle( @pcParametername = "title", description = "Activity Title") String title)615 public void fullSetTitle( 616 @RpcParameter(name = "title", description = "Activity Title") String title) { 617 if (mFullScreenTask == null) { 618 throw new RuntimeException("No screen displayed."); 619 } 620 mFullScreenTask.setTitle(title); 621 } 622 623 /** 624 * This will override the default behaviour of keys while in the fullscreen mode. ie: 625 * 626 * <pre> 627 * droid.fullKeyOverride([24,25],True) 628 * </pre> 629 * 630 * This will override the default behaviour of the volume keys (codes 24 and 25) so that they do 631 * not actually adjust the volume. <br> 632 * Returns a list of currently overridden keycodes. 633 */ 634 @Rpc(description = "Override default key actions") fullKeyOverride( @pcParametername = "keycodes", description = "List of keycodes to override") JSONArray keycodes, @RpcParameter(name = "enable", description = "Turn overriding or off") @RpcDefault(value = "true") Boolean enable)635 public JSONArray fullKeyOverride( 636 @RpcParameter(name = "keycodes", description = "List of keycodes to override") JSONArray keycodes, 637 @RpcParameter(name = "enable", description = "Turn overriding or off") @RpcDefault(value = "true") Boolean enable) 638 throws JSONException { 639 for (int i = 0; i < keycodes.length(); i++) { 640 int value = (int) keycodes.getLong(i); 641 if (value > 0) { 642 if (enable) { 643 if (!mOverrideKeys.contains(value)) { 644 mOverrideKeys.add(value); 645 } 646 } else { 647 int index = mOverrideKeys.indexOf(value); 648 if (index >= 0) { 649 mOverrideKeys.remove(index); 650 } 651 } 652 } 653 } 654 if (mFullScreenTask != null) { 655 mFullScreenTask.setOverrideKeys(mOverrideKeys); 656 } 657 return new JSONArray(mOverrideKeys); 658 } 659 660 @Rpc(description = "Start tracking mouse cursor x coordinate.") startTrackingMouseXCoord()661 public void startTrackingMouseXCoord() throws InterruptedException { 662 View.OnGenericMotionListener l = new MouseMotionListener(); 663 fullShow(blankLayout, "Blank"); 664 mFullScreenTask.mView.setOnGenericMotionListener(l); 665 } 666 667 @Rpc(description = "Stop tracking mouse cursor x coordinate.") stopTrackingMouseXCoord()668 public void stopTrackingMouseXCoord() throws InterruptedException { 669 fullDismiss(); 670 } 671 672 @Rpc(description = "Return the latest X position of mouse cursor.") getLatestMouseXCoord()673 public float getLatestMouseXCoord() { 674 return mLastXPosition; 675 } 676 677 @Override shutdown()678 public void shutdown() { 679 fullDismiss(); 680 } 681 682 private class UiMenuItem { 683 684 private final String mmTitle; 685 private final String mmEvent; 686 private final Object mmEventData; 687 private final String mmIcon; 688 private final MenuItem.OnMenuItemClickListener mmListener; 689 UiMenuItem(String title, String event, Object data, String icon)690 public UiMenuItem(String title, String event, Object data, String icon) { 691 mmTitle = title; 692 mmEvent = event; 693 mmEventData = data; 694 mmIcon = icon; 695 mmListener = new MenuItem.OnMenuItemClickListener() { 696 @Override 697 public boolean onMenuItemClick(MenuItem item) { 698 // TODO(damonkohler): Does mmEventData need to be cloned somehow? 699 mEventFacade.postEvent(mmEvent, mmEventData); 700 return true; 701 } 702 }; 703 } 704 } 705 } 706