blob: 4e9ce4f9e04e7195932bff41b1a1b509a1fffb2e [file] [log] [blame]
/*
* Copyright (C) 2020 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.plugin.globalactions.wallet;
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.os.UserHandle;
import android.os.Looper;
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.service.quickaccesswallet.WalletServiceEvent;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.FrameLayout;
import com.android.systemui.plugin.globalactions.wallet.WalletPopupMenu.OverflowItem;
import com.android.systemui.plugins.GlobalActionsPanelPlugin;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class WalletPanelViewController implements
GlobalActionsPanelPlugin.PanelViewController,
WalletCardCarousel.OnSelectionListener,
QuickAccessWalletClient.OnWalletCardsRetrievedCallback,
QuickAccessWalletClient.WalletServiceEventListener {
private static final String TAG = "WalletPanelViewCtrl";
private static final int MAX_CARDS = 10;
private static final long SELECTION_DELAY_MILLIS = TimeUnit.SECONDS.toMillis(30);
private static final String PREFS_WALLET_VIEW_HEIGHT = "wallet_view_height";
private static final String PREFS_HAS_CARDS = "has_cards";
private static final String SETTINGS_PKG = "com.android.settings";
private static final String SETTINGS_ACTION = SETTINGS_PKG + ".GLOBAL_ACTIONS_PANEL_SETTINGS";
private final Context mSysuiContext;
private final Context mPluginContext;
private final QuickAccessWalletClient mWalletClient;
private final WalletView mWalletView;
private final WalletCardCarousel mWalletCardCarousel;
private final GlobalActionsPanelPlugin.Callbacks mPluginCallbacks;
private final ExecutorService mExecutor;
private final Handler mHandler;
private final Runnable mSelectionRunnable = this::selectCard;
private final SharedPreferences mPrefs;
private boolean mIsDeviceLocked;
private boolean mIsDismissed;
private boolean mHasRegisteredListener;
private String mSelectedCardId;
public WalletPanelViewController(
Context sysuiContext,
Context pluginContext,
QuickAccessWalletClient walletClient,
GlobalActionsPanelPlugin.Callbacks pluginCallbacks,
boolean isDeviceLocked) {
mSysuiContext = sysuiContext;
mPluginContext = pluginContext;
mWalletClient = walletClient;
mPrefs = mSysuiContext.getSharedPreferences(TAG, Context.MODE_PRIVATE);
mPluginCallbacks = pluginCallbacks;
mIsDeviceLocked = isDeviceLocked;
mWalletView = new WalletView(pluginContext);
mWalletView.setMinimumHeight(getExpectedMinHeight());
mWalletView.setLayoutParams(
new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT));
mWalletCardCarousel = mWalletView.getCardCarousel();
mWalletCardCarousel.setSelectionListener(this);
mHandler = new Handler(Looper.myLooper());
mExecutor = Executors.newSingleThreadExecutor();
if (!mPrefs.getBoolean(PREFS_HAS_CARDS, false)) {
// The empty state view is shown preemptively when cards were not returned last time
// to decrease perceived latency.
showEmptyStateView();
}
}
/**
* Implements {@link GlobalActionsPanelPlugin.PanelViewController}. Returns the {@link View}
* containing the Quick Access Wallet.
*/
@Override
public View getPanelContent() {
return mWalletView;
}
/**
* Implements {@link GlobalActionsPanelPlugin.PanelViewController}. Invoked when the view
* containing the Quick Access Wallet is dismissed.
*/
@Override
public void onDismissed() {
if (mIsDismissed) {
return;
}
mIsDismissed = true;
mSelectedCardId = null;
mHandler.removeCallbacks(mSelectionRunnable);
mWalletClient.notifyWalletDismissed();
mWalletClient.removeWalletServiceEventListener(this);
mWalletView.animateDismissal();
}
/**
* Implements {@link GlobalActionsPanelPlugin.PanelViewController}. Invoked when the device is
* either locked or unlocked while the wallet is visible.
*/
@Override
public void onDeviceLockStateChanged(boolean deviceLocked) {
if (mIsDismissed || mIsDeviceLocked == deviceLocked || !mIsDeviceLocked) {
// Disregard repeat events and events after unlock
return;
}
mIsDeviceLocked = deviceLocked;
// Cards are re-queried because the wallet application may wish to change card art, icons,
// text, or other attributes depending on the lock state of the device.
queryWalletCards();
}
/**
* Query wallet cards from the client and display them on screen.
*/
void queryWalletCards() {
if (mIsDismissed) {
return;
}
if (!mHasRegisteredListener) {
// Listener is registered even when device is locked. Should only be registered once.
mWalletClient.addWalletServiceEventListener(this);
mHasRegisteredListener = true;
}
if (mIsDeviceLocked && !mWalletClient.isWalletFeatureAvailableWhenDeviceLocked()) {
mWalletView.hide();
return;
}
mWalletView.show();
mWalletView.hideErrorMessage();
int cardWidthPx = mWalletCardCarousel.getCardWidthPx();
int cardHeightPx = mWalletCardCarousel.getCardHeightPx();
int iconSizePx = mWalletView.getIconSizePx();
GetWalletCardsRequest request =
new GetWalletCardsRequest(cardWidthPx, cardHeightPx, iconSizePx, MAX_CARDS);
mWalletClient.getWalletCards(mExecutor, request, this);
}
/**
* Implements {@link QuickAccessWalletClient.OnWalletCardsRetrievedCallback}. Called when cards
* are retrieved successfully from the service. This is called on {@link #mExecutor}.
*/
@Override
public void onWalletCardsRetrieved(GetWalletCardsResponse response) {
if (mIsDismissed) {
return;
}
List<WalletCard> walletCards = response.getWalletCards();
List<WalletCardViewInfo> data = new ArrayList<>(walletCards.size());
for (WalletCard card : walletCards) {
data.add(new QAWalletCardViewInfo(card));
}
// Get on main thread for UI updates
mWalletView.post(() -> {
if (mIsDismissed) {
return;
}
if (data.isEmpty()) {
showEmptyStateView();
} else {
mWalletView.showCardCarousel(data, response.getSelectedIndex(), getOverflowItems());
}
// The empty state view will not be shown preemptively next time if cards were returned
mPrefs.edit().putBoolean(PREFS_HAS_CARDS, !data.isEmpty()).apply();
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(GetWalletCardsError error) {
mWalletView.post(() -> {
if (mIsDismissed) {
return;
}
mWalletView.showErrorMessage(error.getMessage());
});
}
/**
* Implements {@link QuickAccessWalletClient.WalletServiceEventListener}. Called when the wallet
* application propagates an event, such as an NFC tap, to the quick access wallet view.
*/
@Override
public void onWalletServiceEvent(WalletServiceEvent event) {
if (mIsDismissed) {
return;
}
switch (event.getEventType()) {
case WalletServiceEvent.TYPE_NFC_PAYMENT_STARTED:
mPluginCallbacks.dismissGlobalActionsMenu();
onDismissed();
break;
case WalletServiceEvent.TYPE_WALLET_CARDS_UPDATED:
queryWalletCards();
break;
default:
Log.w(TAG, "onWalletServiceEvent: Unknown event type");
}
}
/**
* Implements {@link WalletCardCarousel.OnSelectionListener}. Called when the user selects a
* card from the carousel by scrolling to it.
*/
@Override
public void onCardSelected(WalletCardViewInfo card) {
if (mIsDismissed) {
return;
}
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);
}
/**
* Implements {@link WalletCardCarousel.OnSelectionListener}. Called when the user clicks on a
* card.
*/
@Override
public void onCardClicked(WalletCardViewInfo card) {
if (mIsDismissed) {
return;
}
PendingIntent pendingIntent = ((QAWalletCardViewInfo) card).mWalletCard.getPendingIntent();
startPendingIntent(pendingIntent);
}
private OverflowItem[] getOverflowItems() {
CharSequence walletLabel = mWalletClient.getShortcutShortLabel();
Intent walletIntent = mWalletClient.createWalletIntent();
CharSequence settingsLabel = mPluginContext.getString(R.string.settings);
Intent settingsIntent = new Intent(SETTINGS_ACTION).setPackage(SETTINGS_PKG);
OverflowItem settings = new OverflowItem(settingsLabel, () -> startIntent(settingsIntent));
if (!TextUtils.isEmpty(walletLabel) && walletIntent != null) {
OverflowItem wallet = new OverflowItem(walletLabel, () -> startIntent(walletIntent));
return new OverflowItem[]{wallet, settings};
} else {
return new OverflowItem[]{settings};
}
}
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 -> startIntent(intent));
}
}
private void startIntent(Intent intent) {
PendingIntent pendingIntent = PendingIntent.getActivity(mSysuiContext, 0, intent,
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT);
startPendingIntent(pendingIntent);
}
private void startPendingIntent(PendingIntent pendingIntent) {
mPluginCallbacks.startPendingIntentDismissingKeyguard(pendingIntent);
mPluginCallbacks.dismissGlobalActionsMenu();
onDismissed();
}
/**
* The total view height depends on whether cards are shown or not. Since it is not known at
* construction time whether cards will be available, the best we can do is set the height to
* whatever it was the last time. Setting the height correctly ahead of time is important
* because Home Controls are shown below the wallet and may be displayed before card data is
* loaded, causing the home controls to jump down when card data arrives.
*/
private int getExpectedMinHeight() {
int expectedHeight = mPrefs.getInt(PREFS_WALLET_VIEW_HEIGHT, -1);
if (expectedHeight == -1) {
Resources res = mPluginContext.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();
}
});
}
private 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(WalletCard walletCard) {
mWalletCard = walletCard;
Icon cardImage = mWalletCard.getCardImage();
if (cardImage.getType() == Icon.TYPE_URI) {
// Do not allow icon created with content URI.
mCardDrawable = null;
} else {
mCardDrawable =
mWalletCard.getCardImage().loadDrawable(mPluginContext);
}
Icon icon = mWalletCard.getCardIcon();
mIconDrawable = icon == null ? null : icon.loadDrawable(mPluginContext);
}
@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 getText() {
return mWalletCard.getCardLabel();
}
}
}