mobile/android/base/prompts/Prompt.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

     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/. */
     6 package org.mozilla.gecko.prompts;
     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;
    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;
    39 import java.util.ArrayList;
    41 public class Prompt implements OnClickListener, OnCancelListener, OnItemClickListener,
    42                                PromptInput.OnChangeListener {
    43     private static final String LOGTAG = "GeckoPromptService";
    45     private String[] mButtons;
    46     private PromptInput[] mInputs;
    47     private AlertDialog mDialog;
    49     private final LayoutInflater mInflater;
    50     private final Context mContext;
    51     private PromptCallback mCallback;
    52     private String mGuid;
    53     private PromptListAdapter mAdapter;
    55     private static boolean mInitialized = false;
    56     private static int mInputPaddingSize;
    58     public Prompt(Context context, PromptCallback callback) {
    59         this(context);
    60         mCallback = callback;
    61     }
    63     private Prompt(Context context) {
    64         mContext = context;
    65         mInflater = LayoutInflater.from(mContext);
    67         if (!mInitialized) {
    68             Resources res = mContext.getResources();
    69             mInputPaddingSize = (int) (res.getDimension(R.dimen.prompt_service_inputs_padding));
    70             mInitialized = true;
    71         }
    72     }
    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     }
    82     public void show(JSONObject message) {
    83         processMessage(message);
    84     }
    87     public void show(String title, String text, PromptListItem[] listItems, int choiceMode) {
    88         ThreadUtils.assertOnUiThread();
    90         GeckoAppShell.getLayerView().abortPanning();
    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         }
    98         if (!TextUtils.isEmpty(text)) {
    99             builder.setMessage(text);
   100         }
   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         }
   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         }
   122         mDialog = builder.create();
   123         mDialog.setOnCancelListener(Prompt.this);
   124         mDialog.show();
   125     }
   127     public void setButtons(String[] buttons) {
   128         mButtons = buttons;
   129     }
   131     public void setInputs(PromptInput[] inputs) {
   132         mInputs = inputs;
   133     }
   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         }
   145         try {
   146             JSONArray selected = new JSONArray();
   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             }
   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                 }
   161                 result.put("button", which);
   162             }
   164             result.put("list", selected);
   165         } catch(JSONException ex) { }
   166     }
   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     }
   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     }
   197     @Override
   198     public void onClick(DialogInterface dialog, int which) {
   199         ThreadUtils.assertOnUiThread();
   200         closeDialog(which);
   201     }
   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     }
   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);
   242         mAdapter = new PromptListAdapter(mContext, R.layout.select_dialog_multichoice, listItems);
   243         listView.setAdapter(mAdapter);
   244         builder.setView(listView);
   245     }
   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                 }
   266                 // Now select this item.
   267                 mAdapter.toggleSelected(which);
   268                 closeIfNoButtons(which);
   269             }
   270         });
   271     }
   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     }
   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);
   293         linearLayout.addView(input.getView(mContext));
   295         return linearLayout;
   296     }
   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         }
   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
   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             }
   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         }
   347         return true;
   348     }
   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);
   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     }
   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     }
   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     }
   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     }
   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();
   405         addButtonResult(ret, which);
   406         addListResult(ret, which);
   407         addInputValues(ret);
   409         notifyClosing(ret);
   410     }
   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) { }
   420         // poke the Gecko thread in case it's waiting for new events
   421         GeckoAppShell.sendEventToGecko(GeckoEvent.createNoOpEvent());
   423         if (mCallback != null) {
   424             mCallback.onPromptFinished(aReturn.toString());
   425         }
   426     }
   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");
   435         mButtons = getStringArray(geckoObject, "buttons");
   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         }
   446         PromptListItem[] menuitems = PromptListItem.getArray(geckoObject.optJSONArray("listitems"));
   447         String selected = geckoObject.optString("choiceMode");
   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         }
   456         show(title, text, menuitems, choiceMode);
   457     }
   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     }
   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     }
   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     }
   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     }
   503     public interface PromptCallback {
   504         public void onPromptFinished(String jsonResult);
   505     }
   506 }

mercurial