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