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