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