1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/prompts/Prompt.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,506 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko.prompts; 1.10 + 1.11 +import org.json.JSONArray; 1.12 +import org.json.JSONException; 1.13 +import org.json.JSONObject; 1.14 +import org.mozilla.gecko.GeckoAppShell; 1.15 +import org.mozilla.gecko.GeckoEvent; 1.16 +import org.mozilla.gecko.R; 1.17 +import org.mozilla.gecko.util.ThreadUtils; 1.18 + 1.19 +import android.app.AlertDialog; 1.20 +import android.content.Context; 1.21 +import android.content.DialogInterface; 1.22 +import android.content.DialogInterface.OnCancelListener; 1.23 +import android.content.DialogInterface.OnClickListener; 1.24 +import android.content.res.Resources; 1.25 +import android.graphics.Bitmap; 1.26 +import android.graphics.drawable.BitmapDrawable; 1.27 +import android.graphics.drawable.Drawable; 1.28 +import android.text.TextUtils; 1.29 +import android.util.Log; 1.30 +import android.view.LayoutInflater; 1.31 +import android.view.View; 1.32 +import android.view.ViewGroup; 1.33 +import android.widget.AdapterView; 1.34 +import android.widget.AdapterView.OnItemClickListener; 1.35 +import android.widget.ArrayAdapter; 1.36 +import android.widget.CheckedTextView; 1.37 +import android.widget.LinearLayout; 1.38 +import android.widget.ListView; 1.39 +import android.widget.ScrollView; 1.40 +import android.widget.TextView; 1.41 + 1.42 +import java.util.ArrayList; 1.43 + 1.44 +public class Prompt implements OnClickListener, OnCancelListener, OnItemClickListener, 1.45 + PromptInput.OnChangeListener { 1.46 + private static final String LOGTAG = "GeckoPromptService"; 1.47 + 1.48 + private String[] mButtons; 1.49 + private PromptInput[] mInputs; 1.50 + private AlertDialog mDialog; 1.51 + 1.52 + private final LayoutInflater mInflater; 1.53 + private final Context mContext; 1.54 + private PromptCallback mCallback; 1.55 + private String mGuid; 1.56 + private PromptListAdapter mAdapter; 1.57 + 1.58 + private static boolean mInitialized = false; 1.59 + private static int mInputPaddingSize; 1.60 + 1.61 + public Prompt(Context context, PromptCallback callback) { 1.62 + this(context); 1.63 + mCallback = callback; 1.64 + } 1.65 + 1.66 + private Prompt(Context context) { 1.67 + mContext = context; 1.68 + mInflater = LayoutInflater.from(mContext); 1.69 + 1.70 + if (!mInitialized) { 1.71 + Resources res = mContext.getResources(); 1.72 + mInputPaddingSize = (int) (res.getDimension(R.dimen.prompt_service_inputs_padding)); 1.73 + mInitialized = true; 1.74 + } 1.75 + } 1.76 + 1.77 + private View applyInputStyle(View view, PromptInput input) { 1.78 + // Don't add padding to color picker views 1.79 + if (input.canApplyInputStyle()) { 1.80 + view.setPadding(mInputPaddingSize, 0, mInputPaddingSize, 0); 1.81 + } 1.82 + return view; 1.83 + } 1.84 + 1.85 + public void show(JSONObject message) { 1.86 + processMessage(message); 1.87 + } 1.88 + 1.89 + 1.90 + public void show(String title, String text, PromptListItem[] listItems, int choiceMode) { 1.91 + ThreadUtils.assertOnUiThread(); 1.92 + 1.93 + GeckoAppShell.getLayerView().abortPanning(); 1.94 + 1.95 + AlertDialog.Builder builder = new AlertDialog.Builder(mContext); 1.96 + if (!TextUtils.isEmpty(title)) { 1.97 + // Long strings can delay showing the dialog, so we cap the number of characters shown to 256. 1.98 + builder.setTitle(title.substring(0, Math.min(title.length(), 256))); 1.99 + } 1.100 + 1.101 + if (!TextUtils.isEmpty(text)) { 1.102 + builder.setMessage(text); 1.103 + } 1.104 + 1.105 + // Because lists are currently added through the normal Android AlertBuilder interface, they're 1.106 + // incompatible with also adding additional input elements to a dialog. 1.107 + if (listItems != null && listItems.length > 0) { 1.108 + addListItems(builder, listItems, choiceMode); 1.109 + } else if (!addInputs(builder)) { 1.110 + // If we failed to add any requested input elements, don't show the dialog 1.111 + return; 1.112 + } 1.113 + 1.114 + int length = mButtons == null ? 0 : mButtons.length; 1.115 + if (length > 0) { 1.116 + builder.setPositiveButton(mButtons[0], this); 1.117 + if (length > 1) { 1.118 + builder.setNeutralButton(mButtons[1], this); 1.119 + if (length > 2) { 1.120 + builder.setNegativeButton(mButtons[2], this); 1.121 + } 1.122 + } 1.123 + } 1.124 + 1.125 + mDialog = builder.create(); 1.126 + mDialog.setOnCancelListener(Prompt.this); 1.127 + mDialog.show(); 1.128 + } 1.129 + 1.130 + public void setButtons(String[] buttons) { 1.131 + mButtons = buttons; 1.132 + } 1.133 + 1.134 + public void setInputs(PromptInput[] inputs) { 1.135 + mInputs = inputs; 1.136 + } 1.137 + 1.138 + /* Adds to a result value from the lists that can be shown in dialogs. 1.139 + * Will set the selected value(s) to the button attribute of the 1.140 + * object that's passed in. If this is a multi-select dialog, sets a 1.141 + * selected attribute to an array of booleans. 1.142 + */ 1.143 + private void addListResult(final JSONObject result, int which) { 1.144 + if (mAdapter == null) { 1.145 + return; 1.146 + } 1.147 + 1.148 + try { 1.149 + JSONArray selected = new JSONArray(); 1.150 + 1.151 + // If the button has already been filled in 1.152 + ArrayList<Integer> selectedItems = mAdapter.getSelected(); 1.153 + for (Integer item : selectedItems) { 1.154 + selected.put(item); 1.155 + } 1.156 + 1.157 + // If we haven't assigned a button yet, or we assigned it to -1, assign the which 1.158 + // parameter to both selected and the button. 1.159 + if (!result.has("button") || result.optInt("button") == -1) { 1.160 + if (!selectedItems.contains(which)) { 1.161 + selected.put(which); 1.162 + } 1.163 + 1.164 + result.put("button", which); 1.165 + } 1.166 + 1.167 + result.put("list", selected); 1.168 + } catch(JSONException ex) { } 1.169 + } 1.170 + 1.171 + /* Adds to a result value from the inputs that can be shown in dialogs. 1.172 + * Each input will set its own value in the result. 1.173 + */ 1.174 + private void addInputValues(final JSONObject result) { 1.175 + try { 1.176 + if (mInputs != null) { 1.177 + for (int i = 0; i < mInputs.length; i++) { 1.178 + result.put(mInputs[i].getId(), mInputs[i].getValue()); 1.179 + } 1.180 + } 1.181 + } catch(JSONException ex) { } 1.182 + } 1.183 + 1.184 + /* Adds the selected button to a result. This should only be called if there 1.185 + * are no lists shown on the dialog, since they also write their results to the button 1.186 + * attribute. 1.187 + */ 1.188 + private void addButtonResult(final JSONObject result, int which) { 1.189 + int button = -1; 1.190 + switch(which) { 1.191 + case DialogInterface.BUTTON_POSITIVE : button = 0; break; 1.192 + case DialogInterface.BUTTON_NEUTRAL : button = 1; break; 1.193 + case DialogInterface.BUTTON_NEGATIVE : button = 2; break; 1.194 + } 1.195 + try { 1.196 + result.put("button", button); 1.197 + } catch(JSONException ex) { } 1.198 + } 1.199 + 1.200 + @Override 1.201 + public void onClick(DialogInterface dialog, int which) { 1.202 + ThreadUtils.assertOnUiThread(); 1.203 + closeDialog(which); 1.204 + } 1.205 + 1.206 + /* Adds a set of list items to the prompt. This can be used for either context menu type dialogs, checked lists, 1.207 + * or multiple selection lists. 1.208 + * 1.209 + * @param builder 1.210 + * The alert builder currently building this dialog. 1.211 + * @param listItems 1.212 + * The items to add. 1.213 + * @param choiceMode 1.214 + * One of the ListView.CHOICE_MODE constants to designate whether this list shows checkmarks, radios buttons, or nothing. 1.215 + */ 1.216 + private void addListItems(AlertDialog.Builder builder, PromptListItem[] listItems, int choiceMode) { 1.217 + switch(choiceMode) { 1.218 + case ListView.CHOICE_MODE_MULTIPLE_MODAL: 1.219 + case ListView.CHOICE_MODE_MULTIPLE: 1.220 + addMultiSelectList(builder, listItems); 1.221 + break; 1.222 + case ListView.CHOICE_MODE_SINGLE: 1.223 + addSingleSelectList(builder, listItems); 1.224 + break; 1.225 + case ListView.CHOICE_MODE_NONE: 1.226 + default: 1.227 + addMenuList(builder, listItems); 1.228 + } 1.229 + } 1.230 + 1.231 + /* Shows a multi-select list with checkmarks on the side. Android doesn't support using an adapter for 1.232 + * multi-choice lists by default so instead we insert our own custom list so that we can do fancy things 1.233 + * to the rows like disabling/indenting them. 1.234 + * 1.235 + * @param builder 1.236 + * The alert builder currently building this dialog. 1.237 + * @param listItems 1.238 + * The items to add. 1.239 + */ 1.240 + private void addMultiSelectList(AlertDialog.Builder builder, PromptListItem[] listItems) { 1.241 + ListView listView = (ListView) mInflater.inflate(R.layout.select_dialog_list, null); 1.242 + listView.setOnItemClickListener(this); 1.243 + listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); 1.244 + 1.245 + mAdapter = new PromptListAdapter(mContext, R.layout.select_dialog_multichoice, listItems); 1.246 + listView.setAdapter(mAdapter); 1.247 + builder.setView(listView); 1.248 + } 1.249 + 1.250 + /* Shows a single-select list with radio boxes on the side. 1.251 + * 1.252 + * @param builder 1.253 + * the alert builder currently building this dialog. 1.254 + * @param listItems 1.255 + * The items to add. 1.256 + */ 1.257 + private void addSingleSelectList(AlertDialog.Builder builder, PromptListItem[] listItems) { 1.258 + mAdapter = new PromptListAdapter(mContext, R.layout.select_dialog_singlechoice, listItems); 1.259 + builder.setSingleChoiceItems(mAdapter, mAdapter.getSelectedIndex(), new DialogInterface.OnClickListener() { 1.260 + @Override 1.261 + public void onClick(DialogInterface dialog, int which) { 1.262 + // The adapter isn't aware of single vs. multi choice lists, so manually 1.263 + // clear any other selected items first. 1.264 + ArrayList<Integer> selected = mAdapter.getSelected(); 1.265 + for (Integer sel : selected) { 1.266 + mAdapter.toggleSelected(sel); 1.267 + } 1.268 + 1.269 + // Now select this item. 1.270 + mAdapter.toggleSelected(which); 1.271 + closeIfNoButtons(which); 1.272 + } 1.273 + }); 1.274 + } 1.275 + 1.276 + /* Shows a single-select list. 1.277 + * 1.278 + * @param builder 1.279 + * the alert builder currently building this dialog. 1.280 + * @param listItems 1.281 + * The items to add. 1.282 + */ 1.283 + private void addMenuList(AlertDialog.Builder builder, PromptListItem[] listItems) { 1.284 + mAdapter = new PromptListAdapter(mContext, android.R.layout.simple_list_item_1, listItems); 1.285 + builder.setAdapter(mAdapter, this); 1.286 + } 1.287 + 1.288 + /* Wraps an input in a linearlayout. We do this so that we can set padding that appears outside the background 1.289 + * drawable for the view. 1.290 + */ 1.291 + private View wrapInput(final PromptInput input) { 1.292 + final LinearLayout linearLayout = new LinearLayout(mContext); 1.293 + linearLayout.setOrientation(LinearLayout.VERTICAL); 1.294 + applyInputStyle(linearLayout, input); 1.295 + 1.296 + linearLayout.addView(input.getView(mContext)); 1.297 + 1.298 + return linearLayout; 1.299 + } 1.300 + 1.301 + /* Add the requested input elements to the dialog. 1.302 + * 1.303 + * @param builder 1.304 + * the alert builder currently building this dialog. 1.305 + * @return 1.306 + * return true if the inputs were added successfully. This may fail 1.307 + * if the requested input is compatible with this Android verison 1.308 + */ 1.309 + private boolean addInputs(AlertDialog.Builder builder) { 1.310 + int length = mInputs == null ? 0 : mInputs.length; 1.311 + if (length == 0) { 1.312 + return true; 1.313 + } 1.314 + 1.315 + try { 1.316 + View root = null; 1.317 + boolean scrollable = false; // If any of the innuts are scrollable, we won't wrap this in a ScrollView 1.318 + 1.319 + if (length == 1) { 1.320 + root = wrapInput(mInputs[0]); 1.321 + scrollable |= mInputs[0].getScrollable(); 1.322 + } else if (length > 1) { 1.323 + LinearLayout linearLayout = new LinearLayout(mContext); 1.324 + linearLayout.setOrientation(LinearLayout.VERTICAL); 1.325 + for (int i = 0; i < length; i++) { 1.326 + View content = wrapInput(mInputs[i]); 1.327 + linearLayout.addView(content); 1.328 + scrollable |= mInputs[i].getScrollable(); 1.329 + } 1.330 + root = linearLayout; 1.331 + } 1.332 + 1.333 + if (scrollable) { 1.334 + // If we're showing some sort of scrollable list, force an inverse background. 1.335 + builder.setInverseBackgroundForced(true); 1.336 + builder.setView(root); 1.337 + } else { 1.338 + ScrollView view = new ScrollView(mContext); 1.339 + view.addView(root); 1.340 + builder.setView(view); 1.341 + } 1.342 + } catch(Exception ex) { 1.343 + Log.e(LOGTAG, "Error showing prompt inputs", ex); 1.344 + // We cannot display these input widgets with this sdk version, 1.345 + // do not display any dialog and finish the prompt now. 1.346 + cancelDialog(); 1.347 + return false; 1.348 + } 1.349 + 1.350 + return true; 1.351 + } 1.352 + 1.353 + /* AdapterView.OnItemClickListener 1.354 + * Called when a list item is clicked 1.355 + */ 1.356 + @Override 1.357 + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 1.358 + ThreadUtils.assertOnUiThread(); 1.359 + mAdapter.toggleSelected(position); 1.360 + 1.361 + // If there are no buttons on this dialog, then we take selecting an item as a sign to close 1.362 + // the dialog. Note that means it will be hard to select multiple things in this list, but 1.363 + // given there is no way to confirm+close the dialog, it seems reasonable. 1.364 + closeIfNoButtons(position); 1.365 + } 1.366 + 1.367 + private boolean closeIfNoButtons(int selected) { 1.368 + ThreadUtils.assertOnUiThread(); 1.369 + if (mButtons == null || mButtons.length == 0) { 1.370 + closeDialog(selected); 1.371 + return true; 1.372 + } 1.373 + return false; 1.374 + } 1.375 + 1.376 + /* @DialogInterface.OnCancelListener 1.377 + * Called when the user hits back to cancel a dialog. The dialog will close itself when this 1.378 + * ends. Setup the correct return values here. 1.379 + * 1.380 + * @param aDialog 1.381 + * A dialog interface for the dialog that's being closed. 1.382 + */ 1.383 + @Override 1.384 + public void onCancel(DialogInterface aDialog) { 1.385 + ThreadUtils.assertOnUiThread(); 1.386 + cancelDialog(); 1.387 + } 1.388 + 1.389 + /* Called in situations where we want to cancel the dialog . This can happen if the user hits back, 1.390 + * or if the dialog can't be created because of invalid JSON. 1.391 + */ 1.392 + private void cancelDialog() { 1.393 + JSONObject ret = new JSONObject(); 1.394 + try { 1.395 + ret.put("button", -1); 1.396 + } catch(Exception ex) { } 1.397 + addInputValues(ret); 1.398 + notifyClosing(ret); 1.399 + } 1.400 + 1.401 + /* Called any time we're closing the dialog to cleanup and notify listeners that the dialog 1.402 + * is closing. 1.403 + */ 1.404 + private void closeDialog(int which) { 1.405 + JSONObject ret = new JSONObject(); 1.406 + mDialog.dismiss(); 1.407 + 1.408 + addButtonResult(ret, which); 1.409 + addListResult(ret, which); 1.410 + addInputValues(ret); 1.411 + 1.412 + notifyClosing(ret); 1.413 + } 1.414 + 1.415 + /* Called any time we're closing the dialog to cleanup and notify listeners that the dialog 1.416 + * is closing. 1.417 + */ 1.418 + private void notifyClosing(JSONObject aReturn) { 1.419 + try { 1.420 + aReturn.put("guid", mGuid); 1.421 + } catch(JSONException ex) { } 1.422 + 1.423 + // poke the Gecko thread in case it's waiting for new events 1.424 + GeckoAppShell.sendEventToGecko(GeckoEvent.createNoOpEvent()); 1.425 + 1.426 + if (mCallback != null) { 1.427 + mCallback.onPromptFinished(aReturn.toString()); 1.428 + } 1.429 + } 1.430 + 1.431 + /* Handles parsing the initial JSON sent to show dialogs 1.432 + */ 1.433 + private void processMessage(JSONObject geckoObject) { 1.434 + String title = geckoObject.optString("title"); 1.435 + String text = geckoObject.optString("text"); 1.436 + mGuid = geckoObject.optString("guid"); 1.437 + 1.438 + mButtons = getStringArray(geckoObject, "buttons"); 1.439 + 1.440 + JSONArray inputs = getSafeArray(geckoObject, "inputs"); 1.441 + mInputs = new PromptInput[inputs.length()]; 1.442 + for (int i = 0; i < mInputs.length; i++) { 1.443 + try { 1.444 + mInputs[i] = PromptInput.getInput(inputs.getJSONObject(i)); 1.445 + mInputs[i].setListener(this); 1.446 + } catch(Exception ex) { } 1.447 + } 1.448 + 1.449 + PromptListItem[] menuitems = PromptListItem.getArray(geckoObject.optJSONArray("listitems")); 1.450 + String selected = geckoObject.optString("choiceMode"); 1.451 + 1.452 + int choiceMode = ListView.CHOICE_MODE_NONE; 1.453 + if ("single".equals(selected)) { 1.454 + choiceMode = ListView.CHOICE_MODE_SINGLE; 1.455 + } else if ("multiple".equals(selected)) { 1.456 + choiceMode = ListView.CHOICE_MODE_MULTIPLE; 1.457 + } 1.458 + 1.459 + show(title, text, menuitems, choiceMode); 1.460 + } 1.461 + 1.462 + // Called when the prompt inputs on the dialog change 1.463 + @Override 1.464 + public void onChange(PromptInput input) { 1.465 + // If there are no buttons on this dialog, assuming that "changing" an input 1.466 + // means something was selected and we can close. This provides a way to tap 1.467 + // on a list item and close the dialog automatically. 1.468 + closeIfNoButtons(-1); 1.469 + } 1.470 + 1.471 + private static JSONArray getSafeArray(JSONObject json, String key) { 1.472 + try { 1.473 + return json.getJSONArray(key); 1.474 + } catch (Exception e) { 1.475 + return new JSONArray(); 1.476 + } 1.477 + } 1.478 + 1.479 + public static String[] getStringArray(JSONObject aObject, String aName) { 1.480 + JSONArray items = getSafeArray(aObject, aName); 1.481 + int length = items.length(); 1.482 + String[] list = new String[length]; 1.483 + for (int i = 0; i < length; i++) { 1.484 + try { 1.485 + list[i] = items.getString(i); 1.486 + } catch(Exception ex) { } 1.487 + } 1.488 + return list; 1.489 + } 1.490 + 1.491 + private static boolean[] getBooleanArray(JSONObject aObject, String aName) { 1.492 + JSONArray items = new JSONArray(); 1.493 + try { 1.494 + items = aObject.getJSONArray(aName); 1.495 + } catch(Exception ex) { return null; } 1.496 + int length = items.length(); 1.497 + boolean[] list = new boolean[length]; 1.498 + for (int i = 0; i < length; i++) { 1.499 + try { 1.500 + list[i] = items.getBoolean(i); 1.501 + } catch(Exception ex) { } 1.502 + } 1.503 + return list; 1.504 + } 1.505 + 1.506 + public interface PromptCallback { 1.507 + public void onPromptFinished(String jsonResult); 1.508 + } 1.509 +}