• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 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 android.service.quickaccesswallet;
18 
19 import static android.service.quickaccesswallet.Flags.launchWalletOptionOnPowerDoubleTap;
20 
21 import android.annotation.FlaggedApi;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.annotation.SdkConstant;
25 import android.app.PendingIntent;
26 import android.app.Service;
27 import android.content.Intent;
28 import android.os.Build;
29 import android.os.Handler;
30 import android.os.IBinder;
31 import android.os.Looper;
32 import android.os.RemoteException;
33 import android.provider.Settings;
34 import android.util.Log;
35 
36 /**
37  * A {@code QuickAccessWalletService} provides a list of {@code WalletCard}s shown in the Quick
38  * Access Wallet. The Quick Access Wallet allows the user to change their selected payment method
39  * and access other important passes, such as tickets and transit passes, without leaving the
40  * context of their current app.
41  *
42  * <p>An {@code QuickAccessWalletService} is only bound to the Android System for the purposes of
43  * showing wallet cards if:
44  * <ol>
45  *   <li>The application hosting the QuickAccessWalletService is also the default NFC payment
46  *   application. This means that the same application must also have a
47  *   {@link android.nfc.cardemulation.HostApduService} or
48  *   {@link android.nfc.cardemulation.OffHostApduService} that requires the
49  *   android.permission.BIND_NFC_SERVICE permission.
50  *   <li>The user explicitly selected the application as the default payment application in
51  *   the Tap &amp; pay settings screen.
52  *   <li>The QuickAccessWalletService requires that the binding application hold the
53  *   {@code android.permission.BIND_QUICK_ACCESS_WALLET_SERVICE} permission, which only the System
54  *   Service can hold.
55  *   <li>The user explicitly enables it using Android Settings (the
56  *       {@link Settings#ACTION_QUICK_ACCESS_WALLET_SETTINGS} intent can be used to launch it).
57  * </ol>
58  *
59  * <a name="BasicUsage"></a>
60  * <h3>Basic usage</h3>
61  *
62  * <p>The basic Quick Access Wallet process is defined by the workflow below:
63  * <ol>
64  *   <li>User performs a gesture to bring up the Quick Access Wallet, which is displayed by the
65  *   Android System.
66  *   <li>The Android System creates a {@link GetWalletCardsRequest}, binds to the
67  *   {@link QuickAccessWalletService}, and delivers the request.
68  *   <li>The service receives the request through {@link #onWalletCardsRequested}
69  *   <li>The service responds by calling {@link GetWalletCardsCallback#onSuccess} with a
70  *   {@link GetWalletCardsResponse response} that contains between 1 and
71  *   {@link GetWalletCardsRequest#getMaxCards() maxCards} cards.
72  *   <li>The Android System displays the Quick Access Wallet containing the provided cards. The
73  *   card at the {@link GetWalletCardsResponse#getSelectedIndex() selectedIndex} will initially
74  *   be presented as the 'selected' card.
75  *   <li>As soon as the cards are displayed, the Android System will notify the service that the
76  *   card at the selected index has been selected through {@link #onWalletCardSelected}.
77  *   <li>The user interacts with the wallet and may select one or more cards in sequence. Each time
78  *   a new card is selected, the Android System will notify the service through
79  *   {@link #onWalletCardSelected} and will provide the {@link WalletCard#getCardId() cardId} of the
80  *   card that is now selected.
81  *   <li>If the user commences an NFC payment, the service may send a {@link WalletServiceEvent}
82  *   to the System indicating that the wallet application now needs to show the activity associated
83  *   with making a payment. Sending a {@link WalletServiceEvent} of type
84  *   {@link WalletServiceEvent#TYPE_NFC_PAYMENT_STARTED} should cause the quick access wallet UI
85  *   to be dismissed.
86  *   <li>When the wallet is dismissed, the Android System will notify the service through
87  *   {@link #onWalletDismissed}.
88  * </ol>
89  *
90  * <p>The workflow is designed to minimize the time that the Android System is bound to the
91  * service, but connections may be cached and reused to improve performance and conserve memory.
92  * All calls should be considered stateless: if the service needs to keep state between calls, it
93  * must do its own state management (keeping in mind that the service's process might be killed
94  * by the Android System when unbound; for example, if the device is running low in memory).
95  *
96  * <p> The service also provides pending intents to override the system's Quick Access activities
97  * via the {@link #getTargetActivityPendingIntent} and the
98  * {@link #getGestureTargetActivityPendingIntent} method.
99  *
100  * <p>
101  * <a name="ErrorHandling"></a>
102  * <h3>Error handling</h3>
103  * <p>If the service encountered an error processing the request, it should call
104  * {@link GetWalletCardsCallback#onFailure}.
105  * For performance reasons, it's paramount that the service calls either
106  * {@link GetWalletCardsCallback#onSuccess} or
107  * {@link GetWalletCardsCallback#onFailure} for each
108  * {@link #onWalletCardsRequested} received - if it doesn't, the request will eventually time out
109  * and be discarded by the Android System.
110  *
111  * <p>
112  * <a name="ManifestEntry"></a>
113  * <h3>Manifest entry</h3>
114  *
115  * <p>QuickAccessWalletService must require the permission
116  * "android.permission.BIND_QUICK_ACCESS_WALLET_SERVICE".
117  *
118  * <pre class="prettyprint">
119  * {@literal
120  * <service
121  *     android:name=".MyQuickAccessWalletService"
122  *     android:label="@string/my_default_tile_label"
123  *     android:icon="@drawable/my_default_icon_label"
124  *     android:logo="@drawable/my_wallet_logo"
125  *     android:permission="android.permission.BIND_QUICK_ACCESS_WALLET_SERVICE">
126  *     <intent-filter>
127  *         <action android:name="android.service.quickaccesswallet.QuickAccessWalletService" />
128  *         <category android:name="android.intent.category.DEFAULT"/>
129  *     </intent-filter>
130  *     <meta-data android:name="android.quickaccesswallet"
131  *          android:resource="@xml/quickaccesswallet_configuration" />;
132  * </service>}
133  * </pre>
134  * <p>
135  * The {@literal <meta-data>} element includes an android:resource attribute that points to an
136  * XML resource with further details about the service. The {@code quickaccesswallet_configuration}
137  * in the example above specifies an activity that allows the users to view the entire wallet.
138  * The following example shows the quickaccesswallet_configuration XML resource:
139  * <p>
140  * <pre class="prettyprint">
141  * {@literal
142  * <quickaccesswallet-service
143  *   xmlns:android="http://schemas.android.com/apk/res/android"
144  *   android:settingsActivity="com.example.android.SettingsActivity"
145  *   android:shortcutLongLabel="@string/my_wallet_empty_state_text"
146  *   android:shortcutShortLabel="@string/my_wallet_button_text"
147  *   android:targetActivity="com.example.android.WalletActivity"/>
148  * }
149  * </pre>
150  *
151  * <p>The entry for {@code settingsActivity} should contain the fully qualified class name of an
152  * activity that allows the user to modify the settings for this service. The {@code targetActivity}
153  * entry should contain the fully qualified class name of an activity that allows the user to view
154  * their entire wallet. The {@code targetActivity} will be started with the Intent action
155  * {@link #ACTION_VIEW_WALLET} and the {@code settingsActivity} will be started with the Intent
156  * action {@link #ACTION_VIEW_WALLET_SETTINGS}.
157  *
158  * <p>The {@code shortcutShortLabel} and {@code shortcutLongLabel} are used by the QuickAccessWallet
159  * in the buttons that navigate to the wallet app. The {@code shortcutShortLabel} is displayed next
160  * to the cards that are returned by the service and should be no more than 20 characters. The
161  * {@code shortcutLongLabel} is displayed when no cards are returned. This 'empty state' view also
162  * displays the service logo, specified by the {@code android:logo} manifest entry. If the logo is
163  * not specified, the empty state view will show the app icon instead.
164  */
165 public abstract class QuickAccessWalletService extends Service {
166 
167     private static final String TAG = "QAWalletService";
168 
169     /**
170      * The {@link Intent} that must be declared as handled by the service. To be supported, the
171      * service must also require the
172      * {@link android.Manifest.permission#BIND_QUICK_ACCESS_WALLET_SERVICE}
173      * permission so that other applications can not abuse it.
174      */
175     @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION)
176     public static final String SERVICE_INTERFACE =
177             "android.service.quickaccesswallet.QuickAccessWalletService";
178 
179     /**
180      * Intent action to launch an activity to display the wallet.
181      */
182     @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
183     public static final String ACTION_VIEW_WALLET =
184             "android.service.quickaccesswallet.action.VIEW_WALLET";
185 
186     /**
187      * Intent action to launch an activity to display quick access wallet settings.
188      */
189     @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
190     public static final String ACTION_VIEW_WALLET_SETTINGS =
191             "android.service.quickaccesswallet.action.VIEW_WALLET_SETTINGS";
192 
193     /**
194      * Name under which a QuickAccessWalletService component publishes information about itself.
195      * This meta-data should reference an XML resource containing a
196      * <code>&lt;{@link
197      * android.R.styleable#QuickAccessWalletService quickaccesswallet-service}&gt;</code> tag. This
198      * is a a sample XML file configuring an QuickAccessWalletService:
199      * <pre> &lt;quickaccesswallet-service
200      *     android:walletActivity="foo.bar.WalletActivity"
201      *     . . .
202      * /&gt;</pre>
203      */
204     public static final String SERVICE_META_DATA = "android.quickaccesswallet";
205 
206     /**
207      * Name of the QuickAccessWallet tile service meta-data.
208      *
209      * @hide
210      */
211     public static final String TILE_SERVICE_META_DATA = "android.quickaccesswallet.tile";
212 
213     private final Handler mHandler = new Handler(Looper.getMainLooper());
214 
215     /**
216      * The service currently only supports one listener at a time. Multiple connections that
217      * register different listeners will clobber the listener. This field may only be accessed from
218      * the main thread.
219      */
220     @Nullable
221     private String mEventListenerId;
222 
223     /**
224      * The service currently only supports one listener at a time. Multiple connections that
225      * register different listeners will clobber the listener. This field may only be accessed from
226      * the main thread.
227      */
228     @Nullable
229     private IQuickAccessWalletServiceCallbacks mEventListener;
230 
231     private final IQuickAccessWalletService mInterface = new IQuickAccessWalletService.Stub() {
232         @Override
233         public void onWalletCardsRequested(
234                 @NonNull GetWalletCardsRequest request,
235                 @NonNull IQuickAccessWalletServiceCallbacks callback) {
236             mHandler.post(() -> onWalletCardsRequestedInternal(request, callback));
237         }
238 
239         @Override
240         public void onWalletCardSelected(@NonNull SelectWalletCardRequest request) {
241             mHandler.post(() -> QuickAccessWalletService.this.onWalletCardSelected(request));
242         }
243 
244         @Override
245         public void onWalletDismissed() {
246             mHandler.post(QuickAccessWalletService.this::onWalletDismissed);
247         }
248 
249         @Override
250         public void onTargetActivityIntentRequested(
251                 @NonNull IQuickAccessWalletServiceCallbacks callbacks) {
252             mHandler.post(
253                     () -> QuickAccessWalletService.this.onTargetActivityIntentRequestedInternal(
254                             callbacks));
255         }
256 
257         @FlaggedApi(Flags.FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
258         @Override
259         public void onGestureTargetActivityIntentRequested(
260                 @NonNull IQuickAccessWalletServiceCallbacks callbacks) {
261             if (launchWalletOptionOnPowerDoubleTap()) {
262                 mHandler.post(
263                         () ->
264                                 QuickAccessWalletService.this
265                                         .onGestureTargetActivityIntentRequestedInternal(
266                                                 callbacks));
267             }
268         }
269 
270         public void registerWalletServiceEventListener(
271                 @NonNull WalletServiceEventListenerRequest request,
272                 @NonNull IQuickAccessWalletServiceCallbacks callback) {
273             mHandler.post(() -> registerDismissWalletListenerInternal(request, callback));
274         }
275 
276         public void unregisterWalletServiceEventListener(
277                 @NonNull WalletServiceEventListenerRequest request) {
278             mHandler.post(() -> unregisterDismissWalletListenerInternal(request));
279         }
280     };
281 
onWalletCardsRequestedInternal( GetWalletCardsRequest request, IQuickAccessWalletServiceCallbacks callback)282     private void onWalletCardsRequestedInternal(
283             GetWalletCardsRequest request,
284             IQuickAccessWalletServiceCallbacks callback) {
285         onWalletCardsRequested(
286                 request, new GetWalletCardsCallbackImpl(request, callback, mHandler, this));
287     }
288 
onTargetActivityIntentRequestedInternal( IQuickAccessWalletServiceCallbacks callbacks)289     private void onTargetActivityIntentRequestedInternal(
290             IQuickAccessWalletServiceCallbacks callbacks) {
291         try {
292             callbacks.onTargetActivityPendingIntentReceived(getTargetActivityPendingIntent());
293         } catch (RemoteException e) {
294             Log.w(TAG, "Error returning wallet cards", e);
295         }
296     }
297 
onGestureTargetActivityIntentRequestedInternal( IQuickAccessWalletServiceCallbacks callbacks)298     private void onGestureTargetActivityIntentRequestedInternal(
299             IQuickAccessWalletServiceCallbacks callbacks) {
300         if (!Flags.launchWalletOptionOnPowerDoubleTap()) {
301             return;
302         }
303 
304         try {
305             callbacks.onGestureTargetActivityPendingIntentReceived(
306                     getGestureTargetActivityPendingIntent());
307         } catch (RemoteException e) {
308             Log.w(TAG, "Error", e);
309         }
310     }
311 
312     @Override
313     @Nullable
onBind(@onNull Intent intent)314     public IBinder onBind(@NonNull Intent intent) {
315         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
316             // Binding to the QuickAccessWalletService is protected by the
317             // android.permission.BIND_QUICK_ACCESS_WALLET_SERVICE permission, which is defined in
318             // R. Pre-R devices can have other side-loaded applications that claim this permission.
319             // Ensures that the service is only enabled when properly permission protected.
320             Log.w(TAG, "Warning: binding on pre-R device");
321         }
322         if (!SERVICE_INTERFACE.equals(intent.getAction())) {
323             Log.w(TAG, "Wrong action");
324             return null;
325         }
326         return mInterface.asBinder();
327     }
328 
329     /**
330      * Called when the user requests the service to provide wallet cards.
331      *
332      * <p>This method will be called on the main thread, but the callback may be called from any
333      * thread. The callback should be called as quickly as possible. The service must always call
334      * either {@link GetWalletCardsCallback#onSuccess(GetWalletCardsResponse)} or {@link
335      * GetWalletCardsCallback#onFailure(GetWalletCardsError)}. Calling multiple times or calling
336      * both methods will cause an exception to be thrown.
337      */
onWalletCardsRequested( @onNull GetWalletCardsRequest request, @NonNull GetWalletCardsCallback callback)338     public abstract void onWalletCardsRequested(
339             @NonNull GetWalletCardsRequest request,
340             @NonNull GetWalletCardsCallback callback);
341 
342     /**
343      * A wallet card was selected. Sent when the user selects a wallet card from the list of cards.
344      * Selection may indicate that the card is now in the center of the screen, or highlighted in
345      * some other fashion. It does not mean that the user clicked on the card -- clicking on the
346      * card will cause the {@link WalletCard#getPendingIntent()} to be sent.
347      *
348      * <p>Card selection events are especially important to NFC payment applications because
349      * many NFC terminals can only accept one payment card at a time. If the user has several NFC
350      * cards in their wallet, selecting different cards can change which payment method is presented
351      * to the terminal.
352      */
onWalletCardSelected(@onNull SelectWalletCardRequest request)353     public abstract void onWalletCardSelected(@NonNull SelectWalletCardRequest request);
354 
355     /**
356      * Indicates that the wallet was dismissed. This is received when the Quick Access Wallet is no
357      * longer visible.
358      */
onWalletDismissed()359     public abstract void onWalletDismissed();
360 
361     /**
362      * Send a {@link WalletServiceEvent} to the Quick Access Wallet.
363      * <p>
364      * Background events may require that the Quick Access Wallet view be updated. For example, if
365      * the wallet application hosting this service starts to handle an NFC payment while the Quick
366      * Access Wallet is being shown, the Quick Access Wallet will need to be dismissed so that the
367      * Activity showing the payment can be displayed to the user.
368      */
sendWalletServiceEvent(@onNull WalletServiceEvent serviceEvent)369     public final void sendWalletServiceEvent(@NonNull WalletServiceEvent serviceEvent) {
370         mHandler.post(() -> sendWalletServiceEventInternal(serviceEvent));
371     }
372 
373     /**
374      * Specify a {@link PendingIntent} to be launched as the "Quick Access" activity.
375      *
376      * This activity will be launched directly by the system in lieu of the card switcher activity
377      * provided by the system.
378      *
379      * In order to use the system-provided card switcher activity, return null from this method.
380      */
381     @Nullable
getTargetActivityPendingIntent()382     public PendingIntent getTargetActivityPendingIntent() {
383         return null;
384     }
385 
386     /**
387      * Specify a {@link PendingIntent} to be launched on user gesture.
388      *
389      * <p>The pending intent will be sent when the user performs a gesture to open Wallet.
390      * The pending intent should launch an activity.
391      *
392      * <p> If the gesture is performed and this method returns null, the system will launch the
393      * activity specified by the {@link #getTargetActivityPendingIntent} method. If that method
394      * also returns null, the system will launch the system-provided card switcher activity.
395      */
396     @Nullable
397     @FlaggedApi(Flags.FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP)
getGestureTargetActivityPendingIntent()398     public PendingIntent getGestureTargetActivityPendingIntent() {
399         return null;
400     }
401 
sendWalletServiceEventInternal(WalletServiceEvent serviceEvent)402     private void sendWalletServiceEventInternal(WalletServiceEvent serviceEvent) {
403         if (mEventListener == null) {
404             Log.i(TAG, "No dismiss listener registered");
405             return;
406         }
407         try {
408             mEventListener.onWalletServiceEvent(serviceEvent);
409         } catch (RemoteException e) {
410             Log.w(TAG, "onWalletServiceEvent error", e);
411             mEventListenerId = null;
412             mEventListener = null;
413         }
414     }
415 
registerDismissWalletListenerInternal( @onNull WalletServiceEventListenerRequest request, @NonNull IQuickAccessWalletServiceCallbacks callback)416     private void registerDismissWalletListenerInternal(
417             @NonNull WalletServiceEventListenerRequest request,
418             @NonNull IQuickAccessWalletServiceCallbacks callback) {
419         mEventListenerId = request.getListenerId();
420         mEventListener = callback;
421     }
422 
unregisterDismissWalletListenerInternal( @onNull WalletServiceEventListenerRequest request)423     private void unregisterDismissWalletListenerInternal(
424             @NonNull WalletServiceEventListenerRequest request) {
425         if (mEventListenerId != null && mEventListenerId.equals(request.getListenerId())) {
426             mEventListenerId = null;
427             mEventListener = null;
428         } else {
429             Log.w(TAG, "dismiss listener missing or replaced");
430         }
431     }
432 }
433