1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.wallet.ui;
18 
19 import static com.android.systemui.wallet.ui.WalletCardCarousel.CARD_ANIM_ALPHA_DELAY;
20 import static com.android.systemui.wallet.ui.WalletCardCarousel.CARD_ANIM_ALPHA_DURATION;
21 
22 import android.annotation.Nullable;
23 import android.app.PendingIntent;
24 import android.content.Context;
25 import android.content.res.Configuration;
26 import android.graphics.drawable.Drawable;
27 import android.text.TextUtils;
28 import android.util.AttributeSet;
29 import android.util.Log;
30 import android.view.MotionEvent;
31 import android.view.View;
32 import android.view.ViewGroup;
33 import android.view.animation.AnimationUtils;
34 import android.view.animation.Interpolator;
35 import android.widget.Button;
36 import android.widget.FrameLayout;
37 import android.widget.ImageView;
38 import android.widget.TextView;
39 
40 import com.android.internal.annotations.VisibleForTesting;
41 import com.android.settingslib.Utils;
42 import com.android.systemui.R;
43 import com.android.systemui.classifier.FalsingCollector;
44 
45 import java.util.List;
46 
47 /** Layout for the wallet screen. */
48 public class WalletView extends FrameLayout implements WalletCardCarousel.OnCardScrollListener {
49 
50     private static final String TAG = "WalletView";
51     private static final int CAROUSEL_IN_ANIMATION_DURATION = 100;
52     private static final int CAROUSEL_OUT_ANIMATION_DURATION = 200;
53 
54     private final WalletCardCarousel mCardCarousel;
55     private final ImageView mIcon;
56     private final TextView mCardLabel;
57     // Displays at the bottom of the screen, allow user to enter the default wallet app.
58     private final Button mAppButton;
59     // Displays on the top right of the screen, allow user to enter the default wallet app.
60     private final Button mToolbarAppButton;
61     // Displays underneath the carousel, allow user to unlock device, verify card, etc.
62     private final Button mActionButton;
63     private final Interpolator mOutInterpolator;
64     private final float mAnimationTranslationX;
65     private final ViewGroup mCardCarouselContainer;
66     private final TextView mErrorView;
67     private final ViewGroup mEmptyStateView;
68     private boolean mIsDeviceLocked = false;
69     private boolean mIsUdfpsEnabled = false;
70     private OnClickListener mDeviceLockedActionOnClickListener;
71     private OnClickListener mShowWalletAppOnClickListener;
72     private FalsingCollector mFalsingCollector;
73 
WalletView(Context context)74     public WalletView(Context context) {
75         this(context, null);
76     }
77 
WalletView(Context context, AttributeSet attrs)78     public WalletView(Context context, AttributeSet attrs) {
79         super(context, attrs);
80         inflate(context, R.layout.wallet_fullscreen, this);
81         mCardCarouselContainer = requireViewById(R.id.card_carousel_container);
82         mCardCarousel = requireViewById(R.id.card_carousel);
83         mCardCarousel.setCardScrollListener(this);
84         mIcon = requireViewById(R.id.icon);
85         mCardLabel = requireViewById(R.id.label);
86         mAppButton = requireViewById(R.id.wallet_app_button);
87         mToolbarAppButton = requireViewById(R.id.wallet_toolbar_app_button);
88         mActionButton = requireViewById(R.id.wallet_action_button);
89         mErrorView = requireViewById(R.id.error_view);
90         mEmptyStateView = requireViewById(R.id.wallet_empty_state);
91         mOutInterpolator =
92                 AnimationUtils.loadInterpolator(context, android.R.interpolator.accelerate_cubic);
93         mAnimationTranslationX = mCardCarousel.getCardWidthPx() / 4f;
94     }
95 
96     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)97     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
98         super.onLayout(changed, left, top, right, bottom);
99         mCardCarousel.setExpectedViewWidth(getWidth());
100     }
101 
updateViewForOrientation(@onfiguration.Orientation int orientation)102     private void updateViewForOrientation(@Configuration.Orientation int orientation) {
103         if (orientation == Configuration.ORIENTATION_PORTRAIT) {
104             renderViewPortrait();
105         } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
106             renderViewLandscape();
107         }
108         mCardCarousel.resetAdapter(); // necessary to update cards width
109         ViewGroup.LayoutParams params = mCardCarouselContainer.getLayoutParams();
110         if (params instanceof MarginLayoutParams) {
111             ((MarginLayoutParams) params).topMargin =
112                     getResources().getDimensionPixelSize(
113                             R.dimen.wallet_card_carousel_container_top_margin);
114         }
115     }
116 
renderViewPortrait()117     private void renderViewPortrait() {
118         mAppButton.setVisibility(VISIBLE);
119         mToolbarAppButton.setVisibility(GONE);
120         mCardLabel.setVisibility(VISIBLE);
121         requireViewById(R.id.dynamic_placeholder).setVisibility(VISIBLE);
122 
123         mAppButton.setOnClickListener(mShowWalletAppOnClickListener);
124     }
125 
renderViewLandscape()126     private void renderViewLandscape() {
127         mToolbarAppButton.setVisibility(VISIBLE);
128         mAppButton.setVisibility(GONE);
129         mCardLabel.setVisibility(GONE);
130         requireViewById(R.id.dynamic_placeholder).setVisibility(GONE);
131 
132         mToolbarAppButton.setOnClickListener(mShowWalletAppOnClickListener);
133     }
134 
135     @Override
onTouchEvent(MotionEvent event)136     public boolean onTouchEvent(MotionEvent event) {
137         // Forward touch events to card carousel to allow for swiping outside carousel bounds.
138         return mCardCarousel.onTouchEvent(event) || super.onTouchEvent(event);
139     }
140 
141     @Override
onCardScroll(WalletCardViewInfo centerCard, WalletCardViewInfo nextCard, float percentDistanceFromCenter)142     public void onCardScroll(WalletCardViewInfo centerCard, WalletCardViewInfo nextCard,
143             float percentDistanceFromCenter) {
144         CharSequence centerCardText = getLabelText(centerCard);
145         Drawable centerCardIcon = getHeaderIcon(mContext, centerCard);
146         renderActionButton(centerCard, mIsDeviceLocked, mIsUdfpsEnabled);
147         if (centerCard.isUiEquivalent(nextCard)) {
148             mCardLabel.setAlpha(1f);
149             mIcon.setAlpha(1f);
150             mActionButton.setAlpha(1f);
151         } else {
152             mCardLabel.setText(centerCardText);
153             mIcon.setImageDrawable(centerCardIcon);
154             mCardLabel.setAlpha(percentDistanceFromCenter);
155             mIcon.setAlpha(percentDistanceFromCenter);
156             mActionButton.setAlpha(percentDistanceFromCenter);
157         }
158     }
159 
160     /**
161      * Render and show card carousel view.
162      *
163      * <p>This is called only when {@param data} is not empty.</p>
164      *
165      * @param data a list of wallet cards information.
166      * @param selectedIndex index of the current selected card
167      * @param isDeviceLocked indicates whether the device is locked.
168      */
showCardCarousel( List<WalletCardViewInfo> data, int selectedIndex, boolean isDeviceLocked, boolean isUdfpsEnabled)169     void showCardCarousel(
170             List<WalletCardViewInfo> data,
171             int selectedIndex,
172             boolean isDeviceLocked,
173             boolean isUdfpsEnabled) {
174         boolean shouldAnimate =
175                 mCardCarousel.setData(data, selectedIndex, mIsDeviceLocked != isDeviceLocked);
176         mIsDeviceLocked = isDeviceLocked;
177         mIsUdfpsEnabled = isUdfpsEnabled;
178         mCardCarouselContainer.setVisibility(VISIBLE);
179         mCardCarousel.setVisibility(VISIBLE);
180         mErrorView.setVisibility(GONE);
181         mEmptyStateView.setVisibility(GONE);
182         mIcon.setImageDrawable(getHeaderIcon(mContext, data.get(selectedIndex)));
183         mCardLabel.setText(getLabelText(data.get(selectedIndex)));
184         updateViewForOrientation(getResources().getConfiguration().orientation);
185         renderActionButton(data.get(selectedIndex), isDeviceLocked, mIsUdfpsEnabled);
186         if (shouldAnimate) {
187             animateViewsShown(mIcon, mCardLabel, mActionButton);
188         }
189     }
190 
animateDismissal()191     void animateDismissal() {
192         if (mCardCarouselContainer.getVisibility() != VISIBLE) {
193             return;
194         }
195         mCardCarousel.animate().translationX(mAnimationTranslationX)
196                 .setInterpolator(mOutInterpolator)
197                 .setDuration(CAROUSEL_OUT_ANIMATION_DURATION)
198                 .start();
199         mCardCarouselContainer.animate()
200                 .alpha(0f)
201                 .setDuration(CARD_ANIM_ALPHA_DURATION)
202                 .setStartDelay(CARD_ANIM_ALPHA_DELAY)
203                 .start();
204     }
205 
showEmptyStateView(Drawable logo, CharSequence logoContentDescription, CharSequence label, OnClickListener clickListener)206     void showEmptyStateView(Drawable logo, CharSequence logoContentDescription, CharSequence label,
207             OnClickListener clickListener) {
208         mEmptyStateView.setVisibility(VISIBLE);
209         mErrorView.setVisibility(GONE);
210         mCardCarousel.setVisibility(GONE);
211         mIcon.setImageDrawable(logo);
212         mIcon.setContentDescription(logoContentDescription);
213         mCardLabel.setText(R.string.wallet_empty_state_label);
214         ImageView logoView = mEmptyStateView.requireViewById(R.id.empty_state_icon);
215         logoView.setImageDrawable(mContext.getDrawable(R.drawable.ic_qs_plus));
216         mEmptyStateView.<TextView>requireViewById(R.id.empty_state_title).setText(label);
217         mEmptyStateView.setOnClickListener(clickListener);
218     }
219 
showErrorMessage(@ullable CharSequence message)220     void showErrorMessage(@Nullable CharSequence message) {
221         if (TextUtils.isEmpty(message)) {
222             message = getResources().getText(R.string.wallet_error_generic);
223         }
224         mErrorView.setText(message);
225         mErrorView.setVisibility(VISIBLE);
226         mCardCarouselContainer.setVisibility(GONE);
227         mEmptyStateView.setVisibility(GONE);
228     }
229 
setDeviceLockedActionOnClickListener(OnClickListener onClickListener)230     void setDeviceLockedActionOnClickListener(OnClickListener onClickListener) {
231         mDeviceLockedActionOnClickListener = onClickListener;
232     }
233 
setShowWalletAppOnClickListener(OnClickListener onClickListener)234     void setShowWalletAppOnClickListener(OnClickListener onClickListener) {
235         mShowWalletAppOnClickListener = onClickListener;
236     }
237 
hide()238     void hide() {
239         setVisibility(GONE);
240     }
241 
show()242     void show() {
243         setVisibility(VISIBLE);
244     }
245 
hideErrorMessage()246     void hideErrorMessage() {
247         mErrorView.setVisibility(GONE);
248     }
249 
getCardCarousel()250     WalletCardCarousel getCardCarousel() {
251         return mCardCarousel;
252     }
253 
getActionButton()254     Button getActionButton() {
255         return mActionButton;
256     }
257 
258     @VisibleForTesting
getErrorView()259     TextView getErrorView() {
260         return mErrorView;
261     }
262 
263     @VisibleForTesting
getEmptyStateView()264     ViewGroup getEmptyStateView() {
265         return mEmptyStateView;
266     }
267 
268     @VisibleForTesting
getCardCarouselContainer()269     ViewGroup getCardCarouselContainer() {
270         return mCardCarouselContainer;
271     }
272 
273     @VisibleForTesting
getCardLabel()274     TextView getCardLabel() {
275         return mCardLabel;
276     }
277 
278     @Nullable
getHeaderIcon(Context context, WalletCardViewInfo walletCard)279     private static Drawable getHeaderIcon(Context context, WalletCardViewInfo walletCard) {
280         Drawable icon = walletCard.getIcon();
281         if (icon != null) {
282             icon.setTint(
283                     Utils.getColorAttrDefaultColor(
284                             context, com.android.internal.R.attr.colorAccentPrimary));
285         }
286         return icon;
287     }
288 
renderActionButton( WalletCardViewInfo walletCard, boolean isDeviceLocked, boolean isUdfpsEnabled)289     private void renderActionButton(
290             WalletCardViewInfo walletCard, boolean isDeviceLocked, boolean isUdfpsEnabled) {
291         CharSequence actionButtonText = getActionButtonText(walletCard);
292         if (!isUdfpsEnabled && actionButtonText != null) {
293             mActionButton.setVisibility(VISIBLE);
294             mActionButton.setText(actionButtonText);
295             mActionButton.setOnClickListener(
296                     isDeviceLocked
297                             ? mDeviceLockedActionOnClickListener
298                             : v -> {
299                         try {
300                             walletCard.getPendingIntent().send();
301                         } catch (PendingIntent.CanceledException e) {
302                             Log.w(TAG, "Error sending pending intent for wallet card.");
303                         }
304                     }
305             );
306         } else {
307             mActionButton.setVisibility(GONE);
308         }
309     }
310 
animateViewsShown(View... uiElements)311     private static void animateViewsShown(View... uiElements) {
312         for (View view : uiElements) {
313             if (view.getVisibility() == VISIBLE) {
314                 view.setAlpha(0f);
315                 view.animate().alpha(1f).setDuration(CAROUSEL_IN_ANIMATION_DURATION).start();
316             }
317         }
318     }
319 
getLabelText(WalletCardViewInfo card)320     private static CharSequence getLabelText(WalletCardViewInfo card) {
321         String[] rawLabel = card.getLabel().toString().split("\\n");
322         return rawLabel.length == 2 ? rawLabel[0] : card.getLabel();
323     }
324 
325     @Nullable
getActionButtonText(WalletCardViewInfo card)326     private static CharSequence getActionButtonText(WalletCardViewInfo card) {
327         String[] rawLabel = card.getLabel().toString().split("\\n");
328         return rawLabel.length == 2 ? rawLabel[1] : null;
329     }
330 
331     @Override
dispatchTouchEvent(MotionEvent ev)332     public boolean dispatchTouchEvent(MotionEvent ev) {
333         if (mFalsingCollector != null) {
334             mFalsingCollector.onTouchEvent(ev);
335         }
336 
337         boolean result = super.dispatchTouchEvent(ev);
338 
339         if (mFalsingCollector != null) {
340             mFalsingCollector.onMotionEventComplete();
341         }
342 
343         return result;
344     }
345 
setFalsingCollector(FalsingCollector falsingCollector)346     public void setFalsingCollector(FalsingCollector falsingCollector) {
347         mFalsingCollector = falsingCollector;
348     }
349 }
350