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