• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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.wallet.ui;
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.service.quickaccesswallet.GetWalletCardsError;
28 import android.service.quickaccesswallet.GetWalletCardsRequest;
29 import android.service.quickaccesswallet.GetWalletCardsResponse;
30 import android.service.quickaccesswallet.QuickAccessWalletClient;
31 import android.service.quickaccesswallet.SelectWalletCardRequest;
32 import android.service.quickaccesswallet.WalletCard;
33 import android.text.TextUtils;
34 import android.util.Log;
35 import android.view.View;
36 import android.view.ViewGroup;
37 import android.widget.FrameLayout;
38 
39 import androidx.annotation.NonNull;
40 
41 import com.android.internal.annotations.VisibleForTesting;
42 import com.android.internal.logging.UiEventLogger;
43 import com.android.keyguard.KeyguardUpdateMonitor;
44 import com.android.systemui.R;
45 import com.android.systemui.plugins.ActivityStarter;
46 import com.android.systemui.plugins.FalsingManager;
47 import com.android.systemui.settings.UserTracker;
48 import com.android.systemui.statusbar.policy.KeyguardStateController;
49 
50 import java.util.ArrayList;
51 import java.util.List;
52 import java.util.concurrent.Executor;
53 import java.util.concurrent.TimeUnit;
54 
55 /** Controller for the wallet card carousel screen. */
56 public class WalletScreenController implements
57         WalletCardCarousel.OnSelectionListener,
58         QuickAccessWalletClient.OnWalletCardsRetrievedCallback,
59         KeyguardStateController.Callback {
60 
61     private static final String TAG = "WalletScreenCtrl";
62     private static final String PREFS_WALLET_VIEW_HEIGHT = "wallet_view_height";
63     private static final int MAX_CARDS = 10;
64     private static final long SELECTION_DELAY_MILLIS = TimeUnit.SECONDS.toMillis(30);
65 
66     private Context mContext;
67     private final QuickAccessWalletClient mWalletClient;
68     private final ActivityStarter mActivityStarter;
69     private final Executor mExecutor;
70     private final Handler mHandler;
71     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
72     private final KeyguardStateController mKeyguardStateController;
73     private final Runnable mSelectionRunnable = this::selectCard;
74     private final SharedPreferences mPrefs;
75     private final WalletView mWalletView;
76     private final WalletCardCarousel mCardCarousel;
77     private final FalsingManager mFalsingManager;
78     private final UiEventLogger mUiEventLogger;
79 
80 
81     @VisibleForTesting
82     String mSelectedCardId;
83     @VisibleForTesting
84     boolean mIsDismissed;
85 
WalletScreenController( Context context, WalletView walletView, QuickAccessWalletClient walletClient, ActivityStarter activityStarter, Executor executor, Handler handler, UserTracker userTracker, FalsingManager falsingManager, KeyguardUpdateMonitor keyguardUpdateMonitor, KeyguardStateController keyguardStateController, UiEventLogger uiEventLogger)86     public WalletScreenController(
87             Context context,
88             WalletView walletView,
89             QuickAccessWalletClient walletClient,
90             ActivityStarter activityStarter,
91             Executor executor,
92             Handler handler,
93             UserTracker userTracker,
94             FalsingManager falsingManager,
95             KeyguardUpdateMonitor keyguardUpdateMonitor,
96             KeyguardStateController keyguardStateController,
97             UiEventLogger uiEventLogger) {
98         mContext = context;
99         mWalletClient = walletClient;
100         mActivityStarter = activityStarter;
101         mExecutor = executor;
102         mHandler = handler;
103         mFalsingManager = falsingManager;
104         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
105         mKeyguardStateController = keyguardStateController;
106         mUiEventLogger = uiEventLogger;
107         mPrefs = userTracker.getUserContext().getSharedPreferences(TAG, Context.MODE_PRIVATE);
108         mWalletView = walletView;
109         mWalletView.setMinimumHeight(getExpectedMinHeight());
110         mWalletView.setLayoutParams(
111                 new FrameLayout.LayoutParams(
112                         ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
113         mCardCarousel = mWalletView.getCardCarousel();
114         if (mCardCarousel != null) {
115             mCardCarousel.setSelectionListener(this);
116         }
117     }
118 
119     /**
120      * Implements {@link QuickAccessWalletClient.OnWalletCardsRetrievedCallback}. Called when cards
121      * are retrieved successfully from the service. This is called on {@link #mExecutor}.
122      */
123     @Override
onWalletCardsRetrieved(@onNull GetWalletCardsResponse response)124     public void onWalletCardsRetrieved(@NonNull GetWalletCardsResponse response) {
125         if (mIsDismissed) {
126             return;
127         }
128         Log.i(TAG, "Successfully retrieved wallet cards.");
129         List<WalletCard> walletCards = response.getWalletCards();
130 
131         boolean allUnknown = true;
132         for (WalletCard card : walletCards) {
133             if (card.getCardType() != WalletCard.CARD_TYPE_UNKNOWN) {
134                 allUnknown = false;
135                 break;
136             }
137         }
138 
139         List<WalletCardViewInfo> paymentCardData = new ArrayList<>();
140         for (WalletCard card : walletCards) {
141             if (allUnknown || card.getCardType() == WalletCard.CARD_TYPE_PAYMENT) {
142                 paymentCardData.add(new QAWalletCardViewInfo(mContext, card));
143             }
144         }
145 
146         // Get on main thread for UI updates.
147         mHandler.post(() -> {
148             if (mIsDismissed) {
149                 return;
150             }
151             if (paymentCardData.isEmpty()) {
152                 showEmptyStateView();
153             } else {
154                 int selectedIndex = response.getSelectedIndex();
155                 if (selectedIndex >= paymentCardData.size()) {
156                     Log.w(TAG, "Invalid selected card index, showing empty state.");
157                     showEmptyStateView();
158                 } else {
159                     boolean isUdfpsEnabled = mKeyguardUpdateMonitor.isUdfpsEnrolled()
160                             && mKeyguardUpdateMonitor.isFingerprintDetectionRunning();
161                     mWalletView.showCardCarousel(
162                             paymentCardData,
163                             selectedIndex,
164                             !mKeyguardStateController.isUnlocked(),
165                             isUdfpsEnabled);
166                 }
167             }
168             mUiEventLogger.log(WalletUiEvent.QAW_IMPRESSION);
169             removeMinHeightAndRecordHeightOnLayout();
170         });
171     }
172 
173     /**
174      * Implements {@link QuickAccessWalletClient.OnWalletCardsRetrievedCallback}. Called when there
175      * is an error during card retrieval. This will be run on the {@link #mExecutor}.
176      */
177     @Override
onWalletCardRetrievalError(@onNull GetWalletCardsError error)178     public void onWalletCardRetrievalError(@NonNull GetWalletCardsError error) {
179         mHandler.post(() -> {
180             if (mIsDismissed) {
181                 return;
182             }
183             mWalletView.showErrorMessage(error.getMessage());
184         });
185     }
186 
187     @Override
onKeyguardFadingAwayChanged()188     public void onKeyguardFadingAwayChanged() {
189         queryWalletCards();
190     }
191 
192     @Override
onUnlockedChanged()193     public void onUnlockedChanged() {
194         queryWalletCards();
195     }
196 
197     @Override
onUncenteredClick(int position)198     public void onUncenteredClick(int position) {
199         if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
200             return;
201         }
202         mCardCarousel.smoothScrollToPosition(position);
203     }
204 
205     @Override
onCardSelected(@onNull WalletCardViewInfo card)206     public void onCardSelected(@NonNull WalletCardViewInfo card) {
207         if (mIsDismissed) {
208             return;
209         }
210         if (mSelectedCardId != null && !mSelectedCardId.equals(card.getCardId())) {
211             mUiEventLogger.log(WalletUiEvent.QAW_CHANGE_CARD);
212         }
213         mSelectedCardId = card.getCardId();
214         selectCard();
215     }
216 
selectCard()217     private void selectCard() {
218         mHandler.removeCallbacks(mSelectionRunnable);
219         String selectedCardId = mSelectedCardId;
220         if (mIsDismissed || selectedCardId == null) {
221             return;
222         }
223         mWalletClient.selectWalletCard(new SelectWalletCardRequest(selectedCardId));
224         // Re-selecting the card keeps the connection bound so we continue to get service events
225         // even if the user keeps it open for a long time.
226         mHandler.postDelayed(mSelectionRunnable, SELECTION_DELAY_MILLIS);
227     }
228 
229 
230     @Override
onCardClicked(@onNull WalletCardViewInfo cardInfo)231     public void onCardClicked(@NonNull WalletCardViewInfo cardInfo) {
232         if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
233             return;
234         }
235         if (!(cardInfo instanceof QAWalletCardViewInfo)
236                 || ((QAWalletCardViewInfo) cardInfo).mWalletCard == null
237                 || ((QAWalletCardViewInfo) cardInfo).mWalletCard.getPendingIntent() == null) {
238             return;
239         }
240 
241         if (!mKeyguardStateController.isUnlocked()) {
242             mUiEventLogger.log(WalletUiEvent.QAW_UNLOCK_FROM_CARD_CLICK);
243         }
244         mUiEventLogger.log(WalletUiEvent.QAW_CLICK_CARD);
245 
246         mActivityStarter.startPendingIntentDismissingKeyguard(cardInfo.getPendingIntent());
247     }
248 
249     @Override
queryWalletCards()250     public void queryWalletCards() {
251         if (mIsDismissed) {
252             return;
253         }
254         int cardWidthPx = mCardCarousel.getCardWidthPx();
255         int cardHeightPx = mCardCarousel.getCardHeightPx();
256         if (cardWidthPx == 0 || cardHeightPx == 0) {
257             return;
258         }
259 
260         mWalletView.show();
261         mWalletView.hideErrorMessage();
262         int iconSizePx =
263                 mContext
264                         .getResources()
265                         .getDimensionPixelSize(R.dimen.wallet_screen_header_icon_size);
266         GetWalletCardsRequest request =
267                 new GetWalletCardsRequest(cardWidthPx, cardHeightPx, iconSizePx, MAX_CARDS);
268         mWalletClient.getWalletCards(mExecutor, request, this);
269     }
270 
onDismissed()271     void onDismissed() {
272         if (mIsDismissed) {
273             return;
274         }
275         mIsDismissed = true;
276         mSelectedCardId = null;
277         mHandler.removeCallbacks(mSelectionRunnable);
278         mWalletClient.notifyWalletDismissed();
279         mWalletView.animateDismissal();
280         // clear refs to the Wallet Activity
281         mContext = null;
282     }
283 
showEmptyStateView()284     private void showEmptyStateView() {
285         Drawable logo = mWalletClient.getLogo();
286         CharSequence logoContentDesc = mWalletClient.getServiceLabel();
287         CharSequence label = mWalletClient.getShortcutLongLabel();
288         Intent intent = mWalletClient.createWalletIntent();
289         if (logo == null
290                 || TextUtils.isEmpty(logoContentDesc)
291                 || TextUtils.isEmpty(label)
292                 || intent == null) {
293             Log.w(TAG, "QuickAccessWalletService manifest entry mis-configured");
294             // Issue is not likely to be resolved until manifest entries are enabled.
295             // Hide wallet feature until then.
296             mWalletView.hide();
297             mPrefs.edit().putInt(PREFS_WALLET_VIEW_HEIGHT, 0).apply();
298         } else {
299             mWalletView.showEmptyStateView(
300                     logo,
301                     logoContentDesc,
302                     label,
303                     v -> mActivityStarter.startActivity(intent, true));
304         }
305     }
306 
getExpectedMinHeight()307     private int getExpectedMinHeight() {
308         int expectedHeight = mPrefs.getInt(PREFS_WALLET_VIEW_HEIGHT, -1);
309         if (expectedHeight == -1) {
310             Resources res = mContext.getResources();
311             expectedHeight = res.getDimensionPixelSize(R.dimen.min_wallet_empty_height);
312         }
313         return expectedHeight;
314     }
315 
removeMinHeightAndRecordHeightOnLayout()316     private void removeMinHeightAndRecordHeightOnLayout() {
317         mWalletView.setMinimumHeight(0);
318         mWalletView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
319             @Override
320             public void onLayoutChange(View v, int left, int top, int right, int bottom,
321                     int oldLeft, int oldTop, int oldRight, int oldBottom) {
322                 mWalletView.removeOnLayoutChangeListener(this);
323                 mPrefs.edit().putInt(PREFS_WALLET_VIEW_HEIGHT, bottom - top).apply();
324             }
325         });
326     }
327 
328     @VisibleForTesting
329     static class QAWalletCardViewInfo implements WalletCardViewInfo {
330 
331         private final WalletCard mWalletCard;
332         private final Drawable mCardDrawable;
333         private final Drawable mIconDrawable;
334 
335         /**
336          * Constructor is called on background executor, so it is safe to load drawables
337          * synchronously.
338          */
QAWalletCardViewInfo(Context context, WalletCard walletCard)339         QAWalletCardViewInfo(Context context, WalletCard walletCard) {
340             mWalletCard = walletCard;
341             Icon cardImageIcon = mWalletCard.getCardImage();
342             if (cardImageIcon.getType() == Icon.TYPE_URI) {
343                 mCardDrawable = null;
344             } else {
345                 mCardDrawable = mWalletCard.getCardImage().loadDrawable(context);
346             }
347             Icon icon = mWalletCard.getCardIcon();
348             mIconDrawable = icon == null ? null : icon.loadDrawable(context);
349         }
350 
351         @Override
getCardId()352         public String getCardId() {
353             return mWalletCard.getCardId();
354         }
355 
356         @Override
getCardDrawable()357         public Drawable getCardDrawable() {
358             return mCardDrawable;
359         }
360 
361         @Override
getContentDescription()362         public CharSequence getContentDescription() {
363             return mWalletCard.getContentDescription();
364         }
365 
366         @Override
getIcon()367         public Drawable getIcon() {
368             return mIconDrawable;
369         }
370 
371         @Override
getLabel()372         public CharSequence getLabel() {
373             CharSequence label = mWalletCard.getCardLabel();
374             if (label == null) {
375                 return "";
376             }
377             return label;
378         }
379 
380         @Override
getPendingIntent()381         public PendingIntent getPendingIntent() {
382             return mWalletCard.getPendingIntent();
383         }
384     }
385 }
386