data, int selectedIndex) {
+ boolean wasEmpty = mWalletCardCarouselAdapter.getItemCount() == 0;
+ mWalletCardCarouselAdapter.setData(data);
+ if (wasEmpty) {
+ scrollToPosition(selectedIndex);
+ mNumCardsToAnimate = numCardsOnScreen(data.size(), selectedIndex);
+ mCardAnimationStartPosition = Math.max(selectedIndex - 1, 0);
+ }
+ WalletCardViewInfo selectedCard = data.get(selectedIndex);
+ mCardScrollListener.onCardScroll(selectedCard, selectedCard, 0);
+ return wasEmpty;
+ }
+
+ @Override
+ public void scrollToPosition(int position) {
+ super.scrollToPosition(position);
+ mSelectionListener.onCardSelected(mWalletCardCarouselAdapter.mData.get(position));
+ }
+
+ /**
+ * The number of cards shown on screen when one of the cards is position in the center. This is
+ * also the num
+ */
+ private static int numCardsOnScreen(int numCards, int selectedIndex) {
+ if (numCards <= 2) {
+ return numCards;
+ }
+ // When there are 3 or more cards, 3 cards will be shown unless the first or last card is
+ // centered on screen.
+ return selectedIndex > 0 && selectedIndex < (numCards - 1) ? 3 : 2;
+ }
+
+ /**
+ * The padding pushes the first and last cards in the list to the center when they are
+ * selected.
+ */
+ private void updatePadding(int viewWidth) {
+ int paddingHorizontal = (viewWidth - mTotalCardWidth) / 2 - mCardMarginPx;
+ paddingHorizontal = Math.max(0, paddingHorizontal); // just in case
+ setPadding(paddingHorizontal, getPaddingTop(), paddingHorizontal, getPaddingBottom());
+
+ // re-center selected card after changing padding (if card is selected)
+ if (mWalletCardCarouselAdapter != null
+ && mWalletCardCarouselAdapter.getItemCount() > 0
+ && mCenteredAdapterPosition != NO_POSITION) {
+ ViewHolder viewHolder = findViewHolderForAdapterPosition(mCenteredAdapterPosition);
+ if (viewHolder != null) {
+ View cardView = viewHolder.itemView;
+ int cardCenter = (cardView.getLeft() + cardView.getRight()) / 2;
+ int viewCenter = (getLeft() + getRight()) / 2;
+ int scrollX = cardCenter - viewCenter;
+ scrollBy(scrollX, 0);
+ }
+ }
+ }
+
+ private void updateCardView(View view) {
+ WalletCardViewHolder viewHolder = (WalletCardViewHolder) view.getTag();
+ CardView cardView = viewHolder.mCardView;
+ float center = (float) getWidth() / 2f;
+ float viewCenter = (view.getRight() + view.getLeft()) / 2f;
+ float viewWidth = view.getWidth();
+ float position = (viewCenter - center) / viewWidth;
+ float scaleFactor = Math.max(UNSELECTED_CARD_SCALE, 1f - Math.abs(position));
+
+ cardView.setScaleX(scaleFactor);
+ cardView.setScaleY(scaleFactor);
+
+ // a card is the "centered card" until its edge has moved past the center of the recycler
+ // view. note that we also need to factor in the negative margin.
+ // Find the edge that is closer to the center.
+ int edgePosition =
+ viewCenter < center ? view.getRight() + mCardMarginPx
+ : view.getLeft() - mCardMarginPx;
+
+ if (Math.abs(viewCenter - center) < mCardCenterToScreenCenterDistancePx) {
+ int childAdapterPosition = getChildAdapterPosition(view);
+ if (childAdapterPosition == RecyclerView.NO_POSITION) {
+ return;
+ }
+ mCenteredAdapterPosition = getChildAdapterPosition(view);
+ mEdgeToCenterDistance = edgePosition - center;
+ mCardCenterToScreenCenterDistancePx = Math.abs(viewCenter - center);
+ }
+ }
+
+ private class CardCarouselScrollListener extends OnScrollListener {
+
+ private int mOldState = -1;
+
+ @Override
+ public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
+ if (newState == RecyclerView.SCROLL_STATE_IDLE && newState != mOldState) {
+ performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
+ }
+ mOldState = newState;
+ }
+
+ /**
+ * Callback method to be invoked when the RecyclerView has been scrolled. This will be
+ * called after the scroll has completed.
+ *
+ * This callback will also be called if visible item range changes after a layout
+ * calculation. In that case, dx and dy will be 0.
+ *
+ * @param recyclerView The RecyclerView which scrolled.
+ * @param dx The amount of horizontal scroll.
+ * @param dy The amount of vertical scroll.
+ */
+ @Override
+ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+ mCenteredAdapterPosition = RecyclerView.NO_POSITION;
+ mEdgeToCenterDistance = Float.MAX_VALUE;
+ mCardCenterToScreenCenterDistancePx = Float.MAX_VALUE;
+ for (int i = 0; i < getChildCount(); i++) {
+ updateCardView(getChildAt(i));
+ }
+ if (mCenteredAdapterPosition == RecyclerView.NO_POSITION || dx == 0) {
+ return;
+ }
+
+ int nextAdapterPosition =
+ mCenteredAdapterPosition + (mEdgeToCenterDistance > 0 ? 1 : -1);
+ if (nextAdapterPosition < 0
+ || nextAdapterPosition >= mWalletCardCarouselAdapter.mData.size()) {
+ return;
+ }
+
+ // Update the label text based on the currently selected card and the next one
+ WalletCardViewInfo centerCard =
+ mWalletCardCarouselAdapter.mData.get(mCenteredAdapterPosition);
+ WalletCardViewInfo nextCard = mWalletCardCarouselAdapter.mData.get(nextAdapterPosition);
+ float percentDistanceFromCenter =
+ Math.abs(mEdgeToCenterDistance) / mCardEdgeToCenterDistance;
+ mCardScrollListener.onCardScroll(centerCard, nextCard, percentDistanceFromCenter);
+ }
+ }
+
+ private class CarouselSnapHelper extends PagerSnapHelper {
+
+ private static final float MILLISECONDS_PER_INCH = 200.0F;
+ private static final int MAX_SCROLL_ON_FLING_DURATION = 80; // ms
+
+ @Override
+ public View findSnapView(LayoutManager layoutManager) {
+ View view = super.findSnapView(layoutManager);
+ if (view == null) {
+ // implementation decides not to snap
+ return null;
+ }
+ WalletCardViewHolder viewHolder = (WalletCardViewHolder) view.getTag();
+ WalletCardViewInfo card = viewHolder.mCardViewInfo;
+ mSelectionListener.onCardSelected(card);
+ mCardScrollListener.onCardScroll(card, card, 0);
+ return view;
+ }
+
+ /**
+ * The default SnapScroller is a little sluggish
+ */
+ @Override
+ protected LinearSmoothScroller createScroller(LayoutManager layoutManager) {
+ return new LinearSmoothScroller(getContext()) {
+ @Override
+ protected void onTargetFound(View targetView, State state, Action action) {
+ int[] snapDistances = calculateDistanceToFinalSnap(layoutManager, targetView);
+ final int dx = snapDistances[0];
+ final int dy = snapDistances[1];
+ final int time = calculateTimeForDeceleration(
+ Math.max(Math.abs(dx), Math.abs(dy)));
+ if (time > 0) {
+ action.update(dx, dy, time, mDecelerateInterpolator);
+ }
+ }
+
+ @Override
+ protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
+ return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
+ }
+
+ @Override
+ protected int calculateTimeForScrolling(int dx) {
+ return Math.min(MAX_SCROLL_ON_FLING_DURATION,
+ super.calculateTimeForScrolling(dx));
+ }
+ };
+ }
+ }
+
+ private class WalletCardCarouselAdapter extends Adapter {
+
+ private List mData = Collections.EMPTY_LIST;
+
+ @NonNull
+ @Override
+ public WalletCardViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
+ LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
+ View view = inflater.inflate(R.layout.wallet_card_view, viewGroup, false);
+ WalletCardViewHolder viewHolder = new WalletCardViewHolder(view);
+ CardView cardView = viewHolder.mCardView;
+ cardView.setRadius(mCornerRadiusPx);
+ ViewGroup.LayoutParams layoutParams = cardView.getLayoutParams();
+ layoutParams.width = mCardWidthPx;
+ layoutParams.height = mCardHeightPx;
+ view.setTag(viewHolder);
+ return viewHolder;
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull WalletCardViewHolder viewHolder, int position) {
+ WalletCardViewInfo cardViewInfo = mData.get(position);
+ viewHolder.mCardViewInfo = cardViewInfo;
+ if (cardViewInfo.getCardId().isEmpty()) {
+ viewHolder.mImageView.setScaleType(ImageView.ScaleType.CENTER);
+ }
+ viewHolder.mImageView.setImageDrawable(cardViewInfo.getCardDrawable());
+ viewHolder.mCardView.setContentDescription(cardViewInfo.getContentDescription());
+ viewHolder.mCardView.setOnClickListener(
+ v -> {
+ if (position != mCenteredAdapterPosition) {
+ smoothScrollToPosition(position);
+ } else {
+ mSelectionListener.onCardClicked(cardViewInfo);
+ }
+ });
+ if (mNumCardsToAnimate > 0 && (position - mCardAnimationStartPosition < 2)) {
+ mNumCardsToAnimate--;
+ int startDelay = (position - mCardAnimationStartPosition) * CARD_ANIM_ALPHA_DELAY
+ + mExtraAnimationDelay;
+ viewHolder.itemView.setAlpha(0f);
+ viewHolder.itemView.animate().alpha(1f)
+ .setStartDelay(Math.max(0, startDelay))
+ .setDuration(CARD_ANIM_ALPHA_DURATION).start();
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return mData.size();
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mData.get(position).getCardId().hashCode();
+ }
+
+ void setData(List data) {
+ mData = data;
+ notifyDataSetChanged();
+ }
+ }
+
+ private class CardCarouselAccessibilityDelegate extends RecyclerViewAccessibilityDelegate {
+
+ private CardCarouselAccessibilityDelegate(@NonNull RecyclerView recyclerView) {
+ super(recyclerView);
+ }
+
+ @Override
+ public boolean onRequestSendAccessibilityEvent(
+ ViewGroup viewGroup, View view, AccessibilityEvent accessibilityEvent) {
+ int eventType = accessibilityEvent.getEventType();
+ if (eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
+ scrollToPosition(getChildAdapterPosition(view));
+ }
+ return super.onRequestSendAccessibilityEvent(viewGroup, view, accessibilityEvent);
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletCardView.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletCardView.java
new file mode 100644
index 0000000000000..fc1adc37d09be
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletCardView.java
@@ -0,0 +1,54 @@
+/*
+ * 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 android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.cardview.widget.CardView;
+
+import com.android.systemui.R;
+
+/** Customized card view of the wallet card carousel. */
+public class WalletCardView extends CardView {
+ private final Paint mBorderPaint;
+
+ public WalletCardView(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public WalletCardView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ mBorderPaint = new Paint();
+ mBorderPaint.setColor(context.getColor(R.color.wallet_card_border));
+ mBorderPaint.setStrokeWidth(
+ context.getResources().getDimension(R.dimen.wallet_card_border_width));
+ mBorderPaint.setStyle(Paint.Style.STROKE);
+ mBorderPaint.setAntiAlias(true);
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+ float radius = getRadius();
+ canvas.drawRoundRect(0, 0, getWidth(), getHeight(), radius, radius, mBorderPaint);
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletCardViewHolder.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletCardViewHolder.java
new file mode 100644
index 0000000000000..3197976456e38
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletCardViewHolder.java
@@ -0,0 +1,42 @@
+/*
+ * 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 android.view.View;
+import android.widget.ImageView;
+
+import androidx.cardview.widget.CardView;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.systemui.R;
+
+/**
+ * View holder for the quick access wallet card.
+ */
+class WalletCardViewHolder extends RecyclerView.ViewHolder {
+
+ final CardView mCardView;
+ final ImageView mImageView;
+ WalletCardViewInfo mCardViewInfo;
+
+ WalletCardViewHolder(View view) {
+ super(view);
+ mCardView = view.requireViewById(R.id.card);
+ mImageView = mCardView.requireViewById(R.id.card_image);
+ }
+
+}
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletCardViewInfo.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletCardViewInfo.java
new file mode 100644
index 0000000000000..669d6664b305f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletCardViewInfo.java
@@ -0,0 +1,50 @@
+/*
+ * 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 android.graphics.drawable.Drawable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+interface WalletCardViewInfo {
+ String getCardId();
+
+ /**
+ * Image of the card.
+ */
+ @NonNull
+ Drawable getCardDrawable();
+
+ /**
+ * Content description for the card.
+ */
+ @Nullable
+ CharSequence getContentDescription();
+
+ /**
+ * Icon shown above the card.
+ */
+ @Nullable
+ Drawable getIcon();
+
+ /**
+ * Text shown above the card.
+ */
+ @NonNull
+ CharSequence getLabel();
+}
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletScreenController.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletScreenController.java
new file mode 100644
index 0000000000000..a93f0f0ba1651
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletScreenController.java
@@ -0,0 +1,330 @@
+/*
+ * 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 android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.os.Handler;
+import android.service.quickaccesswallet.GetWalletCardsError;
+import android.service.quickaccesswallet.GetWalletCardsRequest;
+import android.service.quickaccesswallet.GetWalletCardsResponse;
+import android.service.quickaccesswallet.QuickAccessWalletClient;
+import android.service.quickaccesswallet.SelectWalletCardRequest;
+import android.service.quickaccesswallet.WalletCard;
+import android.service.quickaccesswallet.WalletServiceEvent;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.R;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.settings.UserTracker;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+/** Controller for the wallet card carousel screen. */
+public class WalletScreenController implements
+ WalletCardCarousel.OnSelectionListener,
+ QuickAccessWalletClient.OnWalletCardsRetrievedCallback,
+ QuickAccessWalletClient.WalletServiceEventListener {
+
+ private static final String TAG = "WalletScreenCtrl";
+ private static final String PREFS_HAS_CARDS = "has_cards";
+ private static final String PREFS_WALLET_VIEW_HEIGHT = "wallet_view_height";
+ private static final int MAX_CARDS = 10;
+ private static final long SELECTION_DELAY_MILLIS = TimeUnit.SECONDS.toMillis(30);
+
+ private Context mContext;
+ private final QuickAccessWalletClient mWalletClient;
+ private final ActivityStarter mActivityStarter;
+ private final Executor mExecutor;
+ private final Handler mHandler;
+ private final Runnable mSelectionRunnable = this::selectCard;
+ private final SharedPreferences mPrefs;
+ private final WalletView mWalletView;
+ private final WalletCardCarousel mCardCarousel;
+
+ @VisibleForTesting String mSelectedCardId;
+ @VisibleForTesting boolean mIsDismissed;
+ private boolean mIsDeviceLocked;
+ private boolean mHasRegisteredListener;
+
+ public WalletScreenController(
+ Context context,
+ WalletView walletView,
+ QuickAccessWalletClient walletClient,
+ ActivityStarter activityStarter,
+ Executor executor,
+ Handler handler,
+ UserTracker userTracker,
+ boolean isDeviceLocked) {
+ mContext = context;
+ mWalletClient = walletClient;
+ mActivityStarter = activityStarter;
+ mExecutor = executor;
+ mHandler = handler;
+ mPrefs = userTracker.getUserContext().getSharedPreferences(TAG, Context.MODE_PRIVATE);
+ mWalletView = walletView;
+ mWalletView.setMinimumHeight(getExpectedMinHeight());
+ mWalletView.setLayoutParams(
+ new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+ mCardCarousel = mWalletView.getCardCarousel();
+ if (mCardCarousel != null) {
+ mCardCarousel.setSelectionListener(this);
+ }
+
+ if (!mPrefs.getBoolean(PREFS_HAS_CARDS, false)) {
+ // The empty state view is shown preemptively when cards were not returned last time
+ // to decrease perceived latency.
+ showEmptyStateView();
+ }
+ mIsDeviceLocked = isDeviceLocked;
+ }
+
+ /**
+ * Implements {@link QuickAccessWalletClient.OnWalletCardsRetrievedCallback}. Called when cards
+ * are retrieved successfully from the service. This is called on {@link #mExecutor}.
+ */
+ @Override
+ public void onWalletCardsRetrieved(@NonNull GetWalletCardsResponse response) {
+ if (mIsDismissed) {
+ return;
+ }
+ List walletCards = response.getWalletCards();
+ List data = new ArrayList<>(walletCards.size());
+ for (WalletCard card : walletCards) {
+ data.add(new QAWalletCardViewInfo(mContext, card));
+ }
+ mHandler.post(() -> {
+ if (data.isEmpty()) {
+ showEmptyStateView();
+ } else {
+ mWalletView.showCardCarousel(data, response.getSelectedIndex(), mIsDeviceLocked);
+ }
+ // The empty state view will not be shown preemptively next time if cards were returned
+ mPrefs.edit().putBoolean(PREFS_HAS_CARDS, !data.isEmpty()).apply();
+ removeMinHeightAndRecordHeightOnLayout();
+ });
+ }
+
+ /**
+ * Implements {@link QuickAccessWalletClient.OnWalletCardsRetrievedCallback}. Called when there
+ * is an error during card retrieval. This will be run on the {@link #mExecutor}.
+ */
+ @Override
+ public void onWalletCardRetrievalError(@NonNull GetWalletCardsError error) {
+ if (mIsDismissed) {
+ return;
+ }
+ mHandler.post(() -> {
+ mWalletView.showErrorMessage(error.getMessage());
+ });
+ }
+
+ /**
+ * Implements {@link QuickAccessWalletClient.WalletServiceEventListener}. Called when the wallet
+ * application propagates an event, such as an NFC tap, to the quick access wallet view.
+ */
+ @Override
+ public void onWalletServiceEvent(WalletServiceEvent event) {
+ if (mIsDismissed) {
+ return;
+ }
+ switch (event.getEventType()) {
+ case WalletServiceEvent.TYPE_NFC_PAYMENT_STARTED:
+ onDismissed();
+ break;
+ case WalletServiceEvent.TYPE_WALLET_CARDS_UPDATED:
+ queryWalletCards();
+ break;
+ default:
+ Log.w(TAG, "onWalletServiceEvent: Unknown event type");
+ }
+ }
+
+ @Override
+ public void onCardSelected(@NonNull WalletCardViewInfo card) {
+ if (mIsDismissed) {
+ return;
+ }
+ mSelectedCardId = card.getCardId();
+ selectCard();
+ }
+
+ private void selectCard() {
+ mHandler.removeCallbacks(mSelectionRunnable);
+ String selectedCardId = mSelectedCardId;
+ if (mIsDismissed || selectedCardId == null) {
+ return;
+ }
+ mWalletClient.selectWalletCard(new SelectWalletCardRequest(selectedCardId));
+ // Re-selecting the card keeps the connection bound so we continue to get service events
+ // even if the user keeps it open for a long time.
+ mHandler.postDelayed(mSelectionRunnable, SELECTION_DELAY_MILLIS);
+ }
+
+
+
+ @Override
+ public void onCardClicked(@NonNull WalletCardViewInfo cardInfo) {
+ if (!(cardInfo instanceof QAWalletCardViewInfo)
+ || ((QAWalletCardViewInfo) cardInfo).mWalletCard == null
+ || ((QAWalletCardViewInfo) cardInfo).mWalletCard.getPendingIntent() == null) {
+ return;
+ }
+ mActivityStarter.startActivity(
+ ((QAWalletCardViewInfo) cardInfo).mWalletCard.getPendingIntent().getIntent(),
+ true);
+ }
+
+ @Override
+ public void queryWalletCards() {
+ if (mIsDismissed) {
+ return;
+ }
+ if (!mHasRegisteredListener) {
+ // Listener is registered even when device is locked. Should only be registered once.
+ mWalletClient.addWalletServiceEventListener(this);
+ mHasRegisteredListener = true;
+ }
+
+ mWalletView.show();
+ mWalletView.hideErrorMessage();
+ int iconSizePx = mContext.getResources().getDimensionPixelSize(R.dimen.wallet_icon_size);
+ int cardWidthPx = mCardCarousel.getCardWidthPx();
+ int cardHeightPx = mCardCarousel.getCardHeightPx();
+ GetWalletCardsRequest request =
+ new GetWalletCardsRequest(cardWidthPx, cardHeightPx, iconSizePx, MAX_CARDS);
+ mWalletClient.getWalletCards(mExecutor, request, this);
+ }
+
+ void onDismissed() {
+ if (mIsDismissed) {
+ return;
+ }
+ mIsDismissed = true;
+ mSelectedCardId = null;
+ mHandler.removeCallbacks(mSelectionRunnable);
+ mWalletClient.notifyWalletDismissed();
+ mWalletClient.removeWalletServiceEventListener(this);
+ mWalletView.animateDismissal();
+ // clear refs to the Wallet Activity
+ mContext = null;
+ }
+
+ private void showEmptyStateView() {
+ Drawable logo = mWalletClient.getLogo();
+ CharSequence logoContentDesc = mWalletClient.getServiceLabel();
+ CharSequence label = mWalletClient.getShortcutLongLabel();
+ Intent intent = mWalletClient.createWalletIntent();
+ if (logo == null
+ || TextUtils.isEmpty(logoContentDesc)
+ || TextUtils.isEmpty(label)
+ || intent == null) {
+ Log.w(TAG, "QuickAccessWalletService manifest entry mis-configured");
+ // Issue is not likely to be resolved until manifest entries are enabled.
+ // Hide wallet feature until then.
+ mWalletView.hide();
+ mPrefs.edit().putInt(PREFS_WALLET_VIEW_HEIGHT, 0).apply();
+ } else {
+ logo.setTint(mContext.getColor(R.color.GM2_grey_900));
+ mWalletView.showEmptyStateView(
+ logo,
+ logoContentDesc,
+ label,
+ v -> mActivityStarter.startActivity(intent, true));
+ }
+ }
+
+ private int getExpectedMinHeight() {
+ int expectedHeight = mPrefs.getInt(PREFS_WALLET_VIEW_HEIGHT, -1);
+ if (expectedHeight == -1) {
+ Resources res = mContext.getResources();
+ expectedHeight = res.getDimensionPixelSize(R.dimen.min_wallet_empty_height);
+ }
+ return expectedHeight;
+ }
+
+ private void removeMinHeightAndRecordHeightOnLayout() {
+ mWalletView.setMinimumHeight(0);
+ mWalletView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ mWalletView.removeOnLayoutChangeListener(this);
+ mPrefs.edit().putInt(PREFS_WALLET_VIEW_HEIGHT, bottom - top).apply();
+ }
+ });
+ }
+
+ @VisibleForTesting
+ static class QAWalletCardViewInfo implements WalletCardViewInfo {
+
+ private final WalletCard mWalletCard;
+ private final Drawable mCardDrawable;
+ private final Drawable mIconDrawable;
+
+ /**
+ * Constructor is called on background executor, so it is safe to load drawables
+ * synchronously.
+ */
+ QAWalletCardViewInfo(Context context, WalletCard walletCard) {
+ mWalletCard = walletCard;
+ mCardDrawable = mWalletCard.getCardImage().loadDrawable(context);
+ Icon icon = mWalletCard.getCardIcon();
+ mIconDrawable = icon == null ? null : icon.loadDrawable(context);
+ }
+
+ @Override
+ public String getCardId() {
+ return mWalletCard.getCardId();
+ }
+
+ @Override
+ public Drawable getCardDrawable() {
+ return mCardDrawable;
+ }
+
+ @Override
+ public CharSequence getContentDescription() {
+ return mWalletCard.getContentDescription();
+ }
+
+ @Override
+ public Drawable getIcon() {
+ return mIconDrawable;
+ }
+
+ @Override
+ public CharSequence getLabel() {
+ return mWalletCard.getCardLabel();
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java
new file mode 100644
index 0000000000000..d2f0720fa66b6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/wallet/ui/WalletView.java
@@ -0,0 +1,242 @@
+/*
+ * 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+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.systemui.R;
+
+import java.util.List;
+
+/** Layout for the wallet screen. */
+public class WalletView extends FrameLayout implements WalletCardCarousel.OnCardScrollListener {
+
+ private static final int CAROUSEL_IN_ANIMATION_DURATION = 300;
+ private static final int CAROUSEL_OUT_ANIMATION_DURATION = 200;
+ private static final int CARD_LABEL_ANIM_DELAY = 133;
+ private static final int CONTACTLESS_ICON_SIZE = 90;
+
+ private final WalletCardCarousel mCardCarousel;
+ private final ImageView mIcon;
+ private final TextView mCardLabel;
+ private final Button mWalletButton;
+ private final Interpolator mInInterpolator;
+ private final Interpolator mOutInterpolator;
+ private final float mAnimationTranslationX;
+ private final ViewGroup mCardCarouselContainer;
+ private final TextView mErrorView;
+ private final ViewGroup mEmptyStateView;
+ private CharSequence mCenterCardText;
+
+ 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);
+ mWalletButton = requireViewById(R.id.wallet_button);
+ mErrorView = requireViewById(R.id.error_view);
+ mEmptyStateView = requireViewById(R.id.wallet_empty_state);
+ mInInterpolator =
+ AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
+ 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
+ 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 = centerCard.getLabel();
+ Drawable icon = centerCard.getIcon();
+ if (icon != null) {
+ mIcon.setImageDrawable(resizeDrawable(getResources(), icon));
+ mIcon.setVisibility(VISIBLE);
+ } else {
+ mIcon.setVisibility(INVISIBLE);
+ }
+ if (!TextUtils.equals(mCenterCardText, centerCardText)) {
+ mCenterCardText = centerCardText;
+ mCardLabel.setText(centerCardText);
+ }
+ if (TextUtils.equals(centerCardText, nextCard.getLabel())) {
+ mCardLabel.setAlpha(1f);
+ } else {
+ mCardLabel.setAlpha(percentDistanceFromCenter);
+ mIcon.setAlpha(percentDistanceFromCenter);
+ }
+ }
+
+ void showCardCarousel(
+ List data, int selectedIndex, boolean isDeviceLocked) {
+ boolean shouldAnimate = mCardCarousel.setData(data, selectedIndex);
+ mCardCarouselContainer.setVisibility(VISIBLE);
+ mErrorView.setVisibility(GONE);
+ if (isDeviceLocked) {
+ // TODO(b/182964813): Add click action to prompt device unlock.
+ mWalletButton.setText(R.string.wallet_button_label_device_locked);
+ } else {
+ mWalletButton.setText(R.string.wallet_button_label_device_unlocked);
+ }
+ if (shouldAnimate) {
+ // If the empty state is visible, animate it away and delay the card carousel animation
+ int emptyStateAnimDelay = 0;
+ if (mEmptyStateView.getVisibility() == VISIBLE) {
+ emptyStateAnimDelay = CARD_ANIM_ALPHA_DURATION;
+ mEmptyStateView.animate()
+ .alpha(0)
+ .setDuration(emptyStateAnimDelay)
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mEmptyStateView.setVisibility(GONE);
+ }
+ })
+ .start();
+ }
+ mCardLabel.setAlpha(0f);
+ mCardLabel.animate().alpha(1f)
+ .setStartDelay(CARD_LABEL_ANIM_DELAY + emptyStateAnimDelay)
+ .setDuration(CARD_ANIM_ALPHA_DURATION)
+ .start();
+ mCardCarousel.setExtraAnimationDelay(emptyStateAnimDelay);
+ mCardCarousel.setTranslationX(mAnimationTranslationX);
+ mCardCarousel.animate().translationX(0)
+ .setInterpolator(mInInterpolator)
+ .setDuration(CAROUSEL_IN_ANIMATION_DURATION)
+ .setStartDelay(emptyStateAnimDelay)
+ .start();
+ }
+ }
+
+ 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);
+ mCardCarouselContainer.setVisibility(GONE);
+ ImageView logoView = mEmptyStateView.requireViewById(R.id.empty_state_icon);
+ logoView.setImageDrawable(logo);
+ logoView.setContentDescription(logoContentDescription);
+ mEmptyStateView.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 hide() {
+ setVisibility(GONE);
+ }
+
+ void show() {
+ setVisibility(VISIBLE);
+ }
+
+ void hideErrorMessage() {
+ mErrorView.setVisibility(GONE);
+ }
+
+ WalletCardCarousel getCardCarousel() {
+ return mCardCarousel;
+ }
+
+ Button getWalletButton() {
+ return mWalletButton;
+ }
+
+ @VisibleForTesting
+ TextView getErrorView() {
+ return mErrorView;
+ }
+
+ @VisibleForTesting
+ ViewGroup getEmptyStateView() {
+ return mEmptyStateView;
+ }
+
+ @VisibleForTesting
+ ViewGroup getCardCarouselContainer() {
+ return mCardCarouselContainer;
+ }
+
+ private static Drawable resizeDrawable(Resources resources, Drawable drawable) {
+ Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
+ return new BitmapDrawable(resources, Bitmap.createScaledBitmap(
+ bitmap, CONTACTLESS_ICON_SIZE, CONTACTLESS_ICON_SIZE, true));
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallet/ui/WalletScreenControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/wallet/ui/WalletScreenControllerTest.java
new file mode 100644
index 0000000000000..f85962b6859bb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/wallet/ui/WalletScreenControllerTest.java
@@ -0,0 +1,274 @@
+/*
+ * 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 android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.os.Handler;
+import android.service.quickaccesswallet.GetWalletCardsError;
+import android.service.quickaccesswallet.GetWalletCardsRequest;
+import android.service.quickaccesswallet.GetWalletCardsResponse;
+import android.service.quickaccesswallet.QuickAccessWalletClient;
+import android.service.quickaccesswallet.QuickAccessWalletService;
+import android.service.quickaccesswallet.WalletCard;
+import android.service.quickaccesswallet.WalletServiceEvent;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.settings.UserTracker;
+
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Collections;
+
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+@SmallTest
+public class WalletScreenControllerTest extends SysuiTestCase {
+
+ private static final int MAX_CARDS = 10;
+ private static final String CARD_ID = "card_id";
+ private static final CharSequence SHORTCUT_SHORT_LABEL = "View all";
+ private static final CharSequence SHORTCUT_LONG_LABEL = "Add a payment method";
+ private static final CharSequence SERVICE_LABEL = "Wallet app";
+ private final WalletView mWalletView = new WalletView(mContext);
+ private final Drawable mWalletLogo = mContext.getDrawable(android.R.drawable.ic_lock_lock);
+ private final Intent mWalletIntent = new Intent(QuickAccessWalletService.ACTION_VIEW_WALLET)
+ .setComponent(new ComponentName(mContext.getPackageName(), "WalletActivity"));
+
+ @Mock
+ QuickAccessWalletClient mWalletClient;
+ @Mock
+ ActivityStarter mActivityStarter;
+ @Mock
+ UserTracker mUserTracker;
+ @Captor
+ ArgumentCaptor mIntentCaptor;
+ @Captor
+ ArgumentCaptor mRequestCaptor;
+ @Captor
+ ArgumentCaptor mCallbackCaptor;
+ @Captor
+ ArgumentCaptor mListenerCaptor;
+ private WalletScreenController mController;
+ private TestableLooper mTestableLooper;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mTestableLooper = TestableLooper.get(this);
+ when(mUserTracker.getUserContext()).thenReturn(mContext);
+ when(mWalletClient.getLogo()).thenReturn(mWalletLogo);
+ when(mWalletClient.getShortcutLongLabel()).thenReturn(SHORTCUT_LONG_LABEL);
+ when(mWalletClient.getShortcutShortLabel()).thenReturn(SHORTCUT_SHORT_LABEL);
+ when(mWalletClient.getServiceLabel()).thenReturn(SERVICE_LABEL);
+ when(mWalletClient.createWalletIntent()).thenReturn(mWalletIntent);
+ mController = new WalletScreenController(
+ mContext,
+ mWalletView,
+ mWalletClient,
+ mActivityStarter,
+ MoreExecutors.directExecutor(),
+ new Handler(mTestableLooper.getLooper()),
+ mUserTracker,
+ /* isDeviceLocked= */false);
+ }
+
+ @Test
+ public void queryCards_hasCards_showCarousel() {
+ GetWalletCardsResponse response =
+ new GetWalletCardsResponse(
+ Collections.singletonList(createWalletCard(mContext)), 0);
+
+ mController.queryWalletCards();
+ mTestableLooper.processAllMessages();
+
+ verify(mWalletClient).getWalletCards(any(), any(), mCallbackCaptor.capture());
+
+ mCallbackCaptor.getValue().onWalletCardsRetrieved(response);
+ mTestableLooper.processAllMessages();
+
+ assertEquals(VISIBLE, mWalletView.getCardCarouselContainer().getVisibility());
+ assertEquals(GONE, mWalletView.getErrorView().getVisibility());
+ }
+
+ @Test
+ public void queryCards_noCards_showEmptyState() {
+ GetWalletCardsResponse response = new GetWalletCardsResponse(Collections.EMPTY_LIST, 0);
+
+ mController.queryWalletCards();
+ mTestableLooper.processAllMessages();
+
+ verify(mWalletClient).getWalletCards(any(), any(), mCallbackCaptor.capture());
+
+ mCallbackCaptor.getValue().onWalletCardsRetrieved(response);
+ mTestableLooper.processAllMessages();
+
+ assertEquals(GONE, mWalletView.getCardCarouselContainer().getVisibility());
+ assertEquals(VISIBLE, mWalletView.getEmptyStateView().getVisibility());
+ assertEquals(GONE, mWalletView.getErrorView().getVisibility());
+ }
+
+ @Test
+ public void queryCards_error_showErrorView() {
+ String errorMessage = "getWalletCardsError";
+ GetWalletCardsError error = new GetWalletCardsError(createIcon(), errorMessage);
+
+ mController.queryWalletCards();
+ mTestableLooper.processAllMessages();
+
+ verify(mWalletClient).getWalletCards(any(), any(), mCallbackCaptor.capture());
+
+ mCallbackCaptor.getValue().onWalletCardRetrievalError(error);
+ mTestableLooper.processAllMessages();
+
+ assertEquals(GONE, mWalletView.getCardCarouselContainer().getVisibility());
+ assertEquals(GONE, mWalletView.getEmptyStateView().getVisibility());
+ assertEquals(VISIBLE, mWalletView.getErrorView().getVisibility());
+ assertEquals(errorMessage, mWalletView.getErrorView().getText().toString());
+ }
+
+ @Test
+ public void onWalletServiceEvent_nfcPaymentStart_dismiss() {
+ WalletServiceEvent event =
+ new WalletServiceEvent(WalletServiceEvent.TYPE_NFC_PAYMENT_STARTED);
+
+ mController.onWalletServiceEvent(event);
+ mTestableLooper.processAllMessages();
+
+ assertNull(mController.mSelectedCardId);
+ assertTrue(mController.mIsDismissed);
+ verify(mWalletClient).notifyWalletDismissed();
+ }
+
+ @Test
+ public void onWalletServiceEvent_walletCardsUpdate_queryCards() {
+ mController.queryWalletCards();
+
+ verify(mWalletClient).addWalletServiceEventListener(mListenerCaptor.capture());
+
+ WalletServiceEvent event =
+ new WalletServiceEvent(WalletServiceEvent.TYPE_WALLET_CARDS_UPDATED);
+
+ QuickAccessWalletClient.WalletServiceEventListener listener = mListenerCaptor.getValue();
+ listener.onWalletServiceEvent(event);
+ mTestableLooper.processAllMessages();
+
+ verify(mWalletClient, times(2))
+ .getWalletCards(any(), mRequestCaptor.capture(), mCallbackCaptor.capture());
+
+ GetWalletCardsRequest request = mRequestCaptor.getValue();
+
+ assertEquals(MAX_CARDS, request.getMaxCards());
+ }
+
+ @Test
+ public void onCardSelected() {
+ mController.onCardSelected(createCardViewInfo());
+
+ assertEquals(CARD_ID, mController.mSelectedCardId);
+ }
+
+ @Test
+ public void onCardClicked_startIntent() {
+ WalletCardViewInfo walletCardViewInfo = createCardViewInfo();
+
+ mController.onCardClicked(walletCardViewInfo);
+
+ verify(mActivityStarter).startActivity(mIntentCaptor.capture(), eq(true));
+
+ assertEquals(mWalletIntent.getAction(), mIntentCaptor.getValue().getAction());
+ assertEquals(mWalletIntent.getComponent(), mIntentCaptor.getValue().getComponent());
+ }
+
+ @Test
+ public void onWalletCardsRetrieved_cardDataEmpty_intentIsNull_hidesWallet() {
+ when(mWalletClient.createWalletIntent()).thenReturn(null);
+ GetWalletCardsResponse response = new GetWalletCardsResponse(Collections.emptyList(), 0);
+
+ mController.onWalletCardsRetrieved(response);
+ mTestableLooper.processAllMessages();
+
+ assertEquals(GONE, mWalletView.getVisibility());
+ }
+
+ @Test
+ public void onWalletCardsRetrieved_cardDataEmpty_logoIsNull_hidesWallet() {
+ when(mWalletClient.getLogo()).thenReturn(null);
+ GetWalletCardsResponse response = new GetWalletCardsResponse(Collections.emptyList(), 0);
+
+ mController.onWalletCardsRetrieved(response);
+ mTestableLooper.processAllMessages();
+
+ assertEquals(GONE, mWalletView.getVisibility());
+ }
+
+ @Test
+ public void onWalletCardsRetrieved_cardDataEmpty_labelIsEmpty_hidesWallet() {
+ when(mWalletClient.getShortcutLongLabel()).thenReturn("");
+ GetWalletCardsResponse response = new GetWalletCardsResponse(Collections.emptyList(), 0);
+
+ mController.onWalletCardsRetrieved(response);
+ mTestableLooper.processAllMessages();
+
+ assertEquals(GONE, mWalletView.getVisibility());
+ }
+
+ private WalletCard createWalletCard(Context context) {
+ PendingIntent pendingIntent =
+ PendingIntent.getActivity(context, 0, mWalletIntent, PendingIntent.FLAG_IMMUTABLE);
+ return new WalletCard.Builder(CARD_ID, createIcon(), "description", pendingIntent).build();
+ }
+
+ private static Icon createIcon() {
+ return Icon.createWithBitmap(Bitmap.createBitmap(70, 44, Bitmap.Config.ARGB_8888));
+ }
+
+ private WalletCardViewInfo createCardViewInfo() {
+ return new WalletScreenController.QAWalletCardViewInfo(
+ mContext, createWalletCard(mContext));
+ }
+}