// 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.ActivityNotFoundException; 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.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.annotations.CalledByNative; import org.chromium.base.metrics.RecordUserAction; import org.chromium.chrome.R; import org.chromium.chrome.browser.ContentSettingsType; import org.chromium.chrome.browser.instantapps.InstantAppsHandler; 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.components.url_formatter.UrlFormatter; 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 mInstantAppButton; 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; // 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; // The intent associated with the instant app for this URL (or null if one does not exist). private Intent mInstantAppIntent; /** * Creates the WebsiteSettingsPopup, but does not display it. Also initializes the corresponding * C++ object and saves a pointer to it. * @param activity Activity which is used for showing a popup. * @param profile Profile of the tab that will show the popup. * @param webContents The WebContents for which to show Website information. This * information is retrieved for the visible entry. * @param offlinePageCreationDate Date when the offline page was created. * @param publisher The name of the content publisher, if any. */ private WebsiteSettingsPopup(Activity activity, Profile profile, WebContents webContents, String offlinePageCreationDate, String publisher) { mContext = activity; mProfile = profile; mWebContents = webContents; if (offlinePageCreationDate != null) { 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", mFullUrl); 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); mInstantAppButton = (Button) mContainer.findViewById(R.id.website_settings_instant_app_button); mInstantAppButton.setOnClickListener(this); 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. mFullUrl = mWebContents.getVisibleUrl(); if (isShowingOfflinePage()) { mFullUrl = OfflinePageUtils.stripSchemeFromOnlineUrl(mFullUrl); } 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); } mInstantAppIntent = mIsInternalPage ? null : InstantAppsHandler.getInstance().getInstantAppIntentForUrl(mFullUrl); if (mInstantAppIntent == null) mInstantAppButton.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 DANGEROUS 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.DANGEROUS && mSecurityLevel != ConnectionSecurityLevel.SECURITY_WARNING && mSecurityLevel != ConnectionSecurityLevel.SECURE_WITH_POLICY_INSTALLED_CERT) { messageBuilder.append( mContext.getString(getConnectionMessageId(mSecurityLevel, mIsInternalPage))); } else { String originToDisplay; try { URI parsedUrl = new URI(mFullUrl); originToDisplay = UrlFormatter.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 || mWindowAndroid.hasPermission(androidPermission); } /** * 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()) { 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 == mInstantAppButton) { try { mContext.startActivity(mInstantAppIntent); RecordUserAction.record("Android.InstantApps.LaunchedFromWebsiteSettingsPopup"); } catch (ActivityNotFoundException e) { mInstantAppButton.setEnabled(false); } } 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); animatableViews.add(mInstantAppButton); 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 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"; } String offlinePageCreationDate = null; OfflinePageItem offlinePage = tab.getOfflinePage(); if (offlinePage != null) { // Get formatted creation date of the offline page. Date creationDate = new Date(offlinePage.getCreationTimeMs()); DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM); offlinePageCreationDate = df.format(creationDate); } new WebsiteSettingsPopup(activity, tab.getProfile(), tab.getWebContents(), offlinePageCreationDate, contentPublisher); } private static native long nativeInit(WebsiteSettingsPopup popup, WebContents webContents); private native void nativeDestroy(long nativeWebsiteSettingsPopupAndroid); private native void nativeRecordWebsiteSettingsAction( long nativeWebsiteSettingsPopupAndroid, int action); }