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