| // 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.autofill_assistant; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.ArgbEvaluator; |
| import android.animation.ValueAnimator; |
| import android.annotation.SuppressLint; |
| import android.graphics.Bitmap; |
| import android.graphics.Color; |
| import android.graphics.RectF; |
| import android.graphics.Typeface; |
| import android.graphics.drawable.Drawable; |
| import android.graphics.drawable.GradientDrawable; |
| import android.media.ThumbnailUtils; |
| import android.support.annotation.ColorInt; |
| import android.support.annotation.IntDef; |
| import android.support.annotation.Nullable; |
| import android.support.design.widget.BottomSheetBehavior; |
| import android.support.v4.graphics.drawable.RoundedBitmapDrawable; |
| import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory; |
| import android.support.v4.text.TextUtilsCompat; |
| import android.support.v4.view.ViewCompat; |
| import android.support.v7.content.res.AppCompatResources; |
| import android.text.TextUtils; |
| import android.util.TypedValue; |
| import android.view.Gravity; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.HorizontalScrollView; |
| import android.widget.ImageView; |
| import android.widget.LinearLayout; |
| import android.widget.TextView; |
| |
| import org.chromium.base.ApiCompatibilityUtils; |
| import org.chromium.base.Callback; |
| import org.chromium.chrome.autofill_assistant.R; |
| import org.chromium.chrome.browser.ChromeActivity; |
| import org.chromium.chrome.browser.autofill.PersonalDataManager.AutofillProfile; |
| import org.chromium.chrome.browser.autofill.PersonalDataManager.CreditCard; |
| import org.chromium.chrome.browser.autofill_assistant.ui.BottomBarAnimations; |
| import org.chromium.chrome.browser.autofill_assistant.ui.TouchEventFilter; |
| import org.chromium.chrome.browser.cached_image_fetcher.CachedImageFetcher; |
| import org.chromium.chrome.browser.compositor.layouts.ChromeAnimation; |
| import org.chromium.chrome.browser.customtabs.CustomTabActivity; |
| import org.chromium.chrome.browser.help.HelpAndFeedback; |
| import org.chromium.chrome.browser.profiles.Profile; |
| import org.chromium.chrome.browser.snackbar.Snackbar; |
| import org.chromium.chrome.browser.snackbar.SnackbarManager; |
| import org.chromium.content_public.browser.WebContents; |
| import org.chromium.payments.mojom.PaymentOptions; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.text.DateFormat; |
| import java.text.SimpleDateFormat; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.Locale; |
| |
| /** Delegate to interact with the assistant UI. */ |
| class AutofillAssistantUiDelegate { |
| private static final String FEEDBACK_CATEGORY_TAG = |
| "com.android.chrome.USER_INITIATED_FEEDBACK_REPORT_AUTOFILL_ASSISTANT"; |
| private static final int PROGRESS_BAR_INITIAL_PROGRESS = 10; |
| private static final int DETAILS_PULSING_DURATION_MS = 1_000; |
| |
| /** How long the snackbars created by {@link #showAutofillAssistantStoppedSnackbar} stay up. */ |
| static final int SNACKBAR_DELAY_MS = 5_000; |
| |
| @SuppressLint("ConstantLocale") |
| private static final SimpleDateFormat sDetailsTimeFormat; |
| static { |
| DateFormat df = DateFormat.getTimeInstance(DateFormat.SHORT, Locale.getDefault()); |
| String timeFormatPattern = |
| (df instanceof SimpleDateFormat) ? ((SimpleDateFormat) df).toPattern() : "H:mm"; |
| sDetailsTimeFormat = new SimpleDateFormat(timeFormatPattern, Locale.getDefault()); |
| } |
| |
| @SuppressLint("ConstantLocale") |
| private static final SimpleDateFormat sDetailsDateFormat = |
| new SimpleDateFormat("EEE, MMM d", Locale.getDefault()); |
| |
| private final ChromeActivity mActivity; |
| private final Client mClient; |
| private final ViewGroup mCoordinatorView; |
| private final View mFullContainer; |
| private final TouchEventFilter mTouchEventFilter; |
| private final LinearLayout mBottomBar; |
| private final BottomSheetBehavior mBottomBarBehavior; |
| private final HorizontalScrollView mCarouselScroll; |
| private final ViewGroup mChipsViewContainer; |
| private final TextView mStatusMessageView; |
| private final AnimatedProgressBar mProgressBar; |
| |
| private final ViewGroup mDetailsViewContainer; |
| private final ImageView mDetailsImage; |
| private final TextView mDetailsTitle; |
| private final TextView mDetailsText; |
| private final int mDetailsImageWidth; |
| private final int mDetailsImageHeight; |
| |
| private final BottomBarAnimations mBottomBarAnimations; |
| private final boolean mIsRightToLeftLayout; |
| private ValueAnimator mDetailsPulseAnimation; |
| |
| private AutofillAssistantPaymentRequest mPaymentRequest; |
| |
| /** |
| * This is a client interface that relays interactions from the UI. |
| */ |
| public interface Client extends TouchEventFilter.Client { |
| /** |
| * Called when the bottom bar is dismissing. |
| */ |
| void onDismiss(); |
| |
| /** |
| * Called when a script has been selected. |
| * |
| * @param scriptPath The path for the selected script. |
| */ |
| void onScriptSelected(String scriptPath); |
| |
| /** Called when a choice has been selected, with the result of {@link Choice#getData()}. */ |
| void onChoice(byte[] serverPayload); |
| |
| /** |
| * Called when an address has been selected. |
| * |
| * @param guid The GUID of the selected address. |
| */ |
| void onAddressSelected(String guid); |
| |
| /** |
| * Called when a credit card has been selected. |
| * |
| * @param guid The GUID of the selected card. |
| */ |
| void onCardSelected(String guid); |
| |
| /** |
| * Called when button for acknowledging Details differ was clicked. |
| * |
| * @param displayedDetails Details that were shown. |
| * @param canContinue Whether the permission to continue was granted. |
| */ |
| void onDetailsAcknowledged(Details displayedDetails, boolean canContinue); |
| |
| /** |
| * Returns current details. |
| */ |
| Details getDetails(); |
| |
| /** |
| * Returns current status message. |
| */ |
| String getStatusMessage(); |
| |
| /** |
| * Used to access information relevant for debugging purposes. |
| * |
| * @return A string describing the current execution context. |
| */ |
| String getDebugContext(); |
| } |
| |
| /** Describes a chip to display. */ |
| static class Chip { |
| private final String mName; |
| private final boolean mHighlight; |
| |
| /** Returns the localized name to display. */ |
| String getName() { |
| return mName; |
| } |
| |
| /** Returns {@code true} if the choice should be highlight. */ |
| boolean isHighlight() { |
| return mHighlight; |
| } |
| |
| Chip(String name, boolean highlight) { |
| mName = name; |
| mHighlight = highlight; |
| } |
| } |
| |
| /** Functional interface that acts on a chip. */ |
| interface ChipAction<T extends Chip> { |
| void apply(T chip); |
| } |
| |
| /** Java side equivalent of autofill_assistant::ScriptHandle. */ |
| static class ScriptHandle extends Chip { |
| private final String mPath; |
| |
| /** Constructor. */ |
| ScriptHandle(String name, boolean highlight, String path) { |
| super(name, highlight); |
| mPath = path; |
| } |
| |
| /** Returns the script path. */ |
| String getPath() { |
| return mPath; |
| } |
| } |
| |
| /** Java side equivalent of autofill_assistant::UiController::Choice. */ |
| static class Choice extends Chip { |
| private final byte[] mServerPayload; |
| |
| /** Constructor. */ |
| Choice(String name, boolean highlight, byte[] serverPayload) { |
| super(name, highlight); |
| mServerPayload = serverPayload; |
| } |
| |
| /** Returns the serverPayload that corresponds to that choice. */ |
| byte[] getServerPayload() { |
| return mServerPayload; |
| } |
| } |
| |
| // Names borrowed from : |
| // - https://guidelines.googleplex.com/googlematerial/components/chips.html |
| // - https://guidelines.googleplex.com/googlematerial/components/buttons.html |
| @IntDef({ChipStyle.CHIP_ASSISTIVE, ChipStyle.BUTTON_FILLED, ChipStyle.BUTTON_HAIRLINE}) |
| @Retention(RetentionPolicy.SOURCE) |
| private @interface ChipStyle { |
| int CHIP_ASSISTIVE = 0; |
| int BUTTON_FILLED = 1; |
| int BUTTON_HAIRLINE = 2; |
| } |
| |
| /** |
| * Constructs an assistant UI delegate. |
| * |
| * @param activity The ChromeActivity |
| * @param client The client to forward events to |
| */ |
| public AutofillAssistantUiDelegate(ChromeActivity activity, Client client) { |
| mActivity = activity; |
| mClient = client; |
| |
| mCoordinatorView = (ViewGroup) mActivity.findViewById(R.id.coordinator); |
| mFullContainer = LayoutInflater.from(mActivity) |
| .inflate(R.layout.autofill_assistant_sheet, mCoordinatorView) |
| .findViewById(R.id.autofill_assistant); |
| // TODO(crbug.com/806868): Set hint text on overlay. |
| mTouchEventFilter = (TouchEventFilter) mFullContainer.findViewById(R.id.touch_event_filter); |
| mBottomBar = mFullContainer.findViewById(R.id.bottombar); |
| mBottomBarBehavior = BottomSheetBehavior.from(mBottomBar); |
| mBottomBar.findViewById(R.id.close_button) |
| .setOnClickListener(unusedView -> mClient.onDismiss()); |
| mBottomBar.findViewById(R.id.feedback_button) |
| .setOnClickListener(unusedView |
| -> HelpAndFeedback.getInstance(mActivity).showFeedback(mActivity, |
| Profile.getLastUsedProfile(), mActivity.getActivityTab().getUrl(), |
| FEEDBACK_CATEGORY_TAG, |
| FeedbackContext.buildContextString(mActivity, mClient, |
| client.getDetails(), client.getStatusMessage(), 4))); |
| mCarouselScroll = mBottomBar.findViewById(R.id.carousel_scroll); |
| mChipsViewContainer = mCarouselScroll.findViewById(R.id.carousel); |
| mStatusMessageView = mBottomBar.findViewById(R.id.status_message); |
| mProgressBar = new AnimatedProgressBar(mBottomBar.findViewById(R.id.progress_bar), |
| ApiCompatibilityUtils.getColor(mActivity.getResources(), R.color.modern_blue_600), |
| ApiCompatibilityUtils.getColor( |
| mActivity.getResources(), R.color.modern_blue_600_alpha_38_opaque)); |
| |
| mDetailsViewContainer = (ViewGroup) mBottomBar.findViewById(R.id.details); |
| mDetailsImage = mDetailsViewContainer.findViewById(R.id.details_image); |
| mDetailsTitle = (TextView) mDetailsViewContainer.findViewById(R.id.details_title); |
| mDetailsText = (TextView) mDetailsViewContainer.findViewById(R.id.details_text); |
| mDetailsImageWidth = mActivity.getResources().getDimensionPixelSize( |
| R.dimen.autofill_assistant_details_image_size); |
| mDetailsImageHeight = mActivity.getResources().getDimensionPixelSize( |
| R.dimen.autofill_assistant_details_image_size); |
| |
| mBottomBarAnimations = new BottomBarAnimations(mBottomBar, mDetailsViewContainer, |
| mChipsViewContainer, mActivity.getResources().getDisplayMetrics()); |
| mIsRightToLeftLayout = TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) |
| == ViewCompat.LAYOUT_DIRECTION_RTL; |
| |
| // Finch experiment to adjust overlay color |
| Integer overlayColor = AutofillAssistantStudy.getOverlayColor(); |
| if (overlayColor != null) { |
| mTouchEventFilter.setGrayOutColor(overlayColor); |
| } |
| |
| // TODO(crbug.com/806868): Listen for contextual search shown so as to hide this UI. |
| } |
| |
| /** |
| * Shows a message in the status bar. |
| * |
| * @param message Message to display. |
| */ |
| public void showStatusMessage(@Nullable String message) { |
| show(); |
| mStatusMessageView.setText(message); |
| mStatusMessageView.announceForAccessibility(message); |
| } |
| |
| /** |
| * Updates the list of scripts in the bar. |
| * |
| * @param scriptHandles List of scripts to show. |
| */ |
| public void updateScripts(List<ScriptHandle> scriptHandles) { |
| if (scriptHandles.isEmpty()) { |
| clearCarousel(); |
| return; |
| } |
| |
| addChips(scriptHandles, scriptHandle -> { |
| clearCarousel(); |
| mClient.onScriptSelected(scriptHandle.getPath()); |
| }); |
| } |
| |
| private <T extends Chip> void addChips(Iterable<T> chips, ChipAction<T> onClick) { |
| boolean alignRight = hasHighlightedScript(chips); |
| @ChipStyle |
| int nonHighlightStyle = alignRight ? ChipStyle.BUTTON_HAIRLINE : ChipStyle.CHIP_ASSISTIVE; |
| List<View> childViews = new ArrayList<>(); |
| for (T chip : chips) { |
| @ChipStyle |
| int chipStyle = chip.isHighlight() ? ChipStyle.BUTTON_FILLED : nonHighlightStyle; |
| TextView chipView = createChipView(chip.getName(), chipStyle); |
| chipView.setOnClickListener((unusedView) -> onClick.apply(chip)); |
| childViews.add(chipView); |
| } |
| setCarouselChildViews(childViews, alignRight); |
| } |
| |
| private boolean hasHighlightedScript(Iterable<? extends Chip> chips) { |
| for (Chip chip : chips) { |
| if (chip.isHighlight()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| public void clearCarousel() { |
| setCarouselChildViews(Collections.emptyList(), /* alignRight= */ false); |
| } |
| |
| private void setCarouselChildViews(List<View> children, boolean alignRight) { |
| // TODO(crbug.com/806868): Pull the carousel logic into its own MVC component. |
| |
| // Reverse alignRight if we are in a RTL layout. |
| alignRight = mIsRightToLeftLayout ? !alignRight : alignRight; |
| |
| // Replace children. |
| // TODO(crbug.com/806868): We might want to animate children change using fade in/out |
| // animations. |
| mChipsViewContainer.removeAllViews(); |
| setCarouselAlignment(alignRight); |
| for (int i = 0; i < children.size(); i++) { |
| // Add children in reverse order if the chips are right aligned. |
| int j = alignRight ? children.size() - i - 1 : i; |
| View child = children.get(j); |
| if (i > 0) { |
| LinearLayout.LayoutParams layoutParams = |
| (LinearLayout.LayoutParams) child.getLayoutParams(); |
| int leftMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, |
| child.getContext().getResources().getDisplayMetrics()); |
| layoutParams.setMargins(leftMargin, 0, 0, 0); |
| child.setLayoutParams(layoutParams); |
| } |
| mChipsViewContainer.addView(child); |
| } |
| |
| if (children.isEmpty()) { |
| mBottomBarAnimations.hideCarousel(); |
| } else { |
| // Make sure the Autofill Assistant is visible. |
| show(); |
| mBottomBarAnimations.showCarousel(); |
| } |
| } |
| |
| private void setCarouselAlignment(boolean alignRight) { |
| // Set carousel scroll gravity. |
| ViewGroup.LayoutParams currentLayoutParams = mCarouselScroll.getLayoutParams(); |
| LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(currentLayoutParams); |
| layoutParams.gravity = alignRight ? Gravity.END : Gravity.START; |
| mCarouselScroll.setLayoutParams(layoutParams); |
| |
| // Reset the scroll position. |
| mCarouselScroll.post( |
| () -> mCarouselScroll.fullScroll(alignRight ? View.FOCUS_RIGHT : View.FOCUS_LEFT)); |
| } |
| |
| private TextView createChipView(String text, @ChipStyle int style) { |
| int resId = -1; |
| switch (style) { |
| case ChipStyle.CHIP_ASSISTIVE: |
| resId = R.layout.autofill_assistant_chip_assistive; |
| break; |
| case ChipStyle.BUTTON_FILLED: |
| resId = R.layout.autofill_assistant_button_filled; |
| break; |
| case ChipStyle.BUTTON_HAIRLINE: |
| resId = R.layout.autofill_assistant_button_hairline; |
| break; |
| } |
| |
| TextView chipView = (TextView) (LayoutInflater.from(mActivity).inflate( |
| resId, mChipsViewContainer, false)); |
| chipView.setText(text); |
| return chipView; |
| } |
| |
| public void show() { |
| if (mFullContainer.getVisibility() != View.VISIBLE) { |
| mTouchEventFilter.init(mClient, mActivity.getFullscreenManager(), |
| mActivity.getActivityTab().getWebContents()); |
| mFullContainer.setVisibility(View.VISIBLE); |
| mBottomBarBehavior.setState(BottomSheetBehavior.STATE_EXPANDED); |
| |
| // Announce Autofill Assistant is available for accessibility. |
| mBottomBar.announceForAccessibility( |
| mActivity.getString(R.string.autofill_assistant_available_accessibility)); |
| |
| // Set the initial progress. It is OK to make multiple calls to this method as it will |
| // have an effect only on the first one. |
| mProgressBar.maybeIncreaseProgress(PROGRESS_BAR_INITIAL_PROGRESS); |
| } |
| } |
| |
| public void hide() { |
| mTouchEventFilter.deInit(); |
| mFullContainer.setVisibility(View.GONE); |
| } |
| |
| public void showAutofillAssistantStoppedSnackbar(SnackbarManager.SnackbarController controller, |
| int stringResourceId, Object... formatArgs) { |
| Snackbar snackBar = |
| Snackbar.make(mActivity.getString(stringResourceId, formatArgs), controller, |
| Snackbar.TYPE_ACTION, Snackbar.UMA_AUTOFILL_ASSISTANT_STOP_UNDO) |
| .setAction(mActivity.getString(R.string.undo), /* actionData= */ null); |
| snackBar.setSingleLine(false); |
| snackBar.setDuration(SNACKBAR_DELAY_MS); |
| mActivity.getSnackbarManager().showSnackbar(snackBar); |
| } |
| |
| public void dismissSnackbar(SnackbarManager.SnackbarController controller) { |
| mActivity.getSnackbarManager().dismissSnackbars(controller); |
| } |
| |
| /** |
| * Enters a simplified pre-shutdown state, with only a goodbye message - the current status |
| * message - and a close button. |
| */ |
| public void enterGracefulShutdownMode() { |
| // TODO(crbug.com/806868): Introduce a proper, separate shutdown dialog, with enough space |
| // to display longer messages. Setting the max lines and hiding the feedback button are |
| // hacks to give enough space to display long messages. |
| mStatusMessageView.setMaxLines(4); |
| mBottomBar.findViewById(R.id.feedback_button).setVisibility(View.GONE); |
| hideOverlay(); |
| mTouchEventFilter.setVisibility(View.GONE); |
| hideProgressBar(); |
| hideDetails(); |
| mBottomBarAnimations.hideCarousel(); |
| } |
| |
| /** |
| * Closes the activity. |
| * |
| * Note: The close() logic here assumes that |mActivity| is a CustomTabActivity since in the |
| * more general case we probably just want to close the active tab instead of the entire Chrome |
| * activity. |
| */ |
| public void close() { |
| assert mActivity instanceof CustomTabActivity; |
| mActivity.finish(); |
| } |
| |
| /** Called to show overlay. */ |
| public void showOverlay() { |
| mTouchEventFilter.setFullOverlay(true); |
| } |
| |
| /** Called to hide overlay. */ |
| public void hideOverlay() { |
| mTouchEventFilter.setFullOverlay(false); |
| } |
| |
| public void hideDetails() { |
| mBottomBarAnimations.hideDetails(); |
| } |
| |
| /** Called to show a message in the status bar that autofill assistant is done. */ |
| public void showGiveUpMessage() { |
| showStatusMessage(mActivity.getString(R.string.autofill_assistant_give_up)); |
| } |
| |
| /** |
| * Shows the details. |
| * |
| * <p>If some fields changed compared to the old details, the diff mode is entered. |
| * */ |
| public void showDetails(Details details) { |
| Drawable defaultImage = AppCompatResources.getDrawable( |
| mActivity, R.drawable.autofill_assistant_default_details); |
| |
| updateDetailsAnimation(details, (GradientDrawable) defaultImage); |
| |
| mDetailsTitle.setText(details.getTitle()); |
| mDetailsText.setText(makeDetailsText(details)); |
| |
| String url = details.getUrl(); |
| if (!url.isEmpty()) { |
| // The URL is safe because it comes from the knowledge graph and is hosted on Google |
| // servers. |
| CachedImageFetcher.getInstance().fetchImage(url, image -> { |
| if (image != null) { |
| mDetailsImage.setImageDrawable(getRoundedImage(image)); |
| mDetailsImage.setVisibility(View.VISIBLE); |
| } else { |
| mDetailsImage.setVisibility(View.GONE); |
| } |
| }); |
| } else { |
| mDetailsImage.setVisibility(View.GONE); |
| if (!details.isFinal()) { |
| mDetailsImage.setImageDrawable(defaultImage); |
| mDetailsImage.setVisibility(View.VISIBLE); |
| } |
| } |
| |
| // Make sure the Autofill Assistant is visible. |
| show(); |
| mBottomBarAnimations.showDetails(); |
| |
| boolean shouldShowDiffMode = details.getFieldsChanged().size() > 0; |
| if (shouldShowDiffMode) { |
| enableDiffModeForDetails(details); |
| } else { |
| mClient.onDetailsAcknowledged(details, /* canContinue= */ true); |
| } |
| } |
| |
| /** |
| * Shows additional UI elements asking to confirm details change. |
| * |
| * TODO(crbug.com/806868): Create own Controller for managing details state. |
| */ |
| private void enableDiffModeForDetails(Details details) { |
| enableProgressBarPulsing(); |
| // For detailsText we compare only Date. |
| if (details.getFieldsChanged().contains(Details.DetailsField.DATE)) { |
| mDetailsText.setTypeface(mDetailsText.getTypeface(), Typeface.BOLD_ITALIC); |
| } else { |
| mDetailsText.setTextColor(ApiCompatibilityUtils.getColor( |
| mActivity.getResources(), R.color.modern_grey_300)); |
| } |
| |
| if (!details.getFieldsChanged().contains(Details.DetailsField.TITLE)) { |
| mDetailsTitle.setTextColor(ApiCompatibilityUtils.getColor( |
| mActivity.getResources(), R.color.modern_grey_300)); |
| } |
| |
| // Show new UI parts. |
| String oldMessage = mClient.getStatusMessage(); |
| showStatusMessage(mActivity.getString(R.string.autofill_assistant_details_differ)); |
| |
| ArrayList<View> childViews = new ArrayList<>(); |
| TextView continueChip = createChipView( |
| mActivity.getString(R.string.continue_button), ChipStyle.BUTTON_FILLED); |
| continueChip.setOnClickListener(unusedView -> { |
| // Reset UI changes. |
| ApiCompatibilityUtils.setTextAppearance(mDetailsTitle, R.style.BlackCaptionDefault); |
| mDetailsTitle.setTypeface(mDetailsTitle.getTypeface(), Typeface.BOLD); |
| ApiCompatibilityUtils.setTextAppearance(mDetailsText, R.style.BlackCaption); |
| clearCarousel(); |
| showStatusMessage(oldMessage); |
| disableProgressBarPulsing(); |
| |
| mClient.onDetailsAcknowledged(details, /* canContinue= */ true); |
| }); |
| childViews.add(continueChip); |
| TextView cancelChip = createChipView( |
| mActivity.getString(R.string.autofill_assistant_details_differ_go_back), |
| ChipStyle.BUTTON_HAIRLINE); |
| cancelChip.setOnClickListener( |
| unusedView -> mClient.onDetailsAcknowledged(details, /* canContinue= */ false)); |
| childViews.add(cancelChip); |
| setCarouselChildViews(childViews, /* alignRight= */ true); |
| } |
| |
| private void updateDetailsAnimation(Details details, GradientDrawable defaultImage) { |
| if (details.isFinal()) { |
| if (mDetailsPulseAnimation != null) { |
| mDetailsPulseAnimation.cancel(); |
| } |
| return; |
| } else { |
| @ColorInt |
| int startColor = ApiCompatibilityUtils.getColor( |
| mActivity.getResources(), R.color.modern_grey_100); |
| @ColorInt |
| int endColor = ApiCompatibilityUtils.getColor( |
| mActivity.getResources(), R.color.modern_grey_50); |
| |
| mDetailsPulseAnimation = ValueAnimator.ofInt(startColor, endColor); |
| mDetailsPulseAnimation.setDuration(DETAILS_PULSING_DURATION_MS); |
| mDetailsPulseAnimation.setEvaluator(new ArgbEvaluator()); |
| mDetailsPulseAnimation.setRepeatCount(ValueAnimator.INFINITE); |
| mDetailsPulseAnimation.setRepeatMode(ValueAnimator.REVERSE); |
| mDetailsPulseAnimation.setInterpolator(ChromeAnimation.getAccelerateInterpolator()); |
| mDetailsPulseAnimation.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationCancel(Animator animation) { |
| mDetailsTitle.setBackgroundColor(Color.WHITE); |
| mDetailsText.setBackgroundColor(Color.WHITE); |
| } |
| }); |
| mDetailsPulseAnimation.addUpdateListener(animation -> { |
| if (details.getTitle().isEmpty()) { |
| mDetailsTitle.setBackgroundColor((int) animation.getAnimatedValue()); |
| } |
| if (makeDetailsText(details).isEmpty()) { |
| mDetailsText.setBackgroundColor((int) animation.getAnimatedValue()); |
| } |
| defaultImage.setColor((int) animation.getAnimatedValue()); |
| }); |
| mDetailsPulseAnimation.start(); |
| } |
| } |
| |
| /** Creates text for display based on date and description.*/ |
| private String makeDetailsText(Details details) { |
| List<String> parts = new ArrayList<>(); |
| |
| Date date = details.getDate(); |
| if (date != null) { |
| parts.add(sDetailsTimeFormat.format(date).toLowerCase(Locale.getDefault())); |
| parts.add(sDetailsDateFormat.format(date)); |
| } |
| |
| if (details.getDescription() != null && !details.getDescription().isEmpty()) { |
| parts.add(details.getDescription()); |
| } |
| |
| // TODO(crbug.com/806868): Use a view instead of this dot text. |
| return TextUtils.join(" • ", parts); |
| } |
| |
| private Drawable getRoundedImage(Bitmap bitmap) { |
| RoundedBitmapDrawable roundedBitmap = RoundedBitmapDrawableFactory.create( |
| mActivity.getResources(), |
| ThumbnailUtils.extractThumbnail(bitmap, mDetailsImageWidth, mDetailsImageHeight)); |
| roundedBitmap.setCornerRadius(TypedValue.applyDimension( |
| TypedValue.COMPLEX_UNIT_DIP, 4, mActivity.getResources().getDisplayMetrics())); |
| return roundedBitmap; |
| } |
| |
| public void showProgressBar(int progress, String message) { |
| show(); |
| mProgressBar.show(); |
| mProgressBar.maybeIncreaseProgress(progress); |
| |
| if (!message.isEmpty()) { |
| mStatusMessageView.setText(message); |
| } |
| } |
| |
| public void hideProgressBar() { |
| mProgressBar.hide(); |
| } |
| |
| public void enableProgressBarPulsing() { |
| mProgressBar.enablePulsing(); |
| } |
| |
| public void disableProgressBarPulsing() { |
| mProgressBar.disablePulsing(); |
| } |
| |
| public void updateTouchableArea(boolean enabled, List<RectF> boxes) { |
| mTouchEventFilter.setPartialOverlay(enabled, boxes); |
| } |
| |
| /** Shows chip with the given choices. */ |
| public void showChoices(List<Choice> choices) { |
| addChips(choices, choice -> { |
| clearCarousel(); |
| mClient.onChoice(choice.getServerPayload()); |
| }); |
| } |
| |
| /** |
| * Show profiles in the bar. |
| * |
| * @param profiles List of profiles to show. |
| */ |
| public void showProfiles(ArrayList<AutofillProfile> profiles) { |
| if (profiles.isEmpty()) { |
| clearCarousel(); |
| mClient.onAddressSelected(""); |
| return; |
| } |
| |
| ArrayList<View> childViews = new ArrayList<>(); |
| for (int i = 0; i < profiles.size(); i++) { |
| AutofillProfile profile = profiles.get(i); |
| // TODO(crbug.com/806868): Show more information than the street. |
| TextView chipView = |
| createChipView(profile.getStreetAddress(), ChipStyle.CHIP_ASSISTIVE); |
| chipView.setOnClickListener((unusedView) -> { |
| clearCarousel(); |
| mClient.onAddressSelected(profile.getGUID()); |
| }); |
| childViews.add(chipView); |
| } |
| setCarouselChildViews(childViews, /* alignRight= */ false); |
| } |
| |
| /** |
| * Show credit cards in the bar. |
| * |
| * @param cards List of cards to show. |
| */ |
| public void showCards(ArrayList<CreditCard> cards) { |
| if (cards.isEmpty()) { |
| mClient.onCardSelected(""); |
| return; |
| } |
| |
| ArrayList<View> childViews = new ArrayList<>(); |
| for (int i = 0; i < cards.size(); i++) { |
| CreditCard card = cards.get(i); |
| // TODO(crbug.com/806868): Show more information than the card number. |
| TextView chipView = |
| createChipView(card.getObfuscatedNumber(), ChipStyle.CHIP_ASSISTIVE); |
| chipView.setOnClickListener((unusedView) -> { |
| clearCarousel(); |
| mClient.onCardSelected(card.getGUID()); |
| }); |
| childViews.add(chipView); |
| } |
| setCarouselChildViews(childViews, /* alignRight= */ false); |
| } |
| |
| /** |
| * Show the payment request UI. |
| * |
| * Show the UI and return the selected information via |callback| when done. |
| * |
| * @param webContents The webContents. |
| * @param paymentOptions Options to request payment information. |
| * @param unusedTitle Unused title. |
| * @param supportedBasicCardNetworks Optional array of supported basic card networks. |
| * @param defaultEmail Optional email. If present Profiles containing this email will be shown |
| * on top. |
| * @param callback Callback to return selected info. |
| */ |
| public void showPaymentRequest(WebContents webContents, PaymentOptions paymentOptions, |
| String unusedTitle, String[] supportedBasicCardNetworks, @Nullable String defaultEmail, |
| Callback<AutofillAssistantPaymentRequest.SelectedPaymentInformation> callback) { |
| assert mPaymentRequest == null; |
| mPaymentRequest = new AutofillAssistantPaymentRequest( |
| webContents, paymentOptions, unusedTitle, supportedBasicCardNetworks, defaultEmail); |
| // Make sure we wrap content in the container. |
| mBottomBarAnimations.setBottomBarHeightToWrapContent(); |
| // Note: We show and hide (below) the carousel so that the margins are adjusted correctly. |
| // This is an intermediate adjustment before the UI refactoring. |
| mBottomBarAnimations.showCarousel(); |
| mPaymentRequest.show(mCarouselScroll, callback); |
| enableProgressBarPulsing(); |
| } |
| |
| /** Close and destroy the payment request UI. */ |
| public void closePaymentRequest() { |
| mPaymentRequest.close(); |
| mPaymentRequest = null; |
| mBottomBarAnimations.setBottomBarHeightToFixed(); |
| mBottomBarAnimations.hideCarousel(); |
| disableProgressBarPulsing(); |
| } |
| } |