|
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 } |