• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.plugin.globalactions.wallet;
18 
19 import android.app.PendingIntent;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.SharedPreferences;
23 import android.content.res.Resources;
24 import android.graphics.drawable.Drawable;
25 import android.graphics.drawable.Icon;
26 import android.os.Handler;
27 import android.os.Looper;
28 import android.service.quickaccesswallet.GetWalletCardsError;
29 import android.service.quickaccesswallet.GetWalletCardsRequest;
30 import android.service.quickaccesswallet.GetWalletCardsResponse;
31 import android.service.quickaccesswallet.QuickAccessWalletClient;
32 import android.service.quickaccesswallet.SelectWalletCardRequest;
33 import android.service.quickaccesswallet.WalletCard;
34 import android.service.quickaccesswallet.WalletServiceEvent;
35 import android.text.TextUtils;
36 import android.util.Log;
37 import android.view.View;
38 import android.widget.FrameLayout;
39 
40 import com.android.systemui.plugin.globalactions.wallet.WalletPopupMenu.OverflowItem;
41 import com.android.systemui.plugins.GlobalActionsPanelPlugin;
42 
43 import java.util.ArrayList;
44 import java.util.List;
45 import java.util.concurrent.ExecutorService;
46 import java.util.concurrent.Executors;
47 import java.util.concurrent.TimeUnit;
48 
49 public class WalletPanelViewController implements
50         GlobalActionsPanelPlugin.PanelViewController,
51         WalletCardCarousel.OnSelectionListener,
52         QuickAccessWalletClient.OnWalletCardsRetrievedCallback,
53         QuickAccessWalletClient.WalletServiceEventListener {
54 
55     private static final String TAG = "WalletPanelViewCtrl";
56     private static final int MAX_CARDS = 10;
57     private static final long SELECTION_DELAY_MILLIS = TimeUnit.SECONDS.toMillis(30);
58     private static final String PREFS_WALLET_VIEW_HEIGHT = "wallet_view_height";
59     private static final String PREFS_HAS_CARDS = "has_cards";
60     private static final String SETTINGS_PKG = "com.android.settings";
61     private static final String SETTINGS_ACTION = SETTINGS_PKG + ".GLOBAL_ACTIONS_PANEL_SETTINGS";
62     private final Context mSysuiContext;
63     private final Context mPluginContext;
64     private final QuickAccessWalletClient mWalletClient;
65     private final WalletView mWalletView;
66     private final WalletCardCarousel mWalletCardCarousel;
67     private final GlobalActionsPanelPlugin.Callbacks mPluginCallbacks;
68     private final ExecutorService mExecutor;
69     private final Handler mHandler;
70     private final Runnable mSelectionRunnable = this::selectCard;
71     private final SharedPreferences mPrefs;
72     private boolean mIsDeviceLocked;
73     private boolean mIsDismissed;
74     private boolean mHasRegisteredListener;
75     private String mSelectedCardId;
76 
WalletPanelViewController( Context sysuiContext, Context pluginContext, QuickAccessWalletClient walletClient, GlobalActionsPanelPlugin.Callbacks pluginCallbacks, boolean isDeviceLocked)77     public WalletPanelViewController(
78             Context sysuiContext,
79             Context pluginContext,
80             QuickAccessWalletClient walletClient,
81             GlobalActionsPanelPlugin.Callbacks pluginCallbacks,
82             boolean isDeviceLocked) {
83         mSysuiContext = sysuiContext;
84         mPluginContext = pluginContext;
85         mWalletClient = walletClient;
86         mPrefs = mSysuiContext.getSharedPreferences(TAG, Context.MODE_PRIVATE);
87         mPluginCallbacks = pluginCallbacks;
88         mIsDeviceLocked = isDeviceLocked;
89         mWalletView = new WalletView(pluginContext);
90         mWalletView.setMinimumHeight(getExpectedMinHeight());
91         mWalletView.setLayoutParams(
92                 new FrameLayout.LayoutParams(
93                         FrameLayout.LayoutParams.MATCH_PARENT,
94                         FrameLayout.LayoutParams.WRAP_CONTENT));
95         mWalletCardCarousel = mWalletView.getCardCarousel();
96         mWalletCardCarousel.setSelectionListener(this);
97         mHandler = new Handler(Looper.myLooper());
98         mExecutor = Executors.newSingleThreadExecutor();
99         if (!mPrefs.getBoolean(PREFS_HAS_CARDS, false)) {
100             // The empty state view is shown preemptively when cards were not returned last time
101             // to decrease perceived latency.
102             showEmptyStateView();
103         }
104     }
105 
106     /**
107      * Implements {@link GlobalActionsPanelPlugin.PanelViewController}. Returns the {@link View}
108      * containing the Quick Access Wallet.
109      */
110     @Override
getPanelContent()111     public View getPanelContent() {
112         return mWalletView;
113     }
114 
115     /**
116      * Implements {@link GlobalActionsPanelPlugin.PanelViewController}. Invoked when the view
117      * containing the Quick Access Wallet is dismissed.
118      */
119     @Override
onDismissed()120     public void onDismissed() {
121         if (mIsDismissed) {
122             return;
123         }
124         mIsDismissed = true;
125         mSelectedCardId = null;
126         mHandler.removeCallbacks(mSelectionRunnable);
127         mWalletClient.notifyWalletDismissed();
128         mWalletClient.removeWalletServiceEventListener(this);
129         mWalletView.animateDismissal();
130     }
131 
132     /**
133      * Implements {@link GlobalActionsPanelPlugin.PanelViewController}. Invoked when the device is
134      * either locked or unlocked while the wallet is visible.
135      */
136     @Override
onDeviceLockStateChanged(boolean deviceLocked)137     public void onDeviceLockStateChanged(boolean deviceLocked) {
138         if (mIsDismissed || mIsDeviceLocked == deviceLocked || !mIsDeviceLocked) {
139             // Disregard repeat events and events after unlock
140             return;
141         }
142         mIsDeviceLocked = deviceLocked;
143         // Cards are re-queried because the wallet application may wish to change card art, icons,
144         // text, or other attributes depending on the lock state of the device.
145         queryWalletCards();
146     }
147 
148     /**
149      * Query wallet cards from the client and display them on screen.
150      */
queryWalletCards()151     void queryWalletCards() {
152         if (mIsDismissed) {
153             return;
154         }
155         if (!mHasRegisteredListener) {
156             // Listener is registered even when device is locked. Should only be registered once.
157             mWalletClient.addWalletServiceEventListener(this);
158             mHasRegisteredListener = true;
159         }
160         if (mIsDeviceLocked && !mWalletClient.isWalletFeatureAvailableWhenDeviceLocked()) {
161             mWalletView.hide();
162             return;
163         }
164         mWalletView.show();
165         mWalletView.hideErrorMessage();
166         int cardWidthPx = mWalletCardCarousel.getCardWidthPx();
167         int cardHeightPx = mWalletCardCarousel.getCardHeightPx();
168         int iconSizePx = mWalletView.getIconSizePx();
169         GetWalletCardsRequest request =
170                 new GetWalletCardsRequest(cardWidthPx, cardHeightPx, iconSizePx, MAX_CARDS);
171         mWalletClient.getWalletCards(mExecutor, request, this);
172     }
173 
174     /**
175      * Implements {@link QuickAccessWalletClient.OnWalletCardsRetrievedCallback}. Called when cards
176      * are retrieved successfully from the service. This is called on {@link #mExecutor}.
177      */
178     @Override
onWalletCardsRetrieved(GetWalletCardsResponse response)179     public void onWalletCardsRetrieved(GetWalletCardsResponse response) {
180         if (mIsDismissed) {
181             return;
182         }
183         List<WalletCard> walletCards = response.getWalletCards();
184         List<WalletCardViewInfo> data = new ArrayList<>(walletCards.size());
185         for (WalletCard card : walletCards) {
186             data.add(new QAWalletCardViewInfo(card));
187         }
188 
189         // Get on main thread for UI updates
190         mWalletView.post(() -> {
191             if (mIsDismissed) {
192                 return;
193             }
194             if (data.isEmpty()) {
195                 showEmptyStateView();
196             } else {
197                 mWalletView.showCardCarousel(data, response.getSelectedIndex(), getOverflowItems());
198             }
199             // The empty state view will not be shown preemptively next time if cards were returned
200             mPrefs.edit().putBoolean(PREFS_HAS_CARDS, !data.isEmpty()).apply();
201             removeMinHeightAndRecordHeightOnLayout();
202         });
203     }
204 
205     /**
206      * Implements {@link QuickAccessWalletClient.OnWalletCardsRetrievedCallback}. Called when there
207      * is an error during card retrieval. This will be run on the {@link #mExecutor}.
208      */
209     @Override
onWalletCardRetrievalError(GetWalletCardsError error)210     public void onWalletCardRetrievalError(GetWalletCardsError error) {
211         mWalletView.post(() -> {
212             if (mIsDismissed) {
213                 return;
214             }
215             mWalletView.showErrorMessage(error.getMessage());
216         });
217     }
218 
219     /**
220      * Implements {@link QuickAccessWalletClient.WalletServiceEventListener}. Called when the wallet
221      * application propagates an event, such as an NFC tap, to the quick access wallet view.
222      */
223     @Override
onWalletServiceEvent(WalletServiceEvent event)224     public void onWalletServiceEvent(WalletServiceEvent event) {
225         if (mIsDismissed) {
226             return;
227         }
228         switch (event.getEventType()) {
229             case WalletServiceEvent.TYPE_NFC_PAYMENT_STARTED:
230                 mPluginCallbacks.dismissGlobalActionsMenu();
231                 onDismissed();
232                 break;
233             case WalletServiceEvent.TYPE_WALLET_CARDS_UPDATED:
234                 queryWalletCards();
235                 break;
236             default:
237                 Log.w(TAG, "onWalletServiceEvent: Unknown event type");
238         }
239     }
240 
241     /**
242      * Implements {@link WalletCardCarousel.OnSelectionListener}. Called when the user selects a
243      * card from the carousel by scrolling to it.
244      */
245     @Override
onCardSelected(WalletCardViewInfo card)246     public void onCardSelected(WalletCardViewInfo card) {
247         if (mIsDismissed) {
248             return;
249         }
250         mSelectedCardId = card.getCardId();
251         selectCard();
252     }
253 
selectCard()254     private void selectCard() {
255         mHandler.removeCallbacks(mSelectionRunnable);
256         String selectedCardId = mSelectedCardId;
257         if (mIsDismissed || selectedCardId == null) {
258             return;
259         }
260         mWalletClient.selectWalletCard(new SelectWalletCardRequest(selectedCardId));
261         // Re-selecting the card keeps the connection bound so we continue to get service events
262         // even if the user keeps it open for a long time.
263         mHandler.postDelayed(mSelectionRunnable, SELECTION_DELAY_MILLIS);
264     }
265 
266     /**
267      * Implements {@link WalletCardCarousel.OnSelectionListener}. Called when the user clicks on a
268      * card.
269      */
270     @Override
onCardClicked(WalletCardViewInfo card)271     public void onCardClicked(WalletCardViewInfo card) {
272         if (mIsDismissed) {
273             return;
274         }
275         PendingIntent pendingIntent = ((QAWalletCardViewInfo) card).mWalletCard.getPendingIntent();
276         startPendingIntent(pendingIntent);
277     }
278 
getOverflowItems()279     private OverflowItem[] getOverflowItems() {
280         CharSequence walletLabel = mWalletClient.getShortcutShortLabel();
281         Intent walletIntent = mWalletClient.createWalletIntent();
282         CharSequence settingsLabel = mPluginContext.getString(R.string.settings);
283         Intent settingsIntent = new Intent(SETTINGS_ACTION).setPackage(SETTINGS_PKG);
284         OverflowItem settings = new OverflowItem(settingsLabel, () -> startIntent(settingsIntent));
285         if (!TextUtils.isEmpty(walletLabel) && walletIntent != null) {
286             OverflowItem wallet = new OverflowItem(walletLabel, () -> startIntent(walletIntent));
287             return new OverflowItem[]{wallet, settings};
288         } else {
289             return new OverflowItem[]{settings};
290         }
291     }
292 
showEmptyStateView()293     private void showEmptyStateView() {
294         Drawable logo = mWalletClient.getLogo();
295         CharSequence logoContentDesc = mWalletClient.getServiceLabel();
296         CharSequence label = mWalletClient.getShortcutLongLabel();
297         Intent intent = mWalletClient.createWalletIntent();
298         if (logo == null
299                 || TextUtils.isEmpty(logoContentDesc)
300                 || TextUtils.isEmpty(label)
301                 || intent == null) {
302             Log.w(TAG, "QuickAccessWalletService manifest entry mis-configured");
303             // Issue is not likely to be resolved until manifest entries are enabled.
304             // Hide wallet feature until then.
305             mWalletView.hide();
306             mPrefs.edit().putInt(PREFS_WALLET_VIEW_HEIGHT, 0).apply();
307         } else {
308             mWalletView.showEmptyStateView(logo, logoContentDesc, label, v -> startIntent(intent));
309         }
310     }
311 
startIntent(Intent intent)312     private void startIntent(Intent intent) {
313         PendingIntent pendingIntent = PendingIntent.getActivity(mSysuiContext, 0, intent,
314                 PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT);
315         startPendingIntent(pendingIntent);
316     }
317 
startPendingIntent(PendingIntent pendingIntent)318     private void startPendingIntent(PendingIntent pendingIntent) {
319         mPluginCallbacks.startPendingIntentDismissingKeyguard(pendingIntent);
320         mPluginCallbacks.dismissGlobalActionsMenu();
321         onDismissed();
322     }
323 
324     /**
325      * The total view height depends on whether cards are shown or not. Since it is not known at
326      * construction time whether cards will be available, the best we can do is set the height to
327      * whatever it was the last time. Setting the height correctly ahead of time is important
328      * because Home Controls are shown below the wallet and may be displayed before card data is
329      * loaded, causing the home controls to jump down when card data arrives.
330      */
getExpectedMinHeight()331     private int getExpectedMinHeight() {
332         int expectedHeight = mPrefs.getInt(PREFS_WALLET_VIEW_HEIGHT, -1);
333         if (expectedHeight == -1) {
334             Resources res = mPluginContext.getResources();
335             expectedHeight = res.getDimensionPixelSize(R.dimen.min_wallet_empty_height);
336         }
337         return expectedHeight;
338     }
339 
removeMinHeightAndRecordHeightOnLayout()340     private void removeMinHeightAndRecordHeightOnLayout() {
341         mWalletView.setMinimumHeight(0);
342         mWalletView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
343             @Override
344             public void onLayoutChange(View v, int left, int top, int right, int bottom,
345                     int oldLeft, int oldTop, int oldRight, int oldBottom) {
346                 mWalletView.removeOnLayoutChangeListener(this);
347                 mPrefs.edit().putInt(PREFS_WALLET_VIEW_HEIGHT, bottom - top).apply();
348             }
349         });
350     }
351 
352     private class QAWalletCardViewInfo implements WalletCardViewInfo {
353 
354         private final WalletCard mWalletCard;
355         private final Drawable mCardDrawable;
356         private final Drawable mIconDrawable;
357 
358         /**
359          * Constructor is called on background executor, so it is safe to load drawables
360          * synchronously.
361          */
QAWalletCardViewInfo(WalletCard walletCard)362         QAWalletCardViewInfo(WalletCard walletCard) {
363             mWalletCard = walletCard;
364             mCardDrawable = mWalletCard.getCardImage().loadDrawable(mPluginContext);
365             Icon icon = mWalletCard.getCardIcon();
366             mIconDrawable = icon == null ? null : icon.loadDrawable(mPluginContext);
367         }
368 
369         @Override
getCardId()370         public String getCardId() {
371             return mWalletCard.getCardId();
372         }
373 
374         @Override
getCardDrawable()375         public Drawable getCardDrawable() {
376             return mCardDrawable;
377         }
378 
379         @Override
getContentDescription()380         public CharSequence getContentDescription() {
381             return mWalletCard.getContentDescription();
382         }
383 
384         @Override
getIcon()385         public Drawable getIcon() {
386             return mIconDrawable;
387         }
388 
389         @Override
getText()390         public CharSequence getText() {
391             return mWalletCard.getCardLabel();
392         }
393     }
394 }
395