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