| /* |
| * 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(); |
| } |
| } |
| } |