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