1 // Copyright 2018 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.chrome.browser.payments.ui; 6 7 import android.animation.Animator; 8 import android.animation.AnimatorListenerAdapter; 9 import android.animation.AnimatorSet; 10 import android.animation.ObjectAnimator; 11 import android.app.Activity; 12 import android.app.Dialog; 13 import android.content.Context; 14 import android.graphics.Color; 15 import android.graphics.drawable.ColorDrawable; 16 import android.os.Build; 17 import android.view.Gravity; 18 import android.view.View; 19 import android.view.View.OnLayoutChangeListener; 20 import android.view.ViewGroup; 21 import android.view.ViewGroup.LayoutParams; 22 import android.view.Window; 23 import android.view.WindowManager; 24 import android.widget.FrameLayout; 25 26 import androidx.annotation.VisibleForTesting; 27 28 import org.chromium.base.ApiCompatibilityUtils; 29 import org.chromium.chrome.R; 30 import org.chromium.components.browser_ui.widget.AlwaysDismissedDialog; 31 import org.chromium.components.browser_ui.widget.animation.Interpolators; 32 import org.chromium.ui.util.ColorUtils; 33 34 import java.util.ArrayList; 35 import java.util.Collection; 36 37 /** 38 * A fullscreen semitransparent dialog used for dimming Chrome when overlaying a bottom sheet 39 * dialog/CCT or an alert dialog on top of it. FLAG_DIM_BEHIND is not being used because it causes 40 * the web contents of a payment handler CCT to also dim on some versions of Android (e.g., Nougat). 41 * 42 * Note: Do not use this class outside of the payments.ui package! 43 * TODO(crbug.com/806868): Revert the visibility to package default again when it is no longer used 44 * by Autofill Assistant. 45 */ 46 /* package */ class DimmingDialog { 47 /** 48 * Length of the animation to either show the UI or expand it to full height. Note that click of 49 * 'Pay' button in PaymentRequestUI is not accepted until the animation is done, so this 50 * duration also serves the function of preventing the user from accidentally double-clicking on 51 * the screen when triggering payment and thus authorizing unwanted transaction. 52 */ 53 private static final int DIALOG_ENTER_ANIMATION_MS = 225; 54 55 /** Length of the animation to hide the bottom sheet UI. */ 56 private static final int DIALOG_EXIT_ANIMATION_MS = 195; 57 58 private final Dialog mDialog; 59 private final ViewGroup mFullContainer; 60 private final int mAnimatorTranslation; 61 private OnDismissListener mDismissListener; 62 private boolean mIsAnimatingDisappearance; 63 64 /** 65 * Listener for the dismissal of the DimmingDialog. 66 */ 67 public interface OnDismissListener { 68 /** Called when the UI is dismissed. */ onDismiss()69 void onDismiss(); 70 } 71 72 /** 73 * Builds the dimming dialog. 74 * 75 * @param activity The activity on top of which the dialog should be displayed. 76 * @param dismissListener The listener for the dismissal of this dialog. 77 */ DimmingDialog(Activity activity, OnDismissListener dismissListener)78 /* package */ DimmingDialog(Activity activity, OnDismissListener dismissListener) { 79 mDismissListener = dismissListener; 80 // To handle the specced animations, the dialog is entirely contained within a translucent 81 // FrameLayout. This could eventually be converted to a real BottomSheetDialog, but that 82 // requires exploration of how interactions would work when the dialog can be sent back and 83 // forth between the peeking and expanded state. 84 mFullContainer = new FrameLayout(activity); 85 mFullContainer.setBackgroundColor(ApiCompatibilityUtils.getColor( 86 activity.getResources(), R.color.modal_dialog_scrim_color)); 87 mDialog = new AlwaysDismissedDialog(activity, R.style.DimmingDialog); 88 mDialog.setOnDismissListener((v) -> notifyListenerDialogDismissed()); 89 mDialog.addContentView(mFullContainer, 90 new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); 91 Window dialogWindow = mDialog.getWindow(); 92 dialogWindow.setGravity(Gravity.CENTER); 93 dialogWindow.setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); 94 dialogWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); 95 setVisibleStatusBarIconColor(dialogWindow); 96 97 mAnimatorTranslation = 98 activity.getResources().getDimensionPixelSize(R.dimen.payments_ui_translation); 99 } 100 101 /** 102 * Makes sure that the color of the icons in the status bar makes the icons visible. 103 * @param window The window whose status bar icon color is being set. 104 */ setVisibleStatusBarIconColor(Window window)105 /* package */ static void setVisibleStatusBarIconColor(Window window) { 106 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return; 107 ApiCompatibilityUtils.setStatusBarIconColor(window.getDecorView().getRootView(), 108 !ColorUtils.shouldUseLightForegroundOnBackground(window.getStatusBarColor())); 109 } 110 111 /** @param bottomSheetView The view to show in the bottom sheet. */ addBottomSheetView(View bottomSheetView)112 /* package */ void addBottomSheetView(View bottomSheetView) { 113 FrameLayout.LayoutParams bottomSheetParams = 114 new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); 115 bottomSheetParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM; 116 mFullContainer.addView(bottomSheetView, bottomSheetParams); 117 bottomSheetView.addOnLayoutChangeListener(new FadeInAnimator()); 118 } 119 120 /** 121 * Show the dialog. 122 * @return Whether the show is successful. 123 */ show()124 /* package */ boolean show() { 125 try { 126 mDialog.show(); 127 return true; 128 } catch (WindowManager.BadTokenException badToken) { 129 // The exception could be thrown according to https://crbug.com/1139441. 130 return false; 131 } 132 } 133 134 /** Hide the dialog without dismissing it. */ hide()135 /* package */ void hide() { 136 mDialog.hide(); 137 } 138 139 /** 140 * Dismiss the dialog. 141 * 142 * @param isAnimated If true, the dialog dismissal is animated. 143 */ dismiss(boolean isAnimated)144 /* package */ void dismiss(boolean isAnimated) { 145 if (isAnimated) { 146 new DisappearingAnimator(true); 147 } else { 148 mDialog.dismiss(); 149 notifyListenerDialogDismissed(); 150 } 151 } 152 notifyListenerDialogDismissed()153 private void notifyListenerDialogDismissed() { 154 if (mDismissListener == null) return; 155 mDismissListener.onDismiss(); 156 mDismissListener = null; 157 } 158 159 /** @param overlay The overlay to show. This can be an error dialog, for example. */ showOverlay(View overlay)160 /* package */ void showOverlay(View overlay) { 161 // Animate the bottom sheet going away. 162 new DisappearingAnimator(false); 163 164 int floatingDialogWidth = DimmingDialog.computeMaxWidth(mFullContainer.getContext(), 165 mFullContainer.getMeasuredWidth(), mFullContainer.getMeasuredHeight()); 166 FrameLayout.LayoutParams overlayParams = 167 new FrameLayout.LayoutParams(floatingDialogWidth, LayoutParams.WRAP_CONTENT); 168 overlayParams.gravity = Gravity.CENTER; 169 mFullContainer.addView(overlay, overlayParams); 170 } 171 172 /** @return Whether the dialog is currently animating disappearance. */ isAnimatingDisappearance()173 /* package */ boolean isAnimatingDisappearance() { 174 return mIsAnimatingDisappearance; 175 } 176 177 /** 178 * Computes the maximum possible width for a dialog box. 179 * 180 * Follows https://www.google.com/design/spec/components/dialogs.html#dialogs-simple-dialogs 181 * 182 * @param context Context to pull resources from. 183 * @param availableWidth Available width for the dialog. 184 * @param availableHeight Available height for the dialog. 185 * @return Maximum possible width for the dialog box. 186 * 187 * TODO(dfalcantara): Revisit this function when the new assets come in. 188 * TODO(dfalcantara): The dialog should listen for configuration changes and resize accordingly. 189 */ computeMaxWidth(Context context, int availableWidth, int availableHeight)190 private static int computeMaxWidth(Context context, int availableWidth, int availableHeight) { 191 int baseUnit = context.getResources().getDimensionPixelSize(R.dimen.dialog_width_unit); 192 int maxSize = Math.min(availableWidth, availableHeight); 193 int multiplier = maxSize / baseUnit; 194 return multiplier * baseUnit; 195 } 196 197 /** 198 * Animates the whole dialog fading in and darkening everything else on screen. 199 * This particular animation is not tracked because it is not meant to be cancellable. 200 */ 201 private class FadeInAnimator extends AnimatorListenerAdapter implements OnLayoutChangeListener { 202 @Override onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)203 public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, 204 int oldTop, int oldRight, int oldBottom) { 205 mFullContainer.getChildAt(0).removeOnLayoutChangeListener(this); 206 207 Animator scrimFader = ObjectAnimator.ofInt(mFullContainer.getBackground(), 208 AnimatorProperties.DRAWABLE_ALPHA_PROPERTY, 0, 255); 209 Animator alphaAnimator = ObjectAnimator.ofFloat(mFullContainer, View.ALPHA, 0f, 1f); 210 211 AnimatorSet alphaSet = new AnimatorSet(); 212 alphaSet.playTogether(scrimFader, alphaAnimator); 213 alphaSet.setDuration(DIALOG_ENTER_ANIMATION_MS); 214 alphaSet.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN_INTERPOLATOR); 215 alphaSet.start(); 216 } 217 } 218 219 /** Animates the bottom sheet (and optionally, the scrim) disappearing off screen. */ 220 private class DisappearingAnimator extends AnimatorListenerAdapter { 221 private final boolean mIsDialogClosing; 222 DisappearingAnimator(boolean removeDialog)223 public DisappearingAnimator(boolean removeDialog) { 224 mIsDialogClosing = removeDialog; 225 226 Collection<Animator> animators = new ArrayList<>(); 227 228 View child = mFullContainer.getChildAt(0); 229 if (child != null) { 230 // Sheet fader. 231 animators.add(ObjectAnimator.ofFloat(child, View.ALPHA, child.getAlpha(), 0f)); 232 // Sheet translator. 233 animators.add(ObjectAnimator.ofFloat( 234 child, View.TRANSLATION_Y, 0f, mAnimatorTranslation)); 235 } 236 237 if (mIsDialogClosing) { 238 // Scrim fader. 239 animators.add(ObjectAnimator.ofInt(mFullContainer.getBackground(), 240 AnimatorProperties.DRAWABLE_ALPHA_PROPERTY, 127, 0)); 241 } 242 243 if (animators.isEmpty()) return; 244 245 mIsAnimatingDisappearance = true; 246 247 AnimatorSet current = new AnimatorSet(); 248 current.setDuration(DIALOG_EXIT_ANIMATION_MS); 249 current.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN_INTERPOLATOR); 250 current.playTogether(animators); 251 current.addListener(this); 252 current.start(); 253 } 254 255 @Override onAnimationEnd(Animator animation)256 public void onAnimationEnd(Animator animation) { 257 mIsAnimatingDisappearance = false; 258 mFullContainer.removeView(mFullContainer.getChildAt(0)); 259 if (mIsDialogClosing) { 260 if (mDialog.isShowing()) mDialog.dismiss(); 261 notifyListenerDialogDismissed(); 262 } 263 } 264 } 265 266 @VisibleForTesting getDialogForTest()267 public Dialog getDialogForTest() { 268 return mDialog; 269 } 270 271 /** 272 * Force the Dialog window to refresh its visual state. 273 */ refresh()274 /* package */ void refresh() { 275 mDialog.getWindow().setAttributes(mDialog.getWindow().getAttributes()); 276 } 277 } 278