blob: 420f84abe0ddd41b45dbcb4bc33128961e10f919 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.wallet.ui;
import static com.android.systemui.wallet.ui.WalletCardCarousel.CARD_ANIM_ALPHA_DELAY;
import static com.android.systemui.wallet.ui.WalletCardCarousel.CARD_ANIM_ALPHA_DURATION;
import android.annotation.Nullable;
import android.app.PendingIntent;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.internal.annotations.VisibleForTesting;
import com.android.settingslib.Utils;
import com.android.systemui.R;
import com.android.systemui.classifier.FalsingCollector;
import java.util.List;
/** Layout for the wallet screen. */
public class WalletView extends FrameLayout implements WalletCardCarousel.OnCardScrollListener {
private static final String TAG = "WalletView";
private static final int CAROUSEL_IN_ANIMATION_DURATION = 100;
private static final int CAROUSEL_OUT_ANIMATION_DURATION = 200;
private final WalletCardCarousel mCardCarousel;
private final ImageView mIcon;
private final TextView mCardLabel;
// Displays at the bottom of the screen, allow user to enter the default wallet app.
private final Button mAppButton;
// Displays on the top right of the screen, allow user to enter the default wallet app.
private final Button mToolbarAppButton;
// Displays underneath the carousel, allow user to unlock device, verify card, etc.
private final Button mActionButton;
private final Interpolator mOutInterpolator;
private final float mAnimationTranslationX;
private final ViewGroup mCardCarouselContainer;
private final TextView mErrorView;
private final ViewGroup mEmptyStateView;
private boolean mIsDeviceLocked = false;
private boolean mIsUdfpsEnabled = false;
private OnClickListener mDeviceLockedActionOnClickListener;
private OnClickListener mShowWalletAppOnClickListener;
private FalsingCollector mFalsingCollector;
public WalletView(Context context) {
this(context, null);
}
public WalletView(Context context, AttributeSet attrs) {
super(context, attrs);
inflate(context, R.layout.wallet_fullscreen, this);
mCardCarouselContainer = requireViewById(R.id.card_carousel_container);
mCardCarousel = requireViewById(R.id.card_carousel);
mCardCarousel.setCardScrollListener(this);
mIcon = requireViewById(R.id.icon);
mCardLabel = requireViewById(R.id.label);
mAppButton = requireViewById(R.id.wallet_app_button);
mToolbarAppButton = requireViewById(R.id.wallet_toolbar_app_button);
mActionButton = requireViewById(R.id.wallet_action_button);
mErrorView = requireViewById(R.id.error_view);
mEmptyStateView = requireViewById(R.id.wallet_empty_state);
mOutInterpolator =
AnimationUtils.loadInterpolator(context, android.R.interpolator.accelerate_cubic);
mAnimationTranslationX = mCardCarousel.getCardWidthPx() / 4f;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
mCardCarousel.setExpectedViewWidth(getWidth());
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
updateViewForOrientation(newConfig.orientation);
}
private void updateViewForOrientation(@Configuration.Orientation int orientation) {
if (orientation == Configuration.ORIENTATION_PORTRAIT) {
renderViewPortrait();
} else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
renderViewLandscape();
}
ViewGroup.LayoutParams params = mCardCarouselContainer.getLayoutParams();
if (params instanceof MarginLayoutParams) {
((MarginLayoutParams) params).topMargin =
getResources().getDimensionPixelSize(
R.dimen.wallet_card_carousel_container_top_margin);
}
}
private void renderViewPortrait() {
mAppButton.setVisibility(VISIBLE);
mToolbarAppButton.setVisibility(GONE);
mCardLabel.setVisibility(VISIBLE);
requireViewById(R.id.dynamic_placeholder).setVisibility(VISIBLE);
mAppButton.setOnClickListener(mShowWalletAppOnClickListener);
}
private void renderViewLandscape() {
mToolbarAppButton.setVisibility(VISIBLE);
mAppButton.setVisibility(GONE);
mCardLabel.setVisibility(GONE);
requireViewById(R.id.dynamic_placeholder).setVisibility(GONE);
mToolbarAppButton.setOnClickListener(mShowWalletAppOnClickListener);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// Forward touch events to card carousel to allow for swiping outside carousel bounds.
return mCardCarousel.onTouchEvent(event) || super.onTouchEvent(event);
}
@Override
public void onCardScroll(WalletCardViewInfo centerCard, WalletCardViewInfo nextCard,
float percentDistanceFromCenter) {
CharSequence centerCardText = getLabelText(centerCard);
Drawable centerCardIcon = getHeaderIcon(mContext, centerCard);
renderActionButton(centerCard, mIsDeviceLocked, mIsUdfpsEnabled);
if (centerCard.isUiEquivalent(nextCard)) {
mCardLabel.setAlpha(1f);
mIcon.setAlpha(1f);
mActionButton.setAlpha(1f);
} else {
mCardLabel.setText(centerCardText);
mIcon.setImageDrawable(centerCardIcon);
mCardLabel.setAlpha(percentDistanceFromCenter);
mIcon.setAlpha(percentDistanceFromCenter);
mActionButton.setAlpha(percentDistanceFromCenter);
}
}
/**
* Render and show card carousel view.
*
* <p>This is called only when {@param data} is not empty.</p>
*
* @param data a list of wallet cards information.
* @param selectedIndex index of the current selected card
* @param isDeviceLocked indicates whether the device is locked.
*/
void showCardCarousel(
List<WalletCardViewInfo> data,
int selectedIndex,
boolean isDeviceLocked,
boolean isUdfpsEnabled) {
boolean shouldAnimate =
mCardCarousel.setData(data, selectedIndex, mIsDeviceLocked != isDeviceLocked);
mIsDeviceLocked = isDeviceLocked;
mIsUdfpsEnabled = isUdfpsEnabled;
mCardCarouselContainer.setVisibility(VISIBLE);
mCardCarousel.setVisibility(VISIBLE);
mErrorView.setVisibility(GONE);
mEmptyStateView.setVisibility(GONE);
mIcon.setImageDrawable(getHeaderIcon(mContext, data.get(selectedIndex)));
mCardLabel.setText(getLabelText(data.get(selectedIndex)));
updateViewForOrientation(getResources().getConfiguration().orientation);
renderActionButton(data.get(selectedIndex), isDeviceLocked, mIsUdfpsEnabled);
if (shouldAnimate) {
animateViewsShown(mIcon, mCardLabel, mActionButton);
}
}
void animateDismissal() {
if (mCardCarouselContainer.getVisibility() != VISIBLE) {
return;
}
mCardCarousel.animate().translationX(mAnimationTranslationX)
.setInterpolator(mOutInterpolator)
.setDuration(CAROUSEL_OUT_ANIMATION_DURATION)
.start();
mCardCarouselContainer.animate()
.alpha(0f)
.setDuration(CARD_ANIM_ALPHA_DURATION)
.setStartDelay(CARD_ANIM_ALPHA_DELAY)
.start();
}
void showEmptyStateView(Drawable logo, CharSequence logoContentDescription, CharSequence label,
OnClickListener clickListener) {
mEmptyStateView.setVisibility(VISIBLE);
mErrorView.setVisibility(GONE);
mCardCarousel.setVisibility(GONE);
mIcon.setImageDrawable(logo);
mIcon.setContentDescription(logoContentDescription);
mCardLabel.setText(R.string.wallet_empty_state_label);
ImageView logoView = mEmptyStateView.requireViewById(R.id.empty_state_icon);
logoView.setImageDrawable(mContext.getDrawable(R.drawable.ic_qs_plus));
mEmptyStateView.<TextView>requireViewById(R.id.empty_state_title).setText(label);
mEmptyStateView.setOnClickListener(clickListener);
}
void showErrorMessage(@Nullable CharSequence message) {
if (TextUtils.isEmpty(message)) {
message = getResources().getText(R.string.wallet_error_generic);
}
mErrorView.setText(message);
mErrorView.setVisibility(VISIBLE);
mCardCarouselContainer.setVisibility(GONE);
mEmptyStateView.setVisibility(GONE);
}
void setDeviceLockedActionOnClickListener(OnClickListener onClickListener) {
mDeviceLockedActionOnClickListener = onClickListener;
}
void setShowWalletAppOnClickListener(OnClickListener onClickListener) {
mShowWalletAppOnClickListener = onClickListener;
}
void hide() {
setVisibility(GONE);
}
void show() {
setVisibility(VISIBLE);
}
void hideErrorMessage() {
mErrorView.setVisibility(GONE);
}
WalletCardCarousel getCardCarousel() {
return mCardCarousel;
}
Button getActionButton() {
return mActionButton;
}
@VisibleForTesting
TextView getErrorView() {
return mErrorView;
}
@VisibleForTesting
ViewGroup getEmptyStateView() {
return mEmptyStateView;
}
@VisibleForTesting
ViewGroup getCardCarouselContainer() {
return mCardCarouselContainer;
}
@VisibleForTesting
TextView getCardLabel() {
return mCardLabel;
}
@Nullable
private static Drawable getHeaderIcon(Context context, WalletCardViewInfo walletCard) {
Drawable icon = walletCard.getIcon();
if (icon != null) {
icon.setTint(
Utils.getColorAttrDefaultColor(
context, com.android.internal.R.attr.colorAccentPrimary));
}
return icon;
}
private void renderActionButton(
WalletCardViewInfo walletCard, boolean isDeviceLocked, boolean isUdfpsEnabled) {
CharSequence actionButtonText = getActionButtonText(walletCard);
if (!isUdfpsEnabled && actionButtonText != null) {
mActionButton.setVisibility(VISIBLE);
mActionButton.setText(actionButtonText);
mActionButton.setOnClickListener(
isDeviceLocked
? mDeviceLockedActionOnClickListener
: v -> {
try {
walletCard.getPendingIntent().send();
} catch (PendingIntent.CanceledException e) {
Log.w(TAG, "Error sending pending intent for wallet card.");
}
}
);
} else {
mActionButton.setVisibility(GONE);
}
}
private static void animateViewsShown(View... uiElements) {
for (View view : uiElements) {
if (view.getVisibility() == VISIBLE) {
view.setAlpha(0f);
view.animate().alpha(1f).setDuration(CAROUSEL_IN_ANIMATION_DURATION).start();
}
}
}
private static CharSequence getLabelText(WalletCardViewInfo card) {
String[] rawLabel = card.getLabel().toString().split("\\n");
return rawLabel.length == 2 ? rawLabel[0] : card.getLabel();
}
@Nullable
private static CharSequence getActionButtonText(WalletCardViewInfo card) {
String[] rawLabel = card.getLabel().toString().split("\\n");
return rawLabel.length == 2 ? rawLabel[1] : null;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mFalsingCollector != null) {
mFalsingCollector.onTouchEvent(ev);
}
boolean result = super.dispatchTouchEvent(ev);
if (mFalsingCollector != null) {
mFalsingCollector.onMotionEventComplete();
}
return result;
}
public void setFalsingCollector(FalsingCollector falsingCollector) {
mFalsingCollector = falsingCollector;
}
}