mobile/android/base/prompts/Prompt.java

changeset 0
6474c204b198
     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 +}

mercurial