Bug 1448056 - Move BasicGeckoViewPrompt and ExamplePermissionDelegate into GeckoView r=jchen draft
authorJames Willcox <snorp@snorp.net>
Fri, 27 Apr 2018 15:12:16 -0500
changeset 789831 cf7dc853e0164d5bed431b44a21a12fcac9ac6d3
parent 789830 ed2bb0b1e6cd8f178c13528ec39b03cf16a07f5a
push id108338
push userbmo:snorp@snorp.net
push dateMon, 30 Apr 2018 15:20:05 +0000
reviewersjchen
bugs1448056
milestone61.0a1
Bug 1448056 - Move BasicGeckoViewPrompt and ExamplePermissionDelegate into GeckoView r=jchen These two are conflated into one class, BasicPromptPermissionDelegate. This makes it easy for an app to get reasonable behavior OOTB for web prompts (alert(), etc) and permission prompts. MozReview-Commit-ID: 90V2tBRevvm
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoBasicPromptPermissionDelegate.java
mobile/android/geckoview/src/main/res/values/strings.xml
mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/BasicGeckoViewPrompt.java
mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
mobile/android/geckoview_example/src/main/res/values/strings.xml
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoBasicPromptPermissionDelegate.java
@@ -0,0 +1,1106 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.geckoview;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ActivityNotFoundException;
+import android.content.ClipData;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.net.Uri;
+import android.os.Build;
+import android.text.InputType;
+import android.text.format.DateFormat;
+import android.util.Log;
+import android.view.InflateException;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.CheckedTextView;
+import android.widget.CompoundButton;
+import android.widget.DatePicker;
+import android.widget.EditText;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.ScrollView;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.TimePicker;
+
+import java.lang.ref.WeakReference;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * This class offers a basic implementation of {@link GeckoSession.PromptDelegate} and
+ * {@link GeckoSession.PermissionDelegate} using things like {@link AlertDialog}.
+ *
+ * To use, create an instance of this class and set the {@link #filePickerRequestCode} and
+ * {@link #androidPermissionRequestCode} fields to well-known values. See the docs on those fields
+ * for more details on why this is necessary. Next, set the instance as the prompt and permission
+ * delegates on a GeckoSession instance using {@link GeckoSession#setPromptDelegate(GeckoSession.PromptDelegate)}
+ * and {@link GeckoSession#setPermissionDelegate(GeckoSession.PermissionDelegate)}.
+ *
+ * There are some strings used in this class. Localized versions are not included, so the app
+ * will need to provide those if desired. The strings currently defined are as follows:
+ *
+ * <pre>{@code
+ * <string name="geckoview_username">Username</string>
+ * <string name="geckoview_password">Password</string>
+ * <string name="geckoview_clear_field">Clear</string>
+ * <string name="geckoview_request_geolocation">Share location with "%1$s"?</string>
+ * <string name="geckoview_request_notification">Allow notifications for "%1$s"?</string>
+ * <string name="geckoview_request_video">Share video with "%1$s"</string>
+ * <string name="geckoview_request_audio">Share audio with "%1$s"</string>
+ * <string name="geckoview_request_media">Share video and audio with "%1$s"</string>
+ * <string name="geckoview_media_back_camera">Back camera</string>
+ * <string name="geckoview_media_front_camera">Front camera</string>
+ * <string name="geckoview_media_microphone">Microphone</string>
+ * <string name="geckoview_media_other">Unknown source</string>
+ * }</pre>
+ *
+ * If an app defines any of these strings itself, those values will be used instead.
+ * This can be used to provide customized strings for the locales the app supports.
+ */
+public class GeckoBasicPromptPermissionDelegate implements
+        GeckoSession.PromptDelegate, GeckoSession.PermissionDelegate {
+    protected static final String LOGTAG = "PromptPermissionDelegate";
+
+    private final WeakReference<Activity> mActivity;
+    private int mFileType;
+    private FileCallback mFileCallback;
+    private Callback mPermissionCallback;
+
+    /**
+     * The request code used for file picker responses. The app must implement
+     * {@link Activity#onActivityResult(int, int, Intent)} and call
+     * {@link #onFileCallbackResult(int, Intent)} if the request code matches this value.
+     *
+     * This must be set to a number &gt;= 0 in order for the delegate to work as expected.
+     * See {@link Activity#startActivityForResult(Intent, int)} for more information.
+     */
+    public int filePickerRequestCode = -1;
+
+    /**
+     * The request code used for Android permission requests. The app must implement
+     * {@link Activity#onActivityResult(int, int, Intent)} and call
+     * {@link #onRequestPermissionsResult(String[], int[])}  if the request code matches this value.
+     *
+     * This must be set to a number &gt;= 0 in order for the delegate to work as expected.
+     * See {@link Activity#requestPermissions(String[], int)} for more information.
+     */
+    public int androidPermissionRequestCode = -1;
+
+    /**
+     * Create a new GeckoBasicPromptPermissionDelegate.
+     * This will provide a basic implementation for {@link GeckoSession.PromptDelegate} and
+     * {@link GeckoSession.PermissionDelegate}. It generally uses things like
+     * {@link AlertDialog} to prompt the user for information when requested by the
+     * {@link GeckoSession}
+     *
+     * @param activity The current Activity, needed to get some strings and request Android
+     *                 permissions via {@link Activity#requestPermissions(String[], int)}
+     */
+    public GeckoBasicPromptPermissionDelegate(final Activity activity) {
+        mActivity = new WeakReference<>(activity);
+    }
+
+    private AlertDialog.Builder addCheckbox(final AlertDialog.Builder builder,
+                                            ViewGroup parent,
+                                            final AlertCallback callback) {
+        if (!callback.hasCheckbox()) {
+            return builder;
+        }
+        final CheckBox checkbox = new CheckBox(builder.getContext());
+        if (callback.getCheckboxMessage() != null) {
+            checkbox.setText(callback.getCheckboxMessage());
+        }
+        checkbox.setChecked(callback.getCheckboxValue());
+        checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+            @Override
+            public void onCheckedChanged(final CompoundButton button,
+                                         final boolean checked) {
+                callback.setCheckboxValue(checked);
+            }
+        });
+        if (parent == null) {
+            final int padding = getViewPadding(builder);
+            parent = new FrameLayout(builder.getContext());
+            parent.setPadding(/* left */ padding, /* top */ 0,
+                              /* right */ padding, /* bottom */ 0);
+            builder.setView(parent);
+        }
+        parent.addView(checkbox);
+        return builder;
+    }
+
+    public void onAlert(final GeckoSession session, final String title, final String msg,
+                      final AlertCallback callback) {
+        final Activity activity = mActivity.get();
+        if (activity == null) {
+            callback.dismiss();
+            return;
+        }
+        final AlertDialog.Builder builder = new AlertDialog.Builder(activity)
+                .setTitle(title)
+                .setMessage(msg)
+                .setPositiveButton(android.R.string.ok, /* onClickListener */ null);
+        createStandardDialog(addCheckbox(builder, /* parent */ null, callback),
+                             callback).show();
+    }
+
+    public void onButtonPrompt(final GeckoSession session, final String title,
+                                final String msg, final String[] btnMsg,
+                                final ButtonCallback callback) {
+        final Activity activity = mActivity.get();
+        if (activity == null) {
+            callback.dismiss();
+            return;
+        }
+        final AlertDialog.Builder builder = new AlertDialog.Builder(activity)
+                .setTitle(title)
+                .setMessage(msg);
+        final DialogInterface.OnClickListener listener =
+            new DialogInterface.OnClickListener() {
+                @Override
+                public void onClick(final DialogInterface dialog, final int which) {
+                    if (which == DialogInterface.BUTTON_POSITIVE) {
+                        callback.confirm(BUTTON_TYPE_POSITIVE);
+                    } else if (which == DialogInterface.BUTTON_NEUTRAL) {
+                        callback.confirm(BUTTON_TYPE_NEUTRAL);
+                    } else if (which == DialogInterface.BUTTON_NEGATIVE) {
+                        callback.confirm(BUTTON_TYPE_NEGATIVE);
+                    } else {
+                        callback.dismiss();
+                    }
+                }
+            };
+        if (btnMsg[BUTTON_TYPE_POSITIVE] != null) {
+            builder.setPositiveButton(btnMsg[BUTTON_TYPE_POSITIVE], listener);
+        }
+        if (btnMsg[BUTTON_TYPE_NEUTRAL] != null) {
+            builder.setNeutralButton(btnMsg[BUTTON_TYPE_NEUTRAL], listener);
+        }
+        if (btnMsg[BUTTON_TYPE_NEGATIVE] != null) {
+            builder.setNegativeButton(btnMsg[BUTTON_TYPE_NEGATIVE], listener);
+        }
+        createStandardDialog(addCheckbox(builder, /* parent */ null, callback),
+                             callback).show();
+    }
+
+    private int getViewPadding(final AlertDialog.Builder builder) {
+        final TypedArray attr = builder.getContext().obtainStyledAttributes(
+                new int[] { android.R.attr.listPreferredItemPaddingLeft });
+        final int padding = attr.getDimensionPixelSize(0, 1);
+        attr.recycle();
+        return padding;
+    }
+
+    private LinearLayout addStandardLayout(final AlertDialog.Builder builder,
+                                           final String title, final String msg) {
+        final ScrollView scrollView = new ScrollView(builder.getContext());
+        final LinearLayout container = new LinearLayout(builder.getContext());
+        final int horizontalPadding = getViewPadding(builder);
+        final int verticalPadding = (msg == null || msg.isEmpty()) ? horizontalPadding : 0;
+        container.setOrientation(LinearLayout.VERTICAL);
+        container.setPadding(/* left */ horizontalPadding, /* top */ verticalPadding,
+                             /* right */ horizontalPadding, /* bottom */ verticalPadding);
+        scrollView.addView(container);
+        builder.setTitle(title)
+               .setMessage(msg)
+               .setView(scrollView);
+        return container;
+    }
+
+    private AlertDialog createStandardDialog(final AlertDialog.Builder builder,
+                                             final AlertCallback callback) {
+        final AlertDialog dialog = builder.create();
+        dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
+                    @Override
+                    public void onDismiss(final DialogInterface dialog) {
+                        callback.dismiss();
+                    }
+                });
+        return dialog;
+    }
+
+    public void onTextPrompt(final GeckoSession session, final String title,
+                              final String msg, final String value,
+                              final TextCallback callback) {
+        final Activity activity = mActivity.get();
+        if (activity == null) {
+            callback.dismiss();
+            return;
+        }
+        final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+        final LinearLayout container = addStandardLayout(builder, title, msg);
+        final EditText editText = new EditText(builder.getContext());
+        editText.setText(value);
+        container.addView(editText);
+
+        builder.setNegativeButton(android.R.string.cancel, /* listener */ null)
+               .setPositiveButton(android.R.string.ok,
+                                  new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(final DialogInterface dialog, final int which) {
+                        callback.confirm(editText.getText().toString());
+                    }
+                });
+
+        createStandardDialog(addCheckbox(builder, container, callback), callback).show();
+    }
+
+    public void onAuthPrompt(final GeckoSession session, final String title,
+                              final String msg, final AuthOptions options,
+                              final AuthCallback callback) {
+        final Activity activity = mActivity.get();
+        if (activity == null) {
+            callback.dismiss();
+            return;
+        }
+        final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+        final LinearLayout container = addStandardLayout(builder, title, msg);
+
+        final int flags = options.flags;
+        final int level = options.level;
+        final EditText username;
+        if ((flags & AuthOptions.AUTH_FLAG_ONLY_PASSWORD) == 0) {
+            username = new EditText(builder.getContext());
+            username.setHint(R.string.geckoview_username);
+            username.setText(options.username);
+            container.addView(username);
+        } else {
+            username = null;
+        }
+
+        final EditText password = new EditText(builder.getContext());
+        password.setHint(R.string.geckoview_password);
+        password.setText(options.password);
+        password.setInputType(InputType.TYPE_CLASS_TEXT |
+                              InputType.TYPE_TEXT_VARIATION_PASSWORD);
+        container.addView(password);
+
+        if (level != AuthOptions.AUTH_LEVEL_NONE) {
+            final ImageView secure = new ImageView(builder.getContext());
+            secure.setImageResource(android.R.drawable.ic_lock_lock);
+            container.addView(secure);
+        }
+
+        builder.setNegativeButton(android.R.string.cancel, /* listener */ null)
+               .setPositiveButton(android.R.string.ok,
+                                  new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(final DialogInterface dialog, final int which) {
+                        if ((flags & AuthOptions.AUTH_FLAG_ONLY_PASSWORD) == 0) {
+                            callback.confirm(username.getText().toString(),
+                                             password.getText().toString());
+                        } else {
+                            callback.confirm(password.getText().toString());
+                        }
+                    }
+                });
+        createStandardDialog(addCheckbox(builder, container, callback), callback).show();
+    }
+
+    private static class ModifiableChoice {
+        public boolean modifiableSelected;
+        public String modifiableLabel;
+        public final Choice choice;
+
+        public ModifiableChoice(Choice c) {
+            choice = c;
+            modifiableSelected = choice.selected;
+            modifiableLabel = choice.label;
+        }
+    }
+
+    private void addChoiceItems(final int type, final ArrayAdapter<ModifiableChoice> list,
+                                final Choice[] items, final String indent) {
+        if (type == Choice.CHOICE_TYPE_MENU) {
+            for (final Choice item : items) {
+                list.add(new ModifiableChoice(item));
+            }
+            return;
+        }
+
+        for (final Choice item : items) {
+            final ModifiableChoice modItem = new ModifiableChoice(item);
+
+            final Choice[] children = item.items;
+
+            if (indent != null && children == null) {
+                modItem.modifiableLabel = indent + modItem.modifiableLabel;
+            }
+            list.add(modItem);
+
+            if (children != null) {
+                final String newIndent;
+                if (type == Choice.CHOICE_TYPE_SINGLE || type == Choice.CHOICE_TYPE_MULTIPLE) {
+                    newIndent = (indent != null) ? indent + '\t' : "\t";
+                } else {
+                    newIndent = null;
+                }
+                addChoiceItems(type, list, children, newIndent);
+            }
+        }
+    }
+
+    public void onChoicePrompt(final GeckoSession session, final String title,
+                                final String msg, final int type,
+                                final Choice[] choices, final ChoiceCallback callback) {
+        final Activity activity = mActivity.get();
+        if (activity == null) {
+            callback.dismiss();
+            return;
+        }
+        final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+        addStandardLayout(builder, title, msg);
+
+        final ListView list = new ListView(builder.getContext());
+        if (type == Choice.CHOICE_TYPE_MULTIPLE) {
+            list.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+        }
+
+        final ArrayAdapter<ModifiableChoice> adapter = new ArrayAdapter<ModifiableChoice>(
+                builder.getContext(), android.R.layout.simple_list_item_1) {
+            private static final int TYPE_MENU_ITEM = 0;
+            private static final int TYPE_MENU_CHECK = 1;
+            private static final int TYPE_SEPARATOR = 2;
+            private static final int TYPE_GROUP = 3;
+            private static final int TYPE_SINGLE = 4;
+            private static final int TYPE_MULTIPLE = 5;
+            private static final int TYPE_COUNT = 6;
+
+            private LayoutInflater mInflater;
+            private View mSeparator;
+
+            @Override
+            public int getViewTypeCount() {
+                return TYPE_COUNT;
+            }
+
+            @Override
+            public int getItemViewType(final int position) {
+                final ModifiableChoice item = getItem(position);
+                if (item.choice.separator) {
+                    return TYPE_SEPARATOR;
+                } else if (type == Choice.CHOICE_TYPE_MENU) {
+                    return item.modifiableSelected ? TYPE_MENU_CHECK : TYPE_MENU_ITEM;
+                } else if (item.choice.items != null) {
+                    return TYPE_GROUP;
+                } else if (type == Choice.CHOICE_TYPE_SINGLE) {
+                    return TYPE_SINGLE;
+                } else if (type == Choice.CHOICE_TYPE_MULTIPLE) {
+                    return TYPE_MULTIPLE;
+                } else {
+                    throw new UnsupportedOperationException();
+                }
+            }
+
+            @Override
+            public boolean isEnabled(final int position) {
+                final ModifiableChoice item = getItem(position);
+                return !item.choice.separator && !item.choice.disabled &&
+                        ((type != Choice.CHOICE_TYPE_SINGLE && type != Choice.CHOICE_TYPE_MULTIPLE) ||
+                         item.choice.items == null);
+            }
+
+            @Override
+            public View getView(final int position, View view,
+                                final ViewGroup parent) {
+                final int itemType = getItemViewType(position);
+                final int layoutId;
+                if (itemType == TYPE_SEPARATOR) {
+                    if (mSeparator == null) {
+                        mSeparator = new View(getContext());
+                        mSeparator.setLayoutParams(new ListView.LayoutParams(
+                                ViewGroup.LayoutParams.MATCH_PARENT, 2, itemType));
+                        final TypedArray attr = getContext().obtainStyledAttributes(
+                                new int[] { android.R.attr.listDivider });
+                        mSeparator.setBackgroundResource(attr.getResourceId(0, 0));
+                        attr.recycle();
+                    }
+                    return mSeparator;
+                } else if (itemType == TYPE_MENU_ITEM) {
+                    layoutId = android.R.layout.simple_list_item_1;
+                } else if (itemType == TYPE_MENU_CHECK) {
+                    layoutId = android.R.layout.simple_list_item_checked;
+                } else if (itemType == TYPE_GROUP) {
+                    layoutId = android.R.layout.preference_category;
+                } else if (itemType == TYPE_SINGLE) {
+                    layoutId = android.R.layout.simple_list_item_single_choice;
+                } else if (itemType == TYPE_MULTIPLE) {
+                    layoutId = android.R.layout.simple_list_item_multiple_choice;
+                } else {
+                    throw new UnsupportedOperationException();
+                }
+
+                if (view == null) {
+                    if (mInflater == null) {
+                        mInflater = LayoutInflater.from(builder.getContext());
+                    }
+                    view = mInflater.inflate(layoutId, parent, false);
+                }
+
+                final ModifiableChoice item = getItem(position);
+                final TextView text = (TextView) view;
+                text.setEnabled(!item.choice.disabled);
+                text.setText(item.modifiableLabel);
+                if (view instanceof CheckedTextView) {
+                    final boolean selected = item.modifiableSelected;
+                    if (itemType == TYPE_MULTIPLE) {
+                        list.setItemChecked(position, selected);
+                    } else {
+                        ((CheckedTextView) view).setChecked(selected);
+                    }
+                }
+                return view;
+            }
+        };
+        addChoiceItems(type, adapter, choices, /* indent */ null);
+
+        list.setAdapter(adapter);
+        builder.setView(list);
+
+        final AlertDialog dialog;
+        if (type == Choice.CHOICE_TYPE_SINGLE || type == Choice.CHOICE_TYPE_MENU) {
+            dialog = createStandardDialog(builder, callback);
+            list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+                @Override
+                public void onItemClick(final AdapterView<?> parent, final View v,
+                                        final int position, final long id) {
+                    final ModifiableChoice item = adapter.getItem(position);
+                    if (type == Choice.CHOICE_TYPE_MENU) {
+                        final Choice[] children = item.choice.items;
+                        if (children != null) {
+                            // Show sub-menu.
+                            dialog.setOnDismissListener(null);
+                            dialog.dismiss();
+                            onChoicePrompt(session, item.modifiableLabel, /* msg */ null,
+                                            type, children, callback);
+                            return;
+                        }
+                    }
+                    callback.confirm(item.choice);
+                    dialog.dismiss();
+                }
+            });
+        } else if (type == Choice.CHOICE_TYPE_MULTIPLE) {
+            list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+                @Override
+                public void onItemClick(final AdapterView<?> parent, final View v,
+                                        final int position, final long id) {
+                    final ModifiableChoice item = adapter.getItem(position);
+                    item.modifiableSelected = ((CheckedTextView) v).isChecked();
+                }
+            });
+            builder.setNegativeButton(android.R.string.cancel, /* listener */ null)
+                   .setPositiveButton(android.R.string.ok,
+                                      new DialogInterface.OnClickListener() {
+                @Override
+                public void onClick(final DialogInterface dialog,
+                                    final int which) {
+                    final int len = adapter.getCount();
+                    ArrayList<String> items = new ArrayList<>(len);
+                    for (int i = 0; i < len; i++) {
+                        final ModifiableChoice item = adapter.getItem(i);
+                        if (item.modifiableSelected) {
+                            items.add(item.choice.id);
+                        }
+                    }
+                    callback.confirm(items.toArray(new String[items.size()]));
+                }
+            });
+            dialog = createStandardDialog(builder, callback);
+        } else {
+            throw new UnsupportedOperationException();
+        }
+        dialog.show();
+    }
+
+    private static int parseColor(final String value, final int def) {
+        try {
+            return Color.parseColor(value);
+        } catch (final IllegalArgumentException e) {
+            return def;
+        }
+    }
+
+    public void onColorPrompt(final GeckoSession session, final String title,
+                               final String value, final TextCallback callback)
+    {
+        final Activity activity = mActivity.get();
+        if (activity == null) {
+            callback.dismiss();
+            return;
+        }
+        final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+        addStandardLayout(builder, title, /* msg */ null);
+
+        final int initial = parseColor(value, /* def */ 0);
+        final ArrayAdapter<Integer> adapter = new ArrayAdapter<Integer>(
+                builder.getContext(), android.R.layout.simple_list_item_1) {
+            private LayoutInflater mInflater;
+
+            @Override
+            public int getViewTypeCount() {
+                return 2;
+            }
+
+            @Override
+            public int getItemViewType(final int position) {
+                return (getItem(position) == initial) ? 1 : 0;
+            }
+
+            @Override
+            public View getView(final int position, View view,
+                                final ViewGroup parent) {
+                if (mInflater == null) {
+                    mInflater = LayoutInflater.from(builder.getContext());
+                }
+                final int color = getItem(position);
+                if (view == null) {
+                    view = mInflater.inflate((color == initial) ?
+                            android.R.layout.simple_list_item_checked :
+                            android.R.layout.simple_list_item_1, parent, false);
+                }
+                view.setBackgroundResource(android.R.drawable.editbox_background);
+                view.getBackground().setColorFilter(color, PorterDuff.Mode.MULTIPLY);
+                return view;
+            }
+        };
+
+        adapter.addAll(0xffff4444 /* holo_red_light */,
+                       0xffcc0000 /* holo_red_dark */,
+                       0xffffbb33 /* holo_orange_light */,
+                       0xffff8800 /* holo_orange_dark */,
+                       0xff99cc00 /* holo_green_light */,
+                       0xff669900 /* holo_green_dark */,
+                       0xff33b5e5 /* holo_blue_light */,
+                       0xff0099cc /* holo_blue_dark */,
+                       0xffaa66cc /* holo_purple */,
+                       0xffffffff /* white */,
+                       0xffaaaaaa /* lighter_gray */,
+                       0xff555555 /* darker_gray */,
+                       0xff000000 /* black */);
+
+        final ListView list = new ListView(builder.getContext());
+        list.setAdapter(adapter);
+        builder.setView(list);
+
+        final AlertDialog dialog = createStandardDialog(builder, callback);
+        list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+            @Override
+            public void onItemClick(final AdapterView<?> parent, final View v,
+                                    final int position, final long id) {
+                callback.confirm(String.format("#%06x", 0xffffff & adapter.getItem(position)));
+                dialog.dismiss();
+            }
+        });
+        dialog.show();
+    }
+
+    private static Date parseDate(final SimpleDateFormat formatter,
+                                  final String value,
+                                  final boolean defaultToNow) {
+        try {
+            if (value != null && !value.isEmpty()) {
+                return formatter.parse(value);
+            }
+        } catch (final ParseException e) {
+        }
+        return defaultToNow ? new Date() : null;
+    }
+
+    @SuppressWarnings("deprecation")
+    private static void setTimePickerTime(final TimePicker picker, final Calendar cal) {
+        if (Build.VERSION.SDK_INT >= 23) {
+            picker.setHour(cal.get(Calendar.HOUR_OF_DAY));
+            picker.setMinute(cal.get(Calendar.MINUTE));
+        } else {
+            picker.setCurrentHour(cal.get(Calendar.HOUR_OF_DAY));
+            picker.setCurrentMinute(cal.get(Calendar.MINUTE));
+        }
+    }
+
+    @SuppressWarnings("deprecation")
+    private static void setCalendarTime(final Calendar cal, final TimePicker picker) {
+        if (Build.VERSION.SDK_INT >= 23) {
+            cal.set(Calendar.HOUR_OF_DAY, picker.getHour());
+            cal.set(Calendar.MINUTE, picker.getMinute());
+        } else {
+            cal.set(Calendar.HOUR_OF_DAY, picker.getCurrentHour());
+            cal.set(Calendar.MINUTE, picker.getCurrentMinute());
+        }
+    }
+
+    public void onDateTimePrompt(final GeckoSession session, final String title,
+                                  final int type, final String value, final String min,
+                                  final String max, final TextCallback callback) {
+        final Activity activity = mActivity.get();
+        if (activity == null) {
+            callback.dismiss();
+            return;
+        }
+        final String format;
+        if (type == DATETIME_TYPE_DATE) {
+            format = "yyyy-MM-dd";
+        } else if (type == DATETIME_TYPE_MONTH) {
+            format = "yyyy-MM";
+        } else if (type == DATETIME_TYPE_WEEK) {
+            format = "yyyy-'W'ww";
+        } else if (type == DATETIME_TYPE_TIME) {
+            format = "HH:mm";
+        } else if (type == DATETIME_TYPE_DATETIME_LOCAL) {
+            format = "yyyy-MM-dd'T'HH:mm";
+        } else {
+            throw new UnsupportedOperationException();
+        }
+
+        final SimpleDateFormat formatter = new SimpleDateFormat(format, Locale.ROOT);
+        final Date minDate = parseDate(formatter, min, /* defaultToNow */ false);
+        final Date maxDate = parseDate(formatter, max, /* defaultToNow */ false);
+        final Date date = parseDate(formatter, value, /* defaultToNow */ true);
+        final Calendar cal = formatter.getCalendar();
+        cal.setTime(date);
+
+        final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+        final LayoutInflater inflater = LayoutInflater.from(builder.getContext());
+        final DatePicker datePicker;
+        if (type == DATETIME_TYPE_DATE || type == DATETIME_TYPE_MONTH ||
+            type == DATETIME_TYPE_WEEK || type == DATETIME_TYPE_DATETIME_LOCAL) {
+            final int resId = builder.getContext().getResources().getIdentifier(
+                    "date_picker_dialog", "layout", "android");
+            DatePicker picker = null;
+            if (resId != 0) {
+                try {
+                    picker = (DatePicker) inflater.inflate(resId, /* root */ null);
+                } catch (final ClassCastException|InflateException e) {
+                }
+            }
+            if (picker == null) {
+                picker = new DatePicker(builder.getContext());
+            }
+            picker.init(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH),
+                        cal.get(Calendar.DAY_OF_MONTH), /* listener */ null);
+            if (minDate != null) {
+                picker.setMinDate(minDate.getTime());
+            }
+            if (maxDate != null) {
+                picker.setMaxDate(maxDate.getTime());
+            }
+            datePicker = picker;
+        } else {
+            datePicker = null;
+        }
+
+        final TimePicker timePicker;
+        if (type == DATETIME_TYPE_TIME || type == DATETIME_TYPE_DATETIME_LOCAL) {
+            final int resId = builder.getContext().getResources().getIdentifier(
+                    "time_picker_dialog", "layout", "android");
+            TimePicker picker = null;
+            if (resId != 0) {
+                try {
+                    picker = (TimePicker) inflater.inflate(resId, /* root */ null);
+                } catch (final ClassCastException|InflateException e) {
+                }
+            }
+            if (picker == null) {
+                picker = new TimePicker(builder.getContext());
+            }
+            setTimePickerTime(picker, cal);
+            picker.setIs24HourView(DateFormat.is24HourFormat(builder.getContext()));
+            timePicker = picker;
+        } else {
+            timePicker = null;
+        }
+
+        final LinearLayout container = addStandardLayout(builder, title, /* msg */ null);
+        container.setPadding(/* left */ 0, /* top */ 0, /* right */ 0, /* bottom */ 0);
+        if (datePicker != null) {
+            container.addView(datePicker);
+        }
+        if (timePicker != null) {
+            container.addView(timePicker);
+        }
+
+        final DialogInterface.OnClickListener listener =
+                new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(final DialogInterface dialog, final int which) {
+                if (which == DialogInterface.BUTTON_NEUTRAL) {
+                    // Clear
+                    callback.confirm("");
+                    return;
+                }
+                if (datePicker != null) {
+                    cal.set(datePicker.getYear(), datePicker.getMonth(),
+                            datePicker.getDayOfMonth());
+                }
+                if (timePicker != null) {
+                    setCalendarTime(cal, timePicker);
+                }
+                callback.confirm(formatter.format(cal.getTime()));
+            }
+        };
+        builder.setNegativeButton(android.R.string.cancel, /* listener */ null)
+               .setNeutralButton(R.string.geckoview_clear_field, listener)
+               .setPositiveButton(android.R.string.ok, listener);
+        createStandardDialog(builder, callback).show();
+    }
+
+    @TargetApi(19)
+    public void onFilePrompt(GeckoSession session, String title, int type,
+                              String[] mimeTypes, FileCallback callback)
+    {
+        final Activity activity = mActivity.get();
+        if (activity == null) {
+            callback.dismiss();
+            return;
+        }
+
+        // Merge all given MIME types into one, using wildcard if needed.
+        String mimeType = null;
+        String mimeSubtype = null;
+        for (final String rawType : mimeTypes) {
+            final String normalizedType = rawType.trim().toLowerCase(Locale.ROOT);
+            final int len = normalizedType.length();
+            int slash = normalizedType.indexOf('/');
+            if (slash < 0) {
+                slash = len;
+            }
+            final String newType = normalizedType.substring(0, slash);
+            final String newSubtype = normalizedType.substring(Math.min(slash + 1, len));
+            if (mimeType == null) {
+                mimeType = newType;
+            } else if (!mimeType.equals(newType)) {
+                mimeType = "*";
+            }
+            if (mimeSubtype == null) {
+                mimeSubtype = newSubtype;
+            } else if (!mimeSubtype.equals(newSubtype)) {
+                mimeSubtype = "*";
+            }
+        }
+
+        final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+        intent.setType((mimeType != null ? mimeType : "*") + '/' +
+                       (mimeSubtype != null ? mimeSubtype : "*"));
+        intent.addCategory(Intent.CATEGORY_OPENABLE);
+        intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
+        if (Build.VERSION.SDK_INT >= 18 && type == FILE_TYPE_MULTIPLE) {
+            intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
+        }
+        if (Build.VERSION.SDK_INT >= 19 && mimeTypes.length > 0) {
+            intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
+        }
+
+        try {
+            mFileType = type;
+            mFileCallback = callback;
+            activity.startActivityForResult(intent, filePickerRequestCode);
+        } catch (final ActivityNotFoundException e) {
+            Log.e(LOGTAG, "Cannot launch activity", e);
+            callback.dismiss();
+        }
+    }
+
+    /**
+     * The app must call this if it receives a callback in its own
+     * {@link Activity#onActivityResult(int, int, Intent)} with a requestCode
+     * that matches {@link #filePickerRequestCode}.
+     * @param resultCode The resultCode from {@link Activity#onActivityResult(int, int, Intent)}
+     * @param data the data from {@link Activity#onActivityResult(int, int, Intent)}
+     */
+    public void onFileCallbackResult(final int resultCode, final Intent data) {
+        final Activity activity = mActivity.get();
+        if (activity == null || mFileCallback == null) {
+            return;
+        }
+
+        final FileCallback callback = mFileCallback;
+        mFileCallback = null;
+
+        if (resultCode != Activity.RESULT_OK || data == null) {
+            callback.dismiss();
+            return;
+        }
+
+        final Uri uri = data.getData();
+        final ClipData clip = data.getClipData();
+
+        if (mFileType == FILE_TYPE_SINGLE ||
+            (mFileType == FILE_TYPE_MULTIPLE && clip == null)) {
+            callback.confirm(activity, uri);
+
+        } else if (mFileType == FILE_TYPE_MULTIPLE) {
+            if (clip == null) {
+                Log.w(LOGTAG, "No selected file");
+                callback.dismiss();
+                return;
+            }
+            final int count = clip.getItemCount();
+            final ArrayList<Uri> uris = new ArrayList<>(count);
+            for (int i = 0; i < count; i++) {
+                uris.add(clip.getItemAt(i).getUri());
+            }
+            callback.confirm(activity, uris.toArray(new Uri[uris.size()]));
+        }
+    }
+
+    public void onPermissionPrompt(final GeckoSession session, final String title,
+                                    final GeckoSession.PermissionDelegate.Callback callback) {
+        final Activity activity = mActivity.get();
+        if (activity == null) {
+            callback.reject();
+            return;
+        }
+        final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+        builder.setTitle(title)
+               .setNegativeButton(android.R.string.cancel, /* onClickListener */ null)
+               .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+                   @Override
+                   public void onClick(final DialogInterface dialog, final int which) {
+                       callback.grant();
+                   }
+               });
+
+        final AlertDialog dialog = builder.create();
+        dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
+                   @Override
+                   public void onDismiss(final DialogInterface dialog) {
+                       callback.reject();
+                   }
+               });
+        dialog.show();
+    }
+
+    private Spinner addMediaSpinner(final Context context, final ViewGroup container,
+                                    final MediaSource[] sources, final String[] sourceNames) {
+        final ArrayAdapter<MediaSource> adapter = new ArrayAdapter<MediaSource>(
+                context, android.R.layout.simple_spinner_item) {
+            private View convertView(final int position, final View view) {
+                if (view != null) {
+                    final MediaSource item = getItem(position);
+                    ((TextView) view).setText(sourceNames != null ? sourceNames[position] : item.name);
+                }
+                return view;
+            }
+
+            @Override
+            public View getView(final int position, View view,
+                                final ViewGroup parent) {
+                return convertView(position, super.getView(position, view, parent));
+            }
+
+            @Override
+            public View getDropDownView(final int position, final View view,
+                                        final ViewGroup parent) {
+                return convertView(position, super.getDropDownView(position, view, parent));
+            }
+        };
+        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+        adapter.addAll(sources);
+
+        final Spinner spinner = new Spinner(context);
+        spinner.setAdapter(adapter);
+        spinner.setSelection(0);
+        container.addView(spinner);
+        return spinner;
+    }
+
+    public void onMediaPrompt(final GeckoSession session, final String title,
+                               final MediaSource[] video, final MediaSource[] audio,
+                               final String[] videoNames, final String[] audioNames,
+                               final GeckoSession.PermissionDelegate.MediaCallback callback) {
+        final Activity activity = mActivity.get();
+        if (activity == null || (video == null && audio == null)) {
+            callback.reject();
+            return;
+        }
+        final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+        final LinearLayout container = addStandardLayout(builder, title, /* msg */ null);
+
+        final Spinner videoSpinner;
+        if (video != null) {
+            videoSpinner = addMediaSpinner(builder.getContext(), container, video, videoNames);
+        } else {
+            videoSpinner = null;
+        }
+
+        final Spinner audioSpinner;
+        if (audio != null) {
+            audioSpinner = addMediaSpinner(builder.getContext(), container, audio, audioNames);
+        } else {
+            audioSpinner = null;
+        }
+
+        builder.setNegativeButton(android.R.string.cancel, /* listener */ null)
+               .setPositiveButton(android.R.string.ok,
+                                  new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(final DialogInterface dialog, final int which) {
+                        final MediaSource video = (videoSpinner != null)
+                                ? (MediaSource) videoSpinner.getSelectedItem() : null;
+                        final MediaSource audio = (audioSpinner != null)
+                                ? (MediaSource) audioSpinner.getSelectedItem() : null;
+                        callback.grant(video, audio);
+                    }
+                });
+
+        final AlertDialog dialog = builder.create();
+        dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
+                    @Override
+                    public void onDismiss(final DialogInterface dialog) {
+                        callback.reject();
+                    }
+                });
+        dialog.show();
+    }
+
+    public void onMediaPrompt(final GeckoSession session, final String title,
+                               final MediaSource[] video, final MediaSource[] audio,
+                               final GeckoSession.PermissionDelegate.MediaCallback callback) {
+        onMediaPrompt(session, title, video, audio, null, null, callback);
+    }
+
+    /**
+     * The app must call this if it receives a callback in its own
+     * {@link Activity#onRequestPermissionsResult(int, String[], int[])} with a
+     * requestCode that match {@link #androidPermissionRequestCode}.
+     *
+     * @param permissions the permissions from {@link Activity#onRequestPermissionsResult(int, String[], int[])}
+     * @param grantResults the grantResults from {@link Activity#onRequestPermissionsResult(int, String[], int[])}
+     */
+    public void onRequestPermissionsResult(final String[] permissions,
+                                           final int[] grantResults) {
+        if (mPermissionCallback == null) {
+            return;
+        }
+
+        final Callback cb = mPermissionCallback;
+        mPermissionCallback = null;
+        for (final int result : grantResults) {
+            if (result != PackageManager.PERMISSION_GRANTED) {
+                // At least one permission was not granted.
+                cb.reject();
+                return;
+            }
+        }
+        cb.grant();
+    }
+
+    @Override
+    public void onAndroidPermissionsRequest(final GeckoSession session, final String[] permissions,
+                                            final Callback callback) {
+        if (Build.VERSION.SDK_INT < 23) {
+            // requestPermissions was introduced in API 23.
+            callback.grant();
+            return;
+        }
+        mPermissionCallback = callback;
+
+        final Activity activity = mActivity.get();
+        if (activity != null) {
+            activity.requestPermissions(permissions, androidPermissionRequestCode);
+        }
+    }
+
+    @Override
+    public void onContentPermissionRequest(final GeckoSession session, final String uri,
+                                           final int type, final String access,
+                                           final Callback callback) {
+        final Activity activity = mActivity.get();
+        if (activity == null) {
+            callback.reject();
+            return;
+        }
+
+        final int resId;
+        if (PERMISSION_GEOLOCATION == type) {
+            resId = R.string.geckoview_request_geolocation;
+        } else if (PERMISSION_DESKTOP_NOTIFICATION == type) {
+            resId = R.string.geckoview_request_notification;
+        } else {
+            Log.w(LOGTAG, "Unknown permission: " + type);
+            callback.reject();
+            return;
+        }
+
+        final String title = activity.getString(resId, Uri.parse(uri).getAuthority());
+        onPermissionPrompt(session, title, callback);
+    }
+
+    private String[] normalizeMediaName(final MediaSource[] sources) {
+        final Activity activity = mActivity.get();
+        if (activity == null || sources == null) {
+            return null;
+        }
+
+        String[] res = new String[sources.length];
+        for (int i = 0; i < sources.length; i++) {
+            final int mediaSource = sources[i].source;
+            final String name = sources[i].name;
+            if (MediaSource.SOURCE_CAMERA == mediaSource) {
+                if (name.toLowerCase(Locale.ENGLISH).contains("front")) {
+                    res[i] = activity.getString(R.string.geckoview_media_front_camera);
+                } else {
+                    res[i] = activity.getString(R.string.geckoview_media_back_camera);
+                }
+            } else if (!name.isEmpty()) {
+                res[i] = name;
+            } else if (MediaSource.SOURCE_MICROPHONE == mediaSource) {
+                res[i] = activity.getString(R.string.geckoview_media_microphone);
+            } else {
+                res[i] = activity.getString(R.string.geckoview_media_other);
+            }
+        }
+
+        return res;
+    }
+
+    @Override
+    public void onMediaPermissionRequest(final GeckoSession session, final String uri,
+                                         final MediaSource[] video, final MediaSource[] audio,
+                                         final MediaCallback callback) {
+        final Activity activity = mActivity.get();
+        if (activity == null) {
+            callback.reject();
+            return;
+        }
+
+        final String host = Uri.parse(uri).getAuthority();
+        final String title;
+        if (audio == null) {
+            title = activity.getString(R.string.geckoview_request_video, host);
+        } else if (video == null) {
+            title = activity.getString(R.string.geckoview_request_audio, host);
+        } else {
+            title = activity.getString(R.string.geckoview_request_media, host);
+        }
+
+        String[] videoNames = normalizeMediaName(video);
+        String[] audioNames = normalizeMediaName(audio);
+
+        onMediaPrompt(session, title, video, audio, videoNames, audioNames, callback);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/res/values/strings.xml
@@ -0,0 +1,15 @@
+<resources>
+    <!-- Keep changes here in sync with the Javadoc for GeckBasicPromptPermissionDelegate -->
+    <string name="geckoview_username">Username</string>
+    <string name="geckoview_password">Password</string>
+    <string name="geckoview_clear_field">Clear</string>
+    <string name="geckoview_request_geolocation">Share location with "%1$s"?</string>
+    <string name="geckoview_request_notification">Allow notifications for "%1$s"?</string>
+    <string name="geckoview_request_video">Share video with "%1$s"</string>
+    <string name="geckoview_request_audio">Share audio with "%1$s"</string>
+    <string name="geckoview_request_media">Share video and audio with "%1$s"</string>
+    <string name="geckoview_media_back_camera">Back camera</string>
+    <string name="geckoview_media_front_camera">Front camera</string>
+    <string name="geckoview_media_microphone">Microphone</string>
+    <string name="geckoview_media_other">Unknown source</string>
+</resources>
deleted file mode 100644
--- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/BasicGeckoViewPrompt.java
+++ /dev/null
@@ -1,916 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.geckoview_example;
-
-import android.annotation.TargetApi;
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.content.ActivityNotFoundException;
-import android.content.ClipData;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.res.TypedArray;
-import android.graphics.Color;
-import android.graphics.PorterDuff;
-import android.net.Uri;
-import android.os.Build;
-import android.text.InputType;
-import android.text.format.DateFormat;
-import android.util.Log;
-import android.view.InflateException;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.ArrayAdapter;
-import android.widget.CheckBox;
-import android.widget.CheckedTextView;
-import android.widget.CompoundButton;
-import android.widget.DatePicker;
-import android.widget.EditText;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.ListView;
-import android.widget.ScrollView;
-import android.widget.Spinner;
-import android.widget.TextView;
-import android.widget.TimePicker;
-
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.Locale;
-
-import org.mozilla.geckoview.GeckoSession;
-import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource;
-
-final class BasicGeckoViewPrompt implements GeckoSession.PromptDelegate {
-    protected static final String LOGTAG = "BasicGeckoViewPrompt";
-
-    private final Activity mActivity;
-    public int filePickerRequestCode = 1;
-    private int mFileType;
-    private FileCallback mFileCallback;
-
-    public BasicGeckoViewPrompt(final Activity activity) {
-        mActivity = activity;
-    }
-
-    private AlertDialog.Builder addCheckbox(final AlertDialog.Builder builder,
-                                            ViewGroup parent,
-                                            final AlertCallback callback) {
-        if (!callback.hasCheckbox()) {
-            return builder;
-        }
-        final CheckBox checkbox = new CheckBox(builder.getContext());
-        if (callback.getCheckboxMessage() != null) {
-            checkbox.setText(callback.getCheckboxMessage());
-        }
-        checkbox.setChecked(callback.getCheckboxValue());
-        checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
-            @Override
-            public void onCheckedChanged(final CompoundButton button,
-                                         final boolean checked) {
-                callback.setCheckboxValue(checked);
-            }
-        });
-        if (parent == null) {
-            final int padding = getViewPadding(builder);
-            parent = new FrameLayout(builder.getContext());
-            parent.setPadding(/* left */ padding, /* top */ 0,
-                              /* right */ padding, /* bottom */ 0);
-            builder.setView(parent);
-        }
-        parent.addView(checkbox);
-        return builder;
-    }
-
-    public void onAlert(final GeckoSession session, final String title, final String msg,
-                      final AlertCallback callback) {
-        final Activity activity = mActivity;
-        if (activity == null) {
-            callback.dismiss();
-            return;
-        }
-        final AlertDialog.Builder builder = new AlertDialog.Builder(activity)
-                .setTitle(title)
-                .setMessage(msg)
-                .setPositiveButton(android.R.string.ok, /* onClickListener */ null);
-        createStandardDialog(addCheckbox(builder, /* parent */ null, callback),
-                             callback).show();
-    }
-
-    public void onButtonPrompt(final GeckoSession session, final String title,
-                                final String msg, final String[] btnMsg,
-                                final ButtonCallback callback) {
-        final Activity activity = mActivity;
-        if (activity == null) {
-            callback.dismiss();
-            return;
-        }
-        final AlertDialog.Builder builder = new AlertDialog.Builder(activity)
-                .setTitle(title)
-                .setMessage(msg);
-        final DialogInterface.OnClickListener listener =
-            new DialogInterface.OnClickListener() {
-                @Override
-                public void onClick(final DialogInterface dialog, final int which) {
-                    if (which == DialogInterface.BUTTON_POSITIVE) {
-                        callback.confirm(BUTTON_TYPE_POSITIVE);
-                    } else if (which == DialogInterface.BUTTON_NEUTRAL) {
-                        callback.confirm(BUTTON_TYPE_NEUTRAL);
-                    } else if (which == DialogInterface.BUTTON_NEGATIVE) {
-                        callback.confirm(BUTTON_TYPE_NEGATIVE);
-                    } else {
-                        callback.dismiss();
-                    }
-                }
-            };
-        if (btnMsg[BUTTON_TYPE_POSITIVE] != null) {
-            builder.setPositiveButton(btnMsg[BUTTON_TYPE_POSITIVE], listener);
-        }
-        if (btnMsg[BUTTON_TYPE_NEUTRAL] != null) {
-            builder.setNeutralButton(btnMsg[BUTTON_TYPE_NEUTRAL], listener);
-        }
-        if (btnMsg[BUTTON_TYPE_NEGATIVE] != null) {
-            builder.setNegativeButton(btnMsg[BUTTON_TYPE_NEGATIVE], listener);
-        }
-        createStandardDialog(addCheckbox(builder, /* parent */ null, callback),
-                             callback).show();
-    }
-
-    private int getViewPadding(final AlertDialog.Builder builder) {
-        final TypedArray attr = builder.getContext().obtainStyledAttributes(
-                new int[] { android.R.attr.listPreferredItemPaddingLeft });
-        final int padding = attr.getDimensionPixelSize(0, 1);
-        attr.recycle();
-        return padding;
-    }
-
-    private LinearLayout addStandardLayout(final AlertDialog.Builder builder,
-                                           final String title, final String msg) {
-        final ScrollView scrollView = new ScrollView(builder.getContext());
-        final LinearLayout container = new LinearLayout(builder.getContext());
-        final int horizontalPadding = getViewPadding(builder);
-        final int verticalPadding = (msg == null || msg.isEmpty()) ? horizontalPadding : 0;
-        container.setOrientation(LinearLayout.VERTICAL);
-        container.setPadding(/* left */ horizontalPadding, /* top */ verticalPadding,
-                             /* right */ horizontalPadding, /* bottom */ verticalPadding);
-        scrollView.addView(container);
-        builder.setTitle(title)
-               .setMessage(msg)
-               .setView(scrollView);
-        return container;
-    }
-
-    private AlertDialog createStandardDialog(final AlertDialog.Builder builder,
-                                             final AlertCallback callback) {
-        final AlertDialog dialog = builder.create();
-        dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
-                    @Override
-                    public void onDismiss(final DialogInterface dialog) {
-                        callback.dismiss();
-                    }
-                });
-        return dialog;
-    }
-
-    public void onTextPrompt(final GeckoSession session, final String title,
-                              final String msg, final String value,
-                              final TextCallback callback) {
-        final Activity activity = mActivity;
-        if (activity == null) {
-            callback.dismiss();
-            return;
-        }
-        final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
-        final LinearLayout container = addStandardLayout(builder, title, msg);
-        final EditText editText = new EditText(builder.getContext());
-        editText.setText(value);
-        container.addView(editText);
-
-        builder.setNegativeButton(android.R.string.cancel, /* listener */ null)
-               .setPositiveButton(android.R.string.ok,
-                                  new DialogInterface.OnClickListener() {
-                    @Override
-                    public void onClick(final DialogInterface dialog, final int which) {
-                        callback.confirm(editText.getText().toString());
-                    }
-                });
-
-        createStandardDialog(addCheckbox(builder, container, callback), callback).show();
-    }
-
-    public void onAuthPrompt(final GeckoSession session, final String title,
-                              final String msg, final AuthOptions options,
-                              final AuthCallback callback) {
-        final Activity activity = mActivity;
-        if (activity == null) {
-            callback.dismiss();
-            return;
-        }
-        final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
-        final LinearLayout container = addStandardLayout(builder, title, msg);
-
-        final int flags = options.flags;
-        final int level = options.level;
-        final EditText username;
-        if ((flags & AuthOptions.AUTH_FLAG_ONLY_PASSWORD) == 0) {
-            username = new EditText(builder.getContext());
-            username.setHint(R.string.username);
-            username.setText(options.username);
-            container.addView(username);
-        } else {
-            username = null;
-        }
-
-        final EditText password = new EditText(builder.getContext());
-        password.setHint(R.string.password);
-        password.setText(options.password);
-        password.setInputType(InputType.TYPE_CLASS_TEXT |
-                              InputType.TYPE_TEXT_VARIATION_PASSWORD);
-        container.addView(password);
-
-        if (level != AuthOptions.AUTH_LEVEL_NONE) {
-            final ImageView secure = new ImageView(builder.getContext());
-            secure.setImageResource(android.R.drawable.ic_lock_lock);
-            container.addView(secure);
-        }
-
-        builder.setNegativeButton(android.R.string.cancel, /* listener */ null)
-               .setPositiveButton(android.R.string.ok,
-                                  new DialogInterface.OnClickListener() {
-                    @Override
-                    public void onClick(final DialogInterface dialog, final int which) {
-                        if ((flags & AuthOptions.AUTH_FLAG_ONLY_PASSWORD) == 0) {
-                            callback.confirm(username.getText().toString(),
-                                             password.getText().toString());
-                        } else {
-                            callback.confirm(password.getText().toString());
-                        }
-                    }
-                });
-        createStandardDialog(addCheckbox(builder, container, callback), callback).show();
-    }
-
-    private static class ModifiableChoice {
-        public boolean modifiableSelected;
-        public String modifiableLabel;
-        public final Choice choice;
-
-        public ModifiableChoice(Choice c) {
-            choice = c;
-            modifiableSelected = choice.selected;
-            modifiableLabel = choice.label;
-        }
-    }
-
-    private void addChoiceItems(final int type, final ArrayAdapter<ModifiableChoice> list,
-                                final Choice[] items, final String indent) {
-        if (type == Choice.CHOICE_TYPE_MENU) {
-            for (final Choice item : items) {
-                list.add(new ModifiableChoice(item));
-            }
-            return;
-        }
-
-        for (final Choice item : items) {
-            final ModifiableChoice modItem = new ModifiableChoice(item);
-
-            final Choice[] children = item.items;
-
-            if (indent != null && children == null) {
-                modItem.modifiableLabel = indent + modItem.modifiableLabel;
-            }
-            list.add(modItem);
-
-            if (children != null) {
-                final String newIndent;
-                if (type == Choice.CHOICE_TYPE_SINGLE || type == Choice.CHOICE_TYPE_MULTIPLE) {
-                    newIndent = (indent != null) ? indent + '\t' : "\t";
-                } else {
-                    newIndent = null;
-                }
-                addChoiceItems(type, list, children, newIndent);
-            }
-        }
-    }
-
-    public void onChoicePrompt(final GeckoSession session, final String title,
-                                final String msg, final int type,
-                                final Choice[] choices, final ChoiceCallback callback) {
-        final Activity activity = mActivity;
-        if (activity == null) {
-            callback.dismiss();
-            return;
-        }
-        final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
-        addStandardLayout(builder, title, msg);
-
-        final ListView list = new ListView(builder.getContext());
-        if (type == Choice.CHOICE_TYPE_MULTIPLE) {
-            list.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
-        }
-
-        final ArrayAdapter<ModifiableChoice> adapter = new ArrayAdapter<ModifiableChoice>(
-                builder.getContext(), android.R.layout.simple_list_item_1) {
-            private static final int TYPE_MENU_ITEM = 0;
-            private static final int TYPE_MENU_CHECK = 1;
-            private static final int TYPE_SEPARATOR = 2;
-            private static final int TYPE_GROUP = 3;
-            private static final int TYPE_SINGLE = 4;
-            private static final int TYPE_MULTIPLE = 5;
-            private static final int TYPE_COUNT = 6;
-
-            private LayoutInflater mInflater;
-            private View mSeparator;
-
-            @Override
-            public int getViewTypeCount() {
-                return TYPE_COUNT;
-            }
-
-            @Override
-            public int getItemViewType(final int position) {
-                final ModifiableChoice item = getItem(position);
-                if (item.choice.separator) {
-                    return TYPE_SEPARATOR;
-                } else if (type == Choice.CHOICE_TYPE_MENU) {
-                    return item.modifiableSelected ? TYPE_MENU_CHECK : TYPE_MENU_ITEM;
-                } else if (item.choice.items != null) {
-                    return TYPE_GROUP;
-                } else if (type == Choice.CHOICE_TYPE_SINGLE) {
-                    return TYPE_SINGLE;
-                } else if (type == Choice.CHOICE_TYPE_MULTIPLE) {
-                    return TYPE_MULTIPLE;
-                } else {
-                    throw new UnsupportedOperationException();
-                }
-            }
-
-            @Override
-            public boolean isEnabled(final int position) {
-                final ModifiableChoice item = getItem(position);
-                return !item.choice.separator && !item.choice.disabled &&
-                        ((type != Choice.CHOICE_TYPE_SINGLE && type != Choice.CHOICE_TYPE_MULTIPLE) ||
-                         item.choice.items == null);
-            }
-
-            @Override
-            public View getView(final int position, View view,
-                                final ViewGroup parent) {
-                final int itemType = getItemViewType(position);
-                final int layoutId;
-                if (itemType == TYPE_SEPARATOR) {
-                    if (mSeparator == null) {
-                        mSeparator = new View(getContext());
-                        mSeparator.setLayoutParams(new ListView.LayoutParams(
-                                ViewGroup.LayoutParams.MATCH_PARENT, 2, itemType));
-                        final TypedArray attr = getContext().obtainStyledAttributes(
-                                new int[] { android.R.attr.listDivider });
-                        mSeparator.setBackgroundResource(attr.getResourceId(0, 0));
-                        attr.recycle();
-                    }
-                    return mSeparator;
-                } else if (itemType == TYPE_MENU_ITEM) {
-                    layoutId = android.R.layout.simple_list_item_1;
-                } else if (itemType == TYPE_MENU_CHECK) {
-                    layoutId = android.R.layout.simple_list_item_checked;
-                } else if (itemType == TYPE_GROUP) {
-                    layoutId = android.R.layout.preference_category;
-                } else if (itemType == TYPE_SINGLE) {
-                    layoutId = android.R.layout.simple_list_item_single_choice;
-                } else if (itemType == TYPE_MULTIPLE) {
-                    layoutId = android.R.layout.simple_list_item_multiple_choice;
-                } else {
-                    throw new UnsupportedOperationException();
-                }
-
-                if (view == null) {
-                    if (mInflater == null) {
-                        mInflater = LayoutInflater.from(builder.getContext());
-                    }
-                    view = mInflater.inflate(layoutId, parent, false);
-                }
-
-                final ModifiableChoice item = getItem(position);
-                final TextView text = (TextView) view;
-                text.setEnabled(!item.choice.disabled);
-                text.setText(item.modifiableLabel);
-                if (view instanceof CheckedTextView) {
-                    final boolean selected = item.modifiableSelected;
-                    if (itemType == TYPE_MULTIPLE) {
-                        list.setItemChecked(position, selected);
-                    } else {
-                        ((CheckedTextView) view).setChecked(selected);
-                    }
-                }
-                return view;
-            }
-        };
-        addChoiceItems(type, adapter, choices, /* indent */ null);
-
-        list.setAdapter(adapter);
-        builder.setView(list);
-
-        final AlertDialog dialog;
-        if (type == Choice.CHOICE_TYPE_SINGLE || type == Choice.CHOICE_TYPE_MENU) {
-            dialog = createStandardDialog(builder, callback);
-            list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
-                @Override
-                public void onItemClick(final AdapterView<?> parent, final View v,
-                                        final int position, final long id) {
-                    final ModifiableChoice item = adapter.getItem(position);
-                    if (type == Choice.CHOICE_TYPE_MENU) {
-                        final Choice[] children = item.choice.items;
-                        if (children != null) {
-                            // Show sub-menu.
-                            dialog.setOnDismissListener(null);
-                            dialog.dismiss();
-                            onChoicePrompt(session, item.modifiableLabel, /* msg */ null,
-                                            type, children, callback);
-                            return;
-                        }
-                    }
-                    callback.confirm(item.choice);
-                    dialog.dismiss();
-                }
-            });
-        } else if (type == Choice.CHOICE_TYPE_MULTIPLE) {
-            list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
-                @Override
-                public void onItemClick(final AdapterView<?> parent, final View v,
-                                        final int position, final long id) {
-                    final ModifiableChoice item = adapter.getItem(position);
-                    item.modifiableSelected = ((CheckedTextView) v).isChecked();
-                }
-            });
-            builder.setNegativeButton(android.R.string.cancel, /* listener */ null)
-                   .setPositiveButton(android.R.string.ok,
-                                      new DialogInterface.OnClickListener() {
-                @Override
-                public void onClick(final DialogInterface dialog,
-                                    final int which) {
-                    final int len = adapter.getCount();
-                    ArrayList<String> items = new ArrayList<>(len);
-                    for (int i = 0; i < len; i++) {
-                        final ModifiableChoice item = adapter.getItem(i);
-                        if (item.modifiableSelected) {
-                            items.add(item.choice.id);
-                        }
-                    }
-                    callback.confirm(items.toArray(new String[items.size()]));
-                }
-            });
-            dialog = createStandardDialog(builder, callback);
-        } else {
-            throw new UnsupportedOperationException();
-        }
-        dialog.show();
-    }
-
-    private static int parseColor(final String value, final int def) {
-        try {
-            return Color.parseColor(value);
-        } catch (final IllegalArgumentException e) {
-            return def;
-        }
-    }
-
-    public void onColorPrompt(final GeckoSession session, final String title,
-                               final String value, final TextCallback callback)
-    {
-        final Activity activity = mActivity;
-        if (activity == null) {
-            callback.dismiss();
-            return;
-        }
-        final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
-        addStandardLayout(builder, title, /* msg */ null);
-
-        final int initial = parseColor(value, /* def */ 0);
-        final ArrayAdapter<Integer> adapter = new ArrayAdapter<Integer>(
-                builder.getContext(), android.R.layout.simple_list_item_1) {
-            private LayoutInflater mInflater;
-
-            @Override
-            public int getViewTypeCount() {
-                return 2;
-            }
-
-            @Override
-            public int getItemViewType(final int position) {
-                return (getItem(position) == initial) ? 1 : 0;
-            }
-
-            @Override
-            public View getView(final int position, View view,
-                                final ViewGroup parent) {
-                if (mInflater == null) {
-                    mInflater = LayoutInflater.from(builder.getContext());
-                }
-                final int color = getItem(position);
-                if (view == null) {
-                    view = mInflater.inflate((color == initial) ?
-                            android.R.layout.simple_list_item_checked :
-                            android.R.layout.simple_list_item_1, parent, false);
-                }
-                view.setBackgroundResource(android.R.drawable.editbox_background);
-                view.getBackground().setColorFilter(color, PorterDuff.Mode.MULTIPLY);
-                return view;
-            }
-        };
-
-        adapter.addAll(0xffff4444 /* holo_red_light */,
-                       0xffcc0000 /* holo_red_dark */,
-                       0xffffbb33 /* holo_orange_light */,
-                       0xffff8800 /* holo_orange_dark */,
-                       0xff99cc00 /* holo_green_light */,
-                       0xff669900 /* holo_green_dark */,
-                       0xff33b5e5 /* holo_blue_light */,
-                       0xff0099cc /* holo_blue_dark */,
-                       0xffaa66cc /* holo_purple */,
-                       0xffffffff /* white */,
-                       0xffaaaaaa /* lighter_gray */,
-                       0xff555555 /* darker_gray */,
-                       0xff000000 /* black */);
-
-        final ListView list = new ListView(builder.getContext());
-        list.setAdapter(adapter);
-        builder.setView(list);
-
-        final AlertDialog dialog = createStandardDialog(builder, callback);
-        list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
-            @Override
-            public void onItemClick(final AdapterView<?> parent, final View v,
-                                    final int position, final long id) {
-                callback.confirm(String.format("#%06x", 0xffffff & adapter.getItem(position)));
-                dialog.dismiss();
-            }
-        });
-        dialog.show();
-    }
-
-    private static Date parseDate(final SimpleDateFormat formatter,
-                                  final String value,
-                                  final boolean defaultToNow) {
-        try {
-            if (value != null && !value.isEmpty()) {
-                return formatter.parse(value);
-            }
-        } catch (final ParseException e) {
-        }
-        return defaultToNow ? new Date() : null;
-    }
-
-    @SuppressWarnings("deprecation")
-    private static void setTimePickerTime(final TimePicker picker, final Calendar cal) {
-        if (Build.VERSION.SDK_INT >= 23) {
-            picker.setHour(cal.get(Calendar.HOUR_OF_DAY));
-            picker.setMinute(cal.get(Calendar.MINUTE));
-        } else {
-            picker.setCurrentHour(cal.get(Calendar.HOUR_OF_DAY));
-            picker.setCurrentMinute(cal.get(Calendar.MINUTE));
-        }
-    }
-
-    @SuppressWarnings("deprecation")
-    private static void setCalendarTime(final Calendar cal, final TimePicker picker) {
-        if (Build.VERSION.SDK_INT >= 23) {
-            cal.set(Calendar.HOUR_OF_DAY, picker.getHour());
-            cal.set(Calendar.MINUTE, picker.getMinute());
-        } else {
-            cal.set(Calendar.HOUR_OF_DAY, picker.getCurrentHour());
-            cal.set(Calendar.MINUTE, picker.getCurrentMinute());
-        }
-    }
-
-    public void onDateTimePrompt(final GeckoSession session, final String title,
-                                  final int type, final String value, final String min,
-                                  final String max, final TextCallback callback) {
-        final Activity activity = mActivity;
-        if (activity == null) {
-            callback.dismiss();
-            return;
-        }
-        final String format;
-        if (type == DATETIME_TYPE_DATE) {
-            format = "yyyy-MM-dd";
-        } else if (type == DATETIME_TYPE_MONTH) {
-            format = "yyyy-MM";
-        } else if (type == DATETIME_TYPE_WEEK) {
-            format = "yyyy-'W'ww";
-        } else if (type == DATETIME_TYPE_TIME) {
-            format = "HH:mm";
-        } else if (type == DATETIME_TYPE_DATETIME_LOCAL) {
-            format = "yyyy-MM-dd'T'HH:mm";
-        } else {
-            throw new UnsupportedOperationException();
-        }
-
-        final SimpleDateFormat formatter = new SimpleDateFormat(format, Locale.ROOT);
-        final Date minDate = parseDate(formatter, min, /* defaultToNow */ false);
-        final Date maxDate = parseDate(formatter, max, /* defaultToNow */ false);
-        final Date date = parseDate(formatter, value, /* defaultToNow */ true);
-        final Calendar cal = formatter.getCalendar();
-        cal.setTime(date);
-
-        final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
-        final LayoutInflater inflater = LayoutInflater.from(builder.getContext());
-        final DatePicker datePicker;
-        if (type == DATETIME_TYPE_DATE || type == DATETIME_TYPE_MONTH ||
-            type == DATETIME_TYPE_WEEK || type == DATETIME_TYPE_DATETIME_LOCAL) {
-            final int resId = builder.getContext().getResources().getIdentifier(
-                    "date_picker_dialog", "layout", "android");
-            DatePicker picker = null;
-            if (resId != 0) {
-                try {
-                    picker = (DatePicker) inflater.inflate(resId, /* root */ null);
-                } catch (final ClassCastException|InflateException e) {
-                }
-            }
-            if (picker == null) {
-                picker = new DatePicker(builder.getContext());
-            }
-            picker.init(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH),
-                        cal.get(Calendar.DAY_OF_MONTH), /* listener */ null);
-            if (minDate != null) {
-                picker.setMinDate(minDate.getTime());
-            }
-            if (maxDate != null) {
-                picker.setMaxDate(maxDate.getTime());
-            }
-            datePicker = picker;
-        } else {
-            datePicker = null;
-        }
-
-        final TimePicker timePicker;
-        if (type == DATETIME_TYPE_TIME || type == DATETIME_TYPE_DATETIME_LOCAL) {
-            final int resId = builder.getContext().getResources().getIdentifier(
-                    "time_picker_dialog", "layout", "android");
-            TimePicker picker = null;
-            if (resId != 0) {
-                try {
-                    picker = (TimePicker) inflater.inflate(resId, /* root */ null);
-                } catch (final ClassCastException|InflateException e) {
-                }
-            }
-            if (picker == null) {
-                picker = new TimePicker(builder.getContext());
-            }
-            setTimePickerTime(picker, cal);
-            picker.setIs24HourView(DateFormat.is24HourFormat(builder.getContext()));
-            timePicker = picker;
-        } else {
-            timePicker = null;
-        }
-
-        final LinearLayout container = addStandardLayout(builder, title, /* msg */ null);
-        container.setPadding(/* left */ 0, /* top */ 0, /* right */ 0, /* bottom */ 0);
-        if (datePicker != null) {
-            container.addView(datePicker);
-        }
-        if (timePicker != null) {
-            container.addView(timePicker);
-        }
-
-        final DialogInterface.OnClickListener listener =
-                new DialogInterface.OnClickListener() {
-            @Override
-            public void onClick(final DialogInterface dialog, final int which) {
-                if (which == DialogInterface.BUTTON_NEUTRAL) {
-                    // Clear
-                    callback.confirm("");
-                    return;
-                }
-                if (datePicker != null) {
-                    cal.set(datePicker.getYear(), datePicker.getMonth(),
-                            datePicker.getDayOfMonth());
-                }
-                if (timePicker != null) {
-                    setCalendarTime(cal, timePicker);
-                }
-                callback.confirm(formatter.format(cal.getTime()));
-            }
-        };
-        builder.setNegativeButton(android.R.string.cancel, /* listener */ null)
-               .setNeutralButton(R.string.clear_field, listener)
-               .setPositiveButton(android.R.string.ok, listener);
-        createStandardDialog(builder, callback).show();
-    }
-
-    @TargetApi(19)
-    public void onFilePrompt(GeckoSession session, String title, int type,
-                              String[] mimeTypes, FileCallback callback)
-    {
-        final Activity activity = mActivity;
-        if (activity == null) {
-            callback.dismiss();
-            return;
-        }
-
-        // Merge all given MIME types into one, using wildcard if needed.
-        String mimeType = null;
-        String mimeSubtype = null;
-        for (final String rawType : mimeTypes) {
-            final String normalizedType = rawType.trim().toLowerCase(Locale.ROOT);
-            final int len = normalizedType.length();
-            int slash = normalizedType.indexOf('/');
-            if (slash < 0) {
-                slash = len;
-            }
-            final String newType = normalizedType.substring(0, slash);
-            final String newSubtype = normalizedType.substring(Math.min(slash + 1, len));
-            if (mimeType == null) {
-                mimeType = newType;
-            } else if (!mimeType.equals(newType)) {
-                mimeType = "*";
-            }
-            if (mimeSubtype == null) {
-                mimeSubtype = newSubtype;
-            } else if (!mimeSubtype.equals(newSubtype)) {
-                mimeSubtype = "*";
-            }
-        }
-
-        final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
-        intent.setType((mimeType != null ? mimeType : "*") + '/' +
-                       (mimeSubtype != null ? mimeSubtype : "*"));
-        intent.addCategory(Intent.CATEGORY_OPENABLE);
-        intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
-        if (Build.VERSION.SDK_INT >= 18 && type == FILE_TYPE_MULTIPLE) {
-            intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
-        }
-        if (Build.VERSION.SDK_INT >= 19 && mimeTypes.length > 0) {
-            intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
-        }
-
-        try {
-            mFileType = type;
-            mFileCallback = callback;
-            activity.startActivityForResult(intent, filePickerRequestCode);
-        } catch (final ActivityNotFoundException e) {
-            Log.e(LOGTAG, "Cannot launch activity", e);
-            callback.dismiss();
-        }
-    }
-
-    public void onFileCallbackResult(final int resultCode, final Intent data) {
-        if (mFileCallback == null) {
-            return;
-        }
-
-        final FileCallback callback = mFileCallback;
-        mFileCallback = null;
-
-        if (resultCode != Activity.RESULT_OK || data == null) {
-            callback.dismiss();
-            return;
-        }
-
-        final Uri uri = data.getData();
-        final ClipData clip = data.getClipData();
-
-        if (mFileType == FILE_TYPE_SINGLE ||
-            (mFileType == FILE_TYPE_MULTIPLE && clip == null)) {
-            callback.confirm(mActivity, uri);
-
-        } else if (mFileType == FILE_TYPE_MULTIPLE) {
-            if (clip == null) {
-                Log.w(LOGTAG, "No selected file");
-                callback.dismiss();
-                return;
-            }
-            final int count = clip.getItemCount();
-            final ArrayList<Uri> uris = new ArrayList<>(count);
-            for (int i = 0; i < count; i++) {
-                uris.add(clip.getItemAt(i).getUri());
-            }
-            callback.confirm(mActivity, uris.toArray(new Uri[uris.size()]));
-        }
-    }
-
-    public void onPermissionPrompt(final GeckoSession session, final String title,
-                                    final GeckoSession.PermissionDelegate.Callback callback) {
-        final Activity activity = mActivity;
-        if (activity == null) {
-            callback.reject();
-            return;
-        }
-        final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
-        builder.setTitle(title)
-               .setNegativeButton(android.R.string.cancel, /* onClickListener */ null)
-               .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
-                   @Override
-                   public void onClick(final DialogInterface dialog, final int which) {
-                       callback.grant();
-                   }
-               });
-
-        final AlertDialog dialog = builder.create();
-        dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
-                   @Override
-                   public void onDismiss(final DialogInterface dialog) {
-                       callback.reject();
-                   }
-               });
-        dialog.show();
-    }
-
-    private Spinner addMediaSpinner(final Context context, final ViewGroup container,
-                                    final MediaSource[] sources, final String[] sourceNames) {
-        final ArrayAdapter<MediaSource> adapter = new ArrayAdapter<MediaSource>(
-                context, android.R.layout.simple_spinner_item) {
-            private View convertView(final int position, final View view) {
-                if (view != null) {
-                    final MediaSource item = getItem(position);
-                    ((TextView) view).setText(sourceNames != null ? sourceNames[position] : item.name);
-                }
-                return view;
-            }
-
-            @Override
-            public View getView(final int position, View view,
-                                final ViewGroup parent) {
-                return convertView(position, super.getView(position, view, parent));
-            }
-
-            @Override
-            public View getDropDownView(final int position, final View view,
-                                        final ViewGroup parent) {
-                return convertView(position, super.getDropDownView(position, view, parent));
-            }
-        };
-        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
-        adapter.addAll(sources);
-
-        final Spinner spinner = new Spinner(context);
-        spinner.setAdapter(adapter);
-        spinner.setSelection(0);
-        container.addView(spinner);
-        return spinner;
-    }
-
-    public void onMediaPrompt(final GeckoSession session, final String title,
-                               final MediaSource[] video, final MediaSource[] audio,
-                               final String[] videoNames, final String[] audioNames,
-                               final GeckoSession.PermissionDelegate.MediaCallback callback) {
-        final Activity activity = mActivity;
-        if (activity == null || (video == null && audio == null)) {
-            callback.reject();
-            return;
-        }
-        final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
-        final LinearLayout container = addStandardLayout(builder, title, /* msg */ null);
-
-        final Spinner videoSpinner;
-        if (video != null) {
-            videoSpinner = addMediaSpinner(builder.getContext(), container, video, videoNames);
-        } else {
-            videoSpinner = null;
-        }
-
-        final Spinner audioSpinner;
-        if (audio != null) {
-            audioSpinner = addMediaSpinner(builder.getContext(), container, audio, audioNames);
-        } else {
-            audioSpinner = null;
-        }
-
-        builder.setNegativeButton(android.R.string.cancel, /* listener */ null)
-               .setPositiveButton(android.R.string.ok,
-                                  new DialogInterface.OnClickListener() {
-                    @Override
-                    public void onClick(final DialogInterface dialog, final int which) {
-                        final MediaSource video = (videoSpinner != null)
-                                ? (MediaSource) videoSpinner.getSelectedItem() : null;
-                        final MediaSource audio = (audioSpinner != null)
-                                ? (MediaSource) audioSpinner.getSelectedItem() : null;
-                        callback.grant(video, audio);
-                    }
-                });
-
-        final AlertDialog dialog = builder.create();
-        dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
-                    @Override
-                    public void onDismiss(final DialogInterface dialog) {
-                        callback.reject();
-                    }
-                });
-        dialog.show();
-    }
-
-    public void onMediaPrompt(final GeckoSession session, final String title,
-                               final MediaSource[] video, final MediaSource[] audio,
-                               final GeckoSession.PermissionDelegate.MediaCallback callback) {
-        onMediaPrompt(session, title, video, audio, null, null, callback);
-    }
-}
--- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
@@ -2,41 +2,38 @@
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.geckoview_example;
 
 import org.mozilla.gecko.GeckoThread;
 import org.mozilla.gecko.process.GeckoProcessManager;
+import org.mozilla.geckoview.GeckoBasicPromptPermissionDelegate;
 import org.mozilla.geckoview.GeckoResponse;
 import org.mozilla.geckoview.GeckoRuntime;
 import org.mozilla.geckoview.GeckoRuntimeSettings;
 import org.mozilla.geckoview.GeckoSession;
 import org.mozilla.geckoview.GeckoSession.TrackingProtectionDelegate;
 import org.mozilla.geckoview.GeckoSessionSettings;
 import org.mozilla.geckoview.GeckoView;
 
 import android.content.Intent;
-import android.content.pm.PackageManager;
 import android.net.Uri;
-import android.os.Build;
 import android.os.Bundle;
 import android.os.SystemClock;
 import android.support.v7.app.ActionBar;
 import android.support.v7.app.AppCompatActivity;
 import android.support.v7.widget.Toolbar;
 import android.util.Log;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.WindowManager;
 
-import java.util.Locale;
-
 public class GeckoViewActivity extends AppCompatActivity {
     private static final String LOGTAG = "GeckoViewActivity";
     private static final String DEFAULT_URL = "https://mozilla.org";
     private static final String USE_MULTIPROCESS_EXTRA = "use_multiprocess";
     private static final String SEARCH_URI_BASE = "https://www.google.com/search?q=";
 
     /* package */ static final int REQUEST_FILE_PICKER = 1;
     private static final int REQUEST_PERMISSIONS = 2;
@@ -133,23 +130,21 @@ public class GeckoViewActivity extends A
 
     private void hookupSession(GeckoSession session) {
         session.setContentDelegate(new ExampleContentDelegate());
         final ExampleTrackingProtectionDelegate tp = new ExampleTrackingProtectionDelegate();
         session.setTrackingProtectionDelegate(tp);
         session.setProgressDelegate(new ExampleProgressDelegate(tp));
         session.setNavigationDelegate(new ExampleNavigationDelegate());
 
-        final BasicGeckoViewPrompt prompt = new BasicGeckoViewPrompt(this);
+        final GeckoBasicPromptPermissionDelegate prompt = new GeckoBasicPromptPermissionDelegate(this);
         prompt.filePickerRequestCode = REQUEST_FILE_PICKER;
+        prompt.androidPermissionRequestCode = REQUEST_PERMISSIONS;
         session.setPromptDelegate(prompt);
-
-        final ExamplePermissionDelegate permission = new ExamplePermissionDelegate();
-        permission.androidPermissionRequestCode = REQUEST_PERMISSIONS;
-        session.setPermissionDelegate(permission);
+        session.setPermissionDelegate(prompt);
 
         updateTrackingProtection(session);
     }
 
     private void recreateSession() {
         mGeckoSession.close();
 
         mGeckoSession = createSession();
@@ -254,30 +249,30 @@ public class GeckoViewActivity extends A
         final Uri uri = intent.getData();
         mGeckoSession.loadUri(uri != null ? uri.toString() : DEFAULT_URL);
     }
 
     @Override
     protected void onActivityResult(final int requestCode, final int resultCode,
                                     final Intent data) {
         if (requestCode == REQUEST_FILE_PICKER) {
-            final BasicGeckoViewPrompt prompt = (BasicGeckoViewPrompt)
+            final GeckoBasicPromptPermissionDelegate prompt = (GeckoBasicPromptPermissionDelegate)
                     mGeckoSession.getPromptDelegate();
             prompt.onFileCallbackResult(resultCode, data);
         } else {
             super.onActivityResult(requestCode, resultCode, data);
         }
     }
 
     @Override
     public void onRequestPermissionsResult(final int requestCode,
                                            final String[] permissions,
                                            final int[] grantResults) {
         if (requestCode == REQUEST_PERMISSIONS) {
-            final ExamplePermissionDelegate permission = (ExamplePermissionDelegate)
+            final GeckoBasicPromptPermissionDelegate permission = (GeckoBasicPromptPermissionDelegate)
                     mGeckoSession.getPermissionDelegate();
             permission.onRequestPermissionsResult(permissions, grantResults);
         } else {
             super.onRequestPermissionsResult(requestCode, permissions, grantResults);
         }
     }
 
     private class ExampleContentDelegate implements GeckoSession.ContentDelegate {
@@ -347,122 +342,16 @@ public class GeckoViewActivity extends A
         }
 
         @Override
         public void onSecurityChange(GeckoSession session, SecurityInformation securityInfo) {
             Log.i(LOGTAG, "Security status changed to " + securityInfo.securityMode);
         }
     }
 
-    private class ExamplePermissionDelegate implements GeckoSession.PermissionDelegate {
-
-        public int androidPermissionRequestCode = 1;
-        private Callback mCallback;
-
-        public void onRequestPermissionsResult(final String[] permissions,
-                                               final int[] grantResults) {
-            if (mCallback == null) {
-                return;
-            }
-
-            final Callback cb = mCallback;
-            mCallback = null;
-            for (final int result : grantResults) {
-                if (result != PackageManager.PERMISSION_GRANTED) {
-                    // At least one permission was not granted.
-                    cb.reject();
-                    return;
-                }
-            }
-            cb.grant();
-        }
-
-        @Override
-        public void onAndroidPermissionsRequest(final GeckoSession session, final String[] permissions,
-                                              final Callback callback) {
-            if (Build.VERSION.SDK_INT < 23) {
-                // requestPermissions was introduced in API 23.
-                callback.grant();
-                return;
-            }
-            mCallback = callback;
-            requestPermissions(permissions, androidPermissionRequestCode);
-        }
-
-        @Override
-        public void onContentPermissionRequest(final GeckoSession session, final String uri,
-                                             final int type, final String access,
-                                             final Callback callback) {
-            final int resId;
-            if (PERMISSION_GEOLOCATION == type) {
-                resId = R.string.request_geolocation;
-            } else if (PERMISSION_DESKTOP_NOTIFICATION == type) {
-                resId = R.string.request_notification;
-            } else {
-                Log.w(LOGTAG, "Unknown permission: " + type);
-                callback.reject();
-                return;
-            }
-
-            final String title = getString(resId, Uri.parse(uri).getAuthority());
-            final BasicGeckoViewPrompt prompt = (BasicGeckoViewPrompt)
-                    mGeckoSession.getPromptDelegate();
-            prompt.onPermissionPrompt(session, title, callback);
-        }
-
-        private String[] normalizeMediaName(final MediaSource[] sources) {
-            if (sources == null) {
-                return null;
-            }
-
-            String[] res = new String[sources.length];
-            for (int i = 0; i < sources.length; i++) {
-                final int mediaSource = sources[i].source;
-                final String name = sources[i].name;
-                if (MediaSource.SOURCE_CAMERA == mediaSource) {
-                    if (name.toLowerCase(Locale.ENGLISH).contains("front")) {
-                        res[i] = getString(R.string.media_front_camera);
-                    } else {
-                        res[i] = getString(R.string.media_back_camera);
-                    }
-                } else if (!name.isEmpty()) {
-                    res[i] = name;
-                } else if (MediaSource.SOURCE_MICROPHONE == mediaSource) {
-                    res[i] = getString(R.string.media_microphone);
-                } else {
-                    res[i] = getString(R.string.media_other);
-                }
-            }
-
-            return res;
-        }
-
-        @Override
-        public void onMediaPermissionRequest(final GeckoSession session, final String uri,
-                                           final MediaSource[] video, final MediaSource[] audio,
-                                           final MediaCallback callback) {
-            final String host = Uri.parse(uri).getAuthority();
-            final String title;
-            if (audio == null) {
-                title = getString(R.string.request_video, host);
-            } else if (video == null) {
-                title = getString(R.string.request_audio, host);
-            } else {
-                title = getString(R.string.request_media, host);
-            }
-
-            String[] videoNames = normalizeMediaName(video);
-            String[] audioNames = normalizeMediaName(audio);
-
-            final BasicGeckoViewPrompt prompt = (BasicGeckoViewPrompt)
-                    mGeckoSession.getPromptDelegate();
-            prompt.onMediaPrompt(session, title, video, audio, videoNames, audioNames, callback);
-        }
-    }
-
     private class ExampleNavigationDelegate implements GeckoSession.NavigationDelegate {
         @Override
         public void onLocationChange(GeckoSession session, final String url) {
             mLocationView.setText(url);
             mCurrentUri = url;
         }
 
         @Override
--- a/mobile/android/geckoview_example/src/main/res/values/strings.xml
+++ b/mobile/android/geckoview_example/src/main/res/values/strings.xml
@@ -1,15 +1,3 @@
 <resources>
     <string name="app_name">geckoview_example</string>
-    <string name="username">Username</string>
-    <string name="password">Password</string>
-    <string name="clear_field">Clear</string>
-    <string name="request_geolocation">Share location with "%1$s"?</string>
-    <string name="request_notification">Allow notifications for "%1$s"?</string>
-    <string name="request_video">Share video with "%1$s"</string>
-    <string name="request_audio">Share audio with "%1$s"</string>
-    <string name="request_media">Share video and audio with "%1$s"</string>
-    <string name="media_back_camera">Back camera</string>
-    <string name="media_front_camera">Front camera</string>
-    <string name="media_microphone">Microphone</string>
-    <string name="media_other">Unknown source</string>
 </resources>