WebViewContextMenu.java revision e307785b44deec1eb2aad5ecf83ea4b581779baa
1/*
2 * Copyright (C) 2011 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mail.browse;
19
20import android.app.Activity;
21import android.content.ClipData;
22import android.content.ClipboardManager;
23import android.content.Context;
24import android.content.Intent;
25import android.content.pm.PackageManager;
26import android.content.pm.ResolveInfo;
27import android.net.Uri;
28import android.provider.ContactsContract;
29import android.view.ContextMenu;
30import android.view.ContextMenu.ContextMenuInfo;
31import android.view.MenuInflater;
32import android.view.MenuItem;
33import android.view.View;
34import android.view.View.OnCreateContextMenuListener;
35import android.webkit.WebView;
36
37import com.android.mail.R;
38
39import java.io.UnsupportedEncodingException;
40import java.net.URLDecoder;
41import java.net.URLEncoder;
42import java.nio.charset.Charset;
43
44/**
45 * <p>Handles display and behavior of the context menu for known actionable content in WebViews.
46 * Requires an Activity to bind to for Context resolution and to start other activites.</p>
47 * <br>
48 * Dependencies:
49 * <ul>
50 * <li>res/menu/webview_context_menu.xml</li>
51 * </ul>
52 */
53public class WebViewContextMenu implements OnCreateContextMenuListener,
54        MenuItem.OnMenuItemClickListener {
55
56    private final boolean mSupportsDial;
57    private final boolean mSupportsSms;
58
59    private Activity mActivity;
60
61    protected static enum MenuType {
62        OPEN_MENU,
63        COPY_LINK_MENU,
64        SHARE_LINK_MENU,
65        DIAL_MENU,
66        SMS_MENU,
67        ADD_CONTACT_MENU,
68        COPY_PHONE_MENU,
69        EMAIL_CONTACT_MENU,
70        COPY_MAIL_MENU,
71        MAP_MENU,
72        COPY_GEO_MENU,
73    }
74
75    public WebViewContextMenu(Activity host) {
76        mActivity = host;
77
78        // Query the package manager to see if the device
79        // has an app that supports ACTION_DIAL or ACTION_SENDTO
80        // with the appropriate uri schemes.
81        final PackageManager pm = mActivity.getPackageManager();
82        mSupportsDial = !pm.queryIntentActivities(
83                new Intent(Intent.ACTION_DIAL, Uri.parse(WebView.SCHEME_TEL)),
84                PackageManager.MATCH_DEFAULT_ONLY).isEmpty();
85        mSupportsSms = !pm.queryIntentActivities(
86                new Intent(Intent.ACTION_SENDTO, Uri.parse("smsto:")),
87                PackageManager.MATCH_DEFAULT_ONLY).isEmpty();
88    }
89
90    // For our copy menu items.
91    private class Copy implements MenuItem.OnMenuItemClickListener {
92        private final CharSequence mText;
93
94        public Copy(CharSequence text) {
95            mText = text;
96        }
97
98        @Override
99        public boolean onMenuItemClick(MenuItem item) {
100            copy(mText);
101            return true;
102        }
103    }
104
105    // For our share menu items.
106    private class Share implements MenuItem.OnMenuItemClickListener {
107        private final String mUri;
108
109        public Share(String text) {
110            mUri = text;
111        }
112
113        @Override
114        public boolean onMenuItemClick(MenuItem item) {
115            shareLink(mUri);
116            return true;
117        }
118    }
119
120    private boolean showShareLinkMenuItem() {
121        PackageManager pm = mActivity.getPackageManager();
122        Intent send = new Intent(Intent.ACTION_SEND);
123        send.setType("text/plain");
124        ResolveInfo ri = pm.resolveActivity(send, PackageManager.MATCH_DEFAULT_ONLY);
125        return ri != null;
126    }
127
128    private void shareLink(String url) {
129        Intent send = new Intent(Intent.ACTION_SEND);
130        send.setType("text/plain");
131        send.putExtra(Intent.EXTRA_TEXT, url);
132
133        try {
134            mActivity.startActivity(Intent.createChooser(send, mActivity.getText(
135                    getChooserTitleStringResIdForMenuType(MenuType.SHARE_LINK_MENU))));
136        } catch(android.content.ActivityNotFoundException ex) {
137            // if no app handles it, do nothing
138        }
139    }
140
141    private void copy(CharSequence text) {
142        ClipboardManager clipboard =
143                (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
144        clipboard.setPrimaryClip(ClipData.newPlainText(null, text));
145    }
146
147    @Override
148    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo info) {
149        // FIXME: This is copied over almost directly from BrowserActivity.
150        // Would like to find a way to combine the two (Bug 1251210).
151
152        WebView webview = (WebView) v;
153        WebView.HitTestResult result = webview.getHitTestResult();
154        if (result == null) {
155            return;
156        }
157
158        int type = result.getType();
159        switch (type) {
160            case WebView.HitTestResult.UNKNOWN_TYPE:
161            case WebView.HitTestResult.EDIT_TEXT_TYPE:
162                return;
163            default:
164                break;
165        }
166
167        // Note, http://b/issue?id=1106666 is requesting that
168        // an inflated menu can be used again. This is not available
169        // yet, so inflate each time (yuk!)
170        MenuInflater inflater = mActivity.getMenuInflater();
171        // Also, we are copying the menu file from browser until
172        // 1251210 is fixed.
173        inflater.inflate(getMenuResourceId(), menu);
174
175        // Initially make set the menu item handler this WebViewContextMenu, which will default to
176        // calling the non-abstract subclass's implementation.
177        for (int i = 0; i < menu.size(); i++) {
178            final MenuItem menuItem = menu.getItem(i);
179            menuItem.setOnMenuItemClickListener(this);
180        }
181
182
183        // Show the correct menu group
184        String extra = result.getExtra();
185        menu.setGroupVisible(R.id.PHONE_MENU, type == WebView.HitTestResult.PHONE_TYPE);
186        menu.setGroupVisible(R.id.EMAIL_MENU, type == WebView.HitTestResult.EMAIL_TYPE);
187        menu.setGroupVisible(R.id.GEO_MENU, type == WebView.HitTestResult.GEO_TYPE);
188        menu.setGroupVisible(R.id.ANCHOR_MENU, type == WebView.HitTestResult.SRC_ANCHOR_TYPE
189                || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
190        menu.setGroupVisible(R.id.IMAGE_MENU, type == WebView.HitTestResult.IMAGE_TYPE
191                || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
192
193        // Setup custom handling depending on the type
194        switch (type) {
195            case WebView.HitTestResult.PHONE_TYPE:
196                String decodedPhoneExtra;
197                try {
198                    decodedPhoneExtra = URLDecoder.decode(extra, Charset.defaultCharset().name());
199                }
200                catch (UnsupportedEncodingException ignore) {
201                    // Should never happen; default charset is UTF-8
202                    decodedPhoneExtra = extra;
203                }
204
205                menu.setHeaderTitle(decodedPhoneExtra);
206                // Dial
207                final MenuItem dialMenuItem =
208                        menu.findItem(getMenuResIdForMenuType(MenuType.DIAL_MENU));
209
210                if (mSupportsDial) {
211                    // remove the on click listener
212                    dialMenuItem.setOnMenuItemClickListener(null);
213                    dialMenuItem.setIntent(new Intent(Intent.ACTION_DIAL,
214                            Uri.parse(WebView.SCHEME_TEL + extra)));
215                } else {
216                    dialMenuItem.setVisible(false);
217                }
218
219                // Send SMS
220                final MenuItem sendSmsMenuItem =
221                        menu.findItem(getMenuResIdForMenuType(MenuType.SMS_MENU));
222                if (mSupportsSms) {
223                    // remove the on click listener
224                    sendSmsMenuItem.setOnMenuItemClickListener(null);
225                    sendSmsMenuItem.setIntent(new Intent(Intent.ACTION_SENDTO,
226                            Uri.parse("smsto:" + extra)));
227                } else {
228                    sendSmsMenuItem.setVisible(false);
229                }
230
231                // Add to contacts
232                final Intent addIntent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
233                addIntent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
234
235                addIntent.putExtra(ContactsContract.Intents.Insert.PHONE, decodedPhoneExtra);
236                final MenuItem addToContactsMenuItem =
237                        menu.findItem(getMenuResIdForMenuType(MenuType.ADD_CONTACT_MENU));
238                // remove the on click listener
239                addToContactsMenuItem.setOnMenuItemClickListener(null);
240                addToContactsMenuItem.setIntent(addIntent);
241
242                // Copy
243                menu.findItem(getMenuResIdForMenuType(MenuType.COPY_PHONE_MENU)).
244                        setOnMenuItemClickListener(new Copy(extra));
245                break;
246
247            case WebView.HitTestResult.EMAIL_TYPE:
248                menu.setHeaderTitle(extra);
249                menu.findItem(getMenuResIdForMenuType(MenuType.EMAIL_CONTACT_MENU)).setIntent(
250                        new Intent(Intent.ACTION_VIEW, Uri
251                                .parse(WebView.SCHEME_MAILTO + extra)));
252                menu.findItem(getMenuResIdForMenuType(MenuType.COPY_MAIL_MENU)).
253                        setOnMenuItemClickListener(new Copy(extra));
254                break;
255
256            case WebView.HitTestResult.GEO_TYPE:
257                menu.setHeaderTitle(extra);
258                String geoExtra = "";
259                try {
260                    geoExtra = URLEncoder.encode(extra, Charset.defaultCharset().name());
261                }
262                catch (UnsupportedEncodingException ignore) {
263                    // Should never happen; default charset is UTF-8
264                }
265                final MenuItem viewMapMenuItem =
266                        menu.findItem(getMenuResIdForMenuType(MenuType.MAP_MENU));
267                // remove the on click listener
268                viewMapMenuItem.setOnMenuItemClickListener(null);
269                viewMapMenuItem.setIntent(new Intent(Intent.ACTION_VIEW,
270                        Uri.parse(WebView.SCHEME_GEO + geoExtra)));
271                menu.findItem(getMenuResIdForMenuType(MenuType.COPY_GEO_MENU)).
272                        setOnMenuItemClickListener(new Copy(extra));
273                break;
274
275            case WebView.HitTestResult.SRC_ANCHOR_TYPE:
276            case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
277                menu.findItem(getMenuResIdForMenuType(MenuType.SHARE_LINK_MENU)).setVisible(
278                        showShareLinkMenuItem());
279
280                // The documentation for WebView indicates that if the HitTestResult is
281                // SRC_ANCHOR_TYPE or the url would be specified in the extra.  We don't need to
282                // call requestFocusNodeHref().  If we wanted to handle UNKNOWN HitTestResults, we
283                // would.  With this knowledge, we can just set the title
284                menu.setHeaderTitle(extra);
285
286                menu.findItem(getMenuResIdForMenuType(MenuType.COPY_LINK_MENU)).
287                        setOnMenuItemClickListener(new Copy(extra));
288
289                final MenuItem openLinkMenuItem =
290                        menu.findItem(getMenuResIdForMenuType(MenuType.OPEN_MENU));
291                // remove the on click listener
292                openLinkMenuItem.setOnMenuItemClickListener(null);
293                openLinkMenuItem.setIntent(new Intent(Intent.ACTION_VIEW, Uri.parse(extra)));
294
295                menu.findItem(getMenuResIdForMenuType(MenuType.SHARE_LINK_MENU)).
296                        setOnMenuItemClickListener(new Share(extra));
297                break;
298            case WebView.HitTestResult.IMAGE_TYPE:
299            default:
300                break;
301        }
302    }
303
304    @Override
305    public boolean onMenuItemClick(MenuItem item) {
306        return onMenuItemSelected(item);
307    }
308
309    /**
310     * Returns the menu resource id for the specified menu type
311     * @param menuType type of the specified menu
312     * @return menu resource id
313     */
314    protected int getMenuResIdForMenuType(MenuType menuType) {
315        switch(menuType) {
316            case OPEN_MENU:
317                return R.id.open_context_menu_id;
318            case COPY_LINK_MENU:
319                return R.id.copy_link_context_menu_id;
320            case SHARE_LINK_MENU:
321                return R.id.share_link_context_menu_id;
322            case DIAL_MENU:
323                return R.id.dial_context_menu_id;
324            case SMS_MENU:
325                return R.id.sms_context_menu_id;
326            case ADD_CONTACT_MENU:
327                return R.id.add_contact_context_menu_id;
328            case COPY_PHONE_MENU:
329                return R.id.copy_phone_context_menu_id;
330            case EMAIL_CONTACT_MENU:
331                return R.id.email_context_menu_id;
332            case COPY_MAIL_MENU:
333                return R.id.copy_mail_context_menu_id;
334            case MAP_MENU:
335                return R.id.map_context_menu_id;
336            case COPY_GEO_MENU:
337                return R.id.copy_geo_context_menu_id;
338            default:
339                throw new IllegalStateException("Unexpected MenuType");
340        }
341    }
342
343    /**
344     * Returns the resource id of the string to be used when showing a chooser for a menu
345     * @param menuType type of the specified menu
346     * @return string resource id
347     */
348    protected int getChooserTitleStringResIdForMenuType(MenuType menuType) {
349        switch(menuType) {
350            case SHARE_LINK_MENU:
351                return R.string.choosertitle_sharevia;
352            default:
353                throw new IllegalStateException("Unexpected MenuType");
354        }
355    }
356
357    /**
358     * Returns the resource id for the web view context menu
359     */
360    protected int getMenuResourceId() {
361        return R.menu.webview_context_menu;
362    }
363
364
365    /**
366     * Called when a menu item is not handled by the context menu.
367     */
368    protected boolean onMenuItemSelected(MenuItem menuItem) {
369        return mActivity.onOptionsItemSelected(menuItem);
370    }
371}
372