From 72fc25f5a3cb5e8d9b1327e1b6e4b0a0a83777f8 Mon Sep 17 00:00:00 2001 From: Sean Pont Date: Tue, 11 Feb 2020 19:02:44 -0800 Subject: [PATCH] Improve QuickAccessWallet documentation and client. Test: manual - launch quick access wallet Test: atest CtsQuickAccessWalletTestCases Bug: b/149575358 Change-Id: I6f0b86dc62c3cd2b85c5b1b378c7b6c9ec663b58 --- api/current.txt | 3 +- api/test-current.txt | 31 ++ core/java/android/provider/Settings.java | 3 + .../GetWalletCardsCallback.java | 56 +-- .../GetWalletCardsCallbackImpl.java | 136 ++++++ .../GetWalletCardsResponse.java | 8 +- .../QuickAccessWalletClient.java | 78 +++- .../QuickAccessWalletClientImpl.java | 400 ++++++++++-------- .../QuickAccessWalletService.java | 59 ++- .../QuickAccessWalletServiceInfo.java | 5 +- .../service/quickaccesswallet/WalletCard.java | 13 +- 11 files changed, 524 insertions(+), 268 deletions(-) create mode 100644 core/java/android/service/quickaccesswallet/GetWalletCardsCallbackImpl.java diff --git a/api/current.txt b/api/current.txt index 5e8974fb82d70..a542dcc94337a 100644 --- a/api/current.txt +++ b/api/current.txt @@ -43965,7 +43965,7 @@ package android.service.notification { package android.service.quickaccesswallet { - public final class GetWalletCardsCallback { + public interface GetWalletCardsCallback { method public void onFailure(@NonNull android.service.quickaccesswallet.GetWalletCardsError); method public void onSuccess(@NonNull android.service.quickaccesswallet.GetWalletCardsResponse); } @@ -44006,7 +44006,6 @@ package android.service.quickaccesswallet { method public abstract void onWalletCardsRequested(@NonNull android.service.quickaccesswallet.GetWalletCardsRequest, @NonNull android.service.quickaccesswallet.GetWalletCardsCallback); method public abstract void onWalletDismissed(); method public final void sendWalletServiceEvent(@NonNull android.service.quickaccesswallet.WalletServiceEvent); - field public static final String ACTION_DISMISS_WALLET = "android.service.quickaccesswallet.action.DISMISS_WALLET"; field public static final String ACTION_VIEW_WALLET = "android.service.quickaccesswallet.action.VIEW_WALLET"; field public static final String ACTION_VIEW_WALLET_SETTINGS = "android.service.quickaccesswallet.action.VIEW_WALLET_SETTINGS"; field public static final String SERVICE_INTERFACE = "android.service.quickaccesswallet.QuickAccessWalletService"; diff --git a/api/test-current.txt b/api/test-current.txt index 4c8bb0290caef..1e4cc965db217 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -2962,6 +2962,9 @@ package android.provider { field public static final String ENABLED_VR_LISTENERS = "enabled_vr_listeners"; field public static final String LOCATION_ACCESS_CHECK_DELAY_MILLIS = "location_access_check_delay_millis"; field public static final String LOCATION_ACCESS_CHECK_INTERVAL_MILLIS = "location_access_check_interval_millis"; + field public static final String LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS = "lock_screen_allow_private_notifications"; + field public static final String LOCK_SCREEN_SHOW_NOTIFICATIONS = "lock_screen_show_notifications"; + field public static final String NFC_PAYMENT_DEFAULT_COMPONENT = "nfc_payment_default_component"; field public static final String NOTIFICATION_BADGING = "notification_badging"; field @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public static final String SYNC_PARENT_SOUNDS = "sync_parent_sounds"; field public static final String USER_SETUP_COMPLETE = "user_setup_complete"; @@ -3385,6 +3388,34 @@ package android.service.notification { } +package android.service.quickaccesswallet { + + public interface QuickAccessWalletClient { + method public void addWalletServiceEventListener(@NonNull android.service.quickaccesswallet.QuickAccessWalletClient.WalletServiceEventListener); + method @NonNull public static android.service.quickaccesswallet.QuickAccessWalletClient create(@NonNull android.content.Context); + method @Nullable public android.content.Intent createWalletIntent(); + method @Nullable public android.content.Intent createWalletSettingsIntent(); + method public void disconnect(); + method public void getWalletCards(@NonNull android.service.quickaccesswallet.GetWalletCardsRequest, @NonNull android.service.quickaccesswallet.QuickAccessWalletClient.OnWalletCardsRetrievedCallback); + method public boolean isWalletFeatureAvailable(); + method public boolean isWalletFeatureAvailableWhenDeviceLocked(); + method public boolean isWalletServiceAvailable(); + method public void notifyWalletDismissed(); + method public void removeWalletServiceEventListener(@NonNull android.service.quickaccesswallet.QuickAccessWalletClient.WalletServiceEventListener); + method public void selectWalletCard(@NonNull android.service.quickaccesswallet.SelectWalletCardRequest); + } + + public static interface QuickAccessWalletClient.OnWalletCardsRetrievedCallback { + method public void onWalletCardRetrievalError(@NonNull android.service.quickaccesswallet.GetWalletCardsError); + method public void onWalletCardsRetrieved(@NonNull android.service.quickaccesswallet.GetWalletCardsResponse); + } + + public static interface QuickAccessWalletClient.WalletServiceEventListener { + method public void onWalletServiceEvent(@NonNull android.service.quickaccesswallet.WalletServiceEvent); + } + +} + package android.service.quicksettings { public class TileService extends android.app.Service { diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 37efec34b3307..f1241a4492189 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -6449,6 +6449,7 @@ public final class Settings { * @hide */ @SystemApi + @TestApi public static final String LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS = "lock_screen_allow_private_notifications"; @@ -7867,6 +7868,7 @@ public final class Settings { * @hide */ @UnsupportedAppUsage + @TestApi public static final String NFC_PAYMENT_DEFAULT_COMPONENT = "nfc_payment_default_component"; /** @@ -8069,6 +8071,7 @@ public final class Settings { * @hide */ @SystemApi + @TestApi public static final String LOCK_SCREEN_SHOW_NOTIFICATIONS = "lock_screen_show_notifications"; diff --git a/core/java/android/service/quickaccesswallet/GetWalletCardsCallback.java b/core/java/android/service/quickaccesswallet/GetWalletCardsCallback.java index 9d210cd82c235..f6a86d9f2a55e 100644 --- a/core/java/android/service/quickaccesswallet/GetWalletCardsCallback.java +++ b/core/java/android/service/quickaccesswallet/GetWalletCardsCallback.java @@ -17,28 +17,11 @@ package android.service.quickaccesswallet; import android.annotation.NonNull; -import android.os.Handler; -import android.os.RemoteException; -import android.util.Log; /** * Handles response from the {@link QuickAccessWalletService} for {@link GetWalletCardsRequest} */ -public final class GetWalletCardsCallback { - - private static final String TAG = "QAWalletCallback"; - - private final IQuickAccessWalletServiceCallbacks mCallback; - private final Handler mHandler; - private boolean mCalled; - - /** - * @hide - */ - GetWalletCardsCallback(IQuickAccessWalletServiceCallbacks callback, Handler handler) { - mCallback = callback; - mHandler = handler; - } +public interface GetWalletCardsCallback { /** * Notifies the Android System that an {@link QuickAccessWalletService#onWalletCardsRequested} @@ -46,11 +29,10 @@ public final class GetWalletCardsCallback { * * @param response The response contains the list of {@link WalletCard walletCards} to be shown * to the user as well as the index of the card that should initially be - * presented as the selected card. + * presented as the selected card. The list should not contain more than the + * maximum number of cards requested. */ - public void onSuccess(@NonNull GetWalletCardsResponse response) { - mHandler.post(() -> onSuccessInternal(response)); - } + void onSuccess(@NonNull GetWalletCardsResponse response); /** * Notifies the Android System that an {@link QuickAccessWalletService#onWalletCardsRequested} @@ -60,33 +42,5 @@ public final class GetWalletCardsCallback { * (Personally Identifiable Information, such as username or email address). * @throws IllegalStateException if this method or {@link #onSuccess} was already called. */ - public void onFailure(@NonNull GetWalletCardsError error) { - mHandler.post(() -> onFailureInternal(error)); - } - - private void onSuccessInternal(GetWalletCardsResponse response) { - if (mCalled) { - Log.w(TAG, "already called"); - return; - } - mCalled = true; - try { - mCallback.onGetWalletCardsSuccess(response); - } catch (RemoteException e) { - Log.e(TAG, "Error returning wallet cards", e); - } - } - - private void onFailureInternal(GetWalletCardsError error) { - if (mCalled) { - Log.w(TAG, "already called"); - return; - } - mCalled = true; - try { - mCallback.onGetWalletCardsFailure(error); - } catch (RemoteException e) { - Log.e(TAG, "Error returning failure message", e); - } - } + void onFailure(@NonNull GetWalletCardsError error); } diff --git a/core/java/android/service/quickaccesswallet/GetWalletCardsCallbackImpl.java b/core/java/android/service/quickaccesswallet/GetWalletCardsCallbackImpl.java new file mode 100644 index 0000000000000..d2494a544d7aa --- /dev/null +++ b/core/java/android/service/quickaccesswallet/GetWalletCardsCallbackImpl.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.quickaccesswallet; + +import android.annotation.NonNull; +import android.graphics.Bitmap; +import android.graphics.drawable.Icon; +import android.os.Handler; +import android.os.RemoteException; +import android.text.TextUtils; +import android.util.Log; + +import java.util.List; + +/** + * Handles response from the {@link QuickAccessWalletService} for {@link GetWalletCardsRequest} + * + * @hide + */ +final class GetWalletCardsCallbackImpl implements GetWalletCardsCallback { + + private static final String TAG = "QAWalletCallback"; + + private final IQuickAccessWalletServiceCallbacks mCallback; + private final GetWalletCardsRequest mRequest; + private final Handler mHandler; + private boolean mCalled; + + GetWalletCardsCallbackImpl(GetWalletCardsRequest request, + IQuickAccessWalletServiceCallbacks callback, Handler handler) { + mRequest = request; + mCallback = callback; + mHandler = handler; + } + + /** + * Notifies the Android System that an {@link QuickAccessWalletService#onWalletCardsRequested} + * was successfully handled by the service. + * + * @param response The response contains the list of {@link WalletCard walletCards} to be shown + * to the user as well as the index of the card that should initially be + * presented as the selected card. + */ + public void onSuccess(@NonNull GetWalletCardsResponse response) { + Log.i(TAG, "onSuccess"); + if (isValidResponse(response)) { + mHandler.post(() -> onSuccessInternal(response)); + } else { + Log.w(TAG, "Invalid GetWalletCards response"); + mHandler.post(() -> onFailureInternal(new GetWalletCardsError(null, null))); + } + } + + /** + * Notifies the Android System that an {@link QuickAccessWalletService#onWalletCardsRequested} + * could not be handled by the service. + * + * @param error The error message. Note: this message should not contain PII + * (Personally Identifiable Information, such as username or email address). + * @throws IllegalStateException if this method or {@link #onSuccess} was already called. + */ + public void onFailure(@NonNull GetWalletCardsError error) { + mHandler.post(() -> onFailureInternal(error)); + } + + private void onSuccessInternal(GetWalletCardsResponse response) { + Log.i(TAG, "onSuccessInternal"); + if (mCalled) { + Log.w(TAG, "already called"); + return; + } + mCalled = true; + try { + mCallback.onGetWalletCardsSuccess(response); + Log.i(TAG, "onSuccessInternal: returned response"); + } catch (RemoteException e) { + Log.w(TAG, "Error returning wallet cards", e); + } + } + + private void onFailureInternal(GetWalletCardsError error) { + if (mCalled) { + Log.w(TAG, "already called"); + return; + } + mCalled = true; + try { + mCallback.onGetWalletCardsFailure(error); + } catch (RemoteException e) { + Log.e(TAG, "Error returning failure message", e); + } + } + + private boolean isValidResponse(@NonNull GetWalletCardsResponse response) { + return response != null + && response.getWalletCards() != null + && response.getSelectedIndex() >= 0 + && (response.getWalletCards().isEmpty() // selectedIndex may be 0 when list is empty + || response.getSelectedIndex() < response.getWalletCards().size()) + && response.getWalletCards().size() < mRequest.getMaxCards() + && areValidCards(response.getWalletCards()); + } + + private boolean areValidCards(List walletCards) { + for (WalletCard walletCard : walletCards) { + if (walletCard == null + || walletCard.getCardId() == null + || walletCard.getCardImage() == null + || TextUtils.isEmpty(walletCard.getContentDescription()) + || walletCard.getPendingIntent() == null) { + return false; + } + Icon cardImage = walletCard.getCardImage(); + if (cardImage.getType() == Icon.TYPE_BITMAP + && walletCard.getCardImage().getBitmap().getConfig() + != Bitmap.Config.HARDWARE) { + Log.w(TAG, "WalletCard bitmaps should be hardware bitmaps"); + } + } + return true; + } +} diff --git a/core/java/android/service/quickaccesswallet/GetWalletCardsResponse.java b/core/java/android/service/quickaccesswallet/GetWalletCardsResponse.java index 996622a3e5aa8..0551e2709de6f 100644 --- a/core/java/android/service/quickaccesswallet/GetWalletCardsResponse.java +++ b/core/java/android/service/quickaccesswallet/GetWalletCardsResponse.java @@ -35,9 +35,13 @@ public final class GetWalletCardsResponse implements Parcelable { /** * Construct a new response. * - * @param walletCards The list of wallet cards. + * @param walletCards The list of wallet cards. The list may be empty but must NOT be larger + * than {@link GetWalletCardsRequest#getMaxCards()}. The list may not + * contain null values. * @param selectedIndex The index of the card that should be presented as the initially - * 'selected' card + * 'selected' card. The index must be greater than or equal to zero and + * less than the size of the list of walletCards (unless the list is empty + * in which case the value may be 0). */ public GetWalletCardsResponse(@NonNull List walletCards, int selectedIndex) { this.mWalletCards = walletCards; diff --git a/core/java/android/service/quickaccesswallet/QuickAccessWalletClient.java b/core/java/android/service/quickaccesswallet/QuickAccessWalletClient.java index cfc6d5777e82b..be9ab11fb3a27 100644 --- a/core/java/android/service/quickaccesswallet/QuickAccessWalletClient.java +++ b/core/java/android/service/quickaccesswallet/QuickAccessWalletClient.java @@ -18,16 +18,16 @@ package android.service.quickaccesswallet; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.TestApi; import android.content.Context; import android.content.Intent; -import java.util.function.Consumer; - /** * Facilitates accessing cards from the {@link QuickAccessWalletService}. * * @hide */ +@TestApi public interface QuickAccessWalletClient { /** @@ -40,17 +40,65 @@ public interface QuickAccessWalletClient { } /** - * @return true if the {@link QuickAccessWalletService} is available. + * @return true if the {@link QuickAccessWalletService} is available. This means that the + * default NFC payment application has an exported service that can provide cards to the Quick + * Access Wallet. However, it does not mean that (1) the call will necessarily be successful, + * nor does it mean that cards may be displayed at this time. Addition checks are required: + *
    + *
  • If {@link #isWalletFeatureAvailable()} is false, cards should not be displayed + *
  • If the device is locked and {@link #isWalletFeatureAvailableWhenDeviceLocked} is + * false, cards should not be displayed while the device remains locked. (A message + * prompting the user to unlock to view cards may be appropriate).
  • + *
*/ boolean isWalletServiceAvailable(); + /** + * Wallet cards should not be displayed if: + *
    + *
  • The wallet service is unavailable
  • + *
  • The device is not provisioned, ie user setup is incomplete
  • + *
  • If the wallet feature has been disabled by the user
  • + *
  • If the phone has been put into lockdown mode
  • + *
+ *

+ * Quick Access Wallet implementers should call this method before calling + * {@link #getWalletCards} to ensure that cards may be displayed. + */ + boolean isWalletFeatureAvailable(); + + /** + * Wallet cards may not be displayed on the lock screen if the user has opted to hide + * notifications or sensitive content on the lock screen. + *

    + *
  • The device is not provisioned, ie user setup is incomplete
  • + *
  • If the wallet feature has been disabled by the user
  • + *
  • If the phone has been put into lockdown mode
  • + *
+ * + *

+ * Quick Access Wallet implementers should call this method before calling + * {@link #getWalletCards} if the device is currently locked. + * + * @return true if cards may be displayed on the lock screen. + */ + boolean isWalletFeatureAvailableWhenDeviceLocked(); + /** * Get wallet cards from the {@link QuickAccessWalletService}. */ void getWalletCards( @NonNull GetWalletCardsRequest request, - @NonNull Consumer onSuccessListener, - @NonNull Consumer onFailureListener); + @NonNull OnWalletCardsRetrievedCallback callback); + + /** + * Callback for getWalletCards + */ + interface OnWalletCardsRetrievedCallback { + void onWalletCardsRetrieved(@NonNull GetWalletCardsResponse response); + + void onWalletCardRetrievalError(@NonNull GetWalletCardsError error); + } /** * Notify the {@link QuickAccessWalletService} service that a wallet card was selected. @@ -65,12 +113,24 @@ public interface QuickAccessWalletClient { /** * Unregister event listener. */ - void registerWalletServiceEventListener(Consumer listener); + void addWalletServiceEventListener(@NonNull WalletServiceEventListener listener); /** * Unregister event listener */ - void unregisterWalletServiceEventListener(Consumer listener); + void removeWalletServiceEventListener(@NonNull WalletServiceEventListener listener); + + /** + * A listener for {@link WalletServiceEvent walletServiceEvents} + */ + interface WalletServiceEventListener { + void onWalletServiceEvent(@NonNull WalletServiceEvent event); + } + + /** + * Unregister all event listeners and disconnect from the service. + */ + void disconnect(); /** * The manifest entry for the QuickAccessWalletService may also publish information about the @@ -78,12 +138,12 @@ public interface QuickAccessWalletClient { * application. */ @Nullable - Intent getWalletActivity(); + Intent createWalletIntent(); /** * The manifest entry for the {@link QuickAccessWalletService} may publish the activity that * hosts the settings */ @Nullable - Intent getSettingsActivity(); + Intent createWalletSettingsIntent(); } diff --git a/core/java/android/service/quickaccesswallet/QuickAccessWalletClientImpl.java b/core/java/android/service/quickaccesswallet/QuickAccessWalletClientImpl.java index 17c287fa8eeb0..37a8703ddebe6 100644 --- a/core/java/android/service/quickaccesswallet/QuickAccessWalletClientImpl.java +++ b/core/java/android/service/quickaccesswallet/QuickAccessWalletClientImpl.java @@ -20,6 +20,7 @@ import static android.service.quickaccesswallet.QuickAccessWalletService.SERVICE import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.ActivityManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -27,79 +28,225 @@ import android.content.ServiceConnection; import android.os.Handler; import android.os.IBinder; import android.os.Looper; -import android.os.Message; import android.os.RemoteException; +import android.os.UserHandle; +import android.provider.Settings; import android.text.TextUtils; import android.util.Log; +import com.android.internal.widget.LockPatternUtils; + import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; import java.util.Queue; import java.util.UUID; -import java.util.function.Consumer; /** + * Implements {@link QuickAccessWalletClient}. The client connects, performs requests, waits for + * responses, and disconnects automatically after a short period of time. The client may * @hide */ -@SuppressWarnings("AndroidJdkLibsChecker") -class QuickAccessWalletClientImpl implements QuickAccessWalletClient, Handler.Callback, - ServiceConnection { +public class QuickAccessWalletClientImpl implements QuickAccessWalletClient, ServiceConnection { private static final String TAG = "QAWalletSClient"; private final Handler mHandler; private final Context mContext; private final Queue mRequestQueue; - private final Map, String> mEventListeners; + private final Map mEventListeners; private boolean mIsConnected; + /** + * Timeout for active service connections (1 minute) + */ + private static final long SERVICE_CONNECTION_TIMEOUT_MS = 60 * 1000; @Nullable private IQuickAccessWalletService mService; - @Nullable private final QuickAccessWalletServiceInfo mServiceInfo; - private static final int MSG_CONNECT = 1; - private static final int MSG_CONNECTED = 2; - private static final int MSG_EXECUTE = 3; - private static final int MSG_DISCONNECT = 4; + private static final int MSG_TIMEOUT_SERVICE = 5; QuickAccessWalletClientImpl(@NonNull Context context) { mContext = context.getApplicationContext(); mServiceInfo = QuickAccessWalletServiceInfo.tryCreate(context); - mHandler = new Handler(Looper.getMainLooper(), this); + mHandler = new Handler(Looper.getMainLooper()); mRequestQueue = new LinkedList<>(); mEventListeners = new HashMap<>(1); } @Override - public boolean handleMessage(Message msg) { - switch (msg.what) { - case MSG_CONNECT: - connectInternal(); - break; - case MSG_CONNECTED: - onConnectedInternal((IQuickAccessWalletService) msg.obj); - break; - case MSG_EXECUTE: - executeInternal((ApiCaller) msg.obj); - break; - case MSG_DISCONNECT: - disconnectInternal(); - break; - default: - Log.w(TAG, "Unknown what: " + msg.what); - return false; + public boolean isWalletServiceAvailable() { + boolean available = mServiceInfo != null; + Log.i(TAG, "isWalletServiceAvailable: " + available); + return available; + } + + @Override + public boolean isWalletFeatureAvailable() { + int currentUser = ActivityManager.getCurrentUser(); + return checkUserSetupComplete() + && checkSecureSetting(Settings.Secure.GLOBAL_ACTIONS_PANEL_ENABLED) + && !new LockPatternUtils(mContext).isUserInLockdown(currentUser); + } + + @Override + public boolean isWalletFeatureAvailableWhenDeviceLocked() { + return checkSecureSetting(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS) + && checkSecureSetting(Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS); + } + + @Override + public void getWalletCards( + @NonNull GetWalletCardsRequest request, + @NonNull OnWalletCardsRetrievedCallback callback) { + + Log.i(TAG, "getWalletCards"); + + if (!isWalletServiceAvailable()) { + callback.onWalletCardRetrievalError(new GetWalletCardsError(null, null)); + return; } - return true; + + BaseCallbacks serviceCallback = new BaseCallbacks() { + @Override + public void onGetWalletCardsSuccess(GetWalletCardsResponse response) { + mHandler.post(() -> callback.onWalletCardsRetrieved(response)); + } + + @Override + public void onGetWalletCardsFailure(GetWalletCardsError error) { + mHandler.post(() -> callback.onWalletCardRetrievalError(error)); + } + }; + + executeApiCall(new ApiCaller("onWalletCardsRequested") { + @Override + public void performApiCall(IQuickAccessWalletService service) throws RemoteException { + service.onWalletCardsRequested(request, serviceCallback); + } + + @Override + public void onApiError() { + serviceCallback.onGetWalletCardsFailure(new GetWalletCardsError(null, null)); + } + }); + } + + @Override + public void selectWalletCard(@NonNull SelectWalletCardRequest request) { + Log.i(TAG, "selectWalletCard"); + if (!isWalletServiceAvailable()) { + return; + } + executeApiCall(new ApiCaller("onWalletCardSelected") { + @Override + public void performApiCall(IQuickAccessWalletService service) throws RemoteException { + service.onWalletCardSelected(request); + } + }); + } + + @Override + public void notifyWalletDismissed() { + if (!isWalletServiceAvailable()) { + return; + } + Log.i(TAG, "notifyWalletDismissed"); + executeApiCall(new ApiCaller("onWalletDismissed") { + @Override + public void performApiCall(IQuickAccessWalletService service) throws RemoteException { + service.onWalletDismissed(); + } + }); + } + + @Override + public void addWalletServiceEventListener(WalletServiceEventListener listener) { + if (!isWalletServiceAvailable()) { + return; + } + Log.i(TAG, "registerWalletServiceEventListener"); + BaseCallbacks callback = new BaseCallbacks() { + @Override + public void onWalletServiceEvent(WalletServiceEvent event) { + Log.i(TAG, "onWalletServiceEvent"); + mHandler.post(() -> listener.onWalletServiceEvent(event)); + } + }; + + executeApiCall(new ApiCaller("registerListener") { + @Override + public void performApiCall(IQuickAccessWalletService service) throws RemoteException { + String listenerId = UUID.randomUUID().toString(); + WalletServiceEventListenerRequest request = + new WalletServiceEventListenerRequest(listenerId); + mEventListeners.put(listener, listenerId); + service.registerWalletServiceEventListener(request, callback); + } + }); + } + + @Override + public void removeWalletServiceEventListener(WalletServiceEventListener listener) { + if (!isWalletServiceAvailable()) { + return; + } + Log.i(TAG, "unregisterWalletServiceEventListener"); + executeApiCall(new ApiCaller("unregisterListener") { + @Override + public void performApiCall(IQuickAccessWalletService service) throws RemoteException { + String listenerId = mEventListeners.remove(listener); + if (listenerId == null) { + return; + } + WalletServiceEventListenerRequest request = + new WalletServiceEventListenerRequest(listenerId); + service.unregisterWalletServiceEventListener(request); + } + }); + } + + @Override + public void disconnect() { + Log.i(TAG, "disconnect"); + mHandler.post(() -> disconnectInternal(true)); + } + + @Override + @Nullable + public Intent createWalletIntent() { + if (mServiceInfo == null || TextUtils.isEmpty(mServiceInfo.getWalletActivity())) { + return null; + } + return new Intent(QuickAccessWalletService.ACTION_VIEW_WALLET) + .setComponent( + new ComponentName( + mServiceInfo.getComponentName().getPackageName(), + mServiceInfo.getWalletActivity())); + } + + @Override + @Nullable + public Intent createWalletSettingsIntent() { + if (mServiceInfo == null || TextUtils.isEmpty(mServiceInfo.getSettingsActivity())) { + return null; + } + return new Intent(QuickAccessWalletService.ACTION_VIEW_WALLET_SETTINGS) + .setComponent( + new ComponentName( + mServiceInfo.getComponentName().getPackageName(), + mServiceInfo.getSettingsActivity())); } private void connect() { - mHandler.sendMessage(mHandler.obtainMessage(MSG_CONNECT)); + Log.i(TAG, "connect"); + mHandler.post(this::connectInternal); } private void connectInternal() { + Log.i(TAG, "connectInternal"); if (mServiceInfo == null) { Log.w(TAG, "Wallet service unavailable"); return; @@ -113,15 +260,18 @@ class QuickAccessWalletClientImpl implements QuickAccessWalletClient, Handler.Ca intent.setComponent(mServiceInfo.getComponentName()); int flags = Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY; mContext.bindService(intent, this, flags); + resetServiceConnectionTimeout(); } private void onConnectedInternal(IQuickAccessWalletService service) { + Log.i(TAG, "onConnectedInternal"); if (!mIsConnected) { Log.w(TAG, "onConnectInternal but connection closed"); mService = null; return; } mService = service; + Log.i(TAG, "onConnectedInternal success: request queue size " + mRequestQueue.size()); for (ApiCaller apiCaller : new ArrayList<>(mRequestQueue)) { try { apiCaller.performApiCall(mService); @@ -135,15 +285,33 @@ class QuickAccessWalletClientImpl implements QuickAccessWalletClient, Handler.Ca } } - private void disconnect() { - mHandler.sendMessage(mHandler.obtainMessage(MSG_DISCONNECT)); + /** + * Resets the idle timeout for this connection by removing any pending timeout messages and + * posting a new delayed message. + */ + private void resetServiceConnectionTimeout() { + Log.i(TAG, "resetServiceConnectionTimeout"); + mHandler.removeMessages(MSG_TIMEOUT_SERVICE); + mHandler.postDelayed( + () -> disconnectInternal(true), + MSG_TIMEOUT_SERVICE, + SERVICE_CONNECTION_TIMEOUT_MS); } - private void disconnectInternal() { + private void disconnectInternal(boolean clearEventListeners) { + Log.i(TAG, "disconnectInternal: " + clearEventListeners); if (!mIsConnected) { Log.w(TAG, "already disconnected"); return; } + if (clearEventListeners && !mEventListeners.isEmpty()) { + Log.i(TAG, "disconnectInternal: clear event listeners"); + for (WalletServiceEventListener listener : mEventListeners.keySet()) { + removeWalletServiceEventListener(listener); + } + mHandler.post(() -> disconnectInternal(false)); + return; + } mIsConnected = false; mContext.unbindService(/*conn=*/this); mService = null; @@ -151,181 +319,81 @@ class QuickAccessWalletClientImpl implements QuickAccessWalletClient, Handler.Ca mRequestQueue.clear(); } - private void execute(ApiCaller apiCaller) { - mHandler.sendMessage(mHandler.obtainMessage(MSG_EXECUTE, apiCaller)); + private void executeApiCall(ApiCaller apiCaller) { + Log.i(TAG, "execute: " + apiCaller.mDesc); + mHandler.post(() -> executeInternal(apiCaller)); } private void executeInternal(ApiCaller apiCall) { + Log.i(TAG, "executeInternal: " + apiCall.mDesc); if (mIsConnected && mService != null) { try { apiCall.performApiCall(mService); + Log.i(TAG, "executeInternal success: " + apiCall.mDesc); + resetServiceConnectionTimeout(); } catch (RemoteException e) { - Log.w(TAG, "executeInternal error", e); + Log.w(TAG, "executeInternal error: " + apiCall.mDesc, e); apiCall.onApiError(); disconnect(); } } else { + Log.i(TAG, "executeInternal: queued" + apiCall.mDesc); mRequestQueue.add(apiCall); connect(); } } - public boolean isWalletServiceAvailable() { - return mServiceInfo != null; - } - private abstract static class ApiCaller { + private final String mDesc; + + private ApiCaller(String desc) { + this.mDesc = desc; + } + abstract void performApiCall(IQuickAccessWalletService service) throws RemoteException; void onApiError() { - Log.w(TAG, "api error"); + Log.w(TAG, "api error: " + mDesc); } } - public void getWalletCards( - @NonNull GetWalletCardsRequest request, - @NonNull Consumer onSuccessListener, - @NonNull Consumer onFailureListener) { - - BaseCallbacks callback = new BaseCallbacks() { - @Override - public void onGetWalletCardsSuccess(GetWalletCardsResponse response) { - mHandler.post(() -> onSuccessListener.accept(response)); - } - - @Override - public void onGetWalletCardsFailure(GetWalletCardsError error) { - mHandler.post(() -> onFailureListener.accept(error)); - } - }; - - execute(new ApiCaller() { - @Override - public void performApiCall(IQuickAccessWalletService service) throws RemoteException { - service.onWalletCardsRequested(request, callback); - } - - @Override - public void onApiError() { - callback.onGetWalletCardsFailure(new GetWalletCardsError(null, null)); - } - }); - } - - public void selectWalletCard(@NonNull SelectWalletCardRequest request) { - execute(new ApiCaller() { - @Override - public void performApiCall(IQuickAccessWalletService service) throws RemoteException { - service.onWalletCardSelected(request); - } - }); - } - - public void notifyWalletDismissed() { - execute(new ApiCaller() { - @Override - public void performApiCall(IQuickAccessWalletService service) throws RemoteException { - service.onWalletDismissed(); - mHandler.sendMessage(mHandler.obtainMessage(MSG_DISCONNECT)); - } - }); - } - - @Override - public void registerWalletServiceEventListener(Consumer listener) { - - BaseCallbacks callback = new BaseCallbacks() { - @Override - public void onWalletServiceEvent(WalletServiceEvent event) { - Log.i(TAG, "onWalletServiceEvent"); - mHandler.post(() -> listener.accept(event)); - } - }; - - execute(new ApiCaller() { - @Override - public void performApiCall(IQuickAccessWalletService service) throws RemoteException { - String listenerId = UUID.randomUUID().toString(); - WalletServiceEventListenerRequest request = - new WalletServiceEventListenerRequest(listenerId); - mEventListeners.put(listener, listenerId); - service.registerWalletServiceEventListener(request, callback); - } - }); - } - - @Override - public void unregisterWalletServiceEventListener(Consumer listener) { - execute(new ApiCaller() { - @Override - public void performApiCall(IQuickAccessWalletService service) throws RemoteException { - String listenerId = mEventListeners.get(listener); - if (listenerId == null) { - return; - } - WalletServiceEventListenerRequest request = - new WalletServiceEventListenerRequest(listenerId); - service.unregisterWalletServiceEventListener(request); - } - }); - } - - @Override - @Nullable - public Intent getWalletActivity() { - if (mServiceInfo == null || TextUtils.isEmpty(mServiceInfo.getWalletActivity())) { - return null; - } - return new Intent(QuickAccessWalletService.ACTION_VIEW_WALLET) - .setComponent( - new ComponentName( - mServiceInfo.getComponentName().getPackageName(), - mServiceInfo.getWalletActivity())); - } - - @Override - @Nullable - public Intent getSettingsActivity() { - if (mServiceInfo == null || TextUtils.isEmpty(mServiceInfo.getSettingsActivity())) { - return null; - } - return new Intent(QuickAccessWalletService.ACTION_VIEW_WALLET_SETTINGS) - .setComponent( - new ComponentName( - mServiceInfo.getComponentName().getPackageName(), - mServiceInfo.getSettingsActivity())); - } - - /** - * Connection to the {@link QuickAccessWalletService} - */ - - - @Override + @Override // ServiceConnection public void onServiceConnected(ComponentName name, IBinder binder) { + Log.i(TAG, "onServiceConnected: " + name); IQuickAccessWalletService service = IQuickAccessWalletService.Stub.asInterface(binder); - mHandler.sendMessage(mHandler.obtainMessage(MSG_CONNECTED, service)); + mHandler.post(() -> onConnectedInternal(service)); } - @Override + @Override // ServiceConnection public void onServiceDisconnected(ComponentName name) { // Do not disconnect, as we may later be re-connected Log.w(TAG, "onServiceDisconnected"); } - @Override + @Override // ServiceConnection public void onBindingDied(ComponentName name) { // This is a recoverable error but the client will need to reconnect. Log.w(TAG, "onBindingDied"); disconnect(); } - @Override + @Override // ServiceConnection public void onNullBinding(ComponentName name) { Log.w(TAG, "onNullBinding"); disconnect(); } + private boolean checkSecureSetting(String name) { + return Settings.Secure.getInt(mContext.getContentResolver(), name, 0) == 1; + } + + private boolean checkUserSetupComplete() { + return Settings.Secure.getIntForUser( + mContext.getContentResolver(), + Settings.Secure.USER_SETUP_COMPLETE, 0, + UserHandle.USER_CURRENT) == 1; + } + private static class BaseCallbacks extends IQuickAccessWalletServiceCallbacks.Stub { public void onGetWalletCardsSuccess(GetWalletCardsResponse response) { throw new IllegalStateException(); diff --git a/core/java/android/service/quickaccesswallet/QuickAccessWalletService.java b/core/java/android/service/quickaccesswallet/QuickAccessWalletService.java index d968405002e30..aba5611581e31 100644 --- a/core/java/android/service/quickaccesswallet/QuickAccessWalletService.java +++ b/core/java/android/service/quickaccesswallet/QuickAccessWalletService.java @@ -45,8 +45,9 @@ import android.util.Log; * android.permission.BIND_NFC_SERVICE permission. *

  • The user explicitly selected the application as the default payment application in * the Tap & pay settings screen. - *
  • The application requires the {@code android.permission.BIND_QUICK_ACCESS_WALLET_SERVICE} - * permission in its manifest. + *
  • The QuickAccessWalletService requires that the binding application hold the + * {@code android.permission.BIND_QUICK_ACCESS_WALLET_SERVICE} permission, which only the System + * Service can hold. *
  • The user explicitly enables it using Android Settings (the * {@link Settings#ACTION_QUICK_ACCESS_WALLET_SETTINGS} intent can be used to launch it). * @@ -73,6 +74,11 @@ import android.util.Log; * a new card is selected, the Android System will notify the service through * {@link #onWalletCardSelected} and will provide the {@link WalletCard#getCardId() cardId} of the * card that is now selected. + *
  • If the user commences an NFC payment, the service may send a {@link WalletServiceEvent} + * to the System indicating that the wallet application now needs to show the activity associated + * with making a payment. Sending a {@link WalletServiceEvent} of type + * {@link WalletServiceEvent#TYPE_NFC_PAYMENT_STARTED} should cause the quick access wallet UI + * to be dismissed. *
  • When the wallet is dismissed, the Android System will notify the service through * {@link #onWalletDismissed}. * @@ -110,6 +116,7 @@ import android.util.Log; * android:permission="android.permission.BIND_QUICK_ACCESS_WALLET_SERVICE"> * * + * * * ; @@ -165,29 +172,6 @@ public abstract class QuickAccessWalletService extends Service { public static final String ACTION_VIEW_WALLET_SETTINGS = "android.service.quickaccesswallet.action.VIEW_WALLET_SETTINGS"; - /** - * Broadcast Action: Sent by the wallet application to dismiss the Quick Access Wallet. - *

    - * The Quick Access Wallet may be shown in a system window on top of other Activities. If the - * user selects a payment card from the Quick Access Wallet and then holds their phone to an NFC - * terminal, the wallet application will need to show a payment Activity. But if the Quick - * Access Wallet is still being shown, it may obscure the payment Activity. To avoid this, the - * wallet application can send a broadcast to the Android System with this action to request - * that the Quick Access Wallet be dismissed. - *

    - * This broadcast must use the {@code android.permission.BIND_QUICK_ACCESS_WALLET_SERVICE} - * permission to ensure that it is only delivered to System UI. Furthermore, your application - * must require the {@code android.permission.DISMISS_QUICK_ACCESS_WALLET} - *

    - *

    -     * context.sendBroadcast(
    -     *     new Intent(ACTION_DISMISS_WALLET), Manifest.permission.BIND_QUICK_ACCESS_WALLET_SERVICE);
    -     * 
    - */ - @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String ACTION_DISMISS_WALLET = - "android.service.quickaccesswallet.action.DISMISS_WALLET"; - /** * Name under which a QuickAccessWalletService component publishes information about itself. * This meta-data should reference an XML resource containing a @@ -202,8 +186,20 @@ public abstract class QuickAccessWalletService extends Service { public static final String SERVICE_META_DATA = "android.quickaccesswallet"; private final Handler mHandler = new Handler(Looper.getMainLooper()); + + /** + * The service currently only supports one listener at a time. Multiple connections that + * register different listeners will clobber the listener. This field may only be accessed from + * the main thread. + */ @Nullable private String mEventListenerId; + + /** + * The service currently only supports one listener at a time. Multiple connections that + * register different listeners will clobber the listener. This field may only be accessed from + * the main thread. + */ @Nullable private IQuickAccessWalletServiceCallbacks mEventListener; @@ -240,7 +236,8 @@ public abstract class QuickAccessWalletService extends Service { private void onWalletCardsRequestedInternal( GetWalletCardsRequest request, IQuickAccessWalletServiceCallbacks callback) { - onWalletCardsRequested(request, new GetWalletCardsCallback(callback, mHandler)); + onWalletCardsRequested(request, + new GetWalletCardsCallbackImpl(request, callback, mHandler)); } @Override @@ -250,14 +247,14 @@ public abstract class QuickAccessWalletService extends Service { // Binding to the QuickAccessWalletService is protected by the // android.permission.BIND_QUICK_ACCESS_WALLET_SERVICE permission, which is defined in // R. Pre-R devices can have other side-loaded applications that claim this permission. - // This ensures that the service is only available when properly permission protected. + // Ensures that the service is only enabled when properly permission protected. Log.w(TAG, "Warning: binding on pre-R device"); } - if (SERVICE_INTERFACE.equals(intent.getAction())) { - return mInterface.asBinder(); + if (!SERVICE_INTERFACE.equals(intent.getAction())) { + Log.w(TAG, "Wrong action"); + return null; } - Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent); - return null; + return mInterface.asBinder(); } /** diff --git a/core/java/android/service/quickaccesswallet/QuickAccessWalletServiceInfo.java b/core/java/android/service/quickaccesswallet/QuickAccessWalletServiceInfo.java index 8793f28bc7085..23173a820a2ee 100644 --- a/core/java/android/service/quickaccesswallet/QuickAccessWalletServiceInfo.java +++ b/core/java/android/service/quickaccesswallet/QuickAccessWalletServiceInfo.java @@ -77,8 +77,9 @@ class QuickAccessWalletServiceInfo { } if (!Manifest.permission.BIND_QUICK_ACCESS_WALLET_SERVICE.equals(serviceInfo.permission)) { - Log.w(TAG, String.format("QuickAccessWalletService from %s does not have permission %s", - serviceInfo.packageName, Manifest.permission.BIND_QUICK_ACCESS_WALLET_SERVICE)); + Log.w(TAG, String.format("%s.%s does not require permission %s", + serviceInfo.packageName, serviceInfo.name, + Manifest.permission.BIND_QUICK_ACCESS_WALLET_SERVICE)); return null; } diff --git a/core/java/android/service/quickaccesswallet/WalletCard.java b/core/java/android/service/quickaccesswallet/WalletCard.java index c3b1a4b0b85f2..e6ae0abef9181 100644 --- a/core/java/android/service/quickaccesswallet/WalletCard.java +++ b/core/java/android/service/quickaccesswallet/WalletCard.java @@ -174,22 +174,25 @@ public final class WalletCard implements Parcelable { * @param cardId The card id must be non-null and unique within the list of * cards returned. Note: * this card ID should not contain PII (Personally - * Identifiable Information, * such as username or email - * address). + * Identifiable Information, such as username or email address). * @param cardImage The visual representation of the card. If the card image Icon * is a bitmap, it should have a width of {@link * GetWalletCardsRequest#getCardWidthPx()} and a height of {@link * GetWalletCardsRequest#getCardHeightPx()}. If the card image * does not have these dimensions, it may appear distorted when it - * is scaled to fit these dimensions on screen. + * is scaled to fit these dimensions on screen. Bitmaps should be + * of type {@link android.graphics.Bitmap.Config#HARDWARE} for + * performance reasons. * @param contentDescription The content description of the card image. This field is - * required. + * required and may not be null or empty. * Note: this message should not contain PII * (Personally Identifiable Information, such as username or email * address). * @param pendingIntent If the user performs a click on the card, this PendingIntent * will be sent. If the device is locked, the wallet will first - * request device unlock before sending the pending intent. + * request device unlock before sending the pending intent. It is + * recommended that the pending intent be immutable (use {@link + * PendingIntent#FLAG_IMMUTABLE}). */ public Builder(@NonNull String cardId, @NonNull Icon cardImage,