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 private final Handler mHandler = new Handler(Looper.getMainLooper()); 199 200 /** 201 * The service currently only supports one listener at a time. Multiple connections that 202 * register different listeners will clobber the listener. This field may only be accessed from 203 * the main thread. 204 */ 205 @Nullable 206 private String mEventListenerId; 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 IQuickAccessWalletServiceCallbacks mEventListener; 215 216 private final IQuickAccessWalletService mInterface = new IQuickAccessWalletService.Stub() { 217 @Override 218 public void onWalletCardsRequested( 219 @NonNull GetWalletCardsRequest request, 220 @NonNull IQuickAccessWalletServiceCallbacks callback) { 221 mHandler.post(() -> onWalletCardsRequestedInternal(request, callback)); 222 } 223 224 @Override 225 public void onWalletCardSelected(@NonNull SelectWalletCardRequest request) { 226 mHandler.post(() -> QuickAccessWalletService.this.onWalletCardSelected(request)); 227 } 228 229 @Override 230 public void onWalletDismissed() { 231 mHandler.post(QuickAccessWalletService.this::onWalletDismissed); 232 } 233 234 public void registerWalletServiceEventListener( 235 @NonNull WalletServiceEventListenerRequest request, 236 @NonNull IQuickAccessWalletServiceCallbacks callback) { 237 mHandler.post(() -> registerDismissWalletListenerInternal(request, callback)); 238 } 239 240 public void unregisterWalletServiceEventListener( 241 @NonNull WalletServiceEventListenerRequest request) { 242 mHandler.post(() -> unregisterDismissWalletListenerInternal(request)); 243 } 244 }; 245 onWalletCardsRequestedInternal( GetWalletCardsRequest request, IQuickAccessWalletServiceCallbacks callback)246 private void onWalletCardsRequestedInternal( 247 GetWalletCardsRequest request, 248 IQuickAccessWalletServiceCallbacks callback) { 249 onWalletCardsRequested(request, 250 new GetWalletCardsCallbackImpl(request, callback, mHandler)); 251 } 252 253 @Override 254 @Nullable onBind(@onNull Intent intent)255 public IBinder onBind(@NonNull Intent intent) { 256 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { 257 // Binding to the QuickAccessWalletService is protected by the 258 // android.permission.BIND_QUICK_ACCESS_WALLET_SERVICE permission, which is defined in 259 // R. Pre-R devices can have other side-loaded applications that claim this permission. 260 // Ensures that the service is only enabled when properly permission protected. 261 Log.w(TAG, "Warning: binding on pre-R device"); 262 } 263 if (!SERVICE_INTERFACE.equals(intent.getAction())) { 264 Log.w(TAG, "Wrong action"); 265 return null; 266 } 267 return mInterface.asBinder(); 268 } 269 270 /** 271 * Called when the user requests the service to provide wallet cards. 272 * 273 * <p>This method will be called on the main thread, but the callback may be called from any 274 * thread. The callback should be called as quickly as possible. The service must always call 275 * either {@link GetWalletCardsCallback#onSuccess(GetWalletCardsResponse)} or {@link 276 * GetWalletCardsCallback#onFailure(GetWalletCardsError)}. Calling multiple times or calling 277 * both methods will cause an exception to be thrown. 278 */ onWalletCardsRequested( @onNull GetWalletCardsRequest request, @NonNull GetWalletCardsCallback callback)279 public abstract void onWalletCardsRequested( 280 @NonNull GetWalletCardsRequest request, 281 @NonNull GetWalletCardsCallback callback); 282 283 /** 284 * A wallet card was selected. Sent when the user selects a wallet card from the list of cards. 285 * Selection may indicate that the card is now in the center of the screen, or highlighted in 286 * some other fashion. It does not mean that the user clicked on the card -- clicking on the 287 * card will cause the {@link WalletCard#getPendingIntent()} to be sent. 288 * 289 * <p>Card selection events are especially important to NFC payment applications because 290 * many NFC terminals can only accept one payment card at a time. If the user has several NFC 291 * cards in their wallet, selecting different cards can change which payment method is presented 292 * to the terminal. 293 */ onWalletCardSelected(@onNull SelectWalletCardRequest request)294 public abstract void onWalletCardSelected(@NonNull SelectWalletCardRequest request); 295 296 /** 297 * Indicates that the wallet was dismissed. This is received when the Quick Access Wallet is no 298 * longer visible. 299 */ onWalletDismissed()300 public abstract void onWalletDismissed(); 301 302 /** 303 * Send a {@link WalletServiceEvent} to the Quick Access Wallet. 304 * <p> 305 * Background events may require that the Quick Access Wallet view be updated. For example, if 306 * the wallet application hosting this service starts to handle an NFC payment while the Quick 307 * Access Wallet is being shown, the Quick Access Wallet will need to be dismissed so that the 308 * Activity showing the payment can be displayed to the user. 309 */ sendWalletServiceEvent(@onNull WalletServiceEvent serviceEvent)310 public final void sendWalletServiceEvent(@NonNull WalletServiceEvent serviceEvent) { 311 mHandler.post(() -> sendWalletServiceEventInternal(serviceEvent)); 312 } 313 sendWalletServiceEventInternal(WalletServiceEvent serviceEvent)314 private void sendWalletServiceEventInternal(WalletServiceEvent serviceEvent) { 315 if (mEventListener == null) { 316 Log.i(TAG, "No dismiss listener registered"); 317 return; 318 } 319 try { 320 mEventListener.onWalletServiceEvent(serviceEvent); 321 } catch (RemoteException e) { 322 Log.w(TAG, "onWalletServiceEvent error", e); 323 mEventListenerId = null; 324 mEventListener = null; 325 } 326 } 327 registerDismissWalletListenerInternal( @onNull WalletServiceEventListenerRequest request, @NonNull IQuickAccessWalletServiceCallbacks callback)328 private void registerDismissWalletListenerInternal( 329 @NonNull WalletServiceEventListenerRequest request, 330 @NonNull IQuickAccessWalletServiceCallbacks callback) { 331 mEventListenerId = request.getListenerId(); 332 mEventListener = callback; 333 } 334 unregisterDismissWalletListenerInternal( @onNull WalletServiceEventListenerRequest request)335 private void unregisterDismissWalletListenerInternal( 336 @NonNull WalletServiceEventListenerRequest request) { 337 if (mEventListenerId != null && mEventListenerId.equals(request.getListenerId())) { 338 mEventListenerId = null; 339 mEventListener = null; 340 } else { 341 Log.w(TAG, "dismiss listener missing or replaced"); 342 } 343 } 344 } 345