| /* |
| * 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; |
| } |
| } |