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 & 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><{@link 190 * android.R.styleable#QuickAccessWalletService quickaccesswallet-service}></code> tag. This 191 * is a a sample XML file configuring an QuickAccessWalletService: 192 * <pre> <quickaccesswallet-service 193 * android:walletActivity="foo.bar.WalletActivity" 194 * . . . 195 * /></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