blob: 2e183b38a7dccaa2b824e9314d3919395b9f5f19 [file] [log] [blame]
/*
* 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();
}
}
}