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.util.WalletCardUtilsKt.getPaymentCards; 20 21 import android.app.PendingIntent; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.SharedPreferences; 25 import android.content.res.Resources; 26 import android.graphics.drawable.Drawable; 27 import android.graphics.drawable.Icon; 28 import android.os.Handler; 29 import android.service.quickaccesswallet.GetWalletCardsError; 30 import android.service.quickaccesswallet.GetWalletCardsRequest; 31 import android.service.quickaccesswallet.GetWalletCardsResponse; 32 import android.service.quickaccesswallet.QuickAccessWalletClient; 33 import android.service.quickaccesswallet.SelectWalletCardRequest; 34 import android.service.quickaccesswallet.WalletCard; 35 import android.text.TextUtils; 36 import android.util.Log; 37 import android.view.View; 38 import android.view.ViewGroup; 39 import android.widget.FrameLayout; 40 41 import androidx.annotation.NonNull; 42 43 import com.android.internal.annotations.VisibleForTesting; 44 import com.android.internal.logging.UiEventLogger; 45 import com.android.keyguard.KeyguardUpdateMonitor; 46 import com.android.systemui.plugins.ActivityStarter; 47 import com.android.systemui.plugins.FalsingManager; 48 import com.android.systemui.res.R; 49 import com.android.systemui.settings.UserTracker; 50 import com.android.systemui.statusbar.policy.KeyguardStateController; 51 52 import java.util.List; 53 import java.util.concurrent.Executor; 54 import java.util.concurrent.TimeUnit; 55 import java.util.stream.Collectors; 56 57 /** Controller for the wallet card carousel screen. */ 58 public class WalletScreenController implements 59 WalletCardCarousel.OnSelectionListener, 60 QuickAccessWalletClient.OnWalletCardsRetrievedCallback, 61 KeyguardStateController.Callback { 62 63 private static final String TAG = "WalletScreenCtrl"; 64 private static final String PREFS_WALLET_VIEW_HEIGHT = "wallet_view_height"; 65 private static final int MAX_CARDS = 10; 66 private static final long SELECTION_DELAY_MILLIS = TimeUnit.SECONDS.toMillis(30); 67 68 private Context mContext; 69 private final QuickAccessWalletClient mWalletClient; 70 private final ActivityStarter mActivityStarter; 71 private final Executor mExecutor; 72 private final Handler mHandler; 73 private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; 74 private final KeyguardStateController mKeyguardStateController; 75 private final Runnable mSelectionRunnable = this::selectCard; 76 private final SharedPreferences mPrefs; 77 private final WalletView mWalletView; 78 private final WalletCardCarousel mCardCarousel; 79 private final FalsingManager mFalsingManager; 80 private final UiEventLogger mUiEventLogger; 81 82 83 @VisibleForTesting 84 String mSelectedCardId; 85 @VisibleForTesting 86 boolean mIsDismissed; 87 WalletScreenController( Context context, WalletView walletView, QuickAccessWalletClient walletClient, ActivityStarter activityStarter, Executor executor, Handler handler, UserTracker userTracker, FalsingManager falsingManager, KeyguardUpdateMonitor keyguardUpdateMonitor, KeyguardStateController keyguardStateController, UiEventLogger uiEventLogger)88 public WalletScreenController( 89 Context context, 90 WalletView walletView, 91 QuickAccessWalletClient walletClient, 92 ActivityStarter activityStarter, 93 Executor executor, 94 Handler handler, 95 UserTracker userTracker, 96 FalsingManager falsingManager, 97 KeyguardUpdateMonitor keyguardUpdateMonitor, 98 KeyguardStateController keyguardStateController, 99 UiEventLogger uiEventLogger) { 100 mContext = context; 101 mWalletClient = walletClient; 102 mActivityStarter = activityStarter; 103 mExecutor = executor; 104 mHandler = handler; 105 mFalsingManager = falsingManager; 106 mKeyguardUpdateMonitor = keyguardUpdateMonitor; 107 mKeyguardStateController = keyguardStateController; 108 mUiEventLogger = uiEventLogger; 109 mPrefs = userTracker.getUserContext().getSharedPreferences(TAG, Context.MODE_PRIVATE); 110 mWalletView = walletView; 111 mWalletView.setMinimumHeight(getExpectedMinHeight()); 112 mWalletView.setLayoutParams( 113 new FrameLayout.LayoutParams( 114 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); 115 mCardCarousel = mWalletView.getCardCarousel(); 116 if (mCardCarousel != null) { 117 mCardCarousel.setSelectionListener(this); 118 } 119 } 120 121 /** 122 * Implements {@link QuickAccessWalletClient.OnWalletCardsRetrievedCallback}. Called when cards 123 * are retrieved successfully from the service. This is called on {@link #mExecutor}. 124 */ 125 @Override onWalletCardsRetrieved(@onNull GetWalletCardsResponse response)126 public void onWalletCardsRetrieved(@NonNull GetWalletCardsResponse response) { 127 if (mIsDismissed) { 128 return; 129 } 130 Log.i(TAG, "Successfully retrieved wallet cards."); 131 List<WalletCard> walletCards = getPaymentCards(response.getWalletCards()); 132 133 List<WalletCardViewInfo> paymentCardData = walletCards.stream().map( 134 card -> new QAWalletCardViewInfo(mContext, card) 135 ).collect(Collectors.toList()); 136 137 // Get on main thread for UI updates. 138 mHandler.post(() -> { 139 if (mIsDismissed) { 140 return; 141 } 142 if (paymentCardData.isEmpty()) { 143 showEmptyStateView(); 144 } else { 145 int selectedIndex = response.getSelectedIndex(); 146 if (selectedIndex >= paymentCardData.size()) { 147 Log.w(TAG, "Invalid selected card index, showing empty state."); 148 showEmptyStateView(); 149 } else { 150 boolean isUdfpsEnabled = mKeyguardUpdateMonitor.isUdfpsEnrolled() 151 && mKeyguardUpdateMonitor.isFingerprintDetectionRunning(); 152 mWalletView.showCardCarousel( 153 paymentCardData, 154 selectedIndex, 155 !mKeyguardStateController.isUnlocked(), 156 isUdfpsEnabled); 157 } 158 } 159 mUiEventLogger.log(WalletUiEvent.QAW_IMPRESSION); 160 removeMinHeightAndRecordHeightOnLayout(); 161 }); 162 } 163 164 /** 165 * Implements {@link QuickAccessWalletClient.OnWalletCardsRetrievedCallback}. Called when there 166 * is an error during card retrieval. This will be run on the {@link #mExecutor}. 167 */ 168 @Override onWalletCardRetrievalError(@onNull GetWalletCardsError error)169 public void onWalletCardRetrievalError(@NonNull GetWalletCardsError error) { 170 mHandler.post(() -> { 171 if (mIsDismissed) { 172 return; 173 } 174 mWalletView.showErrorMessage(error.getMessage()); 175 }); 176 } 177 178 @Override onKeyguardFadingAwayChanged()179 public void onKeyguardFadingAwayChanged() { 180 queryWalletCards(); 181 } 182 183 @Override onUnlockedChanged()184 public void onUnlockedChanged() { 185 queryWalletCards(); 186 } 187 188 @Override onUncenteredClick(int position)189 public void onUncenteredClick(int position) { 190 if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { 191 return; 192 } 193 mCardCarousel.smoothScrollToPosition(position); 194 } 195 196 @Override onCardSelected(@onNull WalletCardViewInfo card)197 public void onCardSelected(@NonNull WalletCardViewInfo card) { 198 if (mIsDismissed) { 199 return; 200 } 201 if (mSelectedCardId != null && !mSelectedCardId.equals(card.getCardId())) { 202 mUiEventLogger.log(WalletUiEvent.QAW_CHANGE_CARD); 203 } 204 mSelectedCardId = card.getCardId(); 205 selectCard(); 206 } 207 selectCard()208 private void selectCard() { 209 mHandler.removeCallbacks(mSelectionRunnable); 210 String selectedCardId = mSelectedCardId; 211 if (mIsDismissed || selectedCardId == null) { 212 return; 213 } 214 mWalletClient.selectWalletCard(new SelectWalletCardRequest(selectedCardId)); 215 // Re-selecting the card keeps the connection bound so we continue to get service events 216 // even if the user keeps it open for a long time. 217 mHandler.postDelayed(mSelectionRunnable, SELECTION_DELAY_MILLIS); 218 } 219 220 221 @Override onCardClicked(@onNull WalletCardViewInfo cardInfo)222 public void onCardClicked(@NonNull WalletCardViewInfo cardInfo) { 223 if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { 224 return; 225 } 226 if (!(cardInfo instanceof QAWalletCardViewInfo) 227 || ((QAWalletCardViewInfo) cardInfo).mWalletCard == null 228 || ((QAWalletCardViewInfo) cardInfo).mWalletCard.getPendingIntent() == null) { 229 return; 230 } 231 232 if (!mKeyguardStateController.isUnlocked()) { 233 mUiEventLogger.log(WalletUiEvent.QAW_UNLOCK_FROM_CARD_CLICK); 234 } 235 mUiEventLogger.log(WalletUiEvent.QAW_CLICK_CARD); 236 237 mActivityStarter.startPendingIntentDismissingKeyguard(cardInfo.getPendingIntent()); 238 } 239 240 @Override queryWalletCards()241 public void queryWalletCards() { 242 if (mIsDismissed) { 243 return; 244 } 245 int cardWidthPx = mCardCarousel.getCardWidthPx(); 246 int cardHeightPx = mCardCarousel.getCardHeightPx(); 247 if (cardWidthPx == 0 || cardHeightPx == 0) { 248 return; 249 } 250 251 mWalletView.show(); 252 mWalletView.hideErrorMessage(); 253 int iconSizePx = 254 mContext 255 .getResources() 256 .getDimensionPixelSize(R.dimen.wallet_screen_header_icon_size); 257 GetWalletCardsRequest request = 258 new GetWalletCardsRequest(cardWidthPx, cardHeightPx, iconSizePx, MAX_CARDS); 259 mWalletClient.getWalletCards(mExecutor, request, this); 260 } 261 onDismissed()262 void onDismissed() { 263 if (mIsDismissed) { 264 return; 265 } 266 mIsDismissed = true; 267 mSelectedCardId = null; 268 mHandler.removeCallbacks(mSelectionRunnable); 269 mWalletClient.notifyWalletDismissed(); 270 mWalletView.animateDismissal(); 271 // clear refs to the Wallet Activity 272 mContext = null; 273 } 274 showEmptyStateView()275 private void showEmptyStateView() { 276 Drawable logo = mWalletClient.getLogo(); 277 CharSequence logoContentDesc = mWalletClient.getServiceLabel(); 278 CharSequence label = mWalletClient.getShortcutLongLabel(); 279 Intent intent = mWalletClient.createWalletIntent(); 280 if (logo == null 281 || TextUtils.isEmpty(logoContentDesc) 282 || TextUtils.isEmpty(label) 283 || intent == null) { 284 Log.w(TAG, "QuickAccessWalletService manifest entry mis-configured"); 285 // Issue is not likely to be resolved until manifest entries are enabled. 286 // Hide wallet feature until then. 287 mWalletView.hide(); 288 mPrefs.edit().putInt(PREFS_WALLET_VIEW_HEIGHT, 0).apply(); 289 } else { 290 mWalletView.showEmptyStateView( 291 logo, 292 logoContentDesc, 293 label, 294 v -> mActivityStarter.startActivity(intent, true)); 295 } 296 } 297 getExpectedMinHeight()298 private int getExpectedMinHeight() { 299 int expectedHeight = mPrefs.getInt(PREFS_WALLET_VIEW_HEIGHT, -1); 300 if (expectedHeight == -1) { 301 Resources res = mContext.getResources(); 302 expectedHeight = res.getDimensionPixelSize(R.dimen.min_wallet_empty_height); 303 } 304 return expectedHeight; 305 } 306 removeMinHeightAndRecordHeightOnLayout()307 private void removeMinHeightAndRecordHeightOnLayout() { 308 mWalletView.setMinimumHeight(0); 309 mWalletView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { 310 @Override 311 public void onLayoutChange(View v, int left, int top, int right, int bottom, 312 int oldLeft, int oldTop, int oldRight, int oldBottom) { 313 mWalletView.removeOnLayoutChangeListener(this); 314 mPrefs.edit().putInt(PREFS_WALLET_VIEW_HEIGHT, bottom - top).apply(); 315 } 316 }); 317 } 318 319 @VisibleForTesting 320 static class QAWalletCardViewInfo implements WalletCardViewInfo { 321 322 private final WalletCard mWalletCard; 323 private final Drawable mCardDrawable; 324 private final Drawable mIconDrawable; 325 326 /** 327 * Constructor is called on background executor, so it is safe to load drawables 328 * synchronously. 329 */ QAWalletCardViewInfo(Context context, WalletCard walletCard)330 QAWalletCardViewInfo(Context context, WalletCard walletCard) { 331 mWalletCard = walletCard; 332 Icon cardImageIcon = mWalletCard.getCardImage(); 333 if (cardImageIcon.getType() == Icon.TYPE_URI) { 334 mCardDrawable = null; 335 } else { 336 mCardDrawable = mWalletCard.getCardImage().loadDrawable(context); 337 } 338 Icon icon = mWalletCard.getCardIcon(); 339 mIconDrawable = icon == null ? null : icon.loadDrawable(context); 340 } 341 342 @Override getCardId()343 public String getCardId() { 344 return mWalletCard.getCardId(); 345 } 346 347 @Override getCardDrawable()348 public Drawable getCardDrawable() { 349 return mCardDrawable; 350 } 351 352 @Override getContentDescription()353 public CharSequence getContentDescription() { 354 return mWalletCard.getContentDescription(); 355 } 356 357 @Override getIcon()358 public Drawable getIcon() { 359 return mIconDrawable; 360 } 361 362 @Override getLabel()363 public CharSequence getLabel() { 364 CharSequence label = mWalletCard.getCardLabel(); 365 if (label == null) { 366 return ""; 367 } 368 return label; 369 } 370 371 @Override getPendingIntent()372 public PendingIntent getPendingIntent() { 373 return mWalletCard.getPendingIntent(); 374 } 375 } 376 } 377