| |
1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- |
| |
2 * This Source Code Form is subject to the terms of the Mozilla Public |
| |
3 * License, v. 2.0. If a copy of the MPL was not distributed with this |
| |
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
| |
5 |
| |
6 package org.mozilla.gecko.prompts; |
| |
7 |
| |
8 import org.json.JSONArray; |
| |
9 import org.json.JSONException; |
| |
10 import org.json.JSONObject; |
| |
11 import org.mozilla.gecko.GeckoAppShell; |
| |
12 import org.mozilla.gecko.GeckoEvent; |
| |
13 import org.mozilla.gecko.R; |
| |
14 import org.mozilla.gecko.util.ThreadUtils; |
| |
15 |
| |
16 import android.app.AlertDialog; |
| |
17 import android.content.Context; |
| |
18 import android.content.DialogInterface; |
| |
19 import android.content.DialogInterface.OnCancelListener; |
| |
20 import android.content.DialogInterface.OnClickListener; |
| |
21 import android.content.res.Resources; |
| |
22 import android.graphics.Bitmap; |
| |
23 import android.graphics.drawable.BitmapDrawable; |
| |
24 import android.graphics.drawable.Drawable; |
| |
25 import android.text.TextUtils; |
| |
26 import android.util.Log; |
| |
27 import android.view.LayoutInflater; |
| |
28 import android.view.View; |
| |
29 import android.view.ViewGroup; |
| |
30 import android.widget.AdapterView; |
| |
31 import android.widget.AdapterView.OnItemClickListener; |
| |
32 import android.widget.ArrayAdapter; |
| |
33 import android.widget.CheckedTextView; |
| |
34 import android.widget.LinearLayout; |
| |
35 import android.widget.ListView; |
| |
36 import android.widget.ScrollView; |
| |
37 import android.widget.TextView; |
| |
38 |
| |
39 import java.util.ArrayList; |
| |
40 |
| |
41 public class Prompt implements OnClickListener, OnCancelListener, OnItemClickListener, |
| |
42 PromptInput.OnChangeListener { |
| |
43 private static final String LOGTAG = "GeckoPromptService"; |
| |
44 |
| |
45 private String[] mButtons; |
| |
46 private PromptInput[] mInputs; |
| |
47 private AlertDialog mDialog; |
| |
48 |
| |
49 private final LayoutInflater mInflater; |
| |
50 private final Context mContext; |
| |
51 private PromptCallback mCallback; |
| |
52 private String mGuid; |
| |
53 private PromptListAdapter mAdapter; |
| |
54 |
| |
55 private static boolean mInitialized = false; |
| |
56 private static int mInputPaddingSize; |
| |
57 |
| |
58 public Prompt(Context context, PromptCallback callback) { |
| |
59 this(context); |
| |
60 mCallback = callback; |
| |
61 } |
| |
62 |
| |
63 private Prompt(Context context) { |
| |
64 mContext = context; |
| |
65 mInflater = LayoutInflater.from(mContext); |
| |
66 |
| |
67 if (!mInitialized) { |
| |
68 Resources res = mContext.getResources(); |
| |
69 mInputPaddingSize = (int) (res.getDimension(R.dimen.prompt_service_inputs_padding)); |
| |
70 mInitialized = true; |
| |
71 } |
| |
72 } |
| |
73 |
| |
74 private View applyInputStyle(View view, PromptInput input) { |
| |
75 // Don't add padding to color picker views |
| |
76 if (input.canApplyInputStyle()) { |
| |
77 view.setPadding(mInputPaddingSize, 0, mInputPaddingSize, 0); |
| |
78 } |
| |
79 return view; |
| |
80 } |
| |
81 |
| |
82 public void show(JSONObject message) { |
| |
83 processMessage(message); |
| |
84 } |
| |
85 |
| |
86 |
| |
87 public void show(String title, String text, PromptListItem[] listItems, int choiceMode) { |
| |
88 ThreadUtils.assertOnUiThread(); |
| |
89 |
| |
90 GeckoAppShell.getLayerView().abortPanning(); |
| |
91 |
| |
92 AlertDialog.Builder builder = new AlertDialog.Builder(mContext); |
| |
93 if (!TextUtils.isEmpty(title)) { |
| |
94 // Long strings can delay showing the dialog, so we cap the number of characters shown to 256. |
| |
95 builder.setTitle(title.substring(0, Math.min(title.length(), 256))); |
| |
96 } |
| |
97 |
| |
98 if (!TextUtils.isEmpty(text)) { |
| |
99 builder.setMessage(text); |
| |
100 } |
| |
101 |
| |
102 // Because lists are currently added through the normal Android AlertBuilder interface, they're |
| |
103 // incompatible with also adding additional input elements to a dialog. |
| |
104 if (listItems != null && listItems.length > 0) { |
| |
105 addListItems(builder, listItems, choiceMode); |
| |
106 } else if (!addInputs(builder)) { |
| |
107 // If we failed to add any requested input elements, don't show the dialog |
| |
108 return; |
| |
109 } |
| |
110 |
| |
111 int length = mButtons == null ? 0 : mButtons.length; |
| |
112 if (length > 0) { |
| |
113 builder.setPositiveButton(mButtons[0], this); |
| |
114 if (length > 1) { |
| |
115 builder.setNeutralButton(mButtons[1], this); |
| |
116 if (length > 2) { |
| |
117 builder.setNegativeButton(mButtons[2], this); |
| |
118 } |
| |
119 } |
| |
120 } |
| |
121 |
| |
122 mDialog = builder.create(); |
| |
123 mDialog.setOnCancelListener(Prompt.this); |
| |
124 mDialog.show(); |
| |
125 } |
| |
126 |
| |
127 public void setButtons(String[] buttons) { |
| |
128 mButtons = buttons; |
| |
129 } |
| |
130 |
| |
131 public void setInputs(PromptInput[] inputs) { |
| |
132 mInputs = inputs; |
| |
133 } |
| |
134 |
| |
135 /* Adds to a result value from the lists that can be shown in dialogs. |
| |
136 * Will set the selected value(s) to the button attribute of the |
| |
137 * object that's passed in. If this is a multi-select dialog, sets a |
| |
138 * selected attribute to an array of booleans. |
| |
139 */ |
| |
140 private void addListResult(final JSONObject result, int which) { |
| |
141 if (mAdapter == null) { |
| |
142 return; |
| |
143 } |
| |
144 |
| |
145 try { |
| |
146 JSONArray selected = new JSONArray(); |
| |
147 |
| |
148 // If the button has already been filled in |
| |
149 ArrayList<Integer> selectedItems = mAdapter.getSelected(); |
| |
150 for (Integer item : selectedItems) { |
| |
151 selected.put(item); |
| |
152 } |
| |
153 |
| |
154 // If we haven't assigned a button yet, or we assigned it to -1, assign the which |
| |
155 // parameter to both selected and the button. |
| |
156 if (!result.has("button") || result.optInt("button") == -1) { |
| |
157 if (!selectedItems.contains(which)) { |
| |
158 selected.put(which); |
| |
159 } |
| |
160 |
| |
161 result.put("button", which); |
| |
162 } |
| |
163 |
| |
164 result.put("list", selected); |
| |
165 } catch(JSONException ex) { } |
| |
166 } |
| |
167 |
| |
168 /* Adds to a result value from the inputs that can be shown in dialogs. |
| |
169 * Each input will set its own value in the result. |
| |
170 */ |
| |
171 private void addInputValues(final JSONObject result) { |
| |
172 try { |
| |
173 if (mInputs != null) { |
| |
174 for (int i = 0; i < mInputs.length; i++) { |
| |
175 result.put(mInputs[i].getId(), mInputs[i].getValue()); |
| |
176 } |
| |
177 } |
| |
178 } catch(JSONException ex) { } |
| |
179 } |
| |
180 |
| |
181 /* Adds the selected button to a result. This should only be called if there |
| |
182 * are no lists shown on the dialog, since they also write their results to the button |
| |
183 * attribute. |
| |
184 */ |
| |
185 private void addButtonResult(final JSONObject result, int which) { |
| |
186 int button = -1; |
| |
187 switch(which) { |
| |
188 case DialogInterface.BUTTON_POSITIVE : button = 0; break; |
| |
189 case DialogInterface.BUTTON_NEUTRAL : button = 1; break; |
| |
190 case DialogInterface.BUTTON_NEGATIVE : button = 2; break; |
| |
191 } |
| |
192 try { |
| |
193 result.put("button", button); |
| |
194 } catch(JSONException ex) { } |
| |
195 } |
| |
196 |
| |
197 @Override |
| |
198 public void onClick(DialogInterface dialog, int which) { |
| |
199 ThreadUtils.assertOnUiThread(); |
| |
200 closeDialog(which); |
| |
201 } |
| |
202 |
| |
203 /* Adds a set of list items to the prompt. This can be used for either context menu type dialogs, checked lists, |
| |
204 * or multiple selection lists. |
| |
205 * |
| |
206 * @param builder |
| |
207 * The alert builder currently building this dialog. |
| |
208 * @param listItems |
| |
209 * The items to add. |
| |
210 * @param choiceMode |
| |
211 * One of the ListView.CHOICE_MODE constants to designate whether this list shows checkmarks, radios buttons, or nothing. |
| |
212 */ |
| |
213 private void addListItems(AlertDialog.Builder builder, PromptListItem[] listItems, int choiceMode) { |
| |
214 switch(choiceMode) { |
| |
215 case ListView.CHOICE_MODE_MULTIPLE_MODAL: |
| |
216 case ListView.CHOICE_MODE_MULTIPLE: |
| |
217 addMultiSelectList(builder, listItems); |
| |
218 break; |
| |
219 case ListView.CHOICE_MODE_SINGLE: |
| |
220 addSingleSelectList(builder, listItems); |
| |
221 break; |
| |
222 case ListView.CHOICE_MODE_NONE: |
| |
223 default: |
| |
224 addMenuList(builder, listItems); |
| |
225 } |
| |
226 } |
| |
227 |
| |
228 /* Shows a multi-select list with checkmarks on the side. Android doesn't support using an adapter for |
| |
229 * multi-choice lists by default so instead we insert our own custom list so that we can do fancy things |
| |
230 * to the rows like disabling/indenting them. |
| |
231 * |
| |
232 * @param builder |
| |
233 * The alert builder currently building this dialog. |
| |
234 * @param listItems |
| |
235 * The items to add. |
| |
236 */ |
| |
237 private void addMultiSelectList(AlertDialog.Builder builder, PromptListItem[] listItems) { |
| |
238 ListView listView = (ListView) mInflater.inflate(R.layout.select_dialog_list, null); |
| |
239 listView.setOnItemClickListener(this); |
| |
240 listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); |
| |
241 |
| |
242 mAdapter = new PromptListAdapter(mContext, R.layout.select_dialog_multichoice, listItems); |
| |
243 listView.setAdapter(mAdapter); |
| |
244 builder.setView(listView); |
| |
245 } |
| |
246 |
| |
247 /* Shows a single-select list with radio boxes on the side. |
| |
248 * |
| |
249 * @param builder |
| |
250 * the alert builder currently building this dialog. |
| |
251 * @param listItems |
| |
252 * The items to add. |
| |
253 */ |
| |
254 private void addSingleSelectList(AlertDialog.Builder builder, PromptListItem[] listItems) { |
| |
255 mAdapter = new PromptListAdapter(mContext, R.layout.select_dialog_singlechoice, listItems); |
| |
256 builder.setSingleChoiceItems(mAdapter, mAdapter.getSelectedIndex(), new DialogInterface.OnClickListener() { |
| |
257 @Override |
| |
258 public void onClick(DialogInterface dialog, int which) { |
| |
259 // The adapter isn't aware of single vs. multi choice lists, so manually |
| |
260 // clear any other selected items first. |
| |
261 ArrayList<Integer> selected = mAdapter.getSelected(); |
| |
262 for (Integer sel : selected) { |
| |
263 mAdapter.toggleSelected(sel); |
| |
264 } |
| |
265 |
| |
266 // Now select this item. |
| |
267 mAdapter.toggleSelected(which); |
| |
268 closeIfNoButtons(which); |
| |
269 } |
| |
270 }); |
| |
271 } |
| |
272 |
| |
273 /* Shows a single-select list. |
| |
274 * |
| |
275 * @param builder |
| |
276 * the alert builder currently building this dialog. |
| |
277 * @param listItems |
| |
278 * The items to add. |
| |
279 */ |
| |
280 private void addMenuList(AlertDialog.Builder builder, PromptListItem[] listItems) { |
| |
281 mAdapter = new PromptListAdapter(mContext, android.R.layout.simple_list_item_1, listItems); |
| |
282 builder.setAdapter(mAdapter, this); |
| |
283 } |
| |
284 |
| |
285 /* Wraps an input in a linearlayout. We do this so that we can set padding that appears outside the background |
| |
286 * drawable for the view. |
| |
287 */ |
| |
288 private View wrapInput(final PromptInput input) { |
| |
289 final LinearLayout linearLayout = new LinearLayout(mContext); |
| |
290 linearLayout.setOrientation(LinearLayout.VERTICAL); |
| |
291 applyInputStyle(linearLayout, input); |
| |
292 |
| |
293 linearLayout.addView(input.getView(mContext)); |
| |
294 |
| |
295 return linearLayout; |
| |
296 } |
| |
297 |
| |
298 /* Add the requested input elements to the dialog. |
| |
299 * |
| |
300 * @param builder |
| |
301 * the alert builder currently building this dialog. |
| |
302 * @return |
| |
303 * return true if the inputs were added successfully. This may fail |
| |
304 * if the requested input is compatible with this Android verison |
| |
305 */ |
| |
306 private boolean addInputs(AlertDialog.Builder builder) { |
| |
307 int length = mInputs == null ? 0 : mInputs.length; |
| |
308 if (length == 0) { |
| |
309 return true; |
| |
310 } |
| |
311 |
| |
312 try { |
| |
313 View root = null; |
| |
314 boolean scrollable = false; // If any of the innuts are scrollable, we won't wrap this in a ScrollView |
| |
315 |
| |
316 if (length == 1) { |
| |
317 root = wrapInput(mInputs[0]); |
| |
318 scrollable |= mInputs[0].getScrollable(); |
| |
319 } else if (length > 1) { |
| |
320 LinearLayout linearLayout = new LinearLayout(mContext); |
| |
321 linearLayout.setOrientation(LinearLayout.VERTICAL); |
| |
322 for (int i = 0; i < length; i++) { |
| |
323 View content = wrapInput(mInputs[i]); |
| |
324 linearLayout.addView(content); |
| |
325 scrollable |= mInputs[i].getScrollable(); |
| |
326 } |
| |
327 root = linearLayout; |
| |
328 } |
| |
329 |
| |
330 if (scrollable) { |
| |
331 // If we're showing some sort of scrollable list, force an inverse background. |
| |
332 builder.setInverseBackgroundForced(true); |
| |
333 builder.setView(root); |
| |
334 } else { |
| |
335 ScrollView view = new ScrollView(mContext); |
| |
336 view.addView(root); |
| |
337 builder.setView(view); |
| |
338 } |
| |
339 } catch(Exception ex) { |
| |
340 Log.e(LOGTAG, "Error showing prompt inputs", ex); |
| |
341 // We cannot display these input widgets with this sdk version, |
| |
342 // do not display any dialog and finish the prompt now. |
| |
343 cancelDialog(); |
| |
344 return false; |
| |
345 } |
| |
346 |
| |
347 return true; |
| |
348 } |
| |
349 |
| |
350 /* AdapterView.OnItemClickListener |
| |
351 * Called when a list item is clicked |
| |
352 */ |
| |
353 @Override |
| |
354 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { |
| |
355 ThreadUtils.assertOnUiThread(); |
| |
356 mAdapter.toggleSelected(position); |
| |
357 |
| |
358 // If there are no buttons on this dialog, then we take selecting an item as a sign to close |
| |
359 // the dialog. Note that means it will be hard to select multiple things in this list, but |
| |
360 // given there is no way to confirm+close the dialog, it seems reasonable. |
| |
361 closeIfNoButtons(position); |
| |
362 } |
| |
363 |
| |
364 private boolean closeIfNoButtons(int selected) { |
| |
365 ThreadUtils.assertOnUiThread(); |
| |
366 if (mButtons == null || mButtons.length == 0) { |
| |
367 closeDialog(selected); |
| |
368 return true; |
| |
369 } |
| |
370 return false; |
| |
371 } |
| |
372 |
| |
373 /* @DialogInterface.OnCancelListener |
| |
374 * Called when the user hits back to cancel a dialog. The dialog will close itself when this |
| |
375 * ends. Setup the correct return values here. |
| |
376 * |
| |
377 * @param aDialog |
| |
378 * A dialog interface for the dialog that's being closed. |
| |
379 */ |
| |
380 @Override |
| |
381 public void onCancel(DialogInterface aDialog) { |
| |
382 ThreadUtils.assertOnUiThread(); |
| |
383 cancelDialog(); |
| |
384 } |
| |
385 |
| |
386 /* Called in situations where we want to cancel the dialog . This can happen if the user hits back, |
| |
387 * or if the dialog can't be created because of invalid JSON. |
| |
388 */ |
| |
389 private void cancelDialog() { |
| |
390 JSONObject ret = new JSONObject(); |
| |
391 try { |
| |
392 ret.put("button", -1); |
| |
393 } catch(Exception ex) { } |
| |
394 addInputValues(ret); |
| |
395 notifyClosing(ret); |
| |
396 } |
| |
397 |
| |
398 /* Called any time we're closing the dialog to cleanup and notify listeners that the dialog |
| |
399 * is closing. |
| |
400 */ |
| |
401 private void closeDialog(int which) { |
| |
402 JSONObject ret = new JSONObject(); |
| |
403 mDialog.dismiss(); |
| |
404 |
| |
405 addButtonResult(ret, which); |
| |
406 addListResult(ret, which); |
| |
407 addInputValues(ret); |
| |
408 |
| |
409 notifyClosing(ret); |
| |
410 } |
| |
411 |
| |
412 /* Called any time we're closing the dialog to cleanup and notify listeners that the dialog |
| |
413 * is closing. |
| |
414 */ |
| |
415 private void notifyClosing(JSONObject aReturn) { |
| |
416 try { |
| |
417 aReturn.put("guid", mGuid); |
| |
418 } catch(JSONException ex) { } |
| |
419 |
| |
420 // poke the Gecko thread in case it's waiting for new events |
| |
421 GeckoAppShell.sendEventToGecko(GeckoEvent.createNoOpEvent()); |
| |
422 |
| |
423 if (mCallback != null) { |
| |
424 mCallback.onPromptFinished(aReturn.toString()); |
| |
425 } |
| |
426 } |
| |
427 |
| |
428 /* Handles parsing the initial JSON sent to show dialogs |
| |
429 */ |
| |
430 private void processMessage(JSONObject geckoObject) { |
| |
431 String title = geckoObject.optString("title"); |
| |
432 String text = geckoObject.optString("text"); |
| |
433 mGuid = geckoObject.optString("guid"); |
| |
434 |
| |
435 mButtons = getStringArray(geckoObject, "buttons"); |
| |
436 |
| |
437 JSONArray inputs = getSafeArray(geckoObject, "inputs"); |
| |
438 mInputs = new PromptInput[inputs.length()]; |
| |
439 for (int i = 0; i < mInputs.length; i++) { |
| |
440 try { |
| |
441 mInputs[i] = PromptInput.getInput(inputs.getJSONObject(i)); |
| |
442 mInputs[i].setListener(this); |
| |
443 } catch(Exception ex) { } |
| |
444 } |
| |
445 |
| |
446 PromptListItem[] menuitems = PromptListItem.getArray(geckoObject.optJSONArray("listitems")); |
| |
447 String selected = geckoObject.optString("choiceMode"); |
| |
448 |
| |
449 int choiceMode = ListView.CHOICE_MODE_NONE; |
| |
450 if ("single".equals(selected)) { |
| |
451 choiceMode = ListView.CHOICE_MODE_SINGLE; |
| |
452 } else if ("multiple".equals(selected)) { |
| |
453 choiceMode = ListView.CHOICE_MODE_MULTIPLE; |
| |
454 } |
| |
455 |
| |
456 show(title, text, menuitems, choiceMode); |
| |
457 } |
| |
458 |
| |
459 // Called when the prompt inputs on the dialog change |
| |
460 @Override |
| |
461 public void onChange(PromptInput input) { |
| |
462 // If there are no buttons on this dialog, assuming that "changing" an input |
| |
463 // means something was selected and we can close. This provides a way to tap |
| |
464 // on a list item and close the dialog automatically. |
| |
465 closeIfNoButtons(-1); |
| |
466 } |
| |
467 |
| |
468 private static JSONArray getSafeArray(JSONObject json, String key) { |
| |
469 try { |
| |
470 return json.getJSONArray(key); |
| |
471 } catch (Exception e) { |
| |
472 return new JSONArray(); |
| |
473 } |
| |
474 } |
| |
475 |
| |
476 public static String[] getStringArray(JSONObject aObject, String aName) { |
| |
477 JSONArray items = getSafeArray(aObject, aName); |
| |
478 int length = items.length(); |
| |
479 String[] list = new String[length]; |
| |
480 for (int i = 0; i < length; i++) { |
| |
481 try { |
| |
482 list[i] = items.getString(i); |
| |
483 } catch(Exception ex) { } |
| |
484 } |
| |
485 return list; |
| |
486 } |
| |
487 |
| |
488 private static boolean[] getBooleanArray(JSONObject aObject, String aName) { |
| |
489 JSONArray items = new JSONArray(); |
| |
490 try { |
| |
491 items = aObject.getJSONArray(aName); |
| |
492 } catch(Exception ex) { return null; } |
| |
493 int length = items.length(); |
| |
494 boolean[] list = new boolean[length]; |
| |
495 for (int i = 0; i < length; i++) { |
| |
496 try { |
| |
497 list[i] = items.getBoolean(i); |
| |
498 } catch(Exception ex) { } |
| |
499 } |
| |
500 return list; |
| |
501 } |
| |
502 |
| |
503 public interface PromptCallback { |
| |
504 public void onPromptFinished(String jsonResult); |
| |
505 } |
| |
506 } |