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 & 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><{@link 197 * android.R.styleable#QuickAccessWalletService quickaccesswallet-service}></code> tag. This 198 * is a a sample XML file configuring an QuickAccessWalletService: 199 * <pre> <quickaccesswallet-service 200 * android:walletActivity="foo.bar.WalletActivity" 201 * . . . 202 * /></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