mobile/android/base/prompts/Prompt.java

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:425db3daade2
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 }

mercurial