blob: a4d5c70686c3b90dc856d7179b25411f402873f3 [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.style.ForegroundColorSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.Spinner;
import android.widget.TextView;
import org.chromium.base.CalledByNative;
import org.chromium.base.CommandLine;
import org.chromium.chrome.ChromeSwitches;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.toolbar.ToolbarModel;
import org.chromium.chrome.browser.ui.toolbar.ToolbarModelSecurityLevel;
import org.chromium.content.browser.WebContentsObserver;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.Clipboard;
import org.chromium.ui.base.DeviceFormFactor;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.List;
/**
* Java side of Android implementation of the website settings UI.
* TODO(sashab): Rename this, and all its resources, to PageInfo* and page_info_* instead of
* WebsiteSettings* and website_settings_*. Do this on the C++ side as well.
*/
public class WebsiteSettingsPopup implements OnClickListener, OnItemSelectedListener {
/**
* An entry in the settings dropdown for a given permission. There are two options for each
* permission: Allow and Block.
*/
private static final class PageInfoPermissionEntry {
public final String name;
public final int type;
public final int value;
PageInfoPermissionEntry(String name, int type, int value) {
this.name = name;
this.type = type;
this.value = value;
}
@Override
public String toString() {
return name;
}
}
private static final int MAX_TABLET_DIALOG_WIDTH_DP = 400;
private final Context mContext;
private final WebContents mWebContents;
// A pointer to the C++ object for this UI.
private final long mNativeWebsiteSettingsPopup;
// The outer container, filled with the layout from website_settings.xml.
private final LinearLayout mContainer;
// UI elements in the dialog.
private final TextView mUrlTitle;
private final TextView mUrlConnectionMessage;
private final LinearLayout mPermissionsList;
private final Button mCopyUrlButton;
private final Button mSiteSettingsButton;
private final View mHorizontalSeparator;
private final View mLowerDialogArea;
// The dialog the container is placed in.
private final Dialog mDialog;
// The full URL from the URL bar, which is copied to the user's clipboard when they select 'Copy
// URL'.
private String mFullUrl;
/**
* Creates the WebsiteSettingsPopup, but does not display it. Also initializes the corresponding
* C++ object and saves a pointer to it.
*
* @param context Context which is used for launching a dialog.
* @param webContents The WebContents for which to show Website information. This information is
* retrieved for the visible entry.
*/
private WebsiteSettingsPopup(Context context, WebContents webContents) {
mContext = context;
mWebContents = webContents;
// Find the container and all it's important subviews.
mContainer = (LinearLayout) LayoutInflater.from(mContext).inflate(
R.layout.website_settings, null);
mUrlTitle = (TextView) mContainer
.findViewById(R.id.website_settings_url);
mUrlConnectionMessage = (TextView) mContainer
.findViewById(R.id.website_settings_connection_message);
mPermissionsList = (LinearLayout) mContainer
.findViewById(R.id.website_settings_permissions_list);
mCopyUrlButton = (Button) mContainer.findViewById(R.id.website_settings_copy_url_button);
mCopyUrlButton.setOnClickListener(this);
mSiteSettingsButton = (Button) mContainer
.findViewById(R.id.website_settings_site_settings_button);
mSiteSettingsButton.setOnClickListener(this);
// Hide the Site Settings button until there's a link to take it to.
// TODO(sashab,finnur): Make this button visible for well-formed, non-internal URLs.
mSiteSettingsButton.setVisibility(View.GONE);
mHorizontalSeparator = mContainer
.findViewById(R.id.website_settings_horizontal_separator);
mLowerDialogArea = mContainer.findViewById(R.id.website_settings_lower_dialog_area);
// Hide the horizontal separator for sites with no permissions.
// TODO(sashab,finnur): Show this for all sites with either the site settings button or
// permissions (ie when the bottom area of the dialog is not empty).
setVisibilityOfLowerDialogArea(false);
// Create the dialog.
mDialog = new Dialog(mContext);
mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
mDialog.setCanceledOnTouchOutside(true);
// On smaller screens, place the dialog at the top of the screen, and remove its border.
if (!DeviceFormFactor.isTablet(mContext)) {
Window window = mDialog.getWindow();
window.setGravity(Gravity.TOP);
window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
}
// This needs to come after other member initialization.
mNativeWebsiteSettingsPopup = nativeInit(this, webContents);
final WebContentsObserver webContentsObserver = new WebContentsObserver(mWebContents) {
@Override
public void navigationEntryCommitted() {
// If a navigation is committed (e.g. from in-page redirect), the data we're showing
// is stale so dismiss the dialog.
mDialog.dismiss();
}
};
mDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
@Override
public void onDismiss(DialogInterface dialog) {
assert mNativeWebsiteSettingsPopup != 0;
webContentsObserver.detachFromWebContents();
nativeDestroy(mNativeWebsiteSettingsPopup);
}
});
}
/**
* Sets the visibility of the lower area of the dialog (containing the permissions and 'Site
* Settings' button).
*
* @param isVisible Whether to show or hide the dialog area.
*/
private void setVisibilityOfLowerDialogArea(boolean isVisible) {
mHorizontalSeparator.setVisibility(isVisible ? View.VISIBLE : View.GONE);
mLowerDialogArea.setVisibility(isVisible ? View.VISIBLE : View.GONE);
}
/**
* Finds the Image resource of the icon to use for the given permission.
*
* @param permission A valid ContentSettingsType that can be displayed in the PageInfo dialog to
* retrieve the image for.
* @return The resource ID of the icon to use for that permission.
*/
private int getImageResourceForPermission(int permission) {
switch (permission) {
case ContentSettingsType.CONTENT_SETTINGS_TYPE_IMAGES:
return R.drawable.permission_images;
case ContentSettingsType.CONTENT_SETTINGS_TYPE_JAVASCRIPT:
return R.drawable.permission_javascript;
case ContentSettingsType.CONTENT_SETTINGS_TYPE_GEOLOCATION:
return R.drawable.permission_location;
case ContentSettingsType.CONTENT_SETTINGS_TYPE_MEDIASTREAM:
return R.drawable.permission_media;
case ContentSettingsType.CONTENT_SETTINGS_TYPE_PUSH_MESSAGING:
return R.drawable.permission_push_notification;
case ContentSettingsType.CONTENT_SETTINGS_TYPE_POPUPS:
return R.drawable.permission_popups;
default:
assert false : "Icon requested for invalid permission: " + permission;
return -1;
}
}
/**
* Gets the color to use for the scheme in the URL title for the given security level. Does not
* apply to internal pages.
*
* @param toolbarModelSecurityLevel A valid ToolbarModelSecurityLevel, which is the security
* level of the page.
* @return The color ID to color the scheme in the URL title.
*/
private int getSchemeColorId(int toolbarModelSecurityLevel) {
switch (toolbarModelSecurityLevel) {
case ToolbarModelSecurityLevel.NONE:
return R.color.website_settings_popup_url_scheme_http;
case ToolbarModelSecurityLevel.SECURE:
case ToolbarModelSecurityLevel.EV_SECURE:
return R.color.website_settings_popup_url_scheme_https;
case ToolbarModelSecurityLevel.SECURITY_WARNING:
case ToolbarModelSecurityLevel.SECURITY_POLICY_WARNING:
return R.color.website_settings_popup_url_scheme_mixed;
case ToolbarModelSecurityLevel.SECURITY_ERROR:
return R.color.website_settings_popup_url_scheme_broken;
default:
assert false : "Invalid security level specified: " + toolbarModelSecurityLevel;
return R.color.website_settings_popup_url_scheme_http;
}
}
/**
* Gets the message to display in the connection message box for the given security level. Does
* not apply to SECURITY_ERROR pages, since these have their own coloured/formatted message.
*
* @param toolbarModelSecurityLevel A valid ToolbarModelSecurityLevel, which is the security
* level of the page.
* @param isInternalPage Whether or not this page is an internal chrome page (e.g. the
* chrome://settings page).
* @return The ID of the message to display in the connection message box.
*/
private int getConnectionMessageId(int toolbarModelSecurityLevel, boolean isInternalPage) {
if (isInternalPage) return R.string.page_info_connection_internal_page;
switch (toolbarModelSecurityLevel) {
case ToolbarModelSecurityLevel.NONE:
return R.string.page_info_connection_http;
case ToolbarModelSecurityLevel.SECURE:
case ToolbarModelSecurityLevel.EV_SECURE:
return R.string.page_info_connection_https;
case ToolbarModelSecurityLevel.SECURITY_WARNING:
case ToolbarModelSecurityLevel.SECURITY_POLICY_WARNING:
return R.string.page_info_connection_mixed;
default:
assert false : "Invalid security level specified: " + toolbarModelSecurityLevel;
return R.string.page_info_connection_http;
}
}
/**
* Updates the details (URL title and connection message) displayed in the popup.
*
* @param isInternalPage Whether or not this page is an internal chrome page (e.g. the
* chrome://settings page).
*/
@CalledByNative
private void updatePageDetails(boolean isInternalPage) {
mFullUrl = mWebContents.getVisibleUrl();
int securityLevel = ToolbarModel.getSecurityLevelForWebContents(mWebContents);
URI parsedUrl;
try {
parsedUrl = new URI(mFullUrl);
} catch (URISyntaxException e) {
parsedUrl = null;
}
if (parsedUrl != null) {
// The URL is valid - color the scheme (and other components) for the security level.
SpannableStringBuilder sb = new SpannableStringBuilder();
int schemeColorId = R.color.website_settings_popup_url_scheme_http;
if (!isInternalPage) {
schemeColorId = getSchemeColorId(securityLevel);
}
String parsedUrlString = parsedUrl.toString();
sb.append(parsedUrlString);
final ForegroundColorSpan schemeColorSpan = new ForegroundColorSpan(
mContext.getResources().getColor(schemeColorId));
sb.setSpan(schemeColorSpan, 0, parsedUrl.getScheme().length(),
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
if (securityLevel == ToolbarModelSecurityLevel.SECURITY_ERROR) {
sb.setSpan(new StrikethroughSpan(), 0, parsedUrl.getScheme().length(),
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
// The domain does not include the '://'.
String originToDisplay = UrlUtilities.getOriginForDisplay(parsedUrl, false);
int originBegin = parsedUrlString.indexOf(originToDisplay,
parsedUrl.getScheme().length());
int originEnd = originBegin + originToDisplay.length();
// In some cases (e.g. 'about:blank') UrlUtilities.getOriginForDisplay will return the
// entire URL. In these cases originBegin will now be -1.
if (originBegin < 0) {
originBegin = parsedUrl.getScheme().length();
// Don't include the ':' in the origin for highlighting if present (it should always
// be there but check to be sure).
if (originBegin < parsedUrlString.length()) originBegin++;
originEnd = parsedUrlString.length();
}
final ForegroundColorSpan domainColorSpan = new ForegroundColorSpan(
mContext.getResources().getColor(R.color.website_settings_popup_url_domain));
sb.setSpan(domainColorSpan, originBegin, originEnd, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
mUrlTitle.setText(sb);
} else {
// The URL is invalid - still display it in the title, but don't apply any coloring.
mUrlTitle.setText(mFullUrl);
}
// Display the appropriate connection message.
SpannableStringBuilder messageBuilder = new SpannableStringBuilder();
if (securityLevel != ToolbarModelSecurityLevel.SECURITY_ERROR) {
messageBuilder.append(mContext.getResources().getString(
getConnectionMessageId(securityLevel, isInternalPage)));
} else {
String originToDisplay;
if (parsedUrl != null) {
originToDisplay = UrlUtilities.getOriginForDisplay(parsedUrl, false);
} else {
// The URL is invalid - just display the full URL.
originToDisplay = mFullUrl;
}
String leadingText = mContext.getResources().getString(
R.string.page_info_connection_broken_leading_text);
String followingText = mContext.getResources().getString(
R.string.page_info_connection_broken_following_text, originToDisplay);
messageBuilder.append(leadingText + " " + followingText);
final ForegroundColorSpan redSpan = new ForegroundColorSpan(mContext.getResources()
.getColor(R.color.website_settings_popup_url_scheme_broken));
final StyleSpan boldSpan = new StyleSpan(android.graphics.Typeface.BOLD);
messageBuilder.setSpan(redSpan, 0, leadingText.length(),
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
messageBuilder.setSpan(boldSpan, 0, leadingText.length(),
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
mUrlConnectionMessage.setText(messageBuilder);
}
/**
* Adds a new row for the given permission.
*
* @param name The title of the permission to display to the user.
* @param type The ContentSettingsType of the permission.
* @param currentSetting The ContentSetting of the currently selected setting.
*/
@CalledByNative
private void addPermissionSection(String name, int type, int currentSetting) {
// We have at least one permission, so show the lower permissions area.
setVisibilityOfLowerDialogArea(true);
LinearLayout permissionRow = (LinearLayout) LayoutInflater.from(mContext).inflate(
R.layout.website_settings_permission_row, null);
ImageView permission_icon = (ImageView) permissionRow.findViewById(
R.id.website_settings_permission_icon);
permission_icon.setImageResource(getImageResourceForPermission(type));
TextView permission_name = (TextView) permissionRow.findViewById(
R.id.website_settings_permission_name);
permission_name.setText(name);
Spinner permission_spinner = (Spinner) permissionRow.findViewById(
R.id.website_settings_permission_spinner);
// Work out the index of the currently selected setting.
int selectedSettingIndex = -1;
switch (currentSetting) {
case ContentSetting.ALLOW:
selectedSettingIndex = 0;
break;
case ContentSetting.BLOCK:
selectedSettingIndex = 1;
break;
default:
assert false : "Invalid setting " + currentSetting + " for permission " + type;
}
List<PageInfoPermissionEntry> settingsChoices = Arrays.asList(
new PageInfoPermissionEntry(mContext.getResources().getString(
R.string.page_info_permission_allow), type, ContentSetting.ALLOW),
new PageInfoPermissionEntry(mContext.getResources().getString(
R.string.page_info_permission_block), type, ContentSetting.BLOCK));
ArrayAdapter<PageInfoPermissionEntry> adapter = new ArrayAdapter<PageInfoPermissionEntry>(
mContext, R.drawable.website_settings_permission_spinner_item, settingsChoices);
adapter.setDropDownViewResource(
R.drawable.website_settings_permission_spinner_dropdown_item);
permission_spinner.setAdapter(adapter);
permission_spinner.setSelection(selectedSettingIndex, false);
permission_spinner.setOnItemSelectedListener(this);
mPermissionsList.addView(permissionRow);
}
/**
* Displays the WebsiteSettingsPopup.
*/
@CalledByNative
private void showDialog() {
if (!DeviceFormFactor.isTablet(mContext)) {
// On smaller screens, make the dialog fill the width of the screen.
ScrollView scrollView = new ScrollView(mContext);
scrollView.addView(mContainer);
mDialog.addContentView(scrollView, new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT));
// This must be called after addContentView, or it won't fully fill to the edge.
Window window = mDialog.getWindow();
window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
} else {
// On larger screens, make the dialog centered in the screen and have a maximum width.
ScrollView scrollView = new ScrollView(mContext) {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int maxDialogWidthInPx = (int) (MAX_TABLET_DIALOG_WIDTH_DP
* mContext.getResources().getDisplayMetrics().density);
if (MeasureSpec.getSize(widthMeasureSpec) > maxDialogWidthInPx) {
widthMeasureSpec = MeasureSpec.makeMeasureSpec(maxDialogWidthInPx,
MeasureSpec.EXACTLY);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
};
scrollView.addView(mContainer);
mDialog.addContentView(scrollView, new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.MATCH_PARENT));
}
mDialog.show();
}
@Override
public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
PageInfoPermissionEntry entry = (PageInfoPermissionEntry) parent.getItemAtPosition(pos);
nativeOnPermissionSettingChanged(mNativeWebsiteSettingsPopup, entry.type, entry.value);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
// Do nothing intentionally.
}
@Override
public void onClick(View view) {
if (view == mCopyUrlButton) {
new Clipboard(mContext).setText(mFullUrl, mFullUrl);
mDialog.dismiss();
} else if (view == mSiteSettingsButton) {
// TODO(sashab,finnur): Make this open the Website Settings dialog.
assert false : "No Website Settings here!";
mDialog.dismiss();
}
}
/**
* Shows a WebsiteSettings dialog for the provided WebContents. The popup adds itself to the
* view hierarchy which owns the reference while it's visible.
*
* @param context Context which is used for launching a dialog.
* @param webContents The WebContents for which to show Website information. This information is
* retrieved for the visible entry.
*/
@SuppressWarnings("unused")
public static void show(Context context, WebContents webContents) {
if (!CommandLine.getInstance().hasSwitch(ChromeSwitches.DISABLE_NEW_WEBSITE_SETTINGS)) {
new WebsiteSettingsPopup(context, webContents);
} else {
WebsiteSettingsPopupLegacy.show(context, webContents);
}
}
private static native long nativeInit(WebsiteSettingsPopup popup, WebContents webContents);
private native void nativeDestroy(long nativeWebsiteSettingsPopupAndroid);
private native void nativeOnPermissionSettingChanged(long nativeWebsiteSettingsPopupAndroid,
int type, int setting);
}