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