Commit 548b91ae authored by Rouslan Solomakhin's avatar Rouslan Solomakhin Committed by Commit Bot

[Payment Handler][CCT] 70% height payment handler window.

Before this patch, Chrome Custom Tab would always be 100% height in all
cases, including for showing a Payment Handler page. This hid the shop
page from the user and increased the chance of the shop page renderer
being killed.

This patch adds a PaymentHandlerActivity that extends CustomTabActivity
with a custom theme that has transparent background. The
PaymentHandler-specific functionality is moved from CustomTabActivity
into the PaymentHandlerActivity. This activity is 70% of the display
height ("bottom sheet"), unless that's less than 500dp, in which case
the height is 500dp. If a device rotates, the window is either in bottom
sheet or fullscreen mode, depending on the amount of available vertical
space.

The underlying activity is dimmed by DimmingDialog, which is a
fullscreen semi-transparent dialog. This was refactored out of
PaymentRequestUI, which now uses the DimmingDialog and optionally adds
opaque content at the bottom.

After this patch, payment handlers are displayed in a bottom sheet with
dimmed background.

Bug: 872833
Change-Id: I4c104373f3e44130d1217cbf2c6923be3b63d1a1
Reviewed-on: https://chromium-review.googlesource.com/1169636
Commit-Queue: Rouslan Solomakhin <rouslan@chromium.org>
Reviewed-by: default avatarPeter Conn <peconn@chromium.org>
Reviewed-by: default avatarTed Choc <tedchoc@chromium.org>
Cr-Commit-Position: refs/heads/master@{#594424}
parent 9e4de63f
......@@ -387,6 +387,15 @@ by a child template that "extends" this file.
{{ self.supports_vr() }}
{{ self.extra_web_rendering_activity_definitions() }}
</activity>
<activity android:name="org.chromium.chrome.browser.customtabs.PaymentHandlerActivity"
android:theme="@style/TranslucentMainTheme"
android:exported="false"
{{ self.chrome_activity_common() }}
{{ self.supports_video_persistence() }}
>
{{ self.supports_vr() }}
{{ self.extra_web_rendering_activity_definitions() }}
</activity>
<!-- SeparateTaskCustomTabActivity is a wrapper of CustomTabActivity. It provides the
general feeling of supporting multi tasks, even for versions that did not fully support
it.
......
......@@ -42,6 +42,10 @@
<item name="android:windowSharedElementExitTransition" tools:targetApi="21">@transition/move_image</item>
</style>
<style name="MainTheme" parent="MainThemeBase" />
<style name="TranslucentMainTheme" parent="MainThemeBase">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowIsTranslucent">true</item>
</style>
<style name="MainButtonStyle" parent="ButtonCompatBorderless">
<item name="android:textAppearance">@style/BlueButtonText2</item>
......
......@@ -430,6 +430,7 @@
<dimen name="payments_ui_max_dialog_width">0dp</dimen>
<dimen name="payments_ui_translation">100dp</dimen>
<dimen name="payments_favicon_size">24dp</dimen>
<dimen name="payments_handler_window_minimum_height">500dp</dimen>
<!-- Preferences dimensions
pref_autofill_field_horizontal_padding exists because TextInputLayouts have an internal
......
......@@ -28,6 +28,7 @@ import org.chromium.base.metrics.CachedMetrics;
import org.chromium.chrome.browser.browserservices.BrowserSessionContentUtils;
import org.chromium.chrome.browser.customtabs.CustomTabActivity;
import org.chromium.chrome.browser.customtabs.CustomTabIntentDataProvider;
import org.chromium.chrome.browser.customtabs.PaymentHandlerActivity;
import org.chromium.chrome.browser.customtabs.SeparateTaskCustomTabActivity;
import org.chromium.chrome.browser.firstrun.FirstRunFlowSequencer;
import org.chromium.chrome.browser.incognito.IncognitoDisclosureActivity;
......@@ -290,6 +291,13 @@ public class LaunchIntentDispatcher implements IntentHandler.IntentHandlerDelega
newIntent.setData(uri);
newIntent.setClassName(context, CustomTabActivity.class.getName());
// Use a custom tab with a unique theme for payment handlers.
if (intent.getIntExtra(CustomTabIntentDataProvider.EXTRA_UI_TYPE,
CustomTabIntentDataProvider.CustomTabsUiType.DEFAULT)
== CustomTabIntentDataProvider.CustomTabsUiType.PAYMENT_REQUEST) {
newIntent.setClassName(context, PaymentHandlerActivity.class.getName());
}
// If |uri| is a content:// URI, we want to propagate the URI permissions. This can't be
// achieved by simply adding the FLAG_GRANT_READ_URI_PERMISSION to the Intent, since the
// data URI on the Intent isn't |uri|, it just has |uri| as a query parameter.
......
......@@ -20,6 +20,7 @@ import android.os.Bundle;
import android.os.StrictMode;
import android.os.SystemClock;
import android.provider.Browser;
import android.support.annotation.CallSuper;
import android.support.annotation.IntDef;
import android.support.annotation.Nullable;
import android.support.customtabs.CustomTabsIntent;
......@@ -83,7 +84,6 @@ import org.chromium.chrome.browser.incognito.IncognitoTabHost;
import org.chromium.chrome.browser.incognito.IncognitoTabHostRegistry;
import org.chromium.chrome.browser.net.spdyproxy.DataReductionProxySettings;
import org.chromium.chrome.browser.page_info.PageInfoController;
import org.chromium.chrome.browser.payments.ServiceWorkerPaymentAppBridge;
import org.chromium.chrome.browser.rappor.RapporServiceBridge;
import org.chromium.chrome.browser.snackbar.SnackbarManager;
import org.chromium.chrome.browser.tab.BrowserControlsVisibilityDelegate;
......@@ -780,7 +780,13 @@ public class CustomTabActivity extends ChromeActivity<CustomTabActivityComponent
return asyncParams.getWebContents();
}
private void initializeMainTab(Tab tab) {
/**
* Initializes tab handlers and observers, e.g., for metrics.
*
* @param tab The tab to initialize.
*/
@CallSuper
protected void initializeMainTab(Tab tab) {
TabRedirectHandler.from(tab).updateIntent(getIntent());
tab.getView().requestFocus();
......@@ -803,12 +809,6 @@ public class CustomTabActivity extends ChromeActivity<CustomTabActivityComponent
// Immediately add the observer to PageLoadMetrics to catch early events that may
// be generated in the middle of tab initialization.
mTabObserverRegistrar.addObserversForTab(tab);
// Let ServiceWorkerPaymentAppBridge observe the opened tab for payment request.
if (mIntentDataProvider.isForPaymentRequest()) {
ServiceWorkerPaymentAppBridge.addTabObserverForPaymentRequestTab(tab);
}
prepareTabBackground(tab);
}
......@@ -1144,13 +1144,6 @@ public class CustomTabActivity extends ChromeActivity<CustomTabActivityComponent
if (mIsClosing) return;
mIsClosing = true;
// Notify the window is closing so as to abort invoking payment app early.
if (mIntentDataProvider.isForPaymentRequest()
&& getActivityTab().getWebContents() != null) {
ServiceWorkerPaymentAppBridge.onClosingPaymentAppWindow(
getActivityTab().getWebContents());
}
if (!reparenting) {
// Closing the activity destroys the renderer as well. Re-create a spare renderer some
// time after, so that we have one ready for the next tab open. This does not increase
......
......@@ -402,9 +402,10 @@ public class CustomTabIntentDataProvider extends BrowserSessionDataProvider {
/**
* @return Whether url bar hiding should be enabled in the custom tab. Default is false.
* It should be impossible to hide the url bar when the tab is opened for Payment Request.
*/
public boolean shouldEnableUrlBarHiding() {
return mEnableUrlBarHiding;
return mEnableUrlBarHiding && !isForPaymentRequest();
}
/**
......
// Copyright 2018 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.customtabs;
import android.view.Gravity;
import android.view.WindowManager;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.payments.ServiceWorkerPaymentAppBridge;
import org.chromium.chrome.browser.tab.Tab;
/**
* Simple wrapper around CustomTabActivity to be used when launching a payment handler tab, which
* uses a different theme.
*/
public class PaymentHandlerActivity extends CustomTabActivity {
private static final double BOTTOM_SHEET_HEIGHT_RATIO = 0.7;
private boolean mHaveNotifiedServiceWorker;
@Override
protected void initializeMainTab(Tab tab) {
super.initializeMainTab(tab);
ServiceWorkerPaymentAppBridge.addTabObserverForPaymentRequestTab(tab);
}
@Override
public void preInflationStartup() {
super.preInflationStartup();
int heightInPhysicalPixels = (int) (getWindowAndroid().getDisplay().getDisplayHeight()
* BOTTOM_SHEET_HEIGHT_RATIO);
int minimumHeightInPhysicalPixels = getResources().getDimensionPixelSize(
R.dimen.payments_handler_window_minimum_height);
if (heightInPhysicalPixels < minimumHeightInPhysicalPixels)
heightInPhysicalPixels = minimumHeightInPhysicalPixels;
WindowManager.LayoutParams attributes = getWindow().getAttributes();
attributes.height = heightInPhysicalPixels;
attributes.gravity = Gravity.BOTTOM;
getWindow().setAttributes(attributes);
}
@Override
protected void handleFinishAndClose() {
// Notify the window is closing so as to abort invoking payment app early.
if (!mHaveNotifiedServiceWorker && getActivityTab().getWebContents() != null) {
mHaveNotifiedServiceWorker = true;
ServiceWorkerPaymentAppBridge.onClosingPaymentAppWindow(
getActivityTab().getWebContents());
}
super.handleFinishAndClose();
}
}
\ No newline at end of file
......@@ -692,6 +692,7 @@ public class PaymentRequestImpl
&& !mIsUserGestureShow)) {
mUI.show();
} else {
mUI.dimBackground();
mDidRecordShowEvent = true;
mShouldRecordAbortReason = true;
mJourneyLogger.setEventOccurred(Event.SKIPPED_SHOW);
......
// Copyright 2018 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.payments.ui;
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.Context;
import android.content.DialogInterface;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.support.v4.view.animation.FastOutLinearInInterpolator;
import android.support.v4.view.animation.LinearOutSlowInInterpolator;
import android.view.Gravity;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.Window;
import android.widget.FrameLayout;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.widget.AlwaysDismissedDialog;
import org.chromium.chrome.browser.widget.animation.AnimatorProperties;
/**
* A fullscreen semitransparent dialog used for dimming Chrome when overlaying a bottom sheet
* dialog/CCT or an alert dialog on top of it. FLAG_DIM_BEHIND is not being used because it causes
* the web contents of a payment handler CCT to also dim on some versions of Android (e.g., Nougat).
*/
/* package */ class DimmingDialog {
/**
* Length of the animation to either show the UI or expand it to full height. Note that click of
* 'Pay' button in PaymentRequestUI is not accepted until the animation is done, so this
* duration also serves the function of preventing the user from accidentally double-clicking on
* the screen when triggering payment and thus authorizing unwanted transaction.
*/
private static final int DIALOG_ENTER_ANIMATION_MS = 225;
/** Length of the animation to hide the bottom sheet UI. */
private static final int DIALOG_EXIT_ANIMATION_MS = 195;
private final Dialog mDialog;
private final ViewGroup mFullContainer;
private final int mAnimatorTranslation;
private boolean mIsAnimatingDisappearance;
/**
* Builds the dimming dialog.
*
* @param activity The activity on top of which the dialog should be displayed.
* @param dismissListener The listener for the dismissal of this dialog.
*/
/* package */ DimmingDialog(
Activity activity, DialogInterface.OnDismissListener dismissListener) {
// To handle the specced animations, the dialog is entirely contained within a translucent
// FrameLayout. This could eventually be converted to a real BottomSheetDialog, but that
// requires exploration of how interactions would work when the dialog can be sent back and
// forth between the peeking and expanded state.
mFullContainer = new FrameLayout(activity);
mFullContainer.setBackgroundColor(ApiCompatibilityUtils.getColor(
activity.getResources(), R.color.modal_dialog_scrim_color));
mDialog = new AlwaysDismissedDialog(activity, R.style.DialogWhenLarge);
mDialog.setOnDismissListener(dismissListener);
mDialog.addContentView(mFullContainer,
new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
Window dialogWindow = mDialog.getWindow();
dialogWindow.setGravity(Gravity.CENTER);
dialogWindow.setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
dialogWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
mAnimatorTranslation =
activity.getResources().getDimensionPixelSize(R.dimen.payments_ui_translation);
}
/** @param bottomSheetView The view to show in the bottom sheet. */
/* package */ void addBottomSheetView(View bottomSheetView) {
FrameLayout.LayoutParams bottomSheetParams =
new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
bottomSheetParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
mFullContainer.addView(bottomSheetView, bottomSheetParams);
bottomSheetView.addOnLayoutChangeListener(new FadeInAnimator());
}
/** Show the dialog. */
/* package */ void show() {
mDialog.show();
}
/** Hide the dialog without dismissing it. */
/* package */ void hide() {
mDialog.hide();
}
/**
* Dismiss the dialog.
*
* @param isAnimated If true, the dialog dismissal is animated.
*/
/* package */ void dismiss(boolean isAnimated) {
if (!mDialog.isShowing()) return;
if (isAnimated) {
new DisappearingAnimator(true);
} else {
mDialog.dismiss();
}
}
/** @param overlay The overlay to show. This can be an error dialog, for example. */
/* package */ void showOverlay(View overlay) {
// Animate the bottom sheet going away.
new DisappearingAnimator(false);
int floatingDialogWidth = DimmingDialog.computeMaxWidth(mFullContainer.getContext(),
mFullContainer.getMeasuredWidth(), mFullContainer.getMeasuredHeight());
FrameLayout.LayoutParams overlayParams =
new FrameLayout.LayoutParams(floatingDialogWidth, LayoutParams.WRAP_CONTENT);
overlayParams.gravity = Gravity.CENTER;
mFullContainer.addView(overlay, overlayParams);
}
/** @return Whether the dialog is currently animating disappearance. */
/* package */ boolean isAnimatingDisappearance() {
return mIsAnimatingDisappearance;
}
/**
* Computes the maximum possible width for a dialog box.
*
* Follows https://www.google.com/design/spec/components/dialogs.html#dialogs-simple-dialogs
*
* @param context Context to pull resources from.
* @param availableWidth Available width for the dialog.
* @param availableHeight Available height for the dialog.
* @return Maximum possible width for the dialog box.
*
* TODO(dfalcantara): Revisit this function when the new assets come in.
* TODO(dfalcantara): The dialog should listen for configuration changes and resize accordingly.
*/
private static int computeMaxWidth(Context context, int availableWidth, int availableHeight) {
int baseUnit = context.getResources().getDimensionPixelSize(R.dimen.dialog_width_unit);
int maxSize = Math.min(availableWidth, availableHeight);
int multiplier = maxSize / baseUnit;
return multiplier * baseUnit;
}
/**
* Animates the whole dialog fading in and darkening everything else on screen.
* This particular animation is not tracked because it is not meant to be cancellable.
*/
private class FadeInAnimator extends AnimatorListenerAdapter implements OnLayoutChangeListener {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
int oldTop, int oldRight, int oldBottom) {
mFullContainer.getChildAt(0).removeOnLayoutChangeListener(this);
Animator scrimFader = ObjectAnimator.ofInt(mFullContainer.getBackground(),
AnimatorProperties.DRAWABLE_ALPHA_PROPERTY, 0, 255);
Animator alphaAnimator = ObjectAnimator.ofFloat(mFullContainer, View.ALPHA, 0f, 1f);
AnimatorSet alphaSet = new AnimatorSet();
alphaSet.playTogether(scrimFader, alphaAnimator);
alphaSet.setDuration(DIALOG_ENTER_ANIMATION_MS);
alphaSet.setInterpolator(new LinearOutSlowInInterpolator());
alphaSet.start();
}
}
/** Animates the bottom sheet (and optionally, the scrim) disappearing off screen. */
private class DisappearingAnimator extends AnimatorListenerAdapter {
private final boolean mIsDialogClosing;
/* package */ DisappearingAnimator(boolean removeDialog) {
mIsDialogClosing = removeDialog;
View child = mFullContainer.getChildAt(0);
assert child != null;
Animator sheetFader = ObjectAnimator.ofFloat(child, View.ALPHA, child.getAlpha(), 0f);
Animator sheetTranslator =
ObjectAnimator.ofFloat(child, View.TRANSLATION_Y, 0f, mAnimatorTranslation);
AnimatorSet current = new AnimatorSet();
current.setDuration(DIALOG_EXIT_ANIMATION_MS);
current.setInterpolator(new FastOutLinearInInterpolator());
if (mIsDialogClosing) {
Animator scrimFader = ObjectAnimator.ofInt(mFullContainer.getBackground(),
AnimatorProperties.DRAWABLE_ALPHA_PROPERTY, 127, 0);
current.playTogether(sheetFader, sheetTranslator, scrimFader);
} else {
current.playTogether(sheetFader, sheetTranslator);
}
mIsAnimatingDisappearance = true;
current.addListener(this);
current.start();
}
@Override
public void onAnimationEnd(Animator animation) {
mIsAnimatingDisappearance = false;
mFullContainer.removeView(mFullContainer.getChildAt(0));
if (mIsDialogClosing && mDialog.isShowing()) mDialog.dismiss();
}
}
@VisibleForTesting
public Dialog getDialogForTest() {
return mDialog;
}
}
......@@ -8,10 +8,8 @@ import android.content.Context;
import android.graphics.Bitmap;
import android.support.v4.view.MarginLayoutParamsCompat;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.widget.BoundedLinearLayout;
......@@ -49,19 +47,11 @@ public class PaymentRequestUiErrorView extends BoundedLinearLayout {
}
/**
* Shows the dialog by attaching it to the given parent.
* Sets the callback to run upon hitting the OK button.
*
* @param parent Parent to attach to.
* @param callback Callback to run upon hitting the OK button.
*/
public void show(ViewGroup parent, final Runnable callback) {
int floatingDialogWidth = PaymentRequestUiErrorView.computeMaxWidth(parent.getContext(),
parent.getMeasuredWidth(), parent.getMeasuredHeight());
FrameLayout.LayoutParams overlayParams =
new FrameLayout.LayoutParams(floatingDialogWidth, LayoutParams.WRAP_CONTENT);
overlayParams.gravity = Gravity.CENTER;
parent.addView(this, overlayParams);
public void setDismissRunnable(final Runnable callback) {
// Make the user explicitly click on the OK button to dismiss the dialog.
View confirmButton = findViewById(R.id.ok_button);
confirmButton.setOnClickListener(new OnClickListener() {
......@@ -80,25 +70,4 @@ public class PaymentRequestUiErrorView extends BoundedLinearLayout {
public void setBitmap(Bitmap bitmap) {
((PaymentRequestHeader) findViewById(R.id.header)).setTitleBitmap(bitmap);
}
/**
* Computes the maximum possible width for a dialog box.
*
* Follows https://www.google.com/design/spec/components/dialogs.html#dialogs-simple-dialogs
*
* @param context Context to pull resources from.
* @param availableWidth Available width for the dialog.
* @param availableHeight Available height for the dialog.
* @return Maximum possible width for the dialog box.
*
* TODO(dfalcantara): Revisit this function when the new assets come in.
* TODO(dfalcantara): The dialog should listen for configuration changes and resize accordingly.
*/
public static int computeMaxWidth(Context context, int availableWidth, int availableHeight) {
int baseUnit = context.getResources().getDimensionPixelSize(R.dimen.dialog_width_unit);
int maxSize = Math.min(availableWidth, availableHeight);
int multiplier = maxSize / baseUnit;
int floatingDialogWidth = multiplier * baseUnit;
return floatingDialogWidth;
}
}
......@@ -384,6 +384,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/customtabs/FirstMeaningfulPaintObserver.java",
"java/src/org/chromium/chrome/browser/customtabs/NavigationInfoCaptureTrigger.java",
"java/src/org/chromium/chrome/browser/customtabs/PageLoadMetricsObserver.java",
"java/src/org/chromium/chrome/browser/customtabs/PaymentHandlerActivity.java",
"java/src/org/chromium/chrome/browser/customtabs/RequestThrottler.java",
"java/src/org/chromium/chrome/browser/customtabs/SeparateTaskCustomTabActivity.java",
"java/src/org/chromium/chrome/browser/customtabs/SeparateTaskCustomTabActivity0.java",
......@@ -1135,6 +1136,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/widget/prefeditor/ExpandableGridView.java",
"java/src/org/chromium/chrome/browser/widget/prefeditor/HintedDropDownAdapter.java",
"java/src/org/chromium/chrome/browser/widget/prefeditor/HintedDropDownAdapterWithPlusIcon.java",
"java/src/org/chromium/chrome/browser/payments/ui/DimmingDialog.java",
"java/src/org/chromium/chrome/browser/payments/ui/LineItem.java",
"java/src/org/chromium/chrome/browser/payments/ui/PaymentInformation.java",
"java/src/org/chromium/chrome/browser/payments/ui/PaymentRequestBottomBar.java",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment