| // 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.page_info; |
| |
| import android.app.Activity; |
| import android.content.ActivityNotFoundException; |
| import android.content.ClipData; |
| import android.content.ClipboardManager; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.provider.Settings; |
| import android.support.annotation.IntDef; |
| import android.text.Spannable; |
| import android.text.SpannableString; |
| import android.text.SpannableStringBuilder; |
| import android.text.TextUtils; |
| import android.text.style.ForegroundColorSpan; |
| import android.text.style.TextAppearanceSpan; |
| |
| import org.chromium.base.ApiCompatibilityUtils; |
| import org.chromium.base.StrictModeContext; |
| import org.chromium.base.VisibleForTesting; |
| import org.chromium.base.annotations.CalledByNative; |
| import org.chromium.base.metrics.RecordHistogram; |
| import org.chromium.base.metrics.RecordUserAction; |
| import org.chromium.chrome.R; |
| import org.chromium.chrome.browser.UrlConstants; |
| import org.chromium.chrome.browser.instantapps.InstantAppsHandler; |
| import org.chromium.chrome.browser.modaldialog.ModalDialogView; |
| import org.chromium.chrome.browser.modaldialog.ModalDialogView.ButtonType; |
| 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.page_info.PageInfoView.ConnectionInfoParams; |
| import org.chromium.chrome.browser.page_info.PageInfoView.PageInfoViewParams; |
| 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.SingleWebsitePreferences; |
| import org.chromium.chrome.browser.ssl.SecurityStateModel; |
| import org.chromium.chrome.browser.tab.Tab; |
| import org.chromium.chrome.browser.util.UrlUtilities; |
| import org.chromium.chrome.browser.vr.VrShellDelegate; |
| import org.chromium.components.security_state.ConnectionSecurityLevel; |
| import org.chromium.components.url_formatter.UrlFormatter; |
| 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.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.Date; |
| |
| /** |
| * Java side of Android implementation of the page info UI. |
| */ |
| public class PageInfoController |
| implements ModalDialogView.Controller, SystemSettingsActivityRequiredListener { |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef({OPENED_FROM_MENU, OPENED_FROM_TOOLBAR, OPENED_FROM_VR}) |
| private @interface OpenedFromSource {} |
| |
| public static final int OPENED_FROM_MENU = 1; |
| public static final int OPENED_FROM_TOOLBAR = 2; |
| public static final int OPENED_FROM_VR = 3; |
| |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef({NOT_OFFLINE_PAGE, TRUSTED_OFFLINE_PAGE, UNTRUSTED_OFFLINE_PAGE}) |
| private @interface OfflinePageState {} |
| |
| public static final int NOT_OFFLINE_PAGE = 1; |
| public static final int TRUSTED_OFFLINE_PAGE = 2; |
| public static final int UNTRUSTED_OFFLINE_PAGE = 3; |
| |
| private final Context mContext; |
| private final WindowAndroid mWindowAndroid; |
| private final Tab mTab; |
| private final PermissionParamsListBuilder mPermissionParamsListBuilder; |
| |
| // A pointer to the C++ object for this UI. |
| private long mNativePageInfoController; |
| |
| // The view inside the popup. |
| private PageInfoView mView; |
| |
| // The dialog the view is placed in. |
| private final PageInfoDialog mDialog; |
| |
| // 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; |
| |
| // Creation date of an offline copy, if web contents contains an offline page. |
| private String mOfflinePageCreationDate; |
| |
| // The state of offline page in the web contents (not offline page, trusted/untrusted offline |
| // page). |
| private @OfflinePageState int mOfflinePageState; |
| |
| // The name of the content publisher, if any. |
| private String mContentPublisher; |
| |
| // Observer for dismissing dialog if web contents get destroyed, navigate etc. |
| private WebContentsObserver mWebContentsObserver; |
| |
| // A task that should be run once the page info popup is animated out and dismissed. Null if no |
| // task is pending. |
| private Runnable mPendingRunAfterDismissTask; |
| |
| /** |
| * Creates the PageInfoController, 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 tab Tab for which the pop up is shown. |
| * @param offlinePageUrl URL that the offline page claims to be generated from. |
| * @param offlinePageCreationDate Date when the offline page was created. |
| * @param offlinePageState State of the tab showing offline page. |
| * @param publisher The name of the content publisher, if any. |
| */ |
| protected PageInfoController(Activity activity, Tab tab, String offlinePageUrl, |
| String offlinePageCreationDate, @OfflinePageState int offlinePageState, |
| String publisher) { |
| mContext = activity; |
| mTab = tab; |
| mOfflinePageState = offlinePageState; |
| PageInfoViewParams viewParams = new PageInfoViewParams(); |
| |
| if (mOfflinePageState != NOT_OFFLINE_PAGE) { |
| mOfflinePageCreationDate = offlinePageCreationDate; |
| } |
| mWindowAndroid = mTab.getWebContents().getTopLevelNativeWindow(); |
| mContentPublisher = publisher; |
| |
| viewParams.urlTitleClickCallback = () -> { |
| // Expand/collapse the displayed URL title. |
| mView.toggleUrlTruncation(); |
| }; |
| // Long press the url text to copy it to the clipboard. |
| viewParams.urlTitleLongClickCallback = () -> { |
| 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(); |
| }; |
| |
| // Work out the URL and connection message and status visibility. |
| mFullUrl = isShowingOfflinePage() ? offlinePageUrl : mTab.getOriginalUrl(); |
| |
| // This can happen if an invalid chrome-distiller:// url was entered. |
| if (mFullUrl == null) mFullUrl = ""; |
| |
| try { |
| mParsedUrl = new URI(mFullUrl); |
| mIsInternalPage = UrlUtilities.isInternalScheme(mParsedUrl); |
| } catch (URISyntaxException e) { |
| mParsedUrl = null; |
| mIsInternalPage = false; |
| } |
| mSecurityLevel = SecurityStateModel.getSecurityLevelForWebContents(mTab.getWebContents()); |
| |
| String displayUrl = UrlFormatter.formatUrlForCopy(mFullUrl); |
| if (isShowingOfflinePage()) { |
| displayUrl = OfflinePageUtils.stripSchemeFromOnlineUrl(mFullUrl); |
| } |
| SpannableStringBuilder displayUrlBuilder = new SpannableStringBuilder(displayUrl); |
| OmniboxUrlEmphasizer.emphasizeUrl(displayUrlBuilder, mContext.getResources(), |
| mTab.getProfile(), mSecurityLevel, mIsInternalPage, true, true); |
| if (mSecurityLevel == ConnectionSecurityLevel.SECURE) { |
| OmniboxUrlEmphasizer.EmphasizeComponentsResponse emphasizeResponse = |
| OmniboxUrlEmphasizer.parseForEmphasizeComponents( |
| mTab.getProfile(), displayUrlBuilder.toString()); |
| if (emphasizeResponse.schemeLength > 0) { |
| displayUrlBuilder.setSpan( |
| new TextAppearanceSpan(mContext, R.style.RobotoMediumStyle), 0, |
| emphasizeResponse.schemeLength, Spannable.SPAN_EXCLUSIVE_INCLUSIVE); |
| } |
| } |
| viewParams.url = displayUrlBuilder; |
| viewParams.urlOriginLength = OmniboxUrlEmphasizer.getOriginEndIndex( |
| displayUrlBuilder.toString(), mTab.getProfile()); |
| |
| if (mParsedUrl == null || mParsedUrl.getScheme() == null || isShowingOfflinePage() |
| || !(mParsedUrl.getScheme().equals(UrlConstants.HTTP_SCHEME) |
| || mParsedUrl.getScheme().equals(UrlConstants.HTTPS_SCHEME))) { |
| viewParams.siteSettingsButtonShown = false; |
| } else { |
| viewParams.siteSettingsButtonClickCallback = () -> { |
| // Delay while the dialog closes. |
| runAfterDismiss(() -> { |
| recordAction(PageInfoAction.PAGE_INFO_SITE_SETTINGS_OPENED); |
| Bundle fragmentArguments = |
| SingleWebsitePreferences.createFragmentArgsForSite(mFullUrl); |
| Intent preferencesIntent = PreferencesLauncher.createIntentForSettingsPage( |
| mContext, SingleWebsitePreferences.class.getName()); |
| preferencesIntent.putExtra( |
| Preferences.EXTRA_SHOW_FRAGMENT_ARGUMENTS, fragmentArguments); |
| // Disabling StrictMode to avoid violations (https://crbug.com/819410). |
| try (StrictModeContext unused = StrictModeContext.allowDiskReads()) { |
| mContext.startActivity(preferencesIntent); |
| } |
| }); |
| }; |
| } |
| |
| if (isShowingOfflinePage()) { |
| boolean isConnected = OfflinePageUtils.isConnected(); |
| RecordHistogram.recordBooleanHistogram( |
| "OfflinePages.WebsiteSettings.OpenOnlineButtonVisible", isConnected); |
| if (isConnected) { |
| viewParams.openOnlineButtonClickCallback = () -> { |
| runAfterDismiss(() -> { |
| // Attempt to reload to an online version of the viewed offline web page. |
| // This attempt might fail if the user is offline, in which case an offline |
| // copy will be reloaded. |
| RecordHistogram.recordBooleanHistogram( |
| "OfflinePages.WebsiteSettings.ConnectedWhenOpenOnlineButtonClicked", |
| OfflinePageUtils.isConnected()); |
| OfflinePageUtils.reload(mTab); |
| }); |
| }; |
| } else { |
| viewParams.openOnlineButtonShown = false; |
| } |
| } else { |
| viewParams.openOnlineButtonShown = false; |
| } |
| |
| InstantAppsHandler instantAppsHandler = InstantAppsHandler.getInstance(); |
| if (!mIsInternalPage && !isShowingOfflinePage() |
| && instantAppsHandler.isInstantAppAvailable(mFullUrl, false /* checkHoldback */, |
| false /* includeUserPrefersBrowser */)) { |
| final Intent instantAppIntent = instantAppsHandler.getInstantAppIntentForUrl(mFullUrl); |
| viewParams.instantAppButtonClickCallback = () -> { |
| try { |
| mContext.startActivity(instantAppIntent); |
| RecordUserAction.record("Android.InstantApps.LaunchedFromWebsiteSettingsPopup"); |
| } catch (ActivityNotFoundException e) { |
| mView.disableInstantAppButton(); |
| } |
| }; |
| RecordUserAction.record("Android.InstantApps.OpenInstantAppButtonShown"); |
| } else { |
| viewParams.instantAppButtonShown = false; |
| } |
| |
| mView = new PageInfoView(mContext, viewParams); |
| mPermissionParamsListBuilder = new PermissionParamsListBuilder( |
| mContext, mWindowAndroid, mFullUrl, this, mView::setPermissions); |
| |
| // This needs to come after other member initialization. |
| mNativePageInfoController = nativeInit(this, mTab.getWebContents()); |
| mWebContentsObserver = new WebContentsObserver(mTab.getWebContents()) { |
| @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(true); |
| } |
| |
| @Override |
| public void wasHidden() { |
| // The web contents were hidden (potentially by loading another URL via an intent), |
| // so dismiss the dialog). |
| mDialog.dismiss(true); |
| } |
| |
| @Override |
| public void destroy() { |
| super.destroy(); |
| // Force the dialog to close immediately in case the destroy was from Chrome |
| // quitting. |
| mDialog.dismiss(false); |
| } |
| }; |
| |
| mDialog = new PageInfoDialog(mContext, mView, mTab.getView(), isSheet(), |
| mTab.getActivity().getModalDialogManager(), this); |
| mDialog.show(); |
| } |
| |
| /** |
| * Whether to show a 'Details' link to the connection info popup. The link is only shown for |
| * HTTPS connections. |
| */ |
| private boolean isConnectionDetailsLinkVisible() { |
| return mContentPublisher == null && !isShowingOfflinePage() && mParsedUrl != null |
| && mParsedUrl.getScheme() != null |
| && mParsedUrl.getScheme().equals(UrlConstants.HTTPS_SCHEME); |
| } |
| |
| /** |
| * 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) { |
| mPermissionParamsListBuilder.addPermissionEntry( |
| name, type, ContentSetting.fromInt(currentSettingValue)); |
| } |
| |
| /** |
| * Update the permissions view based on the contents of mDisplayedPermissions. |
| */ |
| @CalledByNative |
| private void updatePermissionDisplay() { |
| mView.setPermissions(mPermissionParamsListBuilder.build()); |
| } |
| |
| /** |
| * Sets the connection security summary and detailed description strings. These strings may be |
| * overridden based on the state of the Android UI. |
| */ |
| @CalledByNative |
| private void setSecurityDescription(String summary, String details) { |
| ConnectionInfoParams connectionInfoParams = new ConnectionInfoParams(); |
| |
| // 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 (mOfflinePageState == TRUSTED_OFFLINE_PAGE) { |
| messageBuilder.append( |
| String.format(mContext.getString(R.string.page_info_connection_offline), |
| mOfflinePageCreationDate)); |
| } else if (mOfflinePageState == UNTRUSTED_OFFLINE_PAGE) { |
| // For untrusted pages, if there's a creation date, show it in the message. |
| if (TextUtils.isEmpty(mOfflinePageCreationDate)) { |
| messageBuilder.append(mContext.getString( |
| R.string.page_info_offline_page_not_trusted_without_date)); |
| } else { |
| messageBuilder.append(String.format( |
| mContext.getString(R.string.page_info_offline_page_not_trusted_with_date), |
| mOfflinePageCreationDate)); |
| } |
| } else { |
| if (!TextUtils.equals(summary, details)) { |
| connectionInfoParams.summary = summary; |
| } |
| messageBuilder.append(details); |
| } |
| |
| if (isConnectionDetailsLinkVisible()) { |
| messageBuilder.append(" "); |
| SpannableString detailsText = |
| new SpannableString(mContext.getString(R.string.details_link)); |
| final ForegroundColorSpan blueSpan = |
| new ForegroundColorSpan(ApiCompatibilityUtils.getColor( |
| mContext.getResources(), R.color.default_text_color_link)); |
| detailsText.setSpan( |
| blueSpan, 0, detailsText.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE); |
| messageBuilder.append(detailsText); |
| } |
| |
| connectionInfoParams.message = messageBuilder; |
| if (isConnectionDetailsLinkVisible()) { |
| connectionInfoParams.clickCallback = () -> { |
| runAfterDismiss(() -> { |
| if (!mTab.getWebContents().isDestroyed()) { |
| recordAction(PageInfoAction.PAGE_INFO_SECURITY_DETAILS_OPENED); |
| ConnectionInfoPopup.show(mContext, mTab); |
| } |
| }); |
| }; |
| } |
| mView.setConnectionInfo(connectionInfoParams); |
| } |
| |
| @Override |
| public void onSystemSettingsActivityRequired(Intent intentOverride) { |
| runAfterDismiss(() -> { |
| Intent settingsIntent; |
| if (intentOverride != null) { |
| settingsIntent = 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); |
| }); |
| } |
| |
| /** |
| * Dismiss the popup, and then run a task after the animation has completed (if there is one). |
| */ |
| private void runAfterDismiss(Runnable task) { |
| mPendingRunAfterDismissTask = task; |
| mDialog.dismiss(true); |
| } |
| |
| @Override |
| public void onClick(@ButtonType int buttonType) {} |
| |
| @Override |
| public void onCancel() {} |
| |
| @Override |
| public void onDismiss() { |
| assert mNativePageInfoController != 0; |
| if (mPendingRunAfterDismissTask != null) { |
| mPendingRunAfterDismissTask.run(); |
| mPendingRunAfterDismissTask = null; |
| } |
| mWebContentsObserver.destroy(); |
| nativeDestroy(mNativePageInfoController); |
| mNativePageInfoController = 0; |
| } |
| |
| private void recordAction(int action) { |
| if (mNativePageInfoController != 0) { |
| nativeRecordPageInfoAction(mNativePageInfoController, action); |
| } |
| } |
| |
| /** |
| * Whether website dialog is displayed for an offline page. |
| */ |
| private boolean isShowingOfflinePage() { |
| return mOfflinePageState != NOT_OFFLINE_PAGE; |
| } |
| |
| private boolean isSheet() { |
| return !DeviceFormFactor.isNonMultiDisplayContextOnTablet(mContext) |
| && !VrShellDelegate.isInVr(); |
| } |
| |
| @VisibleForTesting |
| public PageInfoView getPageInfoViewForTesting() { |
| return mView; |
| } |
| |
| /** |
| * Shows a PageInfo 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 if (source == OPENED_FROM_VR) { |
| RecordUserAction.record("MobileWebsiteSettingsOpenedFromVR"); |
| } else { |
| assert false : "Invalid source passed"; |
| } |
| |
| String offlinePageUrl = null; |
| String offlinePageCreationDate = null; |
| @OfflinePageState |
| int offlinePageState = NOT_OFFLINE_PAGE; |
| |
| OfflinePageItem offlinePage = OfflinePageUtils.getOfflinePage(tab); |
| if (offlinePage != null) { |
| offlinePageUrl = offlinePage.getUrl(); |
| if (OfflinePageUtils.isShowingTrustedOfflinePage(tab)) { |
| offlinePageState = TRUSTED_OFFLINE_PAGE; |
| } else { |
| offlinePageState = UNTRUSTED_OFFLINE_PAGE; |
| } |
| // Get formatted creation date of the offline page. If the page was shared (so the |
| // creation date cannot be acquired), make date an empty string and there will be |
| // specific processing for showing different string in UI. |
| long pageCreationTimeMs = offlinePage.getCreationTimeMs(); |
| if (pageCreationTimeMs != 0) { |
| Date creationDate = new Date(offlinePage.getCreationTimeMs()); |
| DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM); |
| offlinePageCreationDate = df.format(creationDate); |
| } |
| } |
| |
| new PageInfoController(activity, tab, offlinePageUrl, offlinePageCreationDate, |
| offlinePageState, contentPublisher); |
| } |
| |
| private static native long nativeInit(PageInfoController controller, WebContents webContents); |
| |
| private native void nativeDestroy(long nativePageInfoControllerAndroid); |
| |
| private native void nativeRecordPageInfoAction( |
| long nativePageInfoControllerAndroid, int action); |
| } |