| /* |
| * Copyright (C) 2021 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.systemui.wallet.ui; |
| |
| import android.app.PendingIntent; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.SharedPreferences; |
| import android.content.res.Resources; |
| import android.graphics.drawable.Drawable; |
| import android.graphics.drawable.Icon; |
| import android.os.Handler; |
| import android.service.quickaccesswallet.GetWalletCardsError; |
| import android.service.quickaccesswallet.GetWalletCardsRequest; |
| import android.service.quickaccesswallet.GetWalletCardsResponse; |
| import android.service.quickaccesswallet.QuickAccessWalletClient; |
| import android.service.quickaccesswallet.SelectWalletCardRequest; |
| import android.service.quickaccesswallet.WalletCard; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.FrameLayout; |
| |
| import androidx.annotation.NonNull; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.logging.UiEventLogger; |
| import com.android.keyguard.KeyguardUpdateMonitor; |
| import com.android.systemui.R; |
| import com.android.systemui.plugins.ActivityStarter; |
| import com.android.systemui.plugins.FalsingManager; |
| import com.android.systemui.settings.UserTracker; |
| import com.android.systemui.statusbar.policy.KeyguardStateController; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.concurrent.Executor; |
| import java.util.concurrent.TimeUnit; |
| |
| /** Controller for the wallet card carousel screen. */ |
| public class WalletScreenController implements |
| WalletCardCarousel.OnSelectionListener, |
| QuickAccessWalletClient.OnWalletCardsRetrievedCallback, |
| KeyguardStateController.Callback { |
| |
| private static final String TAG = "WalletScreenCtrl"; |
| private static final String PREFS_WALLET_VIEW_HEIGHT = "wallet_view_height"; |
| private static final int MAX_CARDS = 10; |
| private static final long SELECTION_DELAY_MILLIS = TimeUnit.SECONDS.toMillis(30); |
| |
| private Context mContext; |
| private final QuickAccessWalletClient mWalletClient; |
| private final ActivityStarter mActivityStarter; |
| private final Executor mExecutor; |
| private final Handler mHandler; |
| private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; |
| private final KeyguardStateController mKeyguardStateController; |
| private final Runnable mSelectionRunnable = this::selectCard; |
| private final SharedPreferences mPrefs; |
| private final WalletView mWalletView; |
| private final WalletCardCarousel mCardCarousel; |
| private final FalsingManager mFalsingManager; |
| private final UiEventLogger mUiEventLogger; |
| |
| @VisibleForTesting String mSelectedCardId; |
| @VisibleForTesting boolean mIsDismissed; |
| |
| public WalletScreenController( |
| Context context, |
| WalletView walletView, |
| QuickAccessWalletClient walletClient, |
| ActivityStarter activityStarter, |
| Executor executor, |
| Handler handler, |
| UserTracker userTracker, |
| FalsingManager falsingManager, |
| KeyguardUpdateMonitor keyguardUpdateMonitor, |
| KeyguardStateController keyguardStateController, |
| UiEventLogger uiEventLogger) { |
| mContext = context; |
| mWalletClient = walletClient; |
| mActivityStarter = activityStarter; |
| mExecutor = executor; |
| mHandler = handler; |
| mFalsingManager = falsingManager; |
| mKeyguardUpdateMonitor = keyguardUpdateMonitor; |
| mKeyguardStateController = keyguardStateController; |
| mUiEventLogger = uiEventLogger; |
| mPrefs = userTracker.getUserContext().getSharedPreferences(TAG, Context.MODE_PRIVATE); |
| mWalletView = walletView; |
| mWalletView.setMinimumHeight(getExpectedMinHeight()); |
| mWalletView.setLayoutParams( |
| new FrameLayout.LayoutParams( |
| ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); |
| mCardCarousel = mWalletView.getCardCarousel(); |
| if (mCardCarousel != null) { |
| mCardCarousel.setSelectionListener(this); |
| } |
| } |
| |
| /** |
| * Implements {@link QuickAccessWalletClient.OnWalletCardsRetrievedCallback}. Called when cards |
| * are retrieved successfully from the service. This is called on {@link #mExecutor}. |
| */ |
| @Override |
| public void onWalletCardsRetrieved(@NonNull GetWalletCardsResponse response) { |
| if (mIsDismissed) { |
| return; |
| } |
| Log.i(TAG, "Successfully retrieved wallet cards."); |
| List<WalletCard> walletCards = response.getWalletCards(); |
| List<WalletCardViewInfo> data = new ArrayList<>(walletCards.size()); |
| for (WalletCard card : walletCards) { |
| data.add(new QAWalletCardViewInfo(mContext, card)); |
| } |
| |
| // Get on main thread for UI updates. |
| mHandler.post(() -> { |
| if (mIsDismissed) { |
| return; |
| } |
| if (data.isEmpty()) { |
| showEmptyStateView(); |
| } else { |
| int selectedIndex = response.getSelectedIndex(); |
| if (selectedIndex >= data.size()) { |
| Log.w(TAG, "Invalid selected card index, showing empty state."); |
| showEmptyStateView(); |
| } else { |
| boolean isUdfpsEnabled = mKeyguardUpdateMonitor.isUdfpsEnrolled() |
| && mKeyguardUpdateMonitor.isFingerprintDetectionRunning(); |
| mWalletView.showCardCarousel( |
| data, |
| selectedIndex, |
| !mKeyguardStateController.isUnlocked(), |
| isUdfpsEnabled); |
| } |
| } |
| mUiEventLogger.log(WalletUiEvent.QAW_IMPRESSION); |
| removeMinHeightAndRecordHeightOnLayout(); |
| }); |
| } |
| |
| /** |
| * Implements {@link QuickAccessWalletClient.OnWalletCardsRetrievedCallback}. Called when there |
| * is an error during card retrieval. This will be run on the {@link #mExecutor}. |
| */ |
| @Override |
| public void onWalletCardRetrievalError(@NonNull GetWalletCardsError error) { |
| mHandler.post(() -> { |
| if (mIsDismissed) { |
| return; |
| } |
| mWalletView.showErrorMessage(error.getMessage()); |
| }); |
| } |
| |
| @Override |
| public void onKeyguardFadingAwayChanged() { |
| queryWalletCards(); |
| } |
| |
| @Override |
| public void onUnlockedChanged() { |
| queryWalletCards(); |
| } |
| |
| @Override |
| public void onCardSelected(@NonNull WalletCardViewInfo card) { |
| if (mIsDismissed) { |
| return; |
| } |
| if (mSelectedCardId != null && !mSelectedCardId.equals(card.getCardId())) { |
| mUiEventLogger.log(WalletUiEvent.QAW_CHANGE_CARD); |
| } |
| mSelectedCardId = card.getCardId(); |
| selectCard(); |
| } |
| |
| private void selectCard() { |
| mHandler.removeCallbacks(mSelectionRunnable); |
| String selectedCardId = mSelectedCardId; |
| if (mIsDismissed || selectedCardId == null) { |
| return; |
| } |
| mWalletClient.selectWalletCard(new SelectWalletCardRequest(selectedCardId)); |
| // Re-selecting the card keeps the connection bound so we continue to get service events |
| // even if the user keeps it open for a long time. |
| mHandler.postDelayed(mSelectionRunnable, SELECTION_DELAY_MILLIS); |
| } |
| |
| |
| |
| @Override |
| public void onCardClicked(@NonNull WalletCardViewInfo cardInfo) { |
| if (!mKeyguardStateController.isUnlocked() |
| && mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { |
| return; |
| } |
| if (!(cardInfo instanceof QAWalletCardViewInfo) |
| || ((QAWalletCardViewInfo) cardInfo).mWalletCard == null |
| || ((QAWalletCardViewInfo) cardInfo).mWalletCard.getPendingIntent() == null) { |
| return; |
| } |
| |
| if (!mKeyguardStateController.isUnlocked()) { |
| mUiEventLogger.log(WalletUiEvent.QAW_UNLOCK_FROM_CARD_CLICK); |
| } |
| mUiEventLogger.log(WalletUiEvent.QAW_CLICK_CARD); |
| |
| mActivityStarter.startActivity( |
| ((QAWalletCardViewInfo) cardInfo).mWalletCard.getPendingIntent().getIntent(), true); |
| } |
| |
| @Override |
| public void queryWalletCards() { |
| if (mIsDismissed) { |
| return; |
| } |
| int cardWidthPx = mCardCarousel.getCardWidthPx(); |
| int cardHeightPx = mCardCarousel.getCardHeightPx(); |
| if (cardWidthPx == 0 || cardHeightPx == 0) { |
| return; |
| } |
| |
| mWalletView.show(); |
| mWalletView.hideErrorMessage(); |
| int iconSizePx = |
| mContext |
| .getResources() |
| .getDimensionPixelSize(R.dimen.wallet_screen_header_icon_size); |
| GetWalletCardsRequest request = |
| new GetWalletCardsRequest(cardWidthPx, cardHeightPx, iconSizePx, MAX_CARDS); |
| mWalletClient.getWalletCards(mExecutor, request, this); |
| } |
| |
| void onDismissed() { |
| if (mIsDismissed) { |
| return; |
| } |
| mIsDismissed = true; |
| mSelectedCardId = null; |
| mHandler.removeCallbacks(mSelectionRunnable); |
| mWalletClient.notifyWalletDismissed(); |
| mWalletView.animateDismissal(); |
| // clear refs to the Wallet Activity |
| mContext = null; |
| } |
| |
| private void showEmptyStateView() { |
| Drawable logo = mWalletClient.getLogo(); |
| CharSequence logoContentDesc = mWalletClient.getServiceLabel(); |
| CharSequence label = mWalletClient.getShortcutLongLabel(); |
| Intent intent = mWalletClient.createWalletIntent(); |
| if (logo == null |
| || TextUtils.isEmpty(logoContentDesc) |
| || TextUtils.isEmpty(label) |
| || intent == null) { |
| Log.w(TAG, "QuickAccessWalletService manifest entry mis-configured"); |
| // Issue is not likely to be resolved until manifest entries are enabled. |
| // Hide wallet feature until then. |
| mWalletView.hide(); |
| mPrefs.edit().putInt(PREFS_WALLET_VIEW_HEIGHT, 0).apply(); |
| } else { |
| mWalletView.showEmptyStateView( |
| logo, |
| logoContentDesc, |
| label, |
| v -> mActivityStarter.startActivity(intent, true)); |
| } |
| } |
| |
| private int getExpectedMinHeight() { |
| int expectedHeight = mPrefs.getInt(PREFS_WALLET_VIEW_HEIGHT, -1); |
| if (expectedHeight == -1) { |
| Resources res = mContext.getResources(); |
| expectedHeight = res.getDimensionPixelSize(R.dimen.min_wallet_empty_height); |
| } |
| return expectedHeight; |
| } |
| |
| private void removeMinHeightAndRecordHeightOnLayout() { |
| mWalletView.setMinimumHeight(0); |
| mWalletView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { |
| @Override |
| public void onLayoutChange(View v, int left, int top, int right, int bottom, |
| int oldLeft, int oldTop, int oldRight, int oldBottom) { |
| mWalletView.removeOnLayoutChangeListener(this); |
| mPrefs.edit().putInt(PREFS_WALLET_VIEW_HEIGHT, bottom - top).apply(); |
| } |
| }); |
| } |
| |
| @VisibleForTesting |
| static class QAWalletCardViewInfo implements WalletCardViewInfo { |
| |
| private final WalletCard mWalletCard; |
| private final Drawable mCardDrawable; |
| private final Drawable mIconDrawable; |
| |
| /** |
| * Constructor is called on background executor, so it is safe to load drawables |
| * synchronously. |
| */ |
| QAWalletCardViewInfo(Context context, WalletCard walletCard) { |
| mWalletCard = walletCard; |
| mCardDrawable = mWalletCard.getCardImage().loadDrawable(context); |
| Icon icon = mWalletCard.getCardIcon(); |
| mIconDrawable = icon == null ? null : icon.loadDrawable(context); |
| } |
| |
| @Override |
| public String getCardId() { |
| return mWalletCard.getCardId(); |
| } |
| |
| @Override |
| public Drawable getCardDrawable() { |
| return mCardDrawable; |
| } |
| |
| @Override |
| public CharSequence getContentDescription() { |
| return mWalletCard.getContentDescription(); |
| } |
| |
| @Override |
| public Drawable getIcon() { |
| return mIconDrawable; |
| } |
| |
| @Override |
| public CharSequence getLabel() { |
| CharSequence label = mWalletCard.getCardLabel(); |
| if (label == null) { |
| return ""; |
| } |
| return label; |
| } |
| |
| @Override |
| public PendingIntent getPendingIntent() { |
| return mWalletCard.getPendingIntent(); |
| } |
| } |
| } |