| // 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.pageinfo; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.AnimatorSet; |
| import android.animation.ObjectAnimator; |
| import android.app.Activity; |
| import android.app.Dialog; |
| import android.content.ClipData; |
| import android.content.ClipboardManager; |
| import android.content.Context; |
| import android.content.DialogInterface; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.graphics.Color; |
| import android.graphics.drawable.ColorDrawable; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.Process; |
| import android.provider.Settings; |
| import android.support.annotation.IntDef; |
| import android.support.v7.widget.AppCompatTextView; |
| import android.text.Layout; |
| import android.text.Spannable; |
| import android.text.SpannableString; |
| import android.text.SpannableStringBuilder; |
| import android.text.style.ForegroundColorSpan; |
| import android.text.style.StyleSpan; |
| import android.util.AttributeSet; |
| import android.view.Gravity; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.View.OnClickListener; |
| import android.view.View.OnLongClickListener; |
| import android.view.ViewGroup; |
| import android.view.Window; |
| import android.widget.Button; |
| import android.widget.ImageView; |
| import android.widget.LinearLayout; |
| import android.widget.ScrollView; |
| import android.widget.TextView; |
| |
| import org.chromium.base.ApiCompatibilityUtils; |
| import org.chromium.base.Callback; |
| import org.chromium.base.annotations.CalledByNative; |
| import org.chromium.base.metrics.RecordUserAction; |
| import org.chromium.chrome.R; |
| import org.chromium.chrome.browser.ContentSettingsType; |
| import org.chromium.chrome.browser.offlinepages.OfflinePageBridge; |
| import org.chromium.chrome.browser.offlinepages.OfflinePageItem; |
| import org.chromium.chrome.browser.offlinepages.OfflinePageUtils; |
| import org.chromium.chrome.browser.omnibox.OmniboxUrlEmphasizer; |
| import org.chromium.chrome.browser.preferences.PrefServiceBridge; |
| import org.chromium.chrome.browser.preferences.Preferences; |
| import org.chromium.chrome.browser.preferences.PreferencesLauncher; |
| import org.chromium.chrome.browser.preferences.website.ContentSetting; |
| import org.chromium.chrome.browser.preferences.website.ContentSettingsResources; |
| import org.chromium.chrome.browser.preferences.website.SingleWebsitePreferences; |
| import org.chromium.chrome.browser.profiles.Profile; |
| import org.chromium.chrome.browser.ssl.SecurityStateModel; |
| import org.chromium.chrome.browser.tab.Tab; |
| import org.chromium.chrome.browser.util.UrlUtilities; |
| import org.chromium.components.location.LocationUtils; |
| import org.chromium.components.security_state.ConnectionSecurityLevel; |
| import org.chromium.content.browser.ContentViewCore; |
| import org.chromium.content_public.browser.WebContents; |
| import org.chromium.content_public.browser.WebContentsObserver; |
| import org.chromium.ui.base.DeviceFormFactor; |
| import org.chromium.ui.base.WindowAndroid; |
| import org.chromium.ui.base.WindowAndroid.PermissionCallback; |
| import org.chromium.ui.interpolators.BakedBezierInterpolator; |
| import org.chromium.ui.widget.Toast; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.text.DateFormat; |
| import java.util.ArrayList; |
| import java.util.Date; |
| 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 { |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef({OPENED_FROM_MENU, OPENED_FROM_TOOLBAR}) |
| private @interface OpenedFromSource {} |
| |
| public static final int OPENED_FROM_MENU = 1; |
| public static final int OPENED_FROM_TOOLBAR = 2; |
| |
| /** |
| * 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 ContentSetting setting; |
| |
| PageInfoPermissionEntry(String name, int type, ContentSetting setting) { |
| this.name = name; |
| this.type = type; |
| this.setting = setting; |
| } |
| |
| @Override |
| public String toString() { |
| return name; |
| } |
| } |
| |
| /** |
| * A TextView which truncates and displays a URL such that the origin is always visible. |
| * The URL can be expanded by clicking on the it. |
| */ |
| public static class ElidedUrlTextView extends AppCompatTextView { |
| // The number of lines to display when the URL is truncated. This number |
| // should still allow the origin to be displayed. NULL before |
| // setUrlAfterLayout() is called. |
| private Integer mTruncatedUrlLinesToDisplay; |
| |
| // The number of lines to display when the URL is expanded. This should be enough to display |
| // at most two lines of the fragment if there is one in the URL. |
| private Integer mFullLinesToDisplay; |
| |
| // If true, the text view will show the truncated text. If false, it |
| // will show the full, expanded text. |
| private boolean mIsShowingTruncatedText = true; |
| |
| // The profile to use when getting the end index for the origin. |
| private Profile mProfile = null; |
| |
| // The maximum number of lines currently shown in the view |
| private int mCurrentMaxLines = Integer.MAX_VALUE; |
| |
| /** Constructor for inflating from XML. */ |
| public ElidedUrlTextView(Context context, AttributeSet attrs) { |
| super(context, attrs); |
| } |
| |
| @Override |
| public void setMaxLines(int maxlines) { |
| super.setMaxLines(maxlines); |
| mCurrentMaxLines = maxlines; |
| } |
| |
| /** |
| * Find the number of lines of text which must be shown in order to display the character at |
| * a given index. |
| */ |
| private int getLineForIndex(int index) { |
| Layout layout = getLayout(); |
| int endLine = 0; |
| while (endLine < layout.getLineCount() && layout.getLineEnd(endLine) < index) { |
| endLine++; |
| } |
| // Since endLine is an index, add 1 to get the number of lines. |
| return endLine + 1; |
| } |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| setMaxLines(Integer.MAX_VALUE); |
| super.onMeasure(widthMeasureSpec, heightMeasureSpec); |
| assert mProfile != null : "setProfile() must be called before layout."; |
| String urlText = getText().toString(); |
| |
| // Lay out the URL in a StaticLayout that is the same size as our final |
| // container. |
| int originEndIndex = OmniboxUrlEmphasizer.getOriginEndIndex(urlText, mProfile); |
| |
| // Find the range of lines containing the origin. |
| int originEndLine = getLineForIndex(originEndIndex); |
| |
| // Display an extra line so we don't accidentally hide the origin with |
| // ellipses |
| mTruncatedUrlLinesToDisplay = originEndLine + 1; |
| |
| // Find the line where the fragment starts. Since # is a reserved character, it is safe |
| // to just search for the first # to appear in the url. |
| int fragmentStartIndex = urlText.indexOf('#'); |
| if (fragmentStartIndex == -1) fragmentStartIndex = urlText.length(); |
| |
| int fragmentStartLine = getLineForIndex(fragmentStartIndex); |
| mFullLinesToDisplay = fragmentStartLine + 1; |
| |
| // If there is no origin (according to OmniboxUrlEmphasizer), make sure the fragment is |
| // still hidden correctly. |
| if (mFullLinesToDisplay < mTruncatedUrlLinesToDisplay) { |
| mTruncatedUrlLinesToDisplay = mFullLinesToDisplay; |
| } |
| |
| if (updateMaxLines()) super.onMeasure(widthMeasureSpec, heightMeasureSpec); |
| } |
| |
| /** |
| * Sets the profile to use when calculating the end index of the origin. |
| * Must be called before layout. |
| * |
| * @param profile The profile to use when coloring the URL. |
| */ |
| public void setProfile(Profile profile) { |
| mProfile = profile; |
| } |
| |
| /** |
| * Toggles truncating/expanding the URL text. If the URL text is not |
| * truncated, has no effect. |
| */ |
| public void toggleTruncation() { |
| mIsShowingTruncatedText = !mIsShowingTruncatedText; |
| updateMaxLines(); |
| } |
| |
| private boolean updateMaxLines() { |
| int maxLines = mFullLinesToDisplay; |
| if (mIsShowingTruncatedText) maxLines = mTruncatedUrlLinesToDisplay; |
| if (maxLines != mCurrentMaxLines) { |
| setMaxLines(maxLines); |
| return true; |
| } |
| return false; |
| } |
| } |
| |
| // Delay enter to allow the triggering button to animate before we cover it. |
| private static final int ENTER_START_DELAY = 100; |
| private static final int FADE_DURATION = 200; |
| private static final int FADE_IN_BASE_DELAY = 150; |
| private static final int FADE_IN_DELAY_OFFSET = 20; |
| private static final int CLOSE_CLEANUP_DELAY = 10; |
| |
| private static final int MAX_TABLET_DIALOG_WIDTH_DP = 400; |
| |
| private final Context mContext; |
| private final Profile mProfile; |
| private final WebContents mWebContents; |
| private final WindowAndroid mWindowAndroid; |
| |
| // A pointer to the C++ object for this UI. |
| private long mNativeWebsiteSettingsPopup; |
| |
| // The outer container, filled with the layout from website_settings.xml. |
| private final LinearLayout mContainer; |
| |
| // UI elements in the dialog. |
| private final ElidedUrlTextView mUrlTitle; |
| private final TextView mUrlConnectionMessage; |
| private final LinearLayout mPermissionsList; |
| private final Button mSiteSettingsButton; |
| |
| // The dialog the container is placed in. |
| private final Dialog mDialog; |
| |
| // Animation which is currently running, if there is one. |
| private AnimatorSet mCurrentAnimation = null; |
| |
| private boolean mDismissWithoutAnimation; |
| |
| // The full URL from the URL bar, which is copied to the user's clipboard when they select 'Copy |
| // URL'. |
| private String mFullUrl; |
| |
| // A parsed version of mFullUrl. Is null if the URL is invalid/cannot be |
| // parsed. |
| private URI mParsedUrl; |
| |
| // Whether or not this page is an internal chrome page (e.g. the |
| // chrome://settings page). |
| private boolean mIsInternalPage; |
| |
| // The security level of the page (a valid ConnectionSecurityLevel). |
| private int mSecurityLevel; |
| |
| // Whether the security level of the page was downgraded due to SHA-1. |
| private boolean mDeprecatedSHA1Present; |
| |
| // Whether the security level of the page was downgraded due to passive mixed content. |
| private boolean mPassiveMixedContentPresent; |
| |
| // Permissions available to be displayed in mPermissionsList. |
| private List<PageInfoPermissionEntry> mDisplayedPermissions; |
| |
| // Original URL of an offline copy, if web contents contains an offline page. |
| private String mOfflinePageOriginalUrl; |
| |
| // Creation date of an offline copy, if web contents contains an offline page. |
| private String mOfflinePageCreationDate; |
| |
| // The name of the content publisher, if any. |
| private String mContentPublisher; |
| |
| /** |
| * Creates the WebsiteSettingsPopup, but does not display it. Also initializes the corresponding |
| * C++ object and saves a pointer to it. |
| * @param webContents The WebContents for which to show Website information. This information is |
| * retrieved for the visible entry. |
| * @param publisher The name of the content publisher, if any. |
| */ |
| private WebsiteSettingsPopup(Activity activity, Profile profile, WebContents webContents, |
| String offlinePageOriginalUrl, String offlinePageCreationDate, String publisher) { |
| mContext = activity; |
| mProfile = profile; |
| mWebContents = webContents; |
| if (offlinePageOriginalUrl != null && offlinePageCreationDate != null) { |
| mOfflinePageOriginalUrl = offlinePageOriginalUrl; |
| mOfflinePageCreationDate = offlinePageCreationDate; |
| } |
| mWindowAndroid = ContentViewCore.fromWebContents(mWebContents).getWindowAndroid(); |
| mContentPublisher = publisher; |
| |
| // Find the container and all it's important subviews. |
| mContainer = (LinearLayout) LayoutInflater.from(mContext).inflate( |
| R.layout.website_settings, null); |
| mContainer.setVisibility(View.INVISIBLE); |
| mContainer.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { |
| @Override |
| public void onLayoutChange( |
| View v, int l, int t, int r, int b, int ol, int ot, int or, int ob) { |
| // Trigger the entrance animations once the main container has been laid out and has |
| // a height. |
| mContainer.removeOnLayoutChangeListener(this); |
| mContainer.setVisibility(View.VISIBLE); |
| createAllAnimations(true).start(); |
| } |
| }); |
| |
| mUrlTitle = (ElidedUrlTextView) mContainer.findViewById(R.id.website_settings_url); |
| mUrlTitle.setProfile(mProfile); |
| mUrlTitle.setOnClickListener(this); |
| // Long press the url text to copy it to the clipboard. |
| mUrlTitle.setOnLongClickListener(new OnLongClickListener() { |
| @Override |
| public boolean onLongClick(View v) { |
| ClipboardManager clipboard = (ClipboardManager) mContext |
| .getSystemService(Context.CLIPBOARD_SERVICE); |
| ClipData clip = ClipData.newPlainText("url", mUrlTitle.getText()); |
| clipboard.setPrimaryClip(clip); |
| Toast.makeText(mContext, R.string.url_copied, Toast.LENGTH_SHORT).show(); |
| return true; |
| } |
| }); |
| |
| mUrlConnectionMessage = (TextView) mContainer |
| .findViewById(R.id.website_settings_connection_message); |
| mPermissionsList = (LinearLayout) mContainer |
| .findViewById(R.id.website_settings_permissions_list); |
| |
| mSiteSettingsButton = |
| (Button) mContainer.findViewById(R.id.website_settings_site_settings_button); |
| mSiteSettingsButton.setOnClickListener(this); |
| |
| mDisplayedPermissions = new ArrayList<PageInfoPermissionEntry>(); |
| |
| // Hide the permissions list for sites with no permissions. |
| setVisibilityOfPermissionsList(false); |
| |
| // Create the dialog. |
| mDialog = new Dialog(mContext) { |
| private void superDismiss() { |
| super.dismiss(); |
| } |
| |
| @Override |
| public void dismiss() { |
| if (DeviceFormFactor.isTablet(mContext) || mDismissWithoutAnimation) { |
| // Dismiss the dialog without any custom animations on tablet. |
| super.dismiss(); |
| } else { |
| Animator animator = createAllAnimations(false); |
| animator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| // onAnimationEnd is called during the final frame of the animation. |
| // Delay the cleanup by a tiny amount to give this frame a chance to be |
| // displayed before we destroy the dialog. |
| mContainer.postDelayed(new Runnable() { |
| @Override |
| public void run() { |
| superDismiss(); |
| } |
| }, CLOSE_CLEANUP_DELAY); |
| } |
| }); |
| animator.start(); |
| } |
| } |
| }; |
| 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(); |
| } |
| |
| @Override |
| public void destroy() { |
| super.destroy(); |
| // Force the dialog to close immediately in case the destroy was from Chrome |
| // quitting. |
| mDismissWithoutAnimation = true; |
| mDialog.dismiss(); |
| } |
| }; |
| mDialog.setOnDismissListener(new DialogInterface.OnDismissListener() { |
| @Override |
| public void onDismiss(DialogInterface dialog) { |
| assert mNativeWebsiteSettingsPopup != 0; |
| webContentsObserver.destroy(); |
| nativeDestroy(mNativeWebsiteSettingsPopup); |
| mNativeWebsiteSettingsPopup = 0; |
| } |
| }); |
| |
| // Work out the URL and connection message and status visibility. |
| int statusIconVisibility = View.GONE; |
| if (isShowingOfflinePage()) { |
| mFullUrl = mOfflinePageOriginalUrl; |
| statusIconVisibility = View.VISIBLE; |
| } else { |
| mFullUrl = mWebContents.getVisibleUrl(); |
| } |
| |
| mContainer.findViewById(R.id.offline_icon).setVisibility(statusIconVisibility); |
| |
| try { |
| mParsedUrl = new URI(mFullUrl); |
| mIsInternalPage = UrlUtilities.isInternalScheme(mParsedUrl); |
| } catch (URISyntaxException e) { |
| mParsedUrl = null; |
| mIsInternalPage = false; |
| } |
| mSecurityLevel = SecurityStateModel.getSecurityLevelForWebContents(mWebContents); |
| mDeprecatedSHA1Present = SecurityStateModel.isDeprecatedSHA1Present(mWebContents); |
| mPassiveMixedContentPresent = SecurityStateModel.isPassiveMixedContentPresent(mWebContents); |
| |
| SpannableStringBuilder urlBuilder = new SpannableStringBuilder(mFullUrl); |
| OmniboxUrlEmphasizer.emphasizeUrl(urlBuilder, mContext.getResources(), mProfile, |
| mSecurityLevel, mIsInternalPage, true, true); |
| mUrlTitle.setText(urlBuilder); |
| |
| // Set the URL connection message now, and the URL after layout (so it |
| // can calculate its ideal height). |
| mUrlConnectionMessage.setText(getUrlConnectionMessage()); |
| if (isConnectionDetailsLinkVisible()) mUrlConnectionMessage.setOnClickListener(this); |
| |
| if (mParsedUrl == null || mParsedUrl.getScheme() == null |
| || !(mParsedUrl.getScheme().equals("http") |
| || mParsedUrl.getScheme().equals("https"))) { |
| mSiteSettingsButton.setVisibility(View.GONE); |
| } |
| } |
| |
| /** |
| * Sets the visibility of the permissions list, which contains padding and borders that should |
| * not be shown if a site has no permissions. |
| * |
| * @param isVisible Whether to show or hide the dialog area. |
| */ |
| private void setVisibilityOfPermissionsList(boolean isVisible) { |
| int visibility = isVisible ? View.VISIBLE : View.GONE; |
| mPermissionsList.setVisibility(visibility); |
| } |
| |
| /** |
| * 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) { |
| int icon = ContentSettingsResources.getIcon(permission); |
| assert icon != 0 : "Icon requested for invalid permission: " + permission; |
| return icon; |
| } |
| |
| /** |
| * 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 securityLevel A valid ConnectionSecurityLevel, 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 securityLevel, boolean isInternalPage) { |
| if (isInternalPage) return R.string.page_info_connection_internal_page; |
| |
| switch (securityLevel) { |
| case ConnectionSecurityLevel.NONE: |
| return R.string.page_info_connection_http; |
| case ConnectionSecurityLevel.SECURE: |
| case ConnectionSecurityLevel.EV_SECURE: |
| return R.string.page_info_connection_https; |
| default: |
| assert false : "Invalid security level specified: " + securityLevel; |
| return R.string.page_info_connection_http; |
| } |
| } |
| |
| /** |
| * Whether to show a 'Details' link to the connection info popup. The link is only shown for |
| * HTTPS connections. |
| */ |
| private boolean isConnectionDetailsLinkVisible() { |
| // TODO(tsergeant): If this logic gets any more complicated from additional deprecations, |
| // change it to use something like |SchemeIsCryptographic|. |
| return mContentPublisher == null && !mIsInternalPage |
| && (mSecurityLevel != ConnectionSecurityLevel.NONE || mPassiveMixedContentPresent |
| || mDeprecatedSHA1Present); |
| } |
| |
| /** |
| * Gets the styled connection message to display below the URL. |
| */ |
| private Spannable getUrlConnectionMessage() { |
| // Display the appropriate connection message. |
| SpannableStringBuilder messageBuilder = new SpannableStringBuilder(); |
| if (mContentPublisher != null) { |
| messageBuilder.append( |
| mContext.getString(R.string.page_info_domain_hidden, mContentPublisher)); |
| } else if (mDeprecatedSHA1Present) { |
| messageBuilder.append(mContext.getString(R.string.page_info_connection_sha1)); |
| } else if (mPassiveMixedContentPresent) { |
| messageBuilder.append(mContext.getString(R.string.page_info_connection_mixed)); |
| } else if (isShowingOfflinePage()) { |
| messageBuilder.append(String.format( |
| mContext.getString(R.string.page_info_connection_offline), |
| mOfflinePageCreationDate)); |
| } else if (mSecurityLevel != ConnectionSecurityLevel.SECURITY_ERROR |
| && mSecurityLevel != ConnectionSecurityLevel.SECURITY_WARNING |
| && mSecurityLevel != ConnectionSecurityLevel.SECURITY_POLICY_WARNING) { |
| messageBuilder.append( |
| mContext.getString(getConnectionMessageId(mSecurityLevel, mIsInternalPage))); |
| } else { |
| String originToDisplay; |
| try { |
| URI parsedUrl = new URI(mFullUrl); |
| originToDisplay = UrlUtilities.formatUrlForSecurityDisplay(parsedUrl, false); |
| } catch (URISyntaxException e) { |
| // The URL is invalid - just display the full URL. |
| originToDisplay = mFullUrl; |
| } |
| |
| messageBuilder.append( |
| mContext.getString(R.string.page_info_connection_broken, originToDisplay)); |
| } |
| |
| if (isConnectionDetailsLinkVisible()) { |
| messageBuilder.append(" "); |
| SpannableString detailsText = new SpannableString( |
| mContext.getString(R.string.page_info_details_link)); |
| final ForegroundColorSpan blueSpan = new ForegroundColorSpan( |
| ApiCompatibilityUtils.getColor(mContext.getResources(), |
| R.color.website_settings_popup_text_link)); |
| detailsText.setSpan( |
| blueSpan, 0, detailsText.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); |
| messageBuilder.append(detailsText); |
| } |
| |
| return messageBuilder; |
| } |
| |
| private boolean hasAndroidPermission(int contentSettingType) { |
| String androidPermission = PrefServiceBridge.getAndroidPermissionForContentSetting( |
| contentSettingType); |
| return androidPermission == null |
| || (mContext.checkPermission(androidPermission, Process.myPid(), Process.myUid()) |
| == PackageManager.PERMISSION_GRANTED); |
| } |
| |
| /** |
| * 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 currentSettingValue The ContentSetting value of the currently selected setting. |
| */ |
| @CalledByNative |
| private void addPermissionSection(String name, int type, int currentSettingValue) { |
| // We have at least one permission, so show the lower permissions area. |
| setVisibilityOfPermissionsList(true); |
| mDisplayedPermissions.add(new PageInfoPermissionEntry(name, type, ContentSetting |
| .fromInt(currentSettingValue))); |
| } |
| |
| /** |
| * Update the permissions view based on the contents of mDisplayedPermissions. |
| */ |
| @CalledByNative |
| private void updatePermissionDisplay() { |
| mPermissionsList.removeAllViews(); |
| for (PageInfoPermissionEntry permission : mDisplayedPermissions) { |
| addReadOnlyPermissionSection(permission); |
| } |
| } |
| |
| private void addReadOnlyPermissionSection(PageInfoPermissionEntry permission) { |
| View permissionRow = LayoutInflater.from(mContext).inflate( |
| R.layout.website_settings_permission_row, null); |
| |
| ImageView permissionIcon = (ImageView) permissionRow.findViewById( |
| R.id.website_settings_permission_icon); |
| permissionIcon.setImageResource(getImageResourceForPermission(permission.type)); |
| |
| if (permission.setting == ContentSetting.ALLOW) { |
| int warningTextResource = 0; |
| |
| // If warningTextResource is non-zero, then the view must be tagged with either |
| // permission_intent_override or permission_type. |
| LocationUtils locationUtils = LocationUtils.getInstance(); |
| if (permission.type == ContentSettingsType.CONTENT_SETTINGS_TYPE_GEOLOCATION |
| && !locationUtils.isSystemLocationSettingEnabled(mContext)) { |
| warningTextResource = R.string.page_info_android_location_blocked; |
| permissionRow.setTag(R.id.permission_intent_override, |
| locationUtils.getSystemLocationSettingsIntent()); |
| } else if (!hasAndroidPermission(permission.type)) { |
| warningTextResource = R.string.page_info_android_permission_blocked; |
| permissionRow.setTag(R.id.permission_type, |
| PrefServiceBridge.getAndroidPermissionForContentSetting(permission.type)); |
| } |
| |
| if (warningTextResource != 0) { |
| TextView permissionUnavailable = (TextView) permissionRow.findViewById( |
| R.id.website_settings_permission_unavailable_message); |
| permissionUnavailable.setVisibility(View.VISIBLE); |
| permissionUnavailable.setText(warningTextResource); |
| |
| permissionIcon.setImageResource(R.drawable.exclamation_triangle); |
| permissionIcon.setColorFilter(ApiCompatibilityUtils.getColor( |
| mContext.getResources(), R.color.website_settings_popup_text_link)); |
| |
| permissionRow.setOnClickListener(this); |
| } |
| } |
| |
| TextView permissionStatus = (TextView) permissionRow.findViewById( |
| R.id.website_settings_permission_status); |
| SpannableStringBuilder builder = new SpannableStringBuilder(); |
| SpannableString nameString = new SpannableString(permission.name); |
| final StyleSpan boldSpan = new StyleSpan(android.graphics.Typeface.BOLD); |
| nameString.setSpan(boldSpan, 0, nameString.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); |
| |
| builder.append(nameString); |
| builder.append(" – "); // en-dash. |
| String status_text = ""; |
| switch (permission.setting) { |
| case ALLOW: |
| status_text = mContext.getString(R.string.page_info_permission_allowed); |
| break; |
| case BLOCK: |
| status_text = mContext.getString(R.string.page_info_permission_blocked); |
| break; |
| default: |
| assert false : "Invalid setting " + permission.setting + " for permission " |
| + permission.type; |
| } |
| builder.append(status_text); |
| permissionStatus.setText(builder); |
| 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(); |
| } |
| |
| /** |
| * Dismiss the popup, and then run a task after the animation has completed (if there is one). |
| */ |
| private void runAfterDismiss(Runnable task) { |
| mDialog.dismiss(); |
| if (DeviceFormFactor.isTablet(mContext)) { |
| task.run(); |
| } else { |
| mContainer.postDelayed(task, FADE_DURATION + CLOSE_CLEANUP_DELAY); |
| } |
| } |
| |
| @Override |
| public void onClick(View view) { |
| if (view == mSiteSettingsButton) { |
| // Delay while the WebsiteSettingsPopup closes. |
| runAfterDismiss(new Runnable() { |
| @Override |
| public void run() { |
| recordAction(WebsiteSettingsAction.WEBSITE_SETTINGS_SITE_SETTINGS_OPENED); |
| Bundle fragmentArguments = |
| SingleWebsitePreferences.createFragmentArgsForSite(mFullUrl); |
| fragmentArguments.putParcelable(SingleWebsitePreferences.EXTRA_WEB_CONTENTS, |
| mWebContents); |
| Intent preferencesIntent = PreferencesLauncher.createIntentForSettingsPage( |
| mContext, SingleWebsitePreferences.class.getName()); |
| preferencesIntent.putExtra( |
| Preferences.EXTRA_SHOW_FRAGMENT_ARGUMENTS, fragmentArguments); |
| mContext.startActivity(preferencesIntent); |
| } |
| }); |
| } else if (view == mUrlTitle) { |
| // Expand/collapse the displayed URL title. |
| mUrlTitle.toggleTruncation(); |
| } else if (view == mUrlConnectionMessage) { |
| runAfterDismiss(new Runnable() { |
| @Override |
| public void run() { |
| if (!mWebContents.isDestroyed()) { |
| recordAction( |
| WebsiteSettingsAction.WEBSITE_SETTINGS_SECURITY_DETAILS_OPENED); |
| ConnectionInfoPopup.show(mContext, mWebContents); |
| } |
| } |
| }); |
| } else if (view.getId() == R.id.website_settings_permission_row) { |
| final Object intentOverride = view.getTag(R.id.permission_intent_override); |
| |
| if (intentOverride == null && mWindowAndroid != null) { |
| // Try and immediately request missing Android permissions where possible. |
| final String permissionType = (String) view.getTag(R.id.permission_type); |
| if (mWindowAndroid.canRequestPermission(permissionType)) { |
| final String[] permissionRequest = new String[] {permissionType}; |
| mWindowAndroid.requestPermissions(permissionRequest, new PermissionCallback() { |
| @Override |
| public void onRequestPermissionsResult( |
| String[] permissions, int[] grantResults) { |
| if (grantResults.length > 0 |
| && grantResults[0] == PackageManager.PERMISSION_GRANTED) { |
| updatePermissionDisplay(); |
| } |
| } |
| }); |
| return; |
| } |
| } |
| |
| runAfterDismiss(new Runnable() { |
| @Override |
| public void run() { |
| Intent settingsIntent; |
| if (intentOverride != null) { |
| settingsIntent = (Intent) intentOverride; |
| } else { |
| settingsIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); |
| settingsIntent.setData(Uri.parse("package:" + mContext.getPackageName())); |
| } |
| settingsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| mContext.startActivity(settingsIntent); |
| } |
| }); |
| } |
| } |
| |
| /** |
| * Create a list of all the views which we want to individually fade in. |
| */ |
| private List<View> collectAnimatableViews() { |
| List<View> animatableViews = new ArrayList<View>(); |
| animatableViews.add(mUrlTitle); |
| animatableViews.add(mUrlConnectionMessage); |
| for (int i = 0; i < mPermissionsList.getChildCount(); i++) { |
| animatableViews.add(mPermissionsList.getChildAt(i)); |
| } |
| animatableViews.add(mSiteSettingsButton); |
| |
| return animatableViews; |
| } |
| |
| /** |
| * Create an animator to fade an individual dialog element. |
| */ |
| private Animator createInnerFadeAnimator(final View view, int position, boolean isEnter) { |
| ObjectAnimator alphaAnim; |
| |
| if (isEnter) { |
| view.setAlpha(0f); |
| alphaAnim = ObjectAnimator.ofFloat(view, View.ALPHA, 1f); |
| alphaAnim.setStartDelay(FADE_IN_BASE_DELAY + FADE_IN_DELAY_OFFSET * position); |
| } else { |
| alphaAnim = ObjectAnimator.ofFloat(view, View.ALPHA, 0f); |
| } |
| |
| alphaAnim.setDuration(FADE_DURATION); |
| return alphaAnim; |
| } |
| |
| /** |
| * Create an animator to slide in the entire dialog from the top of the screen. |
| */ |
| private Animator createDialogSlideAnimator(boolean isEnter) { |
| final float animHeight = -1f * mContainer.getHeight(); |
| ObjectAnimator translateAnim; |
| if (isEnter) { |
| mContainer.setTranslationY(animHeight); |
| translateAnim = ObjectAnimator.ofFloat(mContainer, View.TRANSLATION_Y, 0f); |
| translateAnim.setInterpolator(BakedBezierInterpolator.FADE_IN_CURVE); |
| } else { |
| translateAnim = ObjectAnimator.ofFloat(mContainer, View.TRANSLATION_Y, animHeight); |
| translateAnim.setInterpolator(BakedBezierInterpolator.FADE_OUT_CURVE); |
| } |
| translateAnim.setDuration(FADE_DURATION); |
| return translateAnim; |
| } |
| |
| /** |
| * Create animations for showing/hiding the popup. |
| * |
| * Tablets use the default Dialog fade-in instead of sliding in manually. |
| */ |
| private Animator createAllAnimations(boolean isEnter) { |
| AnimatorSet animation = new AnimatorSet(); |
| AnimatorSet.Builder builder = null; |
| Animator startAnim; |
| |
| if (DeviceFormFactor.isTablet(mContext)) { |
| // The start time of the entire AnimatorSet is the start time of the first animation |
| // added to the Builder. We use a blank AnimatorSet on tablet as an easy way to |
| // co-ordinate this start time. |
| startAnim = new AnimatorSet(); |
| } else { |
| startAnim = createDialogSlideAnimator(isEnter); |
| } |
| |
| if (isEnter) startAnim.setStartDelay(ENTER_START_DELAY); |
| builder = animation.play(startAnim); |
| |
| List<View> animatableViews = collectAnimatableViews(); |
| for (int i = 0; i < animatableViews.size(); i++) { |
| View view = animatableViews.get(i); |
| Animator anim = createInnerFadeAnimator(view, i, isEnter); |
| builder.with(anim); |
| } |
| |
| animation.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| mCurrentAnimation = null; |
| } |
| }); |
| if (mCurrentAnimation != null) mCurrentAnimation.cancel(); |
| mCurrentAnimation = animation; |
| return animation; |
| } |
| |
| private void recordAction(int action) { |
| if (mNativeWebsiteSettingsPopup != 0) { |
| nativeRecordWebsiteSettingsAction(mNativeWebsiteSettingsPopup, action); |
| } |
| } |
| |
| /** |
| * Whether website dialog is displayed for an offline page. |
| */ |
| private boolean isShowingOfflinePage() { |
| return mOfflinePageOriginalUrl != null && mOfflinePageCreationDate != null; |
| } |
| |
| /** |
| * Shows a WebsiteSettings dialog for the provided Tab. The popup adds itself to the view |
| * hierarchy which owns the reference while it's visible. |
| * |
| * @param activity Activity which is used for launching a dialog. |
| * @param tab The tab hosting the web contents for which to show Website information. This |
| * information is retrieved for the visible entry. |
| * @param contentPublisher The name of the publisher of the content. |
| * @param source Determines the source that triggered the popup. |
| */ |
| public static void show(final Activity activity, final Tab tab, final String contentPublisher, |
| @OpenedFromSource int source) { |
| if (source == OPENED_FROM_MENU) { |
| RecordUserAction.record("MobileWebsiteSettingsOpenedFromMenu"); |
| } else if (source == OPENED_FROM_TOOLBAR) { |
| RecordUserAction.record("MobileWebsiteSettingsOpenedFromToolbar"); |
| } else { |
| assert false : "Invalid source passed"; |
| } |
| |
| OfflinePageBridge offlinePageBridge = OfflinePageBridge.getForProfile(tab.getProfile()); |
| if (offlinePageBridge == null) { |
| new WebsiteSettingsPopup( |
| activity, tab.getProfile(), tab.getWebContents(), null, null, contentPublisher); |
| return; |
| } |
| |
| Callback<OfflinePageItem> callback = new Callback<OfflinePageItem>() { |
| @Override |
| public void onResult(OfflinePageItem item) { |
| String offlinePageOriginalUrl = null; |
| String offlinePageCreationDate = null; |
| |
| if (item != null) { |
| // Get formatted creation date and original URL of the offline copy. |
| Date creationDate = new Date(item.getCreationTimeMs()); |
| DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM); |
| offlinePageCreationDate = df.format(creationDate); |
| offlinePageOriginalUrl = |
| OfflinePageUtils.stripSchemeFromOnlineUrl(item.getUrl()); |
| } |
| new WebsiteSettingsPopup(activity, tab.getProfile(), tab.getWebContents(), |
| offlinePageOriginalUrl, offlinePageCreationDate, contentPublisher); |
| } |
| }; |
| |
| offlinePageBridge.getPageByOfflineUrl(tab.getWebContents().getVisibleUrl(), callback); |
| } |
| |
| private static native long nativeInit(WebsiteSettingsPopup popup, WebContents webContents); |
| |
| private native void nativeDestroy(long nativeWebsiteSettingsPopupAndroid); |
| |
| private native void nativeRecordWebsiteSettingsAction( |
| long nativeWebsiteSettingsPopupAndroid, int action); |
| } |