/* * 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.BroadcastOptions; 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()); } private void updateViewForOrientation(@Configuration.Orientation int orientation) { if (orientation == Configuration.ORIENTATION_PORTRAIT) { renderViewPortrait(); } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) { renderViewLandscape(); } mCardCarousel.resetAdapter(); // necessary to update cards width 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. * *

This is called only when {@param data} is not empty.

* * @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 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.requireViewById(R.id.empty_state_title).setText(label); mEmptyStateView.setOnClickListener(clickListener); mAppButton.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 Button getAppButton() { return mAppButton; } @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 { BroadcastOptions options = BroadcastOptions.makeBasic(); options.setInteractive(true); options.setPendingIntentBackgroundActivityStartMode( BroadcastOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); walletCard.getPendingIntent().send(options.toBundle()); } 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; } }