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