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