// Copyright 2018 The Chromium Authors
// 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;
import android.annotation.SuppressLint;
import android.content.ComponentCallbacks;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.text.InputType;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.ClickableSpan;
import android.text.style.ImageSpan;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.PopupWindow;
import android.widget.TextView;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.content.ContextCompat;
import androidx.core.content.res.ResourcesCompat;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.Callback;
import org.chromium.base.ContextUtils;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.components.autofill.FieldType;
import org.chromium.components.autofill.payments.LegalMessageLine;
import org.chromium.ui.text.NoUnderlineClickableSpan;
import org.chromium.ui.text.SpanApplier;
import org.chromium.url.GURL;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Calendar;
import java.util.List;
import java.util.Optional;
/** Helper methods that can be used across multiple Autofill UIs. */
public class AutofillUiUtils {
public static final String CAPITAL_ONE_ICON_URL =
"https://www.gstatic.com/autofill/virtualcard/icon/capitalone.png";
/** Interface to provide the horizontal and vertical offset for the tooltip. */
public interface OffsetProvider {
/** Returns the X offset for the tooltip. */
int getXOffset(TextView textView);
/** Returns the Y offset for the tooltip. */
int getYOffset(TextView textView);
}
// 200ms is chosen small enough not to be detectable to human eye, but big
// enough for to avoid any race conditions on modern machines.
private static final int TOOLTIP_DEFERRED_PERIOD_MS = 200;
public static final int EXPIRATION_FIELDS_LENGTH = 2;
@IntDef({
ErrorType.EXPIRATION_MONTH,
ErrorType.EXPIRATION_YEAR,
ErrorType.EXPIRATION_DATE,
ErrorType.CVC,
ErrorType.CVC_AND_EXPIRATION,
ErrorType.NOT_ENOUGH_INFO,
ErrorType.NONE
})
@Retention(RetentionPolicy.SOURCE)
public @interface ErrorType {
int EXPIRATION_MONTH = 1;
int EXPIRATION_YEAR = 2;
int EXPIRATION_DATE = 3;
int CVC = 4;
int CVC_AND_EXPIRATION = 5;
int NOT_ENOUGH_INFO = 6;
int NONE = 7;
}
/** Different sizes in which we show the credit card / bank account art images. */
@IntDef({CardIconSize.SMALL, CardIconSize.LARGE, CardIconSize.SQUARE})
@Retention(RetentionPolicy.SOURCE)
public @interface CardIconSize {
int SMALL = 0;
int LARGE = 1;
int SQUARE = 2;
}
/** Contains dimensional specs for credit card icons. */
public static class CardIconSpecs {
private final Context mContext;
private final int mWidthId;
private final int mHeightId;
private final int mCornerRadiusId;
private final int mBorderWidthId;
/**
* @param context to get the resources.
* @param widthId Resource Id for the icon's width spec.
* @param heightId Resource Id for the icon's height spec.
* @param cornerRadiusId Resource Id for the icon's corner radius spec.
* @param borderWidthId Resource Id for the icon's border width spec.
*/
private CardIconSpecs(
Context context, int widthId, int heightId, int cornerRadiusId, int borderWidthId) {
mContext = context;
mWidthId = widthId;
mHeightId = heightId;
mCornerRadiusId = cornerRadiusId;
mBorderWidthId = borderWidthId;
}
/**
* Create the {@link CardIconSpecs} for the icon based on the size (small or large or
* square) of the icon to be rendered.
*
* @param context to get the resources.
* @param cardIconSize Enum that specifies the icon's size (small or large or square).
* @return {@link CardIconSpecs} instance containing the specs for the card icon.
*/
public static CardIconSpecs create(Context context, @CardIconSize int cardIconSize) {
if (cardIconSize == CardIconSize.LARGE
&& ChromeFeatureList.isEnabled(
ChromeFeatureList.AUTOFILL_ENABLE_NEW_CARD_ART_AND_NETWORK_IMAGES)) {
return new CardIconSpecs(
context,
R.dimen.large_card_icon_width,
R.dimen.large_card_icon_height,
R.dimen.large_card_icon_corner_radius,
R.dimen.card_icon_border_width);
}
if (cardIconSize == CardIconSize.SQUARE) {
return new CardIconSpecs(
context,
R.dimen.square_card_icon_side_length,
R.dimen.square_card_icon_side_length,
R.dimen.square_card_icon_corner_radius,
R.dimen.card_icon_border_width_zero);
}
return new CardIconSpecs(
context,
R.dimen.small_card_icon_width,
R.dimen.small_card_icon_height,
R.dimen.small_card_icon_corner_radius,
R.dimen.card_icon_border_width);
}
public @Px int getWidth() {
return mContext.getResources().getDimensionPixelSize(mWidthId);
}
public @Px int getHeight() {
return mContext.getResources().getDimensionPixelSize(mHeightId);
}
public @Px int getCornerRadius() {
return mContext.getResources().getDimensionPixelSize(mCornerRadiusId);
}
public @Px int getBorderWidth() {
return mContext.getResources().getDimensionPixelSize(mBorderWidthId);
}
}
/**
* Show Tooltip UI.
*
* @param context Context required to get resources.
* @param popup {@PopupWindow} that shows tooltip UI.
* @param text Text to be shown in tool tip UI.
* @param offsetProvider Interface to provide the X and Y offsets.
* @param anchorView Anchor view under which tooltip popup has to be shown
* @param dismissAction Tooltip dismissive action.
*/
public static void showTooltip(
Context context,
PopupWindow popup,
int text,
OffsetProvider offsetProvider,
View anchorView,
final Runnable dismissAction) {
TextView textView = new TextView(context);
textView.setText(text);
textView.setTextAppearance(R.style.TextAppearance_TextMedium_Primary_Baseline_Light);
Resources resources = context.getResources();
int hPadding = resources.getDimensionPixelSize(R.dimen.autofill_tooltip_horizontal_padding);
int vPadding = resources.getDimensionPixelSize(R.dimen.autofill_tooltip_vertical_padding);
textView.setPadding(hPadding, vPadding, hPadding, vPadding);
textView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
popup.setContentView(textView);
popup.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
popup.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
popup.setOutsideTouchable(true);
popup.setBackgroundDrawable(
ApiCompatibilityUtils.getDrawable(
resources, R.drawable.store_locally_tooltip_background));
// An alternate solution is to extend TextView and override onConfigurationChanged. However,
// due to lemon compression, onConfigurationChanged never gets called.
final ComponentCallbacks componentCallbacks =
new ComponentCallbacks() {
@Override
public void onConfigurationChanged(Configuration configuration) {
// If the popup was already showing dismiss it. This may happen during an
// orientation change.
if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
&& popup != null) {
popup.dismiss();
}
}
@Override
public void onLowMemory() {}
};
ContextUtils.getApplicationContext().registerComponentCallbacks(componentCallbacks);
popup.setOnDismissListener(
() -> {
Handler h = new Handler();
h.postDelayed(dismissAction, TOOLTIP_DEFERRED_PERIOD_MS);
ContextUtils.getApplicationContext()
.unregisterComponentCallbacks(componentCallbacks);
});
popup.showAsDropDown(
anchorView,
offsetProvider.getXOffset(textView),
offsetProvider.getYOffset(textView));
textView.announceForAccessibility(textView.getText());
}
/**
* Determines what type of error, if any, is present in the expiration date fields of the
* prompt.
*
* @param monthInput EditText for the month field.
* @param yearInput EditText for the year field.
* @param didFocusOnMonth True if the month field was ever in focus.
* @param didFocusOnYear True if the year field was ever in focus.
* @return The ErrorType value representing the type of error found for the expiration date
* unmask fields.
*/
public static @ErrorType int getExpirationDateErrorType(
EditText monthInput,
EditText yearInput,
boolean didFocusOnMonth,
boolean didFocusOnYear) {
Calendar calendar = Calendar.getInstance();
int thisYear = calendar.get(Calendar.YEAR);
int thisMonth = calendar.get(Calendar.MONTH) + 1; // calendar month is 0-based
int month = getMonth(monthInput);
if (month == -1) {
if (monthInput.getText().length() == EXPIRATION_FIELDS_LENGTH
|| (!monthInput.isFocused() && didFocusOnMonth)) {
return ErrorType.EXPIRATION_MONTH;
}
// If year was focused before, proceed to check if year is valid.
if (!didFocusOnYear) {
return ErrorType.NOT_ENOUGH_INFO;
}
}
int year = getFourDigitYear(yearInput);
if (year == -1) {
if (yearInput.getText().length() == EXPIRATION_FIELDS_LENGTH
|| (!yearInput.isFocused() && didFocusOnYear)) {
return ErrorType.EXPIRATION_YEAR;
}
return ErrorType.NOT_ENOUGH_INFO;
}
// Year is valid but month is still being edited.
if (month == -1) {
return ErrorType.NOT_ENOUGH_INFO;
}
if (year == thisYear && month < thisMonth) {
return ErrorType.EXPIRATION_DATE;
}
return ErrorType.NONE;
}
/**
* @param yearInput EditText for the year field.
* @return The expiration year the user entered.
* Two digit values (such as 17) will be converted to 4 digit years (such as 2017).
* Returns -1 if the input is empty or otherwise not a valid year (previous year or
* more than 10 years in the future).
*/
public static int getFourDigitYear(EditText yearInput) {
Calendar calendar = Calendar.getInstance();
int thisYear = calendar.get(Calendar.YEAR);
try {
int year = Integer.parseInt(yearInput.getText().toString());
if (year < 0) return -1;
if (year < 100) year += thisYear - thisYear % 100;
if (year < thisYear || year > thisYear + 10) return -1;
return year;
} catch (NumberFormatException e) {
return -1;
}
}
/**
* @param monthInput EditText for the month field.
* @return The expiration month the user entered.
* Returns -1 if not a valid month.
*/
@VisibleForTesting
static int getMonth(EditText monthInput) {
try {
int month = Integer.parseInt(monthInput.getText().toString());
if (month < 1 || month > 12) {
return -1;
}
return month;
} catch (NumberFormatException e) {
return -1;
}
}
/**
* @param context Context required to get resources.
* @param errorType Type of the error used to get the resource string.
* @return Error string retrieved from the string resources.
*/
public static String getErrorMessage(Context context, @ErrorType int errorType) {
Resources resources = context.getResources();
switch (errorType) {
case ErrorType.EXPIRATION_MONTH:
return resources.getString(
R.string.autofill_card_unmask_prompt_error_try_again_expiration_month);
case ErrorType.EXPIRATION_YEAR:
return resources.getString(
R.string.autofill_card_unmask_prompt_error_try_again_expiration_year);
case ErrorType.EXPIRATION_DATE:
return resources.getString(
R.string.autofill_card_unmask_prompt_error_try_again_expiration_date);
case ErrorType.CVC:
return resources.getString(
R.string.autofill_card_unmask_prompt_error_try_again_cvc);
case ErrorType.CVC_AND_EXPIRATION:
return resources.getString(
R.string.autofill_card_unmask_prompt_error_try_again_cvc_and_expiration);
case ErrorType.NONE:
case ErrorType.NOT_ENOUGH_INFO:
default:
return "";
}
}
/**
* Shows (or removes) the appropriate error message and apply the error filter to the
* appropriate fields depending on the error type.
*
* @param errorType The type of error detected.
* @param context Context required to get resources,
* @param errorMessageTextView TextView to display the error message.
*/
public static void showDetailedErrorMessage(
@ErrorType int errorType, Context context, TextView errorMessageTextView) {
switch (errorType) {
case ErrorType.NONE:
case ErrorType.NOT_ENOUGH_INFO:
clearInputError(errorMessageTextView);
break;
default:
String errorMessage = getErrorMessage(context, errorType);
showErrorMessage(errorMessage, errorMessageTextView);
}
}
/**
* Sets the error message on the inputs.
* @param message The error message to show.
* @param errorMessageTextView TextView used to display the error message.
*/
public static void showErrorMessage(String message, TextView errorMessageTextView) {
assert message != null;
// Set the message to display;
errorMessageTextView.setText(message);
errorMessageTextView.setVisibility(View.VISIBLE);
// A null message is passed in during card verification, which also makes an announcement.
// Announcing twice in a row may cancel the first announcement.
errorMessageTextView.announceForAccessibility(message);
}
/**
* Removes the error message on the inputs.
* @param errorMessageTextView TextView used to display the error message.
*/
public static void clearInputError(TextView errorMessageTextView) {
errorMessageTextView.setText(null);
errorMessageTextView.setVisibility(View.GONE);
}
/**
* Applies the error filter to the invalid fields based on the errorType.
*
* @param errorType The ErrorType value representing the type of error found for the unmask
* fields.
* @param context Context required to get resources,
* @param monthInput EditText for the month field.
* @param yearInput EditText for the year field.
* @param cvcInput EditText for the cvc field.
*/
public static void updateColorForInputs(
@ErrorType int errorType,
Context context,
EditText monthInput,
EditText yearInput,
EditText cvcInput) {
ColorFilter filter =
new PorterDuffColorFilter(
context.getColor(R.color.input_underline_error_color),
PorterDuff.Mode.SRC_IN);
// Decide on what field(s) to apply the filter.
boolean filterMonth =
errorType == ErrorType.EXPIRATION_MONTH
|| errorType == ErrorType.EXPIRATION_DATE
|| errorType == ErrorType.CVC_AND_EXPIRATION;
boolean filterYear =
errorType == ErrorType.EXPIRATION_YEAR
|| errorType == ErrorType.EXPIRATION_DATE
|| errorType == ErrorType.CVC_AND_EXPIRATION;
updateColorForInput(monthInput, filterMonth ? filter : null);
updateColorForInput(yearInput, filterYear ? filter : null);
if (cvcInput != null) {
boolean filterCvc =
errorType == ErrorType.CVC || errorType == ErrorType.CVC_AND_EXPIRATION;
updateColorForInput(cvcInput, filterCvc ? filter : null);
}
}
/**
* Sets the stroke color for the given input.
* @param input The input to modify.
* @param filter The color filter to apply to the background.
*/
public static void updateColorForInput(EditText input, ColorFilter filter) {
input.getBackground().mutate().setColorFilter(filter);
}
/**
* Appends the title string with a logo and sets it as the text on the TextView.
*
* @param context The context used for fetching the required resources.
* @param titleTextView The TextView containing the title that the title and the logo should be
* set on.
* @param title The title string for the TextView.
* @param logoResourceId The resource id for the icon to inlined within the title string.
*/
public static void inlineTitleStringWithLogo(
Context context, TextView titleTextView, String title, int logoResourceId) {
Drawable mInlineTitleIcon =
ResourcesCompat.getDrawable(
context.getResources(), logoResourceId, context.getTheme());
// The first character will be replaced by the logo, and the consecutive spaces after
// are used as padding.
SpannableString titleWithLogo = new SpannableString(" " + title);
// How much the original logo should scale up in size to match height of text.
float scaleFactor = titleTextView.getTextSize() / mInlineTitleIcon.getIntrinsicHeight();
mInlineTitleIcon.setBounds(
/* left= */ 0,
/* top= */ 0,
/* right */ (int) (scaleFactor * mInlineTitleIcon.getIntrinsicWidth()),
/* bottom */ (int) (scaleFactor * mInlineTitleIcon.getIntrinsicHeight()));
titleWithLogo.setSpan(
new ImageSpan(mInlineTitleIcon, ImageSpan.ALIGN_CENTER),
/* start= */ 0,
/* end= */ 1,
/* flags= */ Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
titleTextView.setText(titleWithLogo, TextView.BufferType.SPANNABLE);
}
/**
* Generates a SpannableString from a list of {@link LegalMessageLine}.
*
* @param context The context used for fetching the required resources.
* @param legalMessageLines The list of LegalMessageLines to be represented as a string.
* @param underlineLinks True if the links in the legal message lines are to be underlined.
* @param onClickCallback The callback for the link clicks.
* @return A {@link SpannableStringBuilder} that can directly be set on a TextView.
*/
public static SpannableStringBuilder getSpannableStringForLegalMessageLines(
Context context,
List<LegalMessageLine> legalMessageLines,
boolean underlineLinks,
Callback<String> onClickCallback) {
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();
for (int i = 0; i < legalMessageLines.size(); i++) {
LegalMessageLine line = legalMessageLines.get(i);
SpannableString text = new SpannableString(line.text);
for (final LegalMessageLine.Link link : line.links) {
if (underlineLinks) {
text.setSpan(
new ClickableSpan() {
@Override
public void onClick(View view) {
onClickCallback.onResult(link.url);
}
},
link.start,
link.end,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
} else {
text.setSpan(
new NoUnderlineClickableSpan(
context, view -> onClickCallback.onResult(link.url)),
link.start,
link.end,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
}
}
spannableStringBuilder.append(text);
if (i != legalMessageLines.size() - 1) {
spannableStringBuilder.append("\n");
}
}
return spannableStringBuilder;
}
/**
* Returns a {@link SpannableString} containing a {@link NoUnderlineClickableSpan} for the text
* contained within the tags <link1></link1>.
* @param context The context required to fetch the resources.
* @param stringResourceId The resource id of the string on which the clickable span should be
* applied.
* @param url The url that should be opened when the clickable span is clicked.
* @param onClickCallback The callback for the link clicks.
* @return {@link SpannableString} that can be directly set on the TextView.
*/
public static SpannableString getSpannableStringWithClickableSpansToOpenLinksInCustomTabs(
Context context, int stringResourceId, String url, Callback<String> onClickCallback) {
return SpanApplier.applySpans(
context.getString(stringResourceId),
new SpanApplier.SpanInfo(
"<link1>",
"</link1>",
new NoUnderlineClickableSpan(
context, view -> onClickCallback.onResult(url))));
}
/**
* Adds dimension params to card art URL for credit cards.
* @param customIconUrl A FIFE URL to fetch the card art icon.
* @param width in pixels.
* @param height in pixels.
* @return {@link GURL} formatted with the icon dimensions to fetch the card art icon.
*/
public static GURL getCreditCardIconUrlWithParams(
GURL customIconUrl, @Px int width, @Px int height) {
// Params can be added to a FIFE URL by appending them at the end like URL[=params]. "w"
// option is used to set the width in pixels, and "h" is used to set the height in pixels.
StringBuilder url = new StringBuilder(customIconUrl.getSpec());
url.append("=w").append(width).append("-h").append(height);
// If SCS supports stretching, add it as a param to fetch images of exact dimensions.
if (ChromeFeatureList.isEnabled(
ChromeFeatureList.AUTOFILL_ENABLE_CARD_ART_SERVER_SIDE_STRETCHING)) {
url.append("-s");
}
return new GURL(url.toString());
}
/**
* Always show the Capital One virtual card icon for virtual cards if the card icon URL is
* available for the card. Never show the Capital One virtual card icon for FPAN. Show rich card
* art when the metadata experiment is enabled.
* @param customIconUrl {@link GURL} for fetching the custom icon.
* @param isVirtualCard Whether or not the card is a virtual card.
* @return True if the custom icon should be shown. False otherwise.
*/
public static boolean shouldShowCustomIcon(GURL customIconUrl, boolean isVirtualCard) {
if (customIconUrl == null) {
return false;
}
if (isVirtualCard && customIconUrl.getSpec().equals(CAPITAL_ONE_ICON_URL)) {
return true;
}
if (!customIconUrl.getSpec().equals(CAPITAL_ONE_ICON_URL)
&& ChromeFeatureList.isEnabled(ChromeFeatureList.AUTOFILL_ENABLE_CARD_ART_IMAGE)) {
return true;
}
return false;
}
/**
* If {@code showCustomIcon} is true, and the {@code cardArtUrl} is valid, it fetches the bitmap
* of the required size from PersonalDataManager. If not, the default icon {@code defaultIconId}
* is fetched from the resources. If the bitmap is not available in cache, then it is fetched
* from the server and stored in cache for the next time.
*
* @param context Context required to get resources.
* @param personalDataManager The PDM associated with the card.
* @param cardArtUrl The URL to fetch the icon.
* @param defaultIconId Resource Id for the default (network) icon if the card art could not be
* retrieved.
* @param cardIconSize Enum that specifies the icon's size (small or large).
* @param showCustomIcon If true, custom card icon is fetched, else, default icon is fetched.
* @return {@link Drawable} that can be set as the card icon. If neither the custom icon nor the
* default icon is available, returns null.
*/
public static @Nullable Drawable getCardIcon(
Context context,
PersonalDataManager personalDataManager,
@Nullable GURL cardArtUrl,
int defaultIconId,
@CardIconSize int cardIconSize,
boolean showCustomIcon) {
Drawable defaultIcon =
defaultIconId == 0 ? null : AppCompatResources.getDrawable(context, defaultIconId);
if (!showCustomIcon || cardArtUrl == null || !cardArtUrl.isValid()) {
return defaultIcon;
}
if (cardArtUrl.getSpec().equals(CAPITAL_ONE_ICON_URL)
&& ChromeFeatureList.isEnabled(
ChromeFeatureList.AUTOFILL_ENABLE_NEW_CARD_ART_AND_NETWORK_IMAGES)) {
return AppCompatResources.getDrawable(context, R.drawable.capitalone_metadata_card);
}
Optional<Bitmap> customIconBitmap =
personalDataManager.getCustomImageForAutofillSuggestionIfAvailable(
cardArtUrl, CardIconSpecs.create(context, cardIconSize));
if (!customIconBitmap.isPresent()) {
return defaultIcon;
}
return new BitmapDrawable(context.getResources(), customIconBitmap.get());
}
/**
* Resize the bitmap to the required specs, round corners, and add grey border.
* @param bitmap to be updated.
* @param cardIconSpecs {@link CardIconSpecs} instance containing the specs for the card icon.
* @param addRoundedCornersAndGreyBorder If true, the bitmap corners are rounded, and a grey
* border is added. If false, no enhancements are applied to the bitmap.
* @return Resized {@link Bitmap} with rounded corners and grey border.
*/
public static Bitmap resizeAndAddRoundedCornersAndGreyBorder(
Bitmap bitmap, CardIconSpecs cardIconSpecs, boolean addRoundedCornersAndGreyBorder) {
// Until AutofillEnableCardArtServerSideStretching is rolled out, the server maintains the
// card art image's aspect ratio, so the fetched image might not be the exact required size.
// Scale the icon to the desired dimension.
// TODO(crbug.com/40274131): Remove scaling when AutofillEnableCardArtServerSideStretching
// is
// rolled out.
if (bitmap.getWidth() != cardIconSpecs.getWidth()
|| bitmap.getHeight() != cardIconSpecs.getHeight()) {
bitmap =
Bitmap.createScaledBitmap(
bitmap,
cardIconSpecs.getWidth(),
cardIconSpecs.getHeight(),
/* filter= */ true);
}
if (!addRoundedCornersAndGreyBorder) {
return bitmap;
}
// Round the corners.
float cornerRadius = cardIconSpecs.getCornerRadius();
Bitmap bitmapWithEnhancements =
Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmapWithEnhancements);
Paint paint = new Paint();
paint.setAntiAlias(true);
Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
RectF rectF = new RectF(rect);
canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
canvas.drawBitmap(bitmap, rect, rect, paint);
// Add the grey border.
Context context = ContextUtils.getApplicationContext();
int greyColor = ContextCompat.getColor(context, R.color.baseline_neutral_90);
paint.setColor(greyColor);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(cardIconSpecs.getBorderWidth());
canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, paint);
return bitmapWithEnhancements;
}
/**
* Adds credit card details in the card details section.
*
* @param context to get the resources.
* @param personalDataManager The PDM associated with the card.
* @param parentView View that contains the card details section.
* @param cardName Card's nickname/product name/network name.
* @param cardNumber Card's obfuscated last 4 digits.
* @param cardLabel Card's label.
* @param cardArtUrl URL to fetch custom card art.
* @param defaultIconId Resource Id for the default (network) icon if the card art doesn't exist
* or couldn't be retrieved.
* @param cardIconSize Enum that specifies the icon's size (small or large).
* @param iconEndMarginId Resource Id for the margin between the icon and the card details
* section.
* @param cardNameAndNumberTextAppearance Text appearance Id for the card name and the card
* number.
* @param cardLabelTextAppearance Text appearance Id for the card label.
* @param showCustomIcon If true, custom card icon is shown, else, default icon is shown.
*/
public static void addCardDetails(
Context context,
PersonalDataManager personalDataManager,
View parentView,
String cardName,
String cardNumber,
String cardLabel,
GURL cardArtUrl,
int defaultIconId,
@CardIconSize int cardIconSize,
int iconEndMarginId,
int cardNameAndNumberTextAppearance,
int cardLabelTextAppearance,
boolean showCustomIcon) {
ImageView cardIconView = parentView.findViewById(R.id.card_icon);
cardIconView.setImageDrawable(
getCardIcon(
context,
personalDataManager,
cardArtUrl,
defaultIconId,
cardIconSize,
showCustomIcon));
// Set margin between the card icon and the card details.
MarginLayoutParams params = (MarginLayoutParams) cardIconView.getLayoutParams();
params.setMarginEnd(context.getResources().getDimensionPixelSize(iconEndMarginId));
TextView cardNameView = parentView.findViewById(R.id.card_name);
cardNameView.setText(cardName);
cardNameView.setTextAppearance(cardNameAndNumberTextAppearance);
TextView cardNumberView = parentView.findViewById(R.id.card_number);
cardNumberView.setText(cardNumber);
cardNumberView.setTextAppearance(cardNameAndNumberTextAppearance);
TextView cardLabelView = parentView.findViewById(R.id.card_label);
cardLabelView.setText(cardLabel);
cardLabelView.setTextAppearance(cardLabelTextAppearance);
}
public static int getInputTypeForField(@FieldType int type) {
switch (type) {
case FieldType.NAME_FULL:
return InputType.TYPE_CLASS_TEXT
| InputType.TYPE_TEXT_FLAG_CAP_WORDS
| InputType.TYPE_TEXT_VARIATION_PERSON_NAME;
case FieldType.ADDRESS_HOME_SORTING_CODE:
case FieldType.ADDRESS_HOME_ZIP:
return InputType.TYPE_CLASS_TEXT
| InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
| InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS;
case FieldType.PHONE_HOME_WHOLE_NUMBER:
// Show the keyboard with numbers and phone-related symbols.
return InputType.TYPE_CLASS_PHONE;
case FieldType.ADDRESS_HOME_STREET_ADDRESS:
return InputType.TYPE_CLASS_TEXT
| InputType.TYPE_TEXT_FLAG_CAP_WORDS
| InputType.TYPE_TEXT_FLAG_MULTI_LINE
| InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS;
case FieldType.EMAIL_ADDRESS:
return InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
default:
return InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_WORDS;
}
}
/**
* Sets the touch event filter on the provided `view` so that touch events are ignored if
* something is drawn on top of the `view`. This is done to mitigate the clickjacking attacks.
*
* @param view The view to set the touch event filter on.
*/
@SuppressLint("ClickableViewAccessibility")
public static void setFilterTouchForSecurity(View view) {
if (!ChromeFeatureList.isEnabled(
ChromeFeatureList.AUTOFILL_ENABLE_SECURITY_TOUCH_EVENT_FILTERING_ANDROID)) {
return;
}
view.setFilterTouchesWhenObscured(true);
view.setOnTouchListener(
(View v, MotionEvent ev) ->
(ev.getFlags() & MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED) != 0);
}
}