blob: fc6ac63f4af214ba972bed7c0357285af1565873 [file] [log] [blame]
/*
* Copyright (C) 2011 Google Inc.
* Licensed to The Android Open Source Project.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.mail.browse;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.provider.ContactsContract;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnCreateContextMenuListener;
import android.webkit.WebView;
import com.android.mail.R;
import com.android.mail.analytics.Analytics;
import com.android.mail.providers.Message;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
/**
* <p>Handles display and behavior of the context menu for known actionable content in WebViews.
* Requires an Activity to bind to for Context resolution and to start other activites.</p>
* <br>
* Dependencies:
* <ul>
* <li>res/menu/webview_context_menu.xml</li>
* </ul>
*/
public class WebViewContextMenu implements OnCreateContextMenuListener,
MenuItem.OnMenuItemClickListener {
private final Activity mActivity;
private final InlineAttachmentViewIntentBuilder mIntentBuilder;
private final boolean mSupportsDial;
private final boolean mSupportsSms;
private Callbacks mCallbacks;
// Strings used for analytics.
private static final String CATEGORY_WEB_CONTEXT_MENU = "web_context_menu";
private static final String ACTION_LONG_PRESS = "long_press";
private static final String ACTION_CLICK = "menu_clicked";
protected static enum MenuType {
OPEN_MENU,
COPY_LINK_MENU,
SHARE_LINK_MENU,
DIAL_MENU,
SMS_MENU,
ADD_CONTACT_MENU,
COPY_PHONE_MENU,
EMAIL_CONTACT_MENU,
COPY_MAIL_MENU,
MAP_MENU,
COPY_GEO_MENU,
}
public interface Callbacks {
/**
* Given a URL the user clicks/long-presses on, get the {@link Message} whose body contains
* that URL.
*
* @param url URL of a selected link
* @return Message containing that URL
*/
Message getMessageForClickedUrl(String url);
}
public WebViewContextMenu(Activity host, InlineAttachmentViewIntentBuilder builder) {
mActivity = host;
mIntentBuilder = builder;
// Query the package manager to see if the device
// has an app that supports ACTION_DIAL or ACTION_SENDTO
// with the appropriate uri schemes.
final PackageManager pm = mActivity.getPackageManager();
mSupportsDial = !pm.queryIntentActivities(
new Intent(Intent.ACTION_DIAL, Uri.parse(WebView.SCHEME_TEL)),
PackageManager.MATCH_DEFAULT_ONLY).isEmpty();
mSupportsSms = !pm.queryIntentActivities(
new Intent(Intent.ACTION_SENDTO, Uri.parse("smsto:")),
PackageManager.MATCH_DEFAULT_ONLY).isEmpty();
}
public void setCallbacks(Callbacks cb) {
mCallbacks = cb;
}
/**
* Abstract base class that automates sending an analytics event
* when the menu item is clicked.
*/
private abstract class AnalyticsClick implements MenuItem.OnMenuItemClickListener {
private final String mAnalyticsLabel;
public AnalyticsClick(String analyticsLabel) {
mAnalyticsLabel = analyticsLabel;
}
@Override
public final boolean onMenuItemClick(MenuItem item) {
Analytics.getInstance().sendEvent(
CATEGORY_WEB_CONTEXT_MENU, ACTION_CLICK, mAnalyticsLabel, 0);
return onClick();
}
public abstract boolean onClick();
}
// For our copy menu items.
private class Copy extends AnalyticsClick {
private final CharSequence mText;
public Copy(CharSequence text, String analyticsLabel) {
super(analyticsLabel);
mText = text;
}
@Override
public boolean onClick() {
ClipboardManager clipboard =
(ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(ClipData.newPlainText(null, mText));
return true;
}
}
/**
* Sends an intent and reports the analytics event.
*/
private class SendIntent extends AnalyticsClick {
private Intent mIntent;
public SendIntent(String analyticsLabel) {
super(analyticsLabel);
}
public SendIntent(Intent intent, String analyticsLabel) {
super(analyticsLabel);
setIntent(intent);
}
void setIntent(Intent intent) {
mIntent = intent;
}
@Override
public final boolean onClick() {
try {
mActivity.startActivity(mIntent);
} catch(android.content.ActivityNotFoundException ex) {
// if no app handles it, do nothing
}
return true;
}
}
// For our share menu items.
private class Share extends SendIntent {
public Share(String url, String analyticsLabel) {
super(analyticsLabel);
final Intent send = new Intent(Intent.ACTION_SEND);
send.setType("text/plain");
send.putExtra(Intent.EXTRA_TEXT, url);
setIntent(Intent.createChooser(send, mActivity.getText(
getChooserTitleStringResIdForMenuType(MenuType.SHARE_LINK_MENU))));
}
}
private boolean showShareLinkMenuItem() {
PackageManager pm = mActivity.getPackageManager();
Intent send = new Intent(Intent.ACTION_SEND);
send.setType("text/plain");
ResolveInfo ri = pm.resolveActivity(send, PackageManager.MATCH_DEFAULT_ONLY);
return ri != null;
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo info) {
// FIXME: This is copied over almost directly from BrowserActivity.
// Would like to find a way to combine the two (Bug 1251210).
WebView webview = (WebView) v;
WebView.HitTestResult result = webview.getHitTestResult();
if (result == null) {
return;
}
int type = result.getType();
switch (type) {
case WebView.HitTestResult.UNKNOWN_TYPE:
Analytics.getInstance().sendEvent(
CATEGORY_WEB_CONTEXT_MENU, ACTION_LONG_PRESS, "unknown", 0);
return;
case WebView.HitTestResult.EDIT_TEXT_TYPE:
Analytics.getInstance().sendEvent(
CATEGORY_WEB_CONTEXT_MENU, ACTION_LONG_PRESS, "edit_text", 0);
return;
default:
break;
}
// Note, http://b/issue?id=1106666 is requesting that
// an inflated menu can be used again. This is not available
// yet, so inflate each time (yuk!)
MenuInflater inflater = mActivity.getMenuInflater();
// Also, we are copying the menu file from browser until
// 1251210 is fixed.
inflater.inflate(getMenuResourceId(), menu);
// Initially make set the menu item handler this WebViewContextMenu, which will default to
// calling the non-abstract subclass's implementation.
for (int i = 0; i < menu.size(); i++) {
final MenuItem menuItem = menu.getItem(i);
menuItem.setOnMenuItemClickListener(this);
}
// Show the correct menu group
String extra = result.getExtra();
menu.setGroupVisible(R.id.PHONE_MENU, type == WebView.HitTestResult.PHONE_TYPE);
menu.setGroupVisible(R.id.EMAIL_MENU, type == WebView.HitTestResult.EMAIL_TYPE);
menu.setGroupVisible(R.id.GEO_MENU, type == WebView.HitTestResult.GEO_TYPE);
menu.setGroupVisible(R.id.ANCHOR_MENU, type == WebView.HitTestResult.SRC_ANCHOR_TYPE
|| type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
menu.setGroupVisible(R.id.IMAGE_MENU, type == WebView.HitTestResult.IMAGE_TYPE
|| type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
// Setup custom handling depending on the type
switch (type) {
case WebView.HitTestResult.PHONE_TYPE:
Analytics.getInstance().sendEvent(
CATEGORY_WEB_CONTEXT_MENU, ACTION_LONG_PRESS, "phone", 0);
String decodedPhoneExtra;
try {
decodedPhoneExtra = URLDecoder.decode(extra, Charset.defaultCharset().name());
} catch (UnsupportedEncodingException ignore) {
// Should never happen; default charset is UTF-8
decodedPhoneExtra = extra;
}
menu.setHeaderTitle(decodedPhoneExtra);
// Dial
final MenuItem dialMenuItem =
menu.findItem(getMenuResIdForMenuType(MenuType.DIAL_MENU));
if (mSupportsDial) {
final Intent intent = new Intent(Intent.ACTION_DIAL,
Uri.parse(WebView.SCHEME_TEL + extra));
dialMenuItem.setOnMenuItemClickListener(new SendIntent(intent, "dial"));
} else {
dialMenuItem.setVisible(false);
}
// Send SMS
final MenuItem sendSmsMenuItem =
menu.findItem(getMenuResIdForMenuType(MenuType.SMS_MENU));
if (mSupportsSms) {
final Intent intent =
new Intent(Intent.ACTION_SENDTO, Uri.parse("smsto:" + extra));
sendSmsMenuItem.setOnMenuItemClickListener(new SendIntent(intent, "sms"));
} else {
sendSmsMenuItem.setVisible(false);
}
// Add to contacts
final Intent addIntent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
addIntent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
addIntent.putExtra(ContactsContract.Intents.Insert.PHONE, decodedPhoneExtra);
final MenuItem addToContactsMenuItem =
menu.findItem(getMenuResIdForMenuType(MenuType.ADD_CONTACT_MENU));
addToContactsMenuItem.setOnMenuItemClickListener(
new SendIntent(addIntent, "add_contact"));
// Copy
menu.findItem(getMenuResIdForMenuType(MenuType.COPY_PHONE_MENU)).
setOnMenuItemClickListener(new Copy(extra, "copy_phone"));
break;
case WebView.HitTestResult.EMAIL_TYPE:
Analytics.getInstance().sendEvent(
CATEGORY_WEB_CONTEXT_MENU, ACTION_LONG_PRESS, "email", 0);
menu.setHeaderTitle(extra);
final Intent mailtoIntent =
new Intent(Intent.ACTION_VIEW, Uri.parse(WebView.SCHEME_MAILTO + extra));
menu.findItem(getMenuResIdForMenuType(MenuType.EMAIL_CONTACT_MENU))
.setOnMenuItemClickListener(new SendIntent(mailtoIntent, "send_email"));
menu.findItem(getMenuResIdForMenuType(MenuType.COPY_MAIL_MENU)).
setOnMenuItemClickListener(new Copy(extra, "copy_email"));
break;
case WebView.HitTestResult.GEO_TYPE:
Analytics.getInstance().sendEvent(
CATEGORY_WEB_CONTEXT_MENU, ACTION_LONG_PRESS, "geo", 0);
menu.setHeaderTitle(extra);
String geoExtra = "";
try {
geoExtra = URLEncoder.encode(extra, Charset.defaultCharset().name());
} catch (UnsupportedEncodingException ignore) {
// Should never happen; default charset is UTF-8
}
final MenuItem viewMapMenuItem =
menu.findItem(getMenuResIdForMenuType(MenuType.MAP_MENU));
final Intent viewMap =
new Intent(Intent.ACTION_VIEW, Uri.parse(WebView.SCHEME_GEO + geoExtra));
viewMapMenuItem.setOnMenuItemClickListener(new SendIntent(viewMap, "view_map"));
menu.findItem(getMenuResIdForMenuType(MenuType.COPY_GEO_MENU)).
setOnMenuItemClickListener(new Copy(extra, "copy_geo"));
break;
case WebView.HitTestResult.SRC_ANCHOR_TYPE:
Analytics.getInstance().sendEvent(
CATEGORY_WEB_CONTEXT_MENU, ACTION_LONG_PRESS, "src_anchor", 0);
setupAnchorMenu(extra, menu);
break;
case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
Analytics.getInstance().sendEvent(
CATEGORY_WEB_CONTEXT_MENU, ACTION_LONG_PRESS, "src_image_anchor", 0);
setupAnchorMenu(extra, menu);
setupImageMenu(extra, menu);
break;
case WebView.HitTestResult.IMAGE_TYPE:
Analytics.getInstance().sendEvent(
CATEGORY_WEB_CONTEXT_MENU, ACTION_LONG_PRESS, "image", 0);
setupImageMenu(extra, menu);
break;
default:
break;
}
}
private void setupAnchorMenu(String extra, ContextMenu menu) {
menu.findItem(getMenuResIdForMenuType(MenuType.SHARE_LINK_MENU)).setVisible(
showShareLinkMenuItem());
// The documentation for WebView indicates that if the HitTestResult is
// SRC_ANCHOR_TYPE or the url would be specified in the extra. We don't need to
// call requestFocusNodeHref(). If we wanted to handle UNKNOWN HitTestResults, we
// would. With this knowledge, we can just set the title
menu.setHeaderTitle(extra);
menu.findItem(getMenuResIdForMenuType(MenuType.COPY_LINK_MENU)).
setOnMenuItemClickListener(new Copy(extra, "copy_link"));
final MenuItem openLinkMenuItem =
menu.findItem(getMenuResIdForMenuType(MenuType.OPEN_MENU));
openLinkMenuItem.setOnMenuItemClickListener(
new SendIntent(new Intent(Intent.ACTION_VIEW, Uri.parse(extra)), "open_link"));
menu.findItem(getMenuResIdForMenuType(MenuType.SHARE_LINK_MENU)).
setOnMenuItemClickListener(new Share(extra, "share_link"));
}
/**
* Used to setup the image menu group if the {@link android.webkit.WebView.HitTestResult}
* is of type {@link android.webkit.WebView.HitTestResult#IMAGE_TYPE} or
* {@link android.webkit.WebView.HitTestResult#SRC_IMAGE_ANCHOR_TYPE}.
* @param url Url that was long pressed.
* @param menu The {@link android.view.ContextMenu} that is about to be shown.
*/
private void setupImageMenu(String url, ContextMenu menu) {
final Message msg = (mCallbacks != null) ? mCallbacks.getMessageForClickedUrl(url) : null;
if (msg == null) {
menu.setGroupVisible(R.id.IMAGE_MENU, false);
return;
}
final Intent intent = mIntentBuilder.createInlineAttachmentViewIntent(mActivity, url, msg);
if (intent == null) {
menu.setGroupVisible(R.id.IMAGE_MENU, false);
return;
}
final MenuItem menuItem = menu.findItem(R.id.view_image_context_menu_id);
menuItem.setOnMenuItemClickListener(new SendIntent(intent, "view_image"));
menu.setGroupVisible(R.id.IMAGE_MENU, true);
}
@Override
public boolean onMenuItemClick(MenuItem item) {
return onMenuItemSelected(item);
}
/**
* Returns the menu resource id for the specified menu type
* @param menuType type of the specified menu
* @return menu resource id
*/
protected int getMenuResIdForMenuType(MenuType menuType) {
switch(menuType) {
case OPEN_MENU:
return R.id.open_context_menu_id;
case COPY_LINK_MENU:
return R.id.copy_link_context_menu_id;
case SHARE_LINK_MENU:
return R.id.share_link_context_menu_id;
case DIAL_MENU:
return R.id.dial_context_menu_id;
case SMS_MENU:
return R.id.sms_context_menu_id;
case ADD_CONTACT_MENU:
return R.id.add_contact_context_menu_id;
case COPY_PHONE_MENU:
return R.id.copy_phone_context_menu_id;
case EMAIL_CONTACT_MENU:
return R.id.email_context_menu_id;
case COPY_MAIL_MENU:
return R.id.copy_mail_context_menu_id;
case MAP_MENU:
return R.id.map_context_menu_id;
case COPY_GEO_MENU:
return R.id.copy_geo_context_menu_id;
default:
throw new IllegalStateException("Unexpected MenuType");
}
}
/**
* Returns the resource id of the string to be used when showing a chooser for a menu
* @param menuType type of the specified menu
* @return string resource id
*/
protected int getChooserTitleStringResIdForMenuType(MenuType menuType) {
switch(menuType) {
case SHARE_LINK_MENU:
return R.string.choosertitle_sharevia;
default:
throw new IllegalStateException("Unexpected MenuType");
}
}
/**
* Returns the resource id for the web view context menu
*/
protected int getMenuResourceId() {
return R.menu.webview_context_menu;
}
/**
* Called when a menu item is not handled by the context menu.
*/
protected boolean onMenuItemSelected(MenuItem menuItem) {
return mActivity.onOptionsItemSelected(menuItem);
}
}