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