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.mailcommon;
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.LayoutInflater;
32import android.view.MenuInflater;
33import android.view.MenuItem;
34import android.view.View;
35import android.view.View.OnCreateContextMenuListener;
36import android.webkit.WebView;
37import android.widget.TextView;
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 * <p>Activity/Fragment clients must forward the 'onContextItemSelected' method.</p>
49 * <br>
50 * Dependencies:
51 * <ul>
52 * <li>res/menu/webview_context_menu.xml</li>
53 * <li>res/values/webview_context_menu_strings.xml</li>
54 * </ul>
55 */
56public abstract class WebViewContextMenu implements OnCreateContextMenuListener,
57        MenuItem.OnMenuItemClickListener {
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    protected static enum MenuGroupType {
76        PHONE_GROUP,
77        EMAIL_GROUP,
78        GEO_GROUP,
79        ANCHOR_GROUP,
80    }
81
82    public WebViewContextMenu(Activity host) {
83        this.mActivity = host;
84    }
85
86    // For our copy menu items.
87    private class Copy implements MenuItem.OnMenuItemClickListener {
88        private final CharSequence mText;
89
90        public Copy(CharSequence text) {
91            mText = text;
92        }
93
94        @Override
95        public boolean onMenuItemClick(MenuItem item) {
96            copy(mText);
97            return true;
98        }
99    }
100
101    // For our share menu items.
102    private class Share implements MenuItem.OnMenuItemClickListener {
103        private final String mUri;
104
105        public Share(String text) {
106            mUri = text;
107        }
108
109        @Override
110        public boolean onMenuItemClick(MenuItem item) {
111            shareLink(mUri);
112            return true;
113        }
114    }
115
116    private boolean showShareLinkMenuItem() {
117        PackageManager pm = mActivity.getPackageManager();
118        Intent send = new Intent(Intent.ACTION_SEND);
119        send.setType("text/plain");
120        ResolveInfo ri = pm.resolveActivity(send, PackageManager.MATCH_DEFAULT_ONLY);
121        return ri != null;
122    }
123
124    private void shareLink(String url) {
125        Intent send = new Intent(Intent.ACTION_SEND);
126        send.setType("text/plain");
127        send.putExtra(Intent.EXTRA_TEXT, url);
128
129        try {
130            mActivity.startActivity(Intent.createChooser(send, mActivity.getText(
131                    getChooserTitleStringResIdForMenuType(MenuType.SHARE_LINK_MENU))));
132        } catch(android.content.ActivityNotFoundException ex) {
133            // if no app handles it, do nothing
134        }
135    }
136
137    private void copy(CharSequence text) {
138        ClipboardManager clipboard =
139                (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
140        clipboard.setPrimaryClip(ClipData.newPlainText(null, text));
141    }
142
143    public void onCreateContextMenu(ContextMenu menu, View v,
144            ContextMenuInfo info) {
145        // FIXME: This is copied over almost directly from BrowserActivity.
146        // Would like to find a way to combine the two (Bug 1251210).
147
148        WebView webview = (WebView) v;
149        WebView.HitTestResult result = webview.getHitTestResult();
150        if (result == null) {
151            return;
152        }
153
154        int type = result.getType();
155        switch (type) {
156            case WebView.HitTestResult.UNKNOWN_TYPE:
157            case WebView.HitTestResult.EDIT_TEXT_TYPE:
158                return;
159            default:
160                break;
161        }
162
163        // Note, http://b/issue?id=1106666 is requesting that
164        // an inflated menu can be used again. This is not available
165        // yet, so inflate each time (yuk!)
166        MenuInflater inflater = mActivity.getMenuInflater();
167        // Also, we are copying the menu file from browser until
168        // 1251210 is fixed.
169        inflater.inflate(getMenuResourceId(), menu);
170
171        // Initially make set the menu item handler this WebViewContextMenu, which will default to
172        // calling the non-abstract subclass's implementation.
173        for (int i = 0; i < menu.size(); i++) {
174            final MenuItem menuItem = menu.getItem(i);
175            menuItem.setOnMenuItemClickListener(this);
176        }
177
178
179        // Show the correct menu group
180        String extra = result.getExtra();
181        menu.setGroupVisible(getMenuGroupResId(MenuGroupType.PHONE_GROUP),
182                type == WebView.HitTestResult.PHONE_TYPE);
183        menu.setGroupVisible(getMenuGroupResId(MenuGroupType.EMAIL_GROUP),
184                type == WebView.HitTestResult.EMAIL_TYPE);
185        menu.setGroupVisible(getMenuGroupResId(MenuGroupType.GEO_GROUP),
186                type == WebView.HitTestResult.GEO_TYPE);
187        menu.setGroupVisible(getMenuGroupResId(MenuGroupType.ANCHOR_GROUP),
188                type == WebView.HitTestResult.SRC_ANCHOR_TYPE
189                || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
190
191        // Setup custom handling depending on the type
192        switch (type) {
193            case WebView.HitTestResult.PHONE_TYPE:
194                String decodedPhoneExtra;
195                try {
196                    decodedPhoneExtra = URLDecoder.decode(extra, Charset.defaultCharset().name());
197                }
198                catch (UnsupportedEncodingException ignore) {
199                    // Should never happen; default charset is UTF-8
200                    decodedPhoneExtra = extra;
201                }
202
203                menu.setHeaderTitle(decodedPhoneExtra);
204                // Dial
205                final MenuItem dialMenuItem =
206                        menu.findItem(getMenuResIdForMenuType(MenuType.DIAL_MENU));
207                // remove the on click listener
208                dialMenuItem.setOnMenuItemClickListener(null);
209                dialMenuItem.setIntent(new Intent(Intent.ACTION_VIEW,
210                        Uri.parse(WebView.SCHEME_TEL + extra)));
211
212                // Send SMS
213                final MenuItem sendSmsMenuItem =
214                        menu.findItem(getMenuResIdForMenuType(MenuType.SMS_MENU));
215                // remove the on click listener
216                sendSmsMenuItem.setOnMenuItemClickListener(null);
217                sendSmsMenuItem.setIntent(new Intent(Intent.ACTION_SENDTO,
218                        Uri.parse("smsto:" + extra)));
219
220                // Add to contacts
221                final Intent addIntent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
222                addIntent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
223
224                addIntent.putExtra(ContactsContract.Intents.Insert.PHONE, decodedPhoneExtra);
225                final MenuItem addToContactsMenuItem =
226                        menu.findItem(getMenuResIdForMenuType(MenuType.ADD_CONTACT_MENU));
227                // remove the on click listener
228                addToContactsMenuItem.setOnMenuItemClickListener(null);
229                addToContactsMenuItem.setIntent(addIntent);
230
231                // Copy
232                menu.findItem(getMenuResIdForMenuType(MenuType.COPY_PHONE_MENU)).
233                        setOnMenuItemClickListener(new Copy(extra));
234                break;
235
236            case WebView.HitTestResult.EMAIL_TYPE:
237                menu.setHeaderTitle(extra);
238                menu.findItem(getMenuResIdForMenuType(MenuType.EMAIL_CONTACT_MENU)).setIntent(
239                        new Intent(Intent.ACTION_VIEW, Uri
240                                .parse(WebView.SCHEME_MAILTO + extra)));
241                menu.findItem(getMenuResIdForMenuType(MenuType.COPY_MAIL_MENU)).
242                        setOnMenuItemClickListener(new Copy(extra));
243                break;
244
245            case WebView.HitTestResult.GEO_TYPE:
246                menu.setHeaderTitle(extra);
247                String geoExtra = "";
248                try {
249                    geoExtra = URLEncoder.encode(extra, Charset.defaultCharset().name());
250                }
251                catch (UnsupportedEncodingException ignore) {
252                    // Should never happen; default charset is UTF-8
253                }
254                final MenuItem viewMapMenuItem =
255                        menu.findItem(getMenuResIdForMenuType(MenuType.MAP_MENU));
256                // remove the on click listener
257                viewMapMenuItem.setOnMenuItemClickListener(null);
258                viewMapMenuItem.setIntent(new Intent(Intent.ACTION_VIEW,
259                        Uri.parse(WebView.SCHEME_GEO + geoExtra)));
260                menu.findItem(getMenuResIdForMenuType(MenuType.COPY_GEO_MENU)).
261                        setOnMenuItemClickListener(new Copy(extra));
262                break;
263
264            case WebView.HitTestResult.SRC_ANCHOR_TYPE:
265            case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
266                // FIXME: Make this look like the normal menu header
267                // We cannot use the normal menu header because we need to
268                // edit the ContextMenu after it has been created.
269                final TextView titleView = (TextView) LayoutInflater.from(mActivity)
270                        .inflate(getTitleViewLayoutResId(MenuType.SHARE_LINK_MENU), null);
271                menu.setHeaderView(titleView);
272
273                menu.findItem(getMenuResIdForMenuType(MenuType.SHARE_LINK_MENU)).setVisible(
274                        showShareLinkMenuItem());
275
276                // The documentation for WebView indicates that if the HitTestResult is
277                // SRC_ANCHOR_TYPE or the url would be specified in the extra.  We don't need to
278                // call requestFocusNodeHref().  If we wanted to handle UNKNOWN HitTestResults, we
279                // would.  With this knowledge, we can just set the title
280                titleView.setText(extra);
281
282                menu.findItem(getMenuResIdForMenuType(MenuType.COPY_LINK_MENU)).
283                        setOnMenuItemClickListener(new Copy(extra));
284
285                final MenuItem openLinkMenuItem =
286                        menu.findItem(getMenuResIdForMenuType(MenuType.OPEN_MENU));
287                // remove the on click listener
288                openLinkMenuItem.setOnMenuItemClickListener(null);
289                openLinkMenuItem.setIntent(new Intent(Intent.ACTION_VIEW, Uri.parse(extra)));
290
291                menu.findItem(getMenuResIdForMenuType(MenuType.SHARE_LINK_MENU)).
292                        setOnMenuItemClickListener(new Share(extra));
293                break;
294            default:
295                break;
296        }
297    }
298
299    @Override
300    public boolean onMenuItemClick(MenuItem item) {
301        return onMenuItemSelected(item);
302    }
303
304    /**
305     * Returns the menu type from the given resource id
306     * @param menuResId resource id of the menu
307     * @return MenuType for the specified menu resource id
308     */
309    abstract protected MenuType getMenuTypeFromResId(int menuResId);
310
311    /**
312     * Returns the menu resource id for the specified menu type
313     * @param menuType type of the specified menu
314     * @return menu resource id
315     */
316    abstract protected int getMenuResIdForMenuType(MenuType menuType);
317
318    /**
319     * Returns the resource id of the string to be used when showing a chooser for a menu
320     * @param menuType type of the specified menu
321     * @return string resource id
322     */
323    abstract protected int getChooserTitleStringResIdForMenuType(MenuType menuType);
324
325    /**
326     * Returns the resource id of the layout to be used for the title of the specified menu
327     * @param menuType type of the specified menu
328     * @return layout resource id
329     */
330    abstract protected int getTitleViewLayoutResId(MenuType menuType);
331
332    /**
333     * Returns the menu group resource id for the specified menu group type.
334     * @param menuGroupType menu group type
335     * @return menu group resource id
336     */
337    abstract protected int getMenuGroupResId(MenuGroupType menuGroupType);
338
339    /**
340     * Returns the resource id for the web view context menu
341     */
342    abstract protected int getMenuResourceId();
343
344
345    /**
346     * Called when a menu item is not handled by the context menu.
347     */
348    abstract protected boolean onMenuItemSelected(MenuItem menuItem);
349}
350