michael@0: /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.prompts; michael@0: michael@0: import org.json.JSONArray; michael@0: import org.json.JSONException; michael@0: import org.json.JSONObject; michael@0: import org.mozilla.gecko.GeckoAppShell; michael@0: import org.mozilla.gecko.GeckoEvent; michael@0: import org.mozilla.gecko.R; michael@0: import org.mozilla.gecko.util.ThreadUtils; michael@0: michael@0: import android.app.AlertDialog; michael@0: import android.content.Context; michael@0: import android.content.DialogInterface; michael@0: import android.content.DialogInterface.OnCancelListener; michael@0: import android.content.DialogInterface.OnClickListener; michael@0: import android.content.res.Resources; michael@0: import android.graphics.Bitmap; michael@0: import android.graphics.drawable.BitmapDrawable; michael@0: import android.graphics.drawable.Drawable; michael@0: import android.text.TextUtils; michael@0: import android.util.Log; michael@0: import android.view.LayoutInflater; michael@0: import android.view.View; michael@0: import android.view.ViewGroup; michael@0: import android.widget.AdapterView; michael@0: import android.widget.AdapterView.OnItemClickListener; michael@0: import android.widget.ArrayAdapter; michael@0: import android.widget.CheckedTextView; michael@0: import android.widget.LinearLayout; michael@0: import android.widget.ListView; michael@0: import android.widget.ScrollView; michael@0: import android.widget.TextView; michael@0: michael@0: import java.util.ArrayList; michael@0: michael@0: public class Prompt implements OnClickListener, OnCancelListener, OnItemClickListener, michael@0: PromptInput.OnChangeListener { michael@0: private static final String LOGTAG = "GeckoPromptService"; michael@0: michael@0: private String[] mButtons; michael@0: private PromptInput[] mInputs; michael@0: private AlertDialog mDialog; michael@0: michael@0: private final LayoutInflater mInflater; michael@0: private final Context mContext; michael@0: private PromptCallback mCallback; michael@0: private String mGuid; michael@0: private PromptListAdapter mAdapter; michael@0: michael@0: private static boolean mInitialized = false; michael@0: private static int mInputPaddingSize; michael@0: michael@0: public Prompt(Context context, PromptCallback callback) { michael@0: this(context); michael@0: mCallback = callback; michael@0: } michael@0: michael@0: private Prompt(Context context) { michael@0: mContext = context; michael@0: mInflater = LayoutInflater.from(mContext); michael@0: michael@0: if (!mInitialized) { michael@0: Resources res = mContext.getResources(); michael@0: mInputPaddingSize = (int) (res.getDimension(R.dimen.prompt_service_inputs_padding)); michael@0: mInitialized = true; michael@0: } michael@0: } michael@0: michael@0: private View applyInputStyle(View view, PromptInput input) { michael@0: // Don't add padding to color picker views michael@0: if (input.canApplyInputStyle()) { michael@0: view.setPadding(mInputPaddingSize, 0, mInputPaddingSize, 0); michael@0: } michael@0: return view; michael@0: } michael@0: michael@0: public void show(JSONObject message) { michael@0: processMessage(message); michael@0: } michael@0: michael@0: michael@0: public void show(String title, String text, PromptListItem[] listItems, int choiceMode) { michael@0: ThreadUtils.assertOnUiThread(); michael@0: michael@0: GeckoAppShell.getLayerView().abortPanning(); michael@0: michael@0: AlertDialog.Builder builder = new AlertDialog.Builder(mContext); michael@0: if (!TextUtils.isEmpty(title)) { michael@0: // Long strings can delay showing the dialog, so we cap the number of characters shown to 256. michael@0: builder.setTitle(title.substring(0, Math.min(title.length(), 256))); michael@0: } michael@0: michael@0: if (!TextUtils.isEmpty(text)) { michael@0: builder.setMessage(text); michael@0: } michael@0: michael@0: // Because lists are currently added through the normal Android AlertBuilder interface, they're michael@0: // incompatible with also adding additional input elements to a dialog. michael@0: if (listItems != null && listItems.length > 0) { michael@0: addListItems(builder, listItems, choiceMode); michael@0: } else if (!addInputs(builder)) { michael@0: // If we failed to add any requested input elements, don't show the dialog michael@0: return; michael@0: } michael@0: michael@0: int length = mButtons == null ? 0 : mButtons.length; michael@0: if (length > 0) { michael@0: builder.setPositiveButton(mButtons[0], this); michael@0: if (length > 1) { michael@0: builder.setNeutralButton(mButtons[1], this); michael@0: if (length > 2) { michael@0: builder.setNegativeButton(mButtons[2], this); michael@0: } michael@0: } michael@0: } michael@0: michael@0: mDialog = builder.create(); michael@0: mDialog.setOnCancelListener(Prompt.this); michael@0: mDialog.show(); michael@0: } michael@0: michael@0: public void setButtons(String[] buttons) { michael@0: mButtons = buttons; michael@0: } michael@0: michael@0: public void setInputs(PromptInput[] inputs) { michael@0: mInputs = inputs; michael@0: } michael@0: michael@0: /* Adds to a result value from the lists that can be shown in dialogs. michael@0: * Will set the selected value(s) to the button attribute of the michael@0: * object that's passed in. If this is a multi-select dialog, sets a michael@0: * selected attribute to an array of booleans. michael@0: */ michael@0: private void addListResult(final JSONObject result, int which) { michael@0: if (mAdapter == null) { michael@0: return; michael@0: } michael@0: michael@0: try { michael@0: JSONArray selected = new JSONArray(); michael@0: michael@0: // If the button has already been filled in michael@0: ArrayList selectedItems = mAdapter.getSelected(); michael@0: for (Integer item : selectedItems) { michael@0: selected.put(item); michael@0: } michael@0: michael@0: // If we haven't assigned a button yet, or we assigned it to -1, assign the which michael@0: // parameter to both selected and the button. michael@0: if (!result.has("button") || result.optInt("button") == -1) { michael@0: if (!selectedItems.contains(which)) { michael@0: selected.put(which); michael@0: } michael@0: michael@0: result.put("button", which); michael@0: } michael@0: michael@0: result.put("list", selected); michael@0: } catch(JSONException ex) { } michael@0: } michael@0: michael@0: /* Adds to a result value from the inputs that can be shown in dialogs. michael@0: * Each input will set its own value in the result. michael@0: */ michael@0: private void addInputValues(final JSONObject result) { michael@0: try { michael@0: if (mInputs != null) { michael@0: for (int i = 0; i < mInputs.length; i++) { michael@0: result.put(mInputs[i].getId(), mInputs[i].getValue()); michael@0: } michael@0: } michael@0: } catch(JSONException ex) { } michael@0: } michael@0: michael@0: /* Adds the selected button to a result. This should only be called if there michael@0: * are no lists shown on the dialog, since they also write their results to the button michael@0: * attribute. michael@0: */ michael@0: private void addButtonResult(final JSONObject result, int which) { michael@0: int button = -1; michael@0: switch(which) { michael@0: case DialogInterface.BUTTON_POSITIVE : button = 0; break; michael@0: case DialogInterface.BUTTON_NEUTRAL : button = 1; break; michael@0: case DialogInterface.BUTTON_NEGATIVE : button = 2; break; michael@0: } michael@0: try { michael@0: result.put("button", button); michael@0: } catch(JSONException ex) { } michael@0: } michael@0: michael@0: @Override michael@0: public void onClick(DialogInterface dialog, int which) { michael@0: ThreadUtils.assertOnUiThread(); michael@0: closeDialog(which); michael@0: } michael@0: michael@0: /* Adds a set of list items to the prompt. This can be used for either context menu type dialogs, checked lists, michael@0: * or multiple selection lists. michael@0: * michael@0: * @param builder michael@0: * The alert builder currently building this dialog. michael@0: * @param listItems michael@0: * The items to add. michael@0: * @param choiceMode michael@0: * One of the ListView.CHOICE_MODE constants to designate whether this list shows checkmarks, radios buttons, or nothing. michael@0: */ michael@0: private void addListItems(AlertDialog.Builder builder, PromptListItem[] listItems, int choiceMode) { michael@0: switch(choiceMode) { michael@0: case ListView.CHOICE_MODE_MULTIPLE_MODAL: michael@0: case ListView.CHOICE_MODE_MULTIPLE: michael@0: addMultiSelectList(builder, listItems); michael@0: break; michael@0: case ListView.CHOICE_MODE_SINGLE: michael@0: addSingleSelectList(builder, listItems); michael@0: break; michael@0: case ListView.CHOICE_MODE_NONE: michael@0: default: michael@0: addMenuList(builder, listItems); michael@0: } michael@0: } michael@0: michael@0: /* Shows a multi-select list with checkmarks on the side. Android doesn't support using an adapter for michael@0: * multi-choice lists by default so instead we insert our own custom list so that we can do fancy things michael@0: * to the rows like disabling/indenting them. michael@0: * michael@0: * @param builder michael@0: * The alert builder currently building this dialog. michael@0: * @param listItems michael@0: * The items to add. michael@0: */ michael@0: private void addMultiSelectList(AlertDialog.Builder builder, PromptListItem[] listItems) { michael@0: ListView listView = (ListView) mInflater.inflate(R.layout.select_dialog_list, null); michael@0: listView.setOnItemClickListener(this); michael@0: listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); michael@0: michael@0: mAdapter = new PromptListAdapter(mContext, R.layout.select_dialog_multichoice, listItems); michael@0: listView.setAdapter(mAdapter); michael@0: builder.setView(listView); michael@0: } michael@0: michael@0: /* Shows a single-select list with radio boxes on the side. michael@0: * michael@0: * @param builder michael@0: * the alert builder currently building this dialog. michael@0: * @param listItems michael@0: * The items to add. michael@0: */ michael@0: private void addSingleSelectList(AlertDialog.Builder builder, PromptListItem[] listItems) { michael@0: mAdapter = new PromptListAdapter(mContext, R.layout.select_dialog_singlechoice, listItems); michael@0: builder.setSingleChoiceItems(mAdapter, mAdapter.getSelectedIndex(), new DialogInterface.OnClickListener() { michael@0: @Override michael@0: public void onClick(DialogInterface dialog, int which) { michael@0: // The adapter isn't aware of single vs. multi choice lists, so manually michael@0: // clear any other selected items first. michael@0: ArrayList selected = mAdapter.getSelected(); michael@0: for (Integer sel : selected) { michael@0: mAdapter.toggleSelected(sel); michael@0: } michael@0: michael@0: // Now select this item. michael@0: mAdapter.toggleSelected(which); michael@0: closeIfNoButtons(which); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: /* Shows a single-select list. michael@0: * michael@0: * @param builder michael@0: * the alert builder currently building this dialog. michael@0: * @param listItems michael@0: * The items to add. michael@0: */ michael@0: private void addMenuList(AlertDialog.Builder builder, PromptListItem[] listItems) { michael@0: mAdapter = new PromptListAdapter(mContext, android.R.layout.simple_list_item_1, listItems); michael@0: builder.setAdapter(mAdapter, this); michael@0: } michael@0: michael@0: /* Wraps an input in a linearlayout. We do this so that we can set padding that appears outside the background michael@0: * drawable for the view. michael@0: */ michael@0: private View wrapInput(final PromptInput input) { michael@0: final LinearLayout linearLayout = new LinearLayout(mContext); michael@0: linearLayout.setOrientation(LinearLayout.VERTICAL); michael@0: applyInputStyle(linearLayout, input); michael@0: michael@0: linearLayout.addView(input.getView(mContext)); michael@0: michael@0: return linearLayout; michael@0: } michael@0: michael@0: /* Add the requested input elements to the dialog. michael@0: * michael@0: * @param builder michael@0: * the alert builder currently building this dialog. michael@0: * @return michael@0: * return true if the inputs were added successfully. This may fail michael@0: * if the requested input is compatible with this Android verison michael@0: */ michael@0: private boolean addInputs(AlertDialog.Builder builder) { michael@0: int length = mInputs == null ? 0 : mInputs.length; michael@0: if (length == 0) { michael@0: return true; michael@0: } michael@0: michael@0: try { michael@0: View root = null; michael@0: boolean scrollable = false; // If any of the innuts are scrollable, we won't wrap this in a ScrollView michael@0: michael@0: if (length == 1) { michael@0: root = wrapInput(mInputs[0]); michael@0: scrollable |= mInputs[0].getScrollable(); michael@0: } else if (length > 1) { michael@0: LinearLayout linearLayout = new LinearLayout(mContext); michael@0: linearLayout.setOrientation(LinearLayout.VERTICAL); michael@0: for (int i = 0; i < length; i++) { michael@0: View content = wrapInput(mInputs[i]); michael@0: linearLayout.addView(content); michael@0: scrollable |= mInputs[i].getScrollable(); michael@0: } michael@0: root = linearLayout; michael@0: } michael@0: michael@0: if (scrollable) { michael@0: // If we're showing some sort of scrollable list, force an inverse background. michael@0: builder.setInverseBackgroundForced(true); michael@0: builder.setView(root); michael@0: } else { michael@0: ScrollView view = new ScrollView(mContext); michael@0: view.addView(root); michael@0: builder.setView(view); michael@0: } michael@0: } catch(Exception ex) { michael@0: Log.e(LOGTAG, "Error showing prompt inputs", ex); michael@0: // We cannot display these input widgets with this sdk version, michael@0: // do not display any dialog and finish the prompt now. michael@0: cancelDialog(); michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: /* AdapterView.OnItemClickListener michael@0: * Called when a list item is clicked michael@0: */ michael@0: @Override michael@0: public void onItemClick(AdapterView parent, View view, int position, long id) { michael@0: ThreadUtils.assertOnUiThread(); michael@0: mAdapter.toggleSelected(position); michael@0: michael@0: // If there are no buttons on this dialog, then we take selecting an item as a sign to close michael@0: // the dialog. Note that means it will be hard to select multiple things in this list, but michael@0: // given there is no way to confirm+close the dialog, it seems reasonable. michael@0: closeIfNoButtons(position); michael@0: } michael@0: michael@0: private boolean closeIfNoButtons(int selected) { michael@0: ThreadUtils.assertOnUiThread(); michael@0: if (mButtons == null || mButtons.length == 0) { michael@0: closeDialog(selected); michael@0: return true; michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: /* @DialogInterface.OnCancelListener michael@0: * Called when the user hits back to cancel a dialog. The dialog will close itself when this michael@0: * ends. Setup the correct return values here. michael@0: * michael@0: * @param aDialog michael@0: * A dialog interface for the dialog that's being closed. michael@0: */ michael@0: @Override michael@0: public void onCancel(DialogInterface aDialog) { michael@0: ThreadUtils.assertOnUiThread(); michael@0: cancelDialog(); michael@0: } michael@0: michael@0: /* Called in situations where we want to cancel the dialog . This can happen if the user hits back, michael@0: * or if the dialog can't be created because of invalid JSON. michael@0: */ michael@0: private void cancelDialog() { michael@0: JSONObject ret = new JSONObject(); michael@0: try { michael@0: ret.put("button", -1); michael@0: } catch(Exception ex) { } michael@0: addInputValues(ret); michael@0: notifyClosing(ret); michael@0: } michael@0: michael@0: /* Called any time we're closing the dialog to cleanup and notify listeners that the dialog michael@0: * is closing. michael@0: */ michael@0: private void closeDialog(int which) { michael@0: JSONObject ret = new JSONObject(); michael@0: mDialog.dismiss(); michael@0: michael@0: addButtonResult(ret, which); michael@0: addListResult(ret, which); michael@0: addInputValues(ret); michael@0: michael@0: notifyClosing(ret); michael@0: } michael@0: michael@0: /* Called any time we're closing the dialog to cleanup and notify listeners that the dialog michael@0: * is closing. michael@0: */ michael@0: private void notifyClosing(JSONObject aReturn) { michael@0: try { michael@0: aReturn.put("guid", mGuid); michael@0: } catch(JSONException ex) { } michael@0: michael@0: // poke the Gecko thread in case it's waiting for new events michael@0: GeckoAppShell.sendEventToGecko(GeckoEvent.createNoOpEvent()); michael@0: michael@0: if (mCallback != null) { michael@0: mCallback.onPromptFinished(aReturn.toString()); michael@0: } michael@0: } michael@0: michael@0: /* Handles parsing the initial JSON sent to show dialogs michael@0: */ michael@0: private void processMessage(JSONObject geckoObject) { michael@0: String title = geckoObject.optString("title"); michael@0: String text = geckoObject.optString("text"); michael@0: mGuid = geckoObject.optString("guid"); michael@0: michael@0: mButtons = getStringArray(geckoObject, "buttons"); michael@0: michael@0: JSONArray inputs = getSafeArray(geckoObject, "inputs"); michael@0: mInputs = new PromptInput[inputs.length()]; michael@0: for (int i = 0; i < mInputs.length; i++) { michael@0: try { michael@0: mInputs[i] = PromptInput.getInput(inputs.getJSONObject(i)); michael@0: mInputs[i].setListener(this); michael@0: } catch(Exception ex) { } michael@0: } michael@0: michael@0: PromptListItem[] menuitems = PromptListItem.getArray(geckoObject.optJSONArray("listitems")); michael@0: String selected = geckoObject.optString("choiceMode"); michael@0: michael@0: int choiceMode = ListView.CHOICE_MODE_NONE; michael@0: if ("single".equals(selected)) { michael@0: choiceMode = ListView.CHOICE_MODE_SINGLE; michael@0: } else if ("multiple".equals(selected)) { michael@0: choiceMode = ListView.CHOICE_MODE_MULTIPLE; michael@0: } michael@0: michael@0: show(title, text, menuitems, choiceMode); michael@0: } michael@0: michael@0: // Called when the prompt inputs on the dialog change michael@0: @Override michael@0: public void onChange(PromptInput input) { michael@0: // If there are no buttons on this dialog, assuming that "changing" an input michael@0: // means something was selected and we can close. This provides a way to tap michael@0: // on a list item and close the dialog automatically. michael@0: closeIfNoButtons(-1); michael@0: } michael@0: michael@0: private static JSONArray getSafeArray(JSONObject json, String key) { michael@0: try { michael@0: return json.getJSONArray(key); michael@0: } catch (Exception e) { michael@0: return new JSONArray(); michael@0: } michael@0: } michael@0: michael@0: public static String[] getStringArray(JSONObject aObject, String aName) { michael@0: JSONArray items = getSafeArray(aObject, aName); michael@0: int length = items.length(); michael@0: String[] list = new String[length]; michael@0: for (int i = 0; i < length; i++) { michael@0: try { michael@0: list[i] = items.getString(i); michael@0: } catch(Exception ex) { } michael@0: } michael@0: return list; michael@0: } michael@0: michael@0: private static boolean[] getBooleanArray(JSONObject aObject, String aName) { michael@0: JSONArray items = new JSONArray(); michael@0: try { michael@0: items = aObject.getJSONArray(aName); michael@0: } catch(Exception ex) { return null; } michael@0: int length = items.length(); michael@0: boolean[] list = new boolean[length]; michael@0: for (int i = 0; i < length; i++) { michael@0: try { michael@0: list[i] = items.getBoolean(i); michael@0: } catch(Exception ex) { } michael@0: } michael@0: return list; michael@0: } michael@0: michael@0: public interface PromptCallback { michael@0: public void onPromptFinished(String jsonResult); michael@0: } michael@0: }