mobile/android/base/prompts/Prompt.java

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

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

mercurial