1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
2  * This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 
6 package org.mozilla.geckoview_example;
7 
8 import android.annotation.TargetApi;
9 import android.app.Activity;
10 import android.app.AlertDialog;
11 import android.content.ActivityNotFoundException;
12 import android.content.ClipData;
13 import android.content.Context;
14 import android.content.DialogInterface;
15 import android.content.Intent;
16 import android.content.res.TypedArray;
17 import android.graphics.Color;
18 import android.graphics.PorterDuff;
19 import android.net.Uri;
20 import android.os.Build;
21 import android.text.InputType;
22 import android.text.format.DateFormat;
23 import android.util.Log;
24 import android.view.InflateException;
25 import android.view.LayoutInflater;
26 import android.view.View;
27 import android.view.ViewGroup;
28 import android.widget.AdapterView;
29 import android.widget.ArrayAdapter;
30 import android.widget.CheckBox;
31 import android.widget.CheckedTextView;
32 import android.widget.CompoundButton;
33 import android.widget.DatePicker;
34 import android.widget.EditText;
35 import android.widget.FrameLayout;
36 import android.widget.ImageView;
37 import android.widget.LinearLayout;
38 import android.widget.ListView;
39 import android.widget.ScrollView;
40 import android.widget.Spinner;
41 import android.widget.TextView;
42 import android.widget.TimePicker;
43 
44 import java.text.ParseException;
45 import java.text.SimpleDateFormat;
46 import java.util.ArrayList;
47 import java.util.Calendar;
48 import java.util.Date;
49 import java.util.Locale;
50 
51 import org.mozilla.geckoview.GeckoSession;
52 import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource;
53 
54 final class BasicGeckoViewPrompt implements GeckoSession.PromptDelegate {
55     protected static final String LOGTAG = "BasicGeckoViewPrompt";
56 
57     private final Activity mActivity;
58     public int filePickerRequestCode = 1;
59     private int mFileType;
60     private FileCallback mFileCallback;
61 
BasicGeckoViewPrompt(final Activity activity)62     public BasicGeckoViewPrompt(final Activity activity) {
63         mActivity = activity;
64     }
65 
addCheckbox(final AlertDialog.Builder builder, ViewGroup parent, final AlertCallback callback)66     private AlertDialog.Builder addCheckbox(final AlertDialog.Builder builder,
67                                             ViewGroup parent,
68                                             final AlertCallback callback) {
69         if (!callback.hasCheckbox()) {
70             return builder;
71         }
72         final CheckBox checkbox = new CheckBox(builder.getContext());
73         if (callback.getCheckboxMessage() != null) {
74             checkbox.setText(callback.getCheckboxMessage());
75         }
76         checkbox.setChecked(callback.getCheckboxValue());
77         checkbox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
78             @Override
79             public void onCheckedChanged(final CompoundButton button,
80                                          final boolean checked) {
81                 callback.setCheckboxValue(checked);
82             }
83         });
84         if (parent == null) {
85             final int padding = getViewPadding(builder);
86             parent = new FrameLayout(builder.getContext());
87             parent.setPadding(/* left */ padding, /* top */ 0,
88                               /* right */ padding, /* bottom */ 0);
89             builder.setView(parent);
90         }
91         parent.addView(checkbox);
92         return builder;
93     }
94 
onAlert(final GeckoSession session, final String title, final String msg, final AlertCallback callback)95     public void onAlert(final GeckoSession session, final String title, final String msg,
96                       final AlertCallback callback) {
97         final Activity activity = mActivity;
98         if (activity == null) {
99             callback.dismiss();
100             return;
101         }
102         final AlertDialog.Builder builder = new AlertDialog.Builder(activity)
103                 .setTitle(title)
104                 .setMessage(msg)
105                 .setPositiveButton(android.R.string.ok, /* onClickListener */ null);
106         createStandardDialog(addCheckbox(builder, /* parent */ null, callback),
107                              callback).show();
108     }
109 
onButtonPrompt(final GeckoSession session, final String title, final String msg, final String[] btnMsg, final ButtonCallback callback)110     public void onButtonPrompt(final GeckoSession session, final String title,
111                                 final String msg, final String[] btnMsg,
112                                 final ButtonCallback callback) {
113         final Activity activity = mActivity;
114         if (activity == null) {
115             callback.dismiss();
116             return;
117         }
118         final AlertDialog.Builder builder = new AlertDialog.Builder(activity)
119                 .setTitle(title)
120                 .setMessage(msg);
121         final DialogInterface.OnClickListener listener =
122             new DialogInterface.OnClickListener() {
123                 @Override
124                 public void onClick(final DialogInterface dialog, final int which) {
125                     if (which == DialogInterface.BUTTON_POSITIVE) {
126                         callback.confirm(BUTTON_TYPE_POSITIVE);
127                     } else if (which == DialogInterface.BUTTON_NEUTRAL) {
128                         callback.confirm(BUTTON_TYPE_NEUTRAL);
129                     } else if (which == DialogInterface.BUTTON_NEGATIVE) {
130                         callback.confirm(BUTTON_TYPE_NEGATIVE);
131                     } else {
132                         callback.dismiss();
133                     }
134                 }
135             };
136         if (btnMsg[BUTTON_TYPE_POSITIVE] != null) {
137             builder.setPositiveButton(btnMsg[BUTTON_TYPE_POSITIVE], listener);
138         }
139         if (btnMsg[BUTTON_TYPE_NEUTRAL] != null) {
140             builder.setNeutralButton(btnMsg[BUTTON_TYPE_NEUTRAL], listener);
141         }
142         if (btnMsg[BUTTON_TYPE_NEGATIVE] != null) {
143             builder.setNegativeButton(btnMsg[BUTTON_TYPE_NEGATIVE], listener);
144         }
145         createStandardDialog(addCheckbox(builder, /* parent */ null, callback),
146                              callback).show();
147     }
148 
getViewPadding(final AlertDialog.Builder builder)149     private int getViewPadding(final AlertDialog.Builder builder) {
150         final TypedArray attr = builder.getContext().obtainStyledAttributes(
151                 new int[] { android.R.attr.listPreferredItemPaddingLeft });
152         final int padding = attr.getDimensionPixelSize(0, 1);
153         attr.recycle();
154         return padding;
155     }
156 
addStandardLayout(final AlertDialog.Builder builder, final String title, final String msg)157     private LinearLayout addStandardLayout(final AlertDialog.Builder builder,
158                                            final String title, final String msg) {
159         final ScrollView scrollView = new ScrollView(builder.getContext());
160         final LinearLayout container = new LinearLayout(builder.getContext());
161         final int horizontalPadding = getViewPadding(builder);
162         final int verticalPadding = (msg == null || msg.isEmpty()) ? horizontalPadding : 0;
163         container.setOrientation(LinearLayout.VERTICAL);
164         container.setPadding(/* left */ horizontalPadding, /* top */ verticalPadding,
165                              /* right */ horizontalPadding, /* bottom */ verticalPadding);
166         scrollView.addView(container);
167         builder.setTitle(title)
168                .setMessage(msg)
169                .setView(scrollView);
170         return container;
171     }
172 
createStandardDialog(final AlertDialog.Builder builder, final AlertCallback callback)173     private AlertDialog createStandardDialog(final AlertDialog.Builder builder,
174                                              final AlertCallback callback) {
175         final AlertDialog dialog = builder.create();
176         dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
177                     @Override
178                     public void onDismiss(final DialogInterface dialog) {
179                         callback.dismiss();
180                     }
181                 });
182         return dialog;
183     }
184 
onTextPrompt(final GeckoSession session, final String title, final String msg, final String value, final TextCallback callback)185     public void onTextPrompt(final GeckoSession session, final String title,
186                               final String msg, final String value,
187                               final TextCallback callback) {
188         final Activity activity = mActivity;
189         if (activity == null) {
190             callback.dismiss();
191             return;
192         }
193         final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
194         final LinearLayout container = addStandardLayout(builder, title, msg);
195         final EditText editText = new EditText(builder.getContext());
196         editText.setText(value);
197         container.addView(editText);
198 
199         builder.setNegativeButton(android.R.string.cancel, /* listener */ null)
200                .setPositiveButton(android.R.string.ok,
201                                   new DialogInterface.OnClickListener() {
202                     @Override
203                     public void onClick(final DialogInterface dialog, final int which) {
204                         callback.confirm(editText.getText().toString());
205                     }
206                 });
207 
208         createStandardDialog(addCheckbox(builder, container, callback), callback).show();
209     }
210 
onAuthPrompt(final GeckoSession session, final String title, final String msg, final AuthOptions options, final AuthCallback callback)211     public void onAuthPrompt(final GeckoSession session, final String title,
212                               final String msg, final AuthOptions options,
213                               final AuthCallback callback) {
214         final Activity activity = mActivity;
215         if (activity == null) {
216             callback.dismiss();
217             return;
218         }
219         final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
220         final LinearLayout container = addStandardLayout(builder, title, msg);
221 
222         final int flags = options.flags;
223         final int level = options.level;
224         final EditText username;
225         if ((flags & AuthOptions.AUTH_FLAG_ONLY_PASSWORD) == 0) {
226             username = new EditText(builder.getContext());
227             username.setHint(R.string.username);
228             username.setText(options.username);
229             container.addView(username);
230         } else {
231             username = null;
232         }
233 
234         final EditText password = new EditText(builder.getContext());
235         password.setHint(R.string.password);
236         password.setText(options.password);
237         password.setInputType(InputType.TYPE_CLASS_TEXT |
238                               InputType.TYPE_TEXT_VARIATION_PASSWORD);
239         container.addView(password);
240 
241         if (level != AuthOptions.AUTH_LEVEL_NONE) {
242             final ImageView secure = new ImageView(builder.getContext());
243             secure.setImageResource(android.R.drawable.ic_lock_lock);
244             container.addView(secure);
245         }
246 
247         builder.setNegativeButton(android.R.string.cancel, /* listener */ null)
248                .setPositiveButton(android.R.string.ok,
249                                   new DialogInterface.OnClickListener() {
250                     @Override
251                     public void onClick(final DialogInterface dialog, final int which) {
252                         if ((flags & AuthOptions.AUTH_FLAG_ONLY_PASSWORD) == 0) {
253                             callback.confirm(username.getText().toString(),
254                                              password.getText().toString());
255                         } else {
256                             callback.confirm(password.getText().toString());
257                         }
258                     }
259                 });
260         createStandardDialog(addCheckbox(builder, container, callback), callback).show();
261     }
262 
263     private static class ModifiableChoice {
264         public boolean modifiableSelected;
265         public String modifiableLabel;
266         public final Choice choice;
267 
ModifiableChoice(Choice c)268         public ModifiableChoice(Choice c) {
269             choice = c;
270             modifiableSelected = choice.selected;
271             modifiableLabel = choice.label;
272         }
273     }
274 
addChoiceItems(final int type, final ArrayAdapter<ModifiableChoice> list, final Choice[] items, final String indent)275     private void addChoiceItems(final int type, final ArrayAdapter<ModifiableChoice> list,
276                                 final Choice[] items, final String indent) {
277         if (type == Choice.CHOICE_TYPE_MENU) {
278             for (final Choice item : items) {
279                 list.add(new ModifiableChoice(item));
280             }
281             return;
282         }
283 
284         for (final Choice item : items) {
285             final ModifiableChoice modItem = new ModifiableChoice(item);
286 
287             final Choice[] children = item.items;
288 
289             if (indent != null && children == null) {
290                 modItem.modifiableLabel = indent + modItem.modifiableLabel;
291             }
292             list.add(modItem);
293 
294             if (children != null) {
295                 final String newIndent;
296                 if (type == Choice.CHOICE_TYPE_SINGLE || type == Choice.CHOICE_TYPE_MULTIPLE) {
297                     newIndent = (indent != null) ? indent + '\t' : "\t";
298                 } else {
299                     newIndent = null;
300                 }
301                 addChoiceItems(type, list, children, newIndent);
302             }
303         }
304     }
305 
onChoicePrompt(final GeckoSession session, final String title, final String msg, final int type, final Choice[] choices, final ChoiceCallback callback)306     public void onChoicePrompt(final GeckoSession session, final String title,
307                                 final String msg, final int type,
308                                 final Choice[] choices, final ChoiceCallback callback) {
309         final Activity activity = mActivity;
310         if (activity == null) {
311             callback.dismiss();
312             return;
313         }
314         final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
315         addStandardLayout(builder, title, msg);
316 
317         final ListView list = new ListView(builder.getContext());
318         if (type == Choice.CHOICE_TYPE_MULTIPLE) {
319             list.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
320         }
321 
322         final ArrayAdapter<ModifiableChoice> adapter = new ArrayAdapter<ModifiableChoice>(
323                 builder.getContext(), android.R.layout.simple_list_item_1) {
324             private static final int TYPE_MENU_ITEM = 0;
325             private static final int TYPE_MENU_CHECK = 1;
326             private static final int TYPE_SEPARATOR = 2;
327             private static final int TYPE_GROUP = 3;
328             private static final int TYPE_SINGLE = 4;
329             private static final int TYPE_MULTIPLE = 5;
330             private static final int TYPE_COUNT = 6;
331 
332             private LayoutInflater mInflater;
333             private View mSeparator;
334 
335             @Override
336             public int getViewTypeCount() {
337                 return TYPE_COUNT;
338             }
339 
340             @Override
341             public int getItemViewType(final int position) {
342                 final ModifiableChoice item = getItem(position);
343                 if (item.choice.separator) {
344                     return TYPE_SEPARATOR;
345                 } else if (type == Choice.CHOICE_TYPE_MENU) {
346                     return item.modifiableSelected ? TYPE_MENU_CHECK : TYPE_MENU_ITEM;
347                 } else if (item.choice.items != null) {
348                     return TYPE_GROUP;
349                 } else if (type == Choice.CHOICE_TYPE_SINGLE) {
350                     return TYPE_SINGLE;
351                 } else if (type == Choice.CHOICE_TYPE_MULTIPLE) {
352                     return TYPE_MULTIPLE;
353                 } else {
354                     throw new UnsupportedOperationException();
355                 }
356             }
357 
358             @Override
359             public boolean isEnabled(final int position) {
360                 final ModifiableChoice item = getItem(position);
361                 return !item.choice.separator && !item.choice.disabled &&
362                         ((type != Choice.CHOICE_TYPE_SINGLE && type != Choice.CHOICE_TYPE_MULTIPLE) ||
363                          item.choice.items != null);
364             }
365 
366             @Override
367             public View getView(final int position, View view,
368                                 final ViewGroup parent) {
369                 final int itemType = getItemViewType(position);
370                 final int layoutId;
371                 if (itemType == TYPE_SEPARATOR) {
372                     if (mSeparator == null) {
373                         mSeparator = new View(getContext());
374                         mSeparator.setLayoutParams(new ListView.LayoutParams(
375                                 ViewGroup.LayoutParams.MATCH_PARENT, 2, itemType));
376                         final TypedArray attr = getContext().obtainStyledAttributes(
377                                 new int[] { android.R.attr.listDivider });
378                         mSeparator.setBackgroundResource(attr.getResourceId(0, 0));
379                         attr.recycle();
380                     }
381                     return mSeparator;
382                 } else if (itemType == TYPE_MENU_ITEM) {
383                     layoutId = android.R.layout.simple_list_item_1;
384                 } else if (itemType == TYPE_MENU_CHECK) {
385                     layoutId = android.R.layout.simple_list_item_checked;
386                 } else if (itemType == TYPE_GROUP) {
387                     layoutId = android.R.layout.preference_category;
388                 } else if (itemType == TYPE_SINGLE) {
389                     layoutId = android.R.layout.simple_list_item_single_choice;
390                 } else if (itemType == TYPE_MULTIPLE) {
391                     layoutId = android.R.layout.simple_list_item_multiple_choice;
392                 } else {
393                     throw new UnsupportedOperationException();
394                 }
395 
396                 if (view == null) {
397                     if (mInflater == null) {
398                         mInflater = LayoutInflater.from(builder.getContext());
399                     }
400                     view = mInflater.inflate(layoutId, parent, false);
401                 }
402 
403                 final ModifiableChoice item = getItem(position);
404                 final TextView text = (TextView) view;
405                 text.setEnabled(!item.choice.disabled);
406                 text.setText(item.modifiableLabel);
407                 if (view instanceof CheckedTextView) {
408                     final boolean selected = item.modifiableSelected;
409                     if (itemType == TYPE_MULTIPLE) {
410                         list.setItemChecked(position, selected);
411                     } else {
412                         ((CheckedTextView) view).setChecked(selected);
413                     }
414                 }
415                 return view;
416             }
417         };
418         addChoiceItems(type, adapter, choices, /* indent */ null);
419 
420         list.setAdapter(adapter);
421         builder.setView(list);
422 
423         final AlertDialog dialog;
424         if (type == Choice.CHOICE_TYPE_SINGLE || type == Choice.CHOICE_TYPE_MENU) {
425             dialog = createStandardDialog(builder, callback);
426             list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
427                 @Override
428                 public void onItemClick(final AdapterView<?> parent, final View v,
429                                         final int position, final long id) {
430                     final ModifiableChoice item = adapter.getItem(position);
431                     if (type == Choice.CHOICE_TYPE_MENU) {
432                         final Choice[] children = item.choice.items;
433                         if (children != null) {
434                             // Show sub-menu.
435                             dialog.setOnDismissListener(null);
436                             dialog.dismiss();
437                             onChoicePrompt(session, item.modifiableLabel, /* msg */ null,
438                                             type, children, callback);
439                             return;
440                         }
441                     }
442                     callback.confirm(item.choice);
443                     dialog.dismiss();
444                 }
445             });
446         } else if (type == Choice.CHOICE_TYPE_MULTIPLE) {
447             list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
448                 @Override
449                 public void onItemClick(final AdapterView<?> parent, final View v,
450                                         final int position, final long id) {
451                     final ModifiableChoice item = adapter.getItem(position);
452                     item.modifiableSelected = ((CheckedTextView) v).isChecked();
453                 }
454             });
455             builder.setNegativeButton(android.R.string.cancel, /* listener */ null)
456                    .setPositiveButton(android.R.string.ok,
457                                       new DialogInterface.OnClickListener() {
458                 @Override
459                 public void onClick(final DialogInterface dialog,
460                                     final int which) {
461                     final int len = adapter.getCount();
462                     ArrayList<String> items = new ArrayList<>(len);
463                     for (int i = 0; i < len; i++) {
464                         final ModifiableChoice item = adapter.getItem(i);
465                         if (item.modifiableSelected) {
466                             items.add(item.choice.id);
467                         }
468                     }
469                     callback.confirm(items.toArray(new String[items.size()]));
470                 }
471             });
472             dialog = createStandardDialog(builder, callback);
473         } else {
474             throw new UnsupportedOperationException();
475         }
476         dialog.show();
477     }
478 
parseColor(final String value, final int def)479     private static int parseColor(final String value, final int def) {
480         try {
481             return Color.parseColor(value);
482         } catch (final IllegalArgumentException e) {
483             return def;
484         }
485     }
486 
onColorPrompt(final GeckoSession session, final String title, final String value, final TextCallback callback)487     public void onColorPrompt(final GeckoSession session, final String title,
488                                final String value, final TextCallback callback)
489     {
490         final Activity activity = mActivity;
491         if (activity == null) {
492             callback.dismiss();
493             return;
494         }
495         final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
496         addStandardLayout(builder, title, /* msg */ null);
497 
498         final int initial = parseColor(value, /* def */ 0);
499         final ArrayAdapter<Integer> adapter = new ArrayAdapter<Integer>(
500                 builder.getContext(), android.R.layout.simple_list_item_1) {
501             private LayoutInflater mInflater;
502 
503             @Override
504             public int getViewTypeCount() {
505                 return 2;
506             }
507 
508             @Override
509             public int getItemViewType(final int position) {
510                 return (getItem(position) == initial) ? 1 : 0;
511             }
512 
513             @Override
514             public View getView(final int position, View view,
515                                 final ViewGroup parent) {
516                 if (mInflater == null) {
517                     mInflater = LayoutInflater.from(builder.getContext());
518                 }
519                 final int color = getItem(position);
520                 if (view == null) {
521                     view = mInflater.inflate((color == initial) ?
522                             android.R.layout.simple_list_item_checked :
523                             android.R.layout.simple_list_item_1, parent, false);
524                 }
525                 view.setBackgroundResource(android.R.drawable.editbox_background);
526                 view.getBackground().setColorFilter(color, PorterDuff.Mode.MULTIPLY);
527                 return view;
528             }
529         };
530 
531         adapter.addAll(0xffff4444 /* holo_red_light */,
532                        0xffcc0000 /* holo_red_dark */,
533                        0xffffbb33 /* holo_orange_light */,
534                        0xffff8800 /* holo_orange_dark */,
535                        0xff99cc00 /* holo_green_light */,
536                        0xff669900 /* holo_green_dark */,
537                        0xff33b5e5 /* holo_blue_light */,
538                        0xff0099cc /* holo_blue_dark */,
539                        0xffaa66cc /* holo_purple */,
540                        0xffffffff /* white */,
541                        0xffaaaaaa /* lighter_gray */,
542                        0xff555555 /* darker_gray */,
543                        0xff000000 /* black */);
544 
545         final ListView list = new ListView(builder.getContext());
546         list.setAdapter(adapter);
547         builder.setView(list);
548 
549         final AlertDialog dialog = createStandardDialog(builder, callback);
550         list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
551             @Override
552             public void onItemClick(final AdapterView<?> parent, final View v,
553                                     final int position, final long id) {
554                 callback.confirm(String.format("#%06x", 0xffffff & adapter.getItem(position)));
555                 dialog.dismiss();
556             }
557         });
558         dialog.show();
559     }
560 
parseDate(final SimpleDateFormat formatter, final String value, final boolean defaultToNow)561     private static Date parseDate(final SimpleDateFormat formatter,
562                                   final String value,
563                                   final boolean defaultToNow) {
564         try {
565             if (value != null && !value.isEmpty()) {
566                 return formatter.parse(value);
567             }
568         } catch (final ParseException e) {
569         }
570         return defaultToNow ? new Date() : null;
571     }
572 
573     @SuppressWarnings("deprecation")
setTimePickerTime(final TimePicker picker, final Calendar cal)574     private static void setTimePickerTime(final TimePicker picker, final Calendar cal) {
575         if (Build.VERSION.SDK_INT >= 23) {
576             picker.setHour(cal.get(Calendar.HOUR_OF_DAY));
577             picker.setMinute(cal.get(Calendar.MINUTE));
578         } else {
579             picker.setCurrentHour(cal.get(Calendar.HOUR_OF_DAY));
580             picker.setCurrentMinute(cal.get(Calendar.MINUTE));
581         }
582     }
583 
584     @SuppressWarnings("deprecation")
setCalendarTime(final Calendar cal, final TimePicker picker)585     private static void setCalendarTime(final Calendar cal, final TimePicker picker) {
586         if (Build.VERSION.SDK_INT >= 23) {
587             cal.set(Calendar.HOUR_OF_DAY, picker.getHour());
588             cal.set(Calendar.MINUTE, picker.getMinute());
589         } else {
590             cal.set(Calendar.HOUR_OF_DAY, picker.getCurrentHour());
591             cal.set(Calendar.MINUTE, picker.getCurrentMinute());
592         }
593     }
594 
onDateTimePrompt(final GeckoSession session, final String title, final int type, final String value, final String min, final String max, final TextCallback callback)595     public void onDateTimePrompt(final GeckoSession session, final String title,
596                                   final int type, final String value, final String min,
597                                   final String max, final TextCallback callback) {
598         final Activity activity = mActivity;
599         if (activity == null) {
600             callback.dismiss();
601             return;
602         }
603         final String format;
604         if (type == DATETIME_TYPE_DATE) {
605             format = "yyyy-MM-dd";
606         } else if (type == DATETIME_TYPE_MONTH) {
607             format = "yyyy-MM";
608         } else if (type == DATETIME_TYPE_WEEK) {
609             format = "yyyy-'W'ww";
610         } else if (type == DATETIME_TYPE_TIME) {
611             format = "HH:mm";
612         } else if (type == DATETIME_TYPE_DATETIME_LOCAL) {
613             format = "yyyy-MM-dd'T'HH:mm";
614         } else {
615             throw new UnsupportedOperationException();
616         }
617 
618         final SimpleDateFormat formatter = new SimpleDateFormat(format, Locale.ROOT);
619         final Date minDate = parseDate(formatter, min, /* defaultToNow */ false);
620         final Date maxDate = parseDate(formatter, max, /* defaultToNow */ false);
621         final Date date = parseDate(formatter, value, /* defaultToNow */ true);
622         final Calendar cal = formatter.getCalendar();
623         cal.setTime(date);
624 
625         final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
626         final LayoutInflater inflater = LayoutInflater.from(builder.getContext());
627         final DatePicker datePicker;
628         if (type == DATETIME_TYPE_DATE || type == DATETIME_TYPE_MONTH ||
629             type == DATETIME_TYPE_WEEK || type == DATETIME_TYPE_DATETIME_LOCAL) {
630             final int resId = builder.getContext().getResources().getIdentifier(
631                     "date_picker_dialog", "layout", "android");
632             DatePicker picker = null;
633             if (resId != 0) {
634                 try {
635                     picker = (DatePicker) inflater.inflate(resId, /* root */ null);
636                 } catch (final ClassCastException|InflateException e) {
637                 }
638             }
639             if (picker == null) {
640                 picker = new DatePicker(builder.getContext());
641             }
642             picker.init(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH),
643                         cal.get(Calendar.DAY_OF_MONTH), /* listener */ null);
644             if (minDate != null) {
645                 picker.setMinDate(minDate.getTime());
646             }
647             if (maxDate != null) {
648                 picker.setMaxDate(maxDate.getTime());
649             }
650             datePicker = picker;
651         } else {
652             datePicker = null;
653         }
654 
655         final TimePicker timePicker;
656         if (type == DATETIME_TYPE_TIME || type == DATETIME_TYPE_DATETIME_LOCAL) {
657             final int resId = builder.getContext().getResources().getIdentifier(
658                     "time_picker_dialog", "layout", "android");
659             TimePicker picker = null;
660             if (resId != 0) {
661                 try {
662                     picker = (TimePicker) inflater.inflate(resId, /* root */ null);
663                 } catch (final ClassCastException|InflateException e) {
664                 }
665             }
666             if (picker == null) {
667                 picker = new TimePicker(builder.getContext());
668             }
669             setTimePickerTime(picker, cal);
670             picker.setIs24HourView(DateFormat.is24HourFormat(builder.getContext()));
671             timePicker = picker;
672         } else {
673             timePicker = null;
674         }
675 
676         final LinearLayout container = addStandardLayout(builder, title, /* msg */ null);
677         container.setPadding(/* left */ 0, /* top */ 0, /* right */ 0, /* bottom */ 0);
678         if (datePicker != null) {
679             container.addView(datePicker);
680         }
681         if (timePicker != null) {
682             container.addView(timePicker);
683         }
684 
685         final DialogInterface.OnClickListener listener =
686                 new DialogInterface.OnClickListener() {
687             @Override
688             public void onClick(final DialogInterface dialog, final int which) {
689                 if (which == DialogInterface.BUTTON_NEUTRAL) {
690                     // Clear
691                     callback.confirm("");
692                     return;
693                 }
694                 if (datePicker != null) {
695                     cal.set(datePicker.getYear(), datePicker.getMonth(),
696                             datePicker.getDayOfMonth());
697                 }
698                 if (timePicker != null) {
699                     setCalendarTime(cal, timePicker);
700                 }
701                 callback.confirm(formatter.format(cal.getTime()));
702             }
703         };
704         builder.setNegativeButton(android.R.string.cancel, /* listener */ null)
705                .setNeutralButton(R.string.clear_field, listener)
706                .setPositiveButton(android.R.string.ok, listener);
707         createStandardDialog(builder, callback).show();
708     }
709 
710     @TargetApi(19)
onFilePrompt(GeckoSession session, String title, int type, String[] mimeTypes, FileCallback callback)711     public void onFilePrompt(GeckoSession session, String title, int type,
712                               String[] mimeTypes, FileCallback callback)
713     {
714         final Activity activity = mActivity;
715         if (activity == null) {
716             callback.dismiss();
717             return;
718         }
719 
720         // Merge all given MIME types into one, using wildcard if needed.
721         String mimeType = null;
722         String mimeSubtype = null;
723         for (final String rawType : mimeTypes) {
724             final String normalizedType = rawType.trim().toLowerCase(Locale.ROOT);
725             final int len = normalizedType.length();
726             int slash = normalizedType.indexOf('/');
727             if (slash < 0) {
728                 slash = len;
729             }
730             final String newType = normalizedType.substring(0, slash);
731             final String newSubtype = normalizedType.substring(Math.min(slash + 1, len));
732             if (mimeType == null) {
733                 mimeType = newType;
734             } else if (!mimeType.equals(newType)) {
735                 mimeType = "*";
736             }
737             if (mimeSubtype == null) {
738                 mimeSubtype = newSubtype;
739             } else if (!mimeSubtype.equals(newSubtype)) {
740                 mimeSubtype = "*";
741             }
742         }
743 
744         final Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
745         intent.setType((mimeType != null ? mimeType : "*") + '/' +
746                        (mimeSubtype != null ? mimeSubtype : "*"));
747         intent.addCategory(Intent.CATEGORY_OPENABLE);
748         intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
749         if (Build.VERSION.SDK_INT >= 18 && type == FILE_TYPE_MULTIPLE) {
750             intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
751         }
752         if (Build.VERSION.SDK_INT >= 19 && mimeTypes.length > 0) {
753             intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
754         }
755 
756         try {
757             mFileType = type;
758             mFileCallback = callback;
759             activity.startActivityForResult(intent, filePickerRequestCode);
760         } catch (final ActivityNotFoundException e) {
761             Log.e(LOGTAG, "Cannot launch activity", e);
762             callback.dismiss();
763         }
764     }
765 
onFileCallbackResult(final int resultCode, final Intent data)766     public void onFileCallbackResult(final int resultCode, final Intent data) {
767         if (mFileCallback == null) {
768             return;
769         }
770 
771         final FileCallback callback = mFileCallback;
772         mFileCallback = null;
773 
774         if (resultCode != Activity.RESULT_OK || data == null) {
775             callback.dismiss();
776             return;
777         }
778 
779         final Uri uri = data.getData();
780         final ClipData clip = data.getClipData();
781 
782         if (mFileType == FILE_TYPE_SINGLE ||
783             (mFileType == FILE_TYPE_MULTIPLE && clip == null)) {
784             callback.confirm(mActivity, uri);
785 
786         } else if (mFileType == FILE_TYPE_MULTIPLE) {
787             if (clip == null) {
788                 Log.w(LOGTAG, "No selected file");
789                 callback.dismiss();
790                 return;
791             }
792             final int count = clip.getItemCount();
793             final ArrayList<Uri> uris = new ArrayList<>(count);
794             for (int i = 0; i < count; i++) {
795                 uris.add(clip.getItemAt(i).getUri());
796             }
797             callback.confirm(mActivity, uris.toArray(new Uri[uris.size()]));
798         }
799     }
800 
onPermissionPrompt(final GeckoSession session, final String title, final GeckoSession.PermissionDelegate.Callback callback)801     public void onPermissionPrompt(final GeckoSession session, final String title,
802                                     final GeckoSession.PermissionDelegate.Callback callback) {
803         final Activity activity = mActivity;
804         if (activity == null) {
805             callback.reject();
806             return;
807         }
808         final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
809         builder.setTitle(title)
810                .setNegativeButton(android.R.string.cancel, /* onClickListener */ null)
811                .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
812                    @Override
813                    public void onClick(final DialogInterface dialog, final int which) {
814                        callback.grant();
815                    }
816                });
817 
818         final AlertDialog dialog = builder.create();
819         dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
820                    @Override
821                    public void onDismiss(final DialogInterface dialog) {
822                        callback.reject();
823                    }
824                });
825         dialog.show();
826     }
827 
addMediaSpinner(final Context context, final ViewGroup container, final MediaSource[] sources, final String[] sourceNames)828     private Spinner addMediaSpinner(final Context context, final ViewGroup container,
829                                     final MediaSource[] sources, final String[] sourceNames) {
830         final ArrayAdapter<MediaSource> adapter = new ArrayAdapter<MediaSource>(
831                 context, android.R.layout.simple_spinner_item) {
832             private View convertView(final int position, final View view) {
833                 if (view != null) {
834                     final MediaSource item = getItem(position);
835                     ((TextView) view).setText(sourceNames != null ? sourceNames[position] : item.name);
836                 }
837                 return view;
838             }
839 
840             @Override
841             public View getView(final int position, View view,
842                                 final ViewGroup parent) {
843                 return convertView(position, super.getView(position, view, parent));
844             }
845 
846             @Override
847             public View getDropDownView(final int position, final View view,
848                                         final ViewGroup parent) {
849                 return convertView(position, super.getDropDownView(position, view, parent));
850             }
851         };
852         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
853         adapter.addAll(sources);
854 
855         final Spinner spinner = new Spinner(context);
856         spinner.setAdapter(adapter);
857         spinner.setSelection(0);
858         container.addView(spinner);
859         return spinner;
860     }
861 
onMediaPrompt(final GeckoSession session, final String title, final MediaSource[] video, final MediaSource[] audio, final String[] videoNames, final String[] audioNames, final GeckoSession.PermissionDelegate.MediaCallback callback)862     public void onMediaPrompt(final GeckoSession session, final String title,
863                                final MediaSource[] video, final MediaSource[] audio,
864                                final String[] videoNames, final String[] audioNames,
865                                final GeckoSession.PermissionDelegate.MediaCallback callback) {
866         final Activity activity = mActivity;
867         if (activity == null || (video == null && audio == null)) {
868             callback.reject();
869             return;
870         }
871         final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
872         final LinearLayout container = addStandardLayout(builder, title, /* msg */ null);
873 
874         final Spinner videoSpinner;
875         if (video != null) {
876             videoSpinner = addMediaSpinner(builder.getContext(), container, video, videoNames);
877         } else {
878             videoSpinner = null;
879         }
880 
881         final Spinner audioSpinner;
882         if (audio != null) {
883             audioSpinner = addMediaSpinner(builder.getContext(), container, audio, audioNames);
884         } else {
885             audioSpinner = null;
886         }
887 
888         builder.setNegativeButton(android.R.string.cancel, /* listener */ null)
889                .setPositiveButton(android.R.string.ok,
890                                   new DialogInterface.OnClickListener() {
891                     @Override
892                     public void onClick(final DialogInterface dialog, final int which) {
893                         final MediaSource video = (videoSpinner != null)
894                                 ? (MediaSource) videoSpinner.getSelectedItem() : null;
895                         final MediaSource audio = (audioSpinner != null)
896                                 ? (MediaSource) audioSpinner.getSelectedItem() : null;
897                         callback.grant(video, audio);
898                     }
899                 });
900 
901         final AlertDialog dialog = builder.create();
902         dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
903                     @Override
904                     public void onDismiss(final DialogInterface dialog) {
905                         callback.reject();
906                     }
907                 });
908         dialog.show();
909     }
910 
onMediaPrompt(final GeckoSession session, final String title, final MediaSource[] video, final MediaSource[] audio, final GeckoSession.PermissionDelegate.MediaCallback callback)911     public void onMediaPrompt(final GeckoSession session, final String title,
912                                final MediaSource[] video, final MediaSource[] audio,
913                                final GeckoSession.PermissionDelegate.MediaCallback callback) {
914         onMediaPrompt(session, title, video, audio, null, null, callback);
915     }
916 }
917