blob: 172586fb57a9c2dbfc45a82b36eb4560a3b09bb3 [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.page_info;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;
import android.provider.Settings;
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 android.view.Window;
import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;
import androidx.core.view.ViewCompat;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.Consumer;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.NativeMethods;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.components.content_settings.ContentSettingValues;
import org.chromium.components.content_settings.CookieControlsEnforcement;
import org.chromium.components.content_settings.CookieControlsObserver;
import org.chromium.components.content_settings.CookieControlsStatus;
import org.chromium.components.dom_distiller.core.DomDistillerUrlUtils;
import org.chromium.components.embedder_support.util.UrlUtilities;
import org.chromium.components.omnibox.AutocompleteSchemeClassifier;
import org.chromium.components.omnibox.OmniboxUrlEmphasizer;
import org.chromium.components.page_info.PageInfoView.ConnectionInfoParams;
import org.chromium.components.page_info.PageInfoView.PageInfoViewParams;
import org.chromium.components.security_state.ConnectionSecurityLevel;
import org.chromium.components.security_state.SecurityStateModel;
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.Clipboard;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.modaldialog.DialogDismissalCause;
import org.chromium.ui.modaldialog.ModalDialogProperties;
import org.chromium.ui.modaldialog.ModalDialogProperties.ButtonType;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.util.ColorUtils;
import org.chromium.url.URI;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.net.URISyntaxException;
/**
* Java side of Android implementation of the page info UI.
*/
public class PageInfoController implements ModalDialogProperties.Controller,
SystemSettingsActivityRequiredListener,
CookieControlsObserver {
@IntDef({OpenedFromSource.MENU, OpenedFromSource.TOOLBAR, OpenedFromSource.VR})
@Retention(RetentionPolicy.SOURCE)
public @interface OpenedFromSource {
int MENU = 1;
int TOOLBAR = 2;
int VR = 3;
}
private Context mContext;
private final WindowAndroid mWindowAndroid;
private final WebContents mWebContents;
private final PageInfoControllerDelegate mDelegate;
// 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;
// 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;
// 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;
private Consumer<Runnable> mRunAfterDismissConsumer;
// Reference to last created PageInfoController for testing.
private static WeakReference<PageInfoController> sLastPageInfoControllerForTesting;
// Whether Version 2 of the PageInfoView is enabled.
private boolean mIsV2Enabled;
/**
* Creates the PageInfoController, but does not display it. Also initializes the corresponding
* C++ object and saves a pointer to it.
* @param webContents The WebContents showing the page that the PageInfo is about.
* @param securityLevel The security level of the page being shown.
* @param publisher The name of the content publisher, if any.
* @param delegate The PageInfoControllerDelegate used to provide
* embedder-specific info.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
public PageInfoController(WebContents webContents, int securityLevel, String publisher,
PageInfoControllerDelegate delegate, boolean isV2Enabled) {
mWebContents = webContents;
mSecurityLevel = securityLevel;
mDelegate = delegate;
mIsV2Enabled = isV2Enabled;
mRunAfterDismissConsumer = new Consumer<Runnable>() {
@Override
public void accept(Runnable r) {
runAfterDismiss(r);
}
};
PageInfoViewParams viewParams = new PageInfoViewParams();
mWindowAndroid = webContents.getTopLevelNativeWindow();
mContext = mWindowAndroid.getContext().get();
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 =
() -> Clipboard.getInstance().copyUrlToClipboard(mFullUrl);
// Work out the URL and connection message and status visibility.
// TODO(crbug.com/1033178): dedupe the DomDistillerUrlUtils#getOriginalUrlFromDistillerUrl()
// calls.
mFullUrl = mDelegate.isShowingOfflinePage()
? mDelegate.getOfflinePageUrl()
: DomDistillerUrlUtils.getOriginalUrlFromDistillerUrl(
webContents.getVisibleUrlString());
// This can happen if an invalid chrome-distiller:// url was entered.
if (mFullUrl == null) mFullUrl = "";
try {
mIsInternalPage = UrlUtilities.isInternalScheme(new URI(mFullUrl));
} catch (URISyntaxException e) {
// Ignore exception since this is for displaying some specific content on page info.
}
String displayUrl = UrlFormatter.formatUrlForSecurityDisplay(mFullUrl);
if (mDelegate.isShowingOfflinePage()) {
displayUrl = UrlUtilities.stripScheme(mFullUrl);
}
SpannableStringBuilder displayUrlBuilder = new SpannableStringBuilder(displayUrl);
AutocompleteSchemeClassifier autocompleteSchemeClassifier =
delegate.createAutocompleteSchemeClassifier();
if (mSecurityLevel == ConnectionSecurityLevel.SECURE) {
OmniboxUrlEmphasizer.EmphasizeComponentsResponse emphasizeResponse =
OmniboxUrlEmphasizer.parseForEmphasizeComponents(
displayUrlBuilder.toString(), autocompleteSchemeClassifier);
if (emphasizeResponse.schemeLength > 0) {
displayUrlBuilder.setSpan(
new TextAppearanceSpan(mContext, R.style.TextAppearance_RobotoMediumStyle),
0, emphasizeResponse.schemeLength, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
}
}
boolean useDarkText = ColorUtils.useDarkColors(mContext);
OmniboxUrlEmphasizer.emphasizeUrl(displayUrlBuilder, mContext.getResources(),
autocompleteSchemeClassifier, mSecurityLevel, mIsInternalPage, useDarkText,
/*emphasizeScheme=*/true);
viewParams.url = displayUrlBuilder;
viewParams.urlOriginLength = OmniboxUrlEmphasizer.getOriginEndIndex(
displayUrlBuilder.toString(), autocompleteSchemeClassifier);
autocompleteSchemeClassifier.destroy();
if (mDelegate.isSiteSettingsAvailable()) {
viewParams.siteSettingsButtonClickCallback = () -> {
// Delay while the dialog closes.
runAfterDismiss(() -> {
recordAction(PageInfoAction.PAGE_INFO_SITE_SETTINGS_OPENED);
mDelegate.showSiteSettings(mFullUrl);
});
};
viewParams.cookieControlsShown = delegate.cookieControlsShown();
} else {
viewParams.siteSettingsButtonShown = false;
viewParams.cookieControlsShown = false;
}
viewParams.onUiClosingCallback = mDelegate::onUiClosing;
mDelegate.initPreviewUiParams(viewParams, mRunAfterDismissConsumer);
mDelegate.initOfflinePageUiParams(viewParams, mRunAfterDismissConsumer);
if (!mIsInternalPage && !mDelegate.isShowingOfflinePage() && !mDelegate.isShowingPreview()
&& mDelegate.isInstantAppAvailable(mFullUrl)) {
final Intent instantAppIntent = mDelegate.getInstantAppIntentForUrl(mFullUrl);
viewParams.instantAppButtonClickCallback = () -> {
try {
mWindowAndroid.getActivity().get().startActivity(instantAppIntent);
RecordUserAction.record("Android.InstantApps.LaunchedFromWebsiteSettingsPopup");
} catch (ActivityNotFoundException e) {
mView.disableInstantAppButton();
}
};
RecordUserAction.record("Android.InstantApps.OpenInstantAppButtonShown");
} else {
viewParams.instantAppButtonShown = false;
}
mView = mIsV2Enabled ? new PageInfoViewV2(mContext, viewParams)
: new PageInfoView(mContext, viewParams);
if (isSheet(mContext)) mView.setBackgroundColor(Color.WHITE);
// TODO(crbug.com/1040091): Remove when cookie controls are launched.
boolean showTitle = viewParams.cookieControlsShown;
mDelegate.createPermissionParamsListBuilder(
mWindowAndroid, mFullUrl, showTitle, this, mView::setPermissions);
mNativePageInfoController = PageInfoControllerJni.get().init(this, mWebContents);
if (mIsV2Enabled) {
PageInfoRowView.ViewParams cookieControlsParams = new PageInfoRowView.ViewParams();
cookieControlsParams.visible = viewParams.cookieControlsShown;
cookieControlsParams.title =
mView.getContext().getResources().getString(R.string.cookies_title);
((PageInfoViewV2) mView).getCookiesRowView().setParams(cookieControlsParams);
} else {
CookieControlsView.CookieControlsParams cookieControlsParams =
new CookieControlsView.CookieControlsParams();
cookieControlsParams.onCheckedChangedCallback = (Boolean blockCookies) -> {
recordAction(blockCookies ? PageInfoAction.PAGE_INFO_COOKIE_BLOCKED_FOR_SITE
: PageInfoAction.PAGE_INFO_COOKIE_ALLOWED_FOR_SITE);
mDelegate.setThirdPartyCookieBlockingEnabledForSite(blockCookies);
};
mView.getCookieControlsView().setParams(cookieControlsParams);
}
mDelegate.createCookieControlsBridge(this);
if (mIsV2Enabled) {
PageInfoRowView.ViewParams performanceParams = new PageInfoRowView.ViewParams();
performanceParams.visible = mDelegate.shouldShowPerformanceBadge(mFullUrl);
performanceParams.title = mView.getContext().getResources().getString(
R.string.page_info_fast_site_summary);
performanceParams.subtitle = mView.getContext().getResources().getString(
R.string.page_info_fast_site_message);
((PageInfoViewV2) mView).getPerformanceRowView().setParams(performanceParams);
} else {
mView.showPerformanceInfo(mDelegate.shouldShowPerformanceBadge(mFullUrl));
}
mWebContentsObserver = new WebContentsObserver(webContents) {
@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,
webContents.getViewAndroidDelegate().getContainerView(), isSheet(mContext),
delegate.getModalDialogManager(), this);
mDialog.show();
}
/**
* Whether to show a 'Details' link to the connection info popup.
*/
private boolean isConnectionDetailsLinkVisible() {
return mContentPublisher == null && !mDelegate.isShowingOfflinePage()
&& !mDelegate.isShowingPreview() && !mIsInternalPage;
}
/**
* 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, @ContentSettingValues int currentSettingValue) {
mDelegate.addPermissionEntry(name, type, currentSettingValue);
}
/**
* Update the permissions view based on the contents of mDisplayedPermissions.
*/
@CalledByNative
private void updatePermissionDisplay() {
mDelegate.updatePermissionDisplay(mView);
}
/**
* 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();
assert mContext != null;
if (mContentPublisher != null) {
messageBuilder.append(
mContext.getString(R.string.page_info_domain_hidden, mContentPublisher));
} else if (mDelegate.isShowingPreview() && mDelegate.isPreviewPageInsecure()) {
connectionInfoParams.summary = summary;
} else if (mDelegate.getOfflinePageConnectionMessage() != null) {
messageBuilder.append(mDelegate.getOfflinePageConnectionMessage());
} 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);
}
// When a preview is being shown for a secure page, the security message is not shown. Thus,
// messageBuilder maybe empty.
if (messageBuilder.length() > 0) {
connectionInfoParams.message = messageBuilder;
}
if (isConnectionDetailsLinkVisible()) {
connectionInfoParams.clickCallback = () -> {
runAfterDismiss(() -> {
if (!mWebContents.isDestroyed()) {
recordAction(PageInfoAction.PAGE_INFO_SECURITY_DETAILS_OPENED);
ConnectionInfoPopup.show(mContext, mWebContents,
mDelegate.getModalDialogManager(), mDelegate.getVrHandler());
}
});
};
}
if (mIsV2Enabled) {
PageInfoRowView.ViewParams connectionRowParams = new PageInfoRowView.ViewParams();
connectionRowParams.title = connectionInfoParams.summary != null
? connectionInfoParams.summary.toString()
: null;
connectionRowParams.subtitle = connectionInfoParams.message != null
? connectionInfoParams.message.toString()
: null;
connectionRowParams.visible =
connectionRowParams.title != null || connectionRowParams.subtitle != null;
((PageInfoViewV2) mView).getConnectionRowView().setParams(connectionRowParams);
} else {
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(PropertyModel model, @ButtonType int buttonType) {}
@Override
public void onDismiss(PropertyModel model, @DialogDismissalCause int dismissalCause) {
assert mNativePageInfoController != 0;
if (mPendingRunAfterDismissTask != null) {
mPendingRunAfterDismissTask.run();
mPendingRunAfterDismissTask = null;
}
mWebContentsObserver.destroy();
PageInfoControllerJni.get().destroy(mNativePageInfoController, PageInfoController.this);
mNativePageInfoController = 0;
mContext = null;
}
private void recordAction(int action) {
if (mNativePageInfoController != 0) {
PageInfoControllerJni.get().recordPageInfoAction(
mNativePageInfoController, PageInfoController.this, action);
}
}
private boolean isSheet(Context context) {
return !DeviceFormFactor.isNonMultiDisplayContextOnTablet(context)
&& (mDelegate.getVrHandler() == null || !mDelegate.getVrHandler().isInVr());
}
@VisibleForTesting
public PageInfoView getPageInfoViewForTesting() {
return mView;
}
/**
* Shows a PageInfo dialog for the provided WebContents. The popup adds itself to the view
* hierarchy which owns the reference while it's visible.
*
* @param activity The activity that is used for launching a dialog.
* @param webContents 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.
* @param delegate The PageInfoControllerDelegate used to provide embedder-specific info.
*/
public static void show(final Activity activity, WebContents webContents,
final String contentPublisher, @OpenedFromSource int source,
PageInfoControllerDelegate delegate) {
// If the activity's decor view is not attached to window, we don't show the dialog because
// the window manager might have revoked the window token for this activity. See
// https://crbug.com/921450.
Window window = activity.getWindow();
if (window == null || !ViewCompat.isAttachedToWindow(window.getDecorView())) return;
if (source == OpenedFromSource.MENU) {
RecordUserAction.record("MobileWebsiteSettingsOpenedFromMenu");
} else if (source == OpenedFromSource.TOOLBAR) {
RecordUserAction.record("MobileWebsiteSettingsOpenedFromToolbar");
} else if (source == OpenedFromSource.VR) {
RecordUserAction.record("MobileWebsiteSettingsOpenedFromVR");
} else {
assert false : "Invalid source passed";
}
sLastPageInfoControllerForTesting = new WeakReference<>(new PageInfoController(webContents,
SecurityStateModel.getSecurityLevelForWebContents(webContents), contentPublisher,
delegate, PageInfoFeatureList.isEnabled(PageInfoFeatureList.PAGE_INFO_V2)));
}
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static PageInfoController getLastPageInfoControllerForTesting() {
return sLastPageInfoControllerForTesting != null ? sLastPageInfoControllerForTesting.get()
: null;
}
@Override
public void onCookieBlockingStatusChanged(
@CookieControlsStatus int status, @CookieControlsEnforcement int enforcement) {
if (!mIsV2Enabled) {
mView.getCookieControlsView().setCookieBlockingStatus(
status, enforcement != CookieControlsEnforcement.NO_ENFORCEMENT);
}
}
@Override
public void onBlockedCookiesCountChanged(int blockedCookies) {
if (mIsV2Enabled) {
String subtitle = mContext.getResources().getQuantityString(
R.plurals.cookie_controls_blocked_cookies, blockedCookies, blockedCookies);
((PageInfoViewV2) mView).getCookiesRowView().updateSubtitle(subtitle);
} else {
mView.getCookieControlsView().setBlockedCookiesCount(blockedCookies);
}
}
@NativeMethods
interface Natives {
long init(PageInfoController controller, WebContents webContents);
void destroy(long nativePageInfoControllerAndroid, PageInfoController caller);
void recordPageInfoAction(
long nativePageInfoControllerAndroid, PageInfoController caller, int action);
}
}