blob: d7d773b8ad45be16fa00041fb03087a7e3af9262 [file] [log] [blame]
// 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.details;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.media.ThumbnailUtils;
import android.os.Build;
import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
import android.text.TextUtils;
import android.util.TypedValue;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.chrome.autofill_assistant.R;
import org.chromium.chrome.browser.cached_image_fetcher.CachedImageFetcher;
import org.chromium.chrome.browser.compositor.animation.CompositorAnimator;
import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
/**
* This class is responsible for pushing updates to the Autofill Assistant details view. These
* updates are pulled from the {@link AssistantDetailsModel} when a notification of an update is
* received.
*/
class AssistantDetailsViewBinder
implements PropertyModelChangeProcessor.ViewBinder<AssistantDetailsModel,
AssistantDetailsViewBinder.ViewHolder, PropertyKey> {
private static final int IMAGE_BORDER_RADIUS = 4;
private static final int PULSING_DURATION_MS = 1_000;
private static final String DETAILS_TIME_FORMAT = "H:mma";
private static final String DETAILS_DATE_FORMAT = "EEE, MMM d";
/**
* A wrapper class that holds the different views of the header.
*/
static class ViewHolder {
final GradientDrawable mDefaultImage;
final ImageView mImageView;
final TextView mTitleView;
final TextView mSubtextView;
public ViewHolder(Context context, View detailsView) {
mDefaultImage = (GradientDrawable) context.getResources().getDrawable(
R.drawable.autofill_assistant_default_details);
mImageView = detailsView.findViewById(R.id.details_image);
mTitleView = detailsView.findViewById(R.id.details_title);
mSubtextView = detailsView.findViewById(R.id.details_text);
}
}
private final Context mContext;
private final int mImageWidth;
private final int mImageHeight;
private final int mPulseAnimationStartColor;
private final int mPulseAnimationEndColor;
private final Set<View> mViewsToAnimate = new HashSet<>();
private ValueAnimator mPulseAnimation;
AssistantDetailsViewBinder(Context context) {
mContext = context;
mImageWidth = context.getResources().getDimensionPixelSize(
R.dimen.autofill_assistant_details_image_size);
mImageHeight = context.getResources().getDimensionPixelSize(
R.dimen.autofill_assistant_details_image_size);
mPulseAnimationStartColor = context.getResources().getColor(R.color.modern_grey_100);
mPulseAnimationEndColor = context.getResources().getColor(R.color.modern_grey_50);
}
@Override
public void bind(AssistantDetailsModel model, ViewHolder view, PropertyKey propertyKey) {
if (AssistantDetailsModel.DETAILS == propertyKey) {
AssistantDetails details = model.get(AssistantDetailsModel.DETAILS);
if (details == null) {
// Handled by the AssistantDetailsCoordinator.
return;
}
setDetails(details, view);
} else {
assert false : "Unhandled property detected in AssistantDetailsViewBinder!";
}
}
private void setDetails(AssistantDetails details, ViewHolder viewHolder) {
String detailsText = makeDetailsText(details);
viewHolder.mTitleView.setText(details.getTitle());
viewHolder.mSubtextView.setText(detailsText);
if (viewHolder.mImageView.getDrawable() == null) {
// Set default image if no image was set before.
viewHolder.mImageView.setImageDrawable(viewHolder.mDefaultImage);
}
setTextStyles(details, viewHolder);
// Download image and then set it in the model.
if (!details.getUrl().isEmpty()) {
CachedImageFetcher.getInstance().fetchImage(details.getUrl(), image -> {
if (image != null) {
viewHolder.mImageView.setImageDrawable(getRoundedImage(image));
}
});
}
}
private void setTextStyles(AssistantDetails details, ViewHolder viewHolder) {
setTitleStyle(details, viewHolder);
setSubtextStyle(details, viewHolder);
}
private void setTitleStyle(AssistantDetails details, ViewHolder viewHolder) {
boolean animateBackground = false;
TextView titleView = viewHolder.mTitleView;
if (details.getUserApprovalRequired() && !details.getHighlightTitle()) {
// De-emphasize title if user needs to accept details but the title should not be
// highlighted.
titleView.setTextColor(ApiCompatibilityUtils.getColor(
mContext.getResources(), R.color.modern_grey_300));
} else {
// Normal style: bold black text.
ApiCompatibilityUtils.setTextAppearance(
titleView, R.style.TextAppearance_BlackCaptionDefault);
titleView.setTypeface(titleView.getTypeface(), Typeface.BOLD);
if (titleView.length() == 0 && details.getShowPlaceholdersForEmptyFields()) {
animateBackground = true;
}
}
if (animateBackground) {
addViewToAnimation(titleView, viewHolder);
} else {
removeViewFromAnimation(titleView);
}
}
private void setSubtextStyle(AssistantDetails details, ViewHolder viewHolder) {
boolean animateBackground = false;
TextView subtextView = viewHolder.mSubtextView;
if (details.getUserApprovalRequired()) {
if (details.getHighlightDate()) {
// Emphasized style.
subtextView.setTypeface(subtextView.getTypeface(), Typeface.BOLD_ITALIC);
} else {
// De-emphasized style.
subtextView.setTextColor(ApiCompatibilityUtils.getColor(
mContext.getResources(), R.color.modern_grey_300));
}
} else {
// Normal style.
ApiCompatibilityUtils.setTextAppearance(
subtextView, R.style.TextAppearance_BlackCaption);
if (subtextView.length() == 0 && details.getShowPlaceholdersForEmptyFields()) {
animateBackground = true;
}
}
if (animateBackground) {
addViewToAnimation(subtextView, viewHolder);
} else {
removeViewFromAnimation(subtextView);
}
}
private String makeDetailsText(AssistantDetails details) {
List<String> parts = new ArrayList<>();
Date date = details.getDate();
if (date != null) {
parts.add(formatDetailsTime(date));
parts.add(formatDetailsDate(date));
}
String description = details.getDescription();
if (description != null && !description.isEmpty()) {
parts.add(description);
}
// TODO(crbug.com/806868): Use a view instead of this dot text.
return TextUtils.join(" • ", parts);
}
private Locale getLocale() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
? mContext.getResources().getConfiguration().getLocales().get(0)
: mContext.getResources().getConfiguration().locale;
}
private String formatDetailsTime(Date date) {
DateFormat df = DateFormat.getTimeInstance(DateFormat.SHORT, Locale.getDefault());
String timeFormatPattern = (df instanceof SimpleDateFormat)
? ((SimpleDateFormat) df).toPattern()
: DETAILS_TIME_FORMAT;
return new SimpleDateFormat(timeFormatPattern, getLocale()).format(date);
}
private String formatDetailsDate(Date date) {
return new SimpleDateFormat(DETAILS_DATE_FORMAT, getLocale()).format(date);
}
private Drawable getRoundedImage(Bitmap bitmap) {
RoundedBitmapDrawable roundedBitmap =
RoundedBitmapDrawableFactory.create(mContext.getResources(),
ThumbnailUtils.extractThumbnail(bitmap, mImageWidth, mImageHeight));
roundedBitmap.setCornerRadius(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
IMAGE_BORDER_RADIUS, mContext.getResources().getDisplayMetrics()));
return roundedBitmap;
}
private void addViewToAnimation(View view, ViewHolder viewHolder) {
mViewsToAnimate.add(view);
if (mPulseAnimation == null) {
mPulseAnimation =
ValueAnimator.ofInt(mPulseAnimationStartColor, mPulseAnimationEndColor);
mPulseAnimation.setDuration(PULSING_DURATION_MS);
mPulseAnimation.setEvaluator(new ArgbEvaluator());
mPulseAnimation.setRepeatCount(ValueAnimator.INFINITE);
mPulseAnimation.setRepeatMode(ValueAnimator.REVERSE);
mPulseAnimation.setInterpolator(CompositorAnimator.ACCELERATE_INTERPOLATOR);
mPulseAnimation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationCancel(Animator animation) {
viewHolder.mTitleView.setBackgroundColor(Color.WHITE);
viewHolder.mSubtextView.setBackgroundColor(Color.WHITE);
viewHolder.mDefaultImage.setColor(mPulseAnimationStartColor);
}
});
mPulseAnimation.addUpdateListener(animation -> {
int animatedValue = (int) animation.getAnimatedValue();
for (View viewToAnimate : mViewsToAnimate) {
viewToAnimate.setBackgroundColor(animatedValue);
}
viewHolder.mDefaultImage.setColor(animatedValue);
});
mPulseAnimation.start();
}
}
private void removeViewFromAnimation(View view) {
mViewsToAnimate.remove(view);
if (mViewsToAnimate.isEmpty() && mPulseAnimation != null) {
mPulseAnimation.cancel();
mPulseAnimation = null;
}
}
}