Home | History | Annotate | Download | only in browse
      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 
     18 package com.android.mail.browse;
     19 
     20 import android.app.Activity;
     21 import android.content.ClipData;
     22 import android.content.ClipboardManager;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.content.pm.PackageManager;
     26 import android.content.pm.ResolveInfo;
     27 import android.net.Uri;
     28 import android.provider.ContactsContract;
     29 import android.view.ContextMenu;
     30 import android.view.ContextMenu.ContextMenuInfo;
     31 import android.view.MenuInflater;
     32 import android.view.MenuItem;
     33 import android.view.View;
     34 import android.view.View.OnCreateContextMenuListener;
     35 import android.webkit.WebView;
     36 
     37 import com.android.mail.R;
     38 
     39 import java.io.UnsupportedEncodingException;
     40 import java.net.URLDecoder;
     41 import java.net.URLEncoder;
     42 import 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  */
     53 public 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     protected static enum MenuGroupType {
     76         PHONE_GROUP,
     77         EMAIL_GROUP,
     78         GEO_GROUP,
     79         ANCHOR_GROUP,
     80     }
     81 
     82     public WebViewContextMenu(Activity host) {
     83         mActivity = host;
     84 
     85         // Query the package manager to see if the device
     86         // has an app that supports ACTION_DIAL or ACTION_SENDTO
     87         // with the appropriate uri schemes.
     88         final PackageManager pm = mActivity.getPackageManager();
     89         mSupportsDial = !pm.queryIntentActivities(
     90                 new Intent(Intent.ACTION_DIAL, Uri.parse(WebView.SCHEME_TEL)),
     91                 PackageManager.MATCH_DEFAULT_ONLY).isEmpty();
     92         mSupportsSms = !pm.queryIntentActivities(
     93                 new Intent(Intent.ACTION_SENDTO, Uri.parse("smsto:")),
     94                 PackageManager.MATCH_DEFAULT_ONLY).isEmpty();
     95         ;
     96     }
     97 
     98     // For our copy menu items.
     99     private class Copy implements MenuItem.OnMenuItemClickListener {
    100         private final CharSequence mText;
    101 
    102         public Copy(CharSequence text) {
    103             mText = text;
    104         }
    105 
    106         @Override
    107         public boolean onMenuItemClick(MenuItem item) {
    108             copy(mText);
    109             return true;
    110         }
    111     }
    112 
    113     // For our share menu items.
    114     private class Share implements MenuItem.OnMenuItemClickListener {
    115         private final String mUri;
    116 
    117         public Share(String text) {
    118             mUri = text;
    119         }
    120 
    121         @Override
    122         public boolean onMenuItemClick(MenuItem item) {
    123             shareLink(mUri);
    124             return true;
    125         }
    126     }
    127 
    128     private boolean showShareLinkMenuItem() {
    129         PackageManager pm = mActivity.getPackageManager();
    130         Intent send = new Intent(Intent.ACTION_SEND);
    131         send.setType("text/plain");
    132         ResolveInfo ri = pm.resolveActivity(send, PackageManager.MATCH_DEFAULT_ONLY);
    133         return ri != null;
    134     }
    135 
    136     private void shareLink(String url) {
    137         Intent send = new Intent(Intent.ACTION_SEND);
    138         send.setType("text/plain");
    139         send.putExtra(Intent.EXTRA_TEXT, url);
    140 
    141         try {
    142             mActivity.startActivity(Intent.createChooser(send, mActivity.getText(
    143                     getChooserTitleStringResIdForMenuType(MenuType.SHARE_LINK_MENU))));
    144         } catch(android.content.ActivityNotFoundException ex) {
    145             // if no app handles it, do nothing
    146         }
    147     }
    148 
    149     private void copy(CharSequence text) {
    150         ClipboardManager clipboard =
    151                 (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
    152         clipboard.setPrimaryClip(ClipData.newPlainText(null, text));
    153     }
    154 
    155     @Override
    156     public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo info) {
    157         // FIXME: This is copied over almost directly from BrowserActivity.
    158         // Would like to find a way to combine the two (Bug 1251210).
    159 
    160         WebView webview = (WebView) v;
    161         WebView.HitTestResult result = webview.getHitTestResult();
    162         if (result == null) {
    163             return;
    164         }
    165 
    166         int type = result.getType();
    167         switch (type) {
    168             case WebView.HitTestResult.UNKNOWN_TYPE:
    169             case WebView.HitTestResult.EDIT_TEXT_TYPE:
    170                 return;
    171             default:
    172                 break;
    173         }
    174 
    175         // Note, http://b/issue?id=1106666 is requesting that
    176         // an inflated menu can be used again. This is not available
    177         // yet, so inflate each time (yuk!)
    178         MenuInflater inflater = mActivity.getMenuInflater();
    179         // Also, we are copying the menu file from browser until
    180         // 1251210 is fixed.
    181         inflater.inflate(getMenuResourceId(), menu);
    182 
    183         // Initially make set the menu item handler this WebViewContextMenu, which will default to
    184         // calling the non-abstract subclass's implementation.
    185         for (int i = 0; i < menu.size(); i++) {
    186             final MenuItem menuItem = menu.getItem(i);
    187             menuItem.setOnMenuItemClickListener(this);
    188         }
    189 
    190 
    191         // Show the correct menu group
    192         String extra = result.getExtra();
    193         menu.setGroupVisible(getMenuGroupResId(MenuGroupType.PHONE_GROUP),
    194                 type == WebView.HitTestResult.PHONE_TYPE);
    195         menu.setGroupVisible(getMenuGroupResId(MenuGroupType.EMAIL_GROUP),
    196                 type == WebView.HitTestResult.EMAIL_TYPE);
    197         menu.setGroupVisible(getMenuGroupResId(MenuGroupType.GEO_GROUP),
    198                 type == WebView.HitTestResult.GEO_TYPE);
    199         menu.setGroupVisible(getMenuGroupResId(MenuGroupType.ANCHOR_GROUP),
    200                 type == WebView.HitTestResult.SRC_ANCHOR_TYPE
    201                 || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
    202 
    203         // Setup custom handling depending on the type
    204         switch (type) {
    205             case WebView.HitTestResult.PHONE_TYPE:
    206                 String decodedPhoneExtra;
    207                 try {
    208                     decodedPhoneExtra = URLDecoder.decode(extra, Charset.defaultCharset().name());
    209                 }
    210                 catch (UnsupportedEncodingException ignore) {
    211                     // Should never happen; default charset is UTF-8
    212                     decodedPhoneExtra = extra;
    213                 }
    214 
    215                 menu.setHeaderTitle(decodedPhoneExtra);
    216                 // Dial
    217                 final MenuItem dialMenuItem =
    218                         menu.findItem(getMenuResIdForMenuType(MenuType.DIAL_MENU));
    219 
    220                 if (mSupportsDial) {
    221                     // remove the on click listener
    222                     dialMenuItem.setOnMenuItemClickListener(null);
    223                     dialMenuItem.setIntent(new Intent(Intent.ACTION_DIAL,
    224                             Uri.parse(WebView.SCHEME_TEL + extra)));
    225                 } else {
    226                     dialMenuItem.setVisible(false);
    227                 }
    228 
    229                 // Send SMS
    230                 final MenuItem sendSmsMenuItem =
    231                         menu.findItem(getMenuResIdForMenuType(MenuType.SMS_MENU));
    232                 if (mSupportsSms) {
    233                     // remove the on click listener
    234                     sendSmsMenuItem.setOnMenuItemClickListener(null);
    235                     sendSmsMenuItem.setIntent(new Intent(Intent.ACTION_SENDTO,
    236                             Uri.parse("smsto:" + extra)));
    237                 } else {
    238                     sendSmsMenuItem.setVisible(false);
    239                 }
    240 
    241                 // Add to contacts
    242                 final Intent addIntent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
    243                 addIntent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
    244 
    245                 addIntent.putExtra(ContactsContract.Intents.Insert.PHONE, decodedPhoneExtra);
    246                 final MenuItem addToContactsMenuItem =
    247                         menu.findItem(getMenuResIdForMenuType(MenuType.ADD_CONTACT_MENU));
    248                 // remove the on click listener
    249                 addToContactsMenuItem.setOnMenuItemClickListener(null);
    250                 addToContactsMenuItem.setIntent(addIntent);
    251 
    252                 // Copy
    253                 menu.findItem(getMenuResIdForMenuType(MenuType.COPY_PHONE_MENU)).
    254                         setOnMenuItemClickListener(new Copy(extra));
    255                 break;
    256 
    257             case WebView.HitTestResult.EMAIL_TYPE:
    258                 menu.setHeaderTitle(extra);
    259                 menu.findItem(getMenuResIdForMenuType(MenuType.EMAIL_CONTACT_MENU)).setIntent(
    260                         new Intent(Intent.ACTION_VIEW, Uri
    261                                 .parse(WebView.SCHEME_MAILTO + extra)));
    262                 menu.findItem(getMenuResIdForMenuType(MenuType.COPY_MAIL_MENU)).
    263                         setOnMenuItemClickListener(new Copy(extra));
    264                 break;
    265 
    266             case WebView.HitTestResult.GEO_TYPE:
    267                 menu.setHeaderTitle(extra);
    268                 String geoExtra = "";
    269                 try {
    270                     geoExtra = URLEncoder.encode(extra, Charset.defaultCharset().name());
    271                 }
    272                 catch (UnsupportedEncodingException ignore) {
    273                     // Should never happen; default charset is UTF-8
    274                 }
    275                 final MenuItem viewMapMenuItem =
    276                         menu.findItem(getMenuResIdForMenuType(MenuType.MAP_MENU));
    277                 // remove the on click listener
    278                 viewMapMenuItem.setOnMenuItemClickListener(null);
    279                 viewMapMenuItem.setIntent(new Intent(Intent.ACTION_VIEW,
    280                         Uri.parse(WebView.SCHEME_GEO + geoExtra)));
    281                 menu.findItem(getMenuResIdForMenuType(MenuType.COPY_GEO_MENU)).
    282                         setOnMenuItemClickListener(new Copy(extra));
    283                 break;
    284 
    285             case WebView.HitTestResult.SRC_ANCHOR_TYPE:
    286             case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
    287                 menu.findItem(getMenuResIdForMenuType(MenuType.SHARE_LINK_MENU)).setVisible(
    288                         showShareLinkMenuItem());
    289 
    290                 // The documentation for WebView indicates that if the HitTestResult is
    291                 // SRC_ANCHOR_TYPE or the url would be specified in the extra.  We don't need to
    292                 // call requestFocusNodeHref().  If we wanted to handle UNKNOWN HitTestResults, we
    293                 // would.  With this knowledge, we can just set the title
    294                 menu.setHeaderTitle(extra);
    295 
    296                 menu.findItem(getMenuResIdForMenuType(MenuType.COPY_LINK_MENU)).
    297                         setOnMenuItemClickListener(new Copy(extra));
    298 
    299                 final MenuItem openLinkMenuItem =
    300                         menu.findItem(getMenuResIdForMenuType(MenuType.OPEN_MENU));
    301                 // remove the on click listener
    302                 openLinkMenuItem.setOnMenuItemClickListener(null);
    303                 openLinkMenuItem.setIntent(new Intent(Intent.ACTION_VIEW, Uri.parse(extra)));
    304 
    305                 menu.findItem(getMenuResIdForMenuType(MenuType.SHARE_LINK_MENU)).
    306                         setOnMenuItemClickListener(new Share(extra));
    307                 break;
    308             default:
    309                 break;
    310         }
    311     }
    312 
    313     @Override
    314     public boolean onMenuItemClick(MenuItem item) {
    315         return onMenuItemSelected(item);
    316     }
    317 
    318     /**
    319      * Returns the menu type from the given resource id
    320      * @param menuResId resource id of the menu
    321      * @return MenuType for the specified menu resource id
    322      */
    323     protected MenuType getMenuTypeFromResId(final int menuResId) {
    324         if (menuResId == R.id.open_context_menu_id) {
    325             return MenuType.OPEN_MENU;
    326         } else if (menuResId == R.id.copy_link_context_menu_id) {
    327             return MenuType.COPY_LINK_MENU;
    328         } else if (menuResId == R.id.share_link_context_menu_id) {
    329             return MenuType.SHARE_LINK_MENU;
    330         } else if (menuResId == R.id.dial_context_menu_id) {
    331             return MenuType.DIAL_MENU;
    332         } else if (menuResId == R.id.sms_context_menu_id) {
    333             return MenuType.SMS_MENU;
    334         } else if (menuResId == R.id.add_contact_context_menu_id) {
    335             return MenuType.ADD_CONTACT_MENU;
    336         } else if (menuResId == R.id.copy_phone_context_menu_id) {
    337             return MenuType.COPY_PHONE_MENU;
    338         } else if (menuResId == R.id.email_context_menu_id) {
    339             return MenuType.EMAIL_CONTACT_MENU;
    340         } else if (menuResId == R.id.copy_mail_context_menu_id) {
    341             return MenuType.COPY_MAIL_MENU;
    342         } else if (menuResId == R.id.map_context_menu_id) {
    343             return MenuType.MAP_MENU;
    344         } else if (menuResId == R.id.copy_geo_context_menu_id) {
    345             return MenuType.COPY_GEO_MENU;
    346         } else {
    347             throw new IllegalStateException("Unexpected resource id");
    348         }
    349     }
    350 
    351     /**
    352      * Returns the menu resource id for the specified menu type
    353      * @param menuType type of the specified menu
    354      * @return menu resource id
    355      */
    356     protected int getMenuResIdForMenuType(MenuType menuType) {
    357         switch(menuType) {
    358             case OPEN_MENU:
    359                 return R.id.open_context_menu_id;
    360             case COPY_LINK_MENU:
    361                 return R.id.copy_link_context_menu_id;
    362             case SHARE_LINK_MENU:
    363                 return R.id.share_link_context_menu_id;
    364             case DIAL_MENU:
    365                 return R.id.dial_context_menu_id;
    366             case SMS_MENU:
    367                 return R.id.sms_context_menu_id;
    368             case ADD_CONTACT_MENU:
    369                 return R.id.add_contact_context_menu_id;
    370             case COPY_PHONE_MENU:
    371                 return R.id.copy_phone_context_menu_id;
    372             case EMAIL_CONTACT_MENU:
    373                 return R.id.email_context_menu_id;
    374             case COPY_MAIL_MENU:
    375                 return R.id.copy_mail_context_menu_id;
    376             case MAP_MENU:
    377                 return R.id.map_context_menu_id;
    378             case COPY_GEO_MENU:
    379                 return R.id.copy_geo_context_menu_id;
    380             default:
    381                 throw new IllegalStateException("Unexpected MenuType");
    382         }
    383     }
    384 
    385     /**
    386      * Returns the resource id of the string to be used when showing a chooser for a menu
    387      * @param menuType type of the specified menu
    388      * @return string resource id
    389      */
    390     protected int getChooserTitleStringResIdForMenuType(MenuType menuType) {
    391         switch(menuType) {
    392             case SHARE_LINK_MENU:
    393                 return R.string.choosertitle_sharevia;
    394             default:
    395                 throw new IllegalStateException("Unexpected MenuType");
    396         }
    397     }
    398 
    399     /**
    400      * Returns the menu group resource id for the specified menu group type.
    401      * @param menuGroupType menu group type
    402      * @return menu group resource id
    403      */
    404     protected int getMenuGroupResId(MenuGroupType menuGroupType) {
    405         switch (menuGroupType) {
    406             case PHONE_GROUP:
    407                 return R.id.PHONE_MENU;
    408             case EMAIL_GROUP:
    409                 return R.id.EMAIL_MENU;
    410             case GEO_GROUP:
    411                 return R.id.GEO_MENU;
    412             case ANCHOR_GROUP:
    413                 return R.id.ANCHOR_MENU;
    414             default:
    415                 throw new IllegalStateException("Unexpected MenuGroupType");
    416         }
    417     }
    418 
    419     /**
    420      * Returns the resource id for the web view context menu
    421      */
    422     protected int getMenuResourceId() {
    423         return R.menu.webview_context_menu;
    424     }
    425 
    426 
    427     /**
    428      * Called when a menu item is not handled by the context menu.
    429      */
    430     protected boolean onMenuItemSelected(MenuItem menuItem) {
    431         return mActivity.onOptionsItemSelected(menuItem);
    432     }
    433 }
    434