Merge "Improve QuickAccessWallet documentation and client."

This commit is contained in:
Sean Pont
2020-02-16 04:30:17 +00:00
committed by Android (Google) Code Review
11 changed files with 524 additions and 268 deletions

View File

@@ -43967,7 +43967,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);
}
@@ -44008,7 +44008,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";

View File

@@ -2976,6 +2976,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";
@@ -3399,6 +3402,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 {

View File

@@ -6462,6 +6462,7 @@ public final class Settings {
* @hide
*/
@SystemApi
@TestApi
public static final String LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS =
"lock_screen_allow_private_notifications";
@@ -7880,6 +7881,7 @@ public final class Settings {
* @hide
*/
@UnsupportedAppUsage
@TestApi
public static final String NFC_PAYMENT_DEFAULT_COMPONENT = "nfc_payment_default_component";
/**
@@ -8082,6 +8084,7 @@ public final class Settings {
* @hide
*/
@SystemApi
@TestApi
public static final String LOCK_SCREEN_SHOW_NOTIFICATIONS =
"lock_screen_show_notifications";

View File

@@ -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);
}

View File

@@ -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. <b>Note: </b> this message should <b>not</b> 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<WalletCard> 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;
}
}

View File

@@ -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<WalletCard> walletCards, int selectedIndex) {
this.mWalletCards = walletCards;

View File

@@ -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:
* <ul>
* <li>If {@link #isWalletFeatureAvailable()} is false, cards should not be displayed
* <li>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).</li>
* </ul>
*/
boolean isWalletServiceAvailable();
/**
* Wallet cards should not be displayed if:
* <ul>
* <li>The wallet service is unavailable</li>
* <li>The device is not provisioned, ie user setup is incomplete</li>
* <li>If the wallet feature has been disabled by the user</li>
* <li>If the phone has been put into lockdown mode</li>
* </ul>
* <p>
* 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.
* <ul>
* <li>The device is not provisioned, ie user setup is incomplete</li>
* <li>If the wallet feature has been disabled by the user</li>
* <li>If the phone has been put into lockdown mode</li>
* </ul>
*
* <p>
* 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<GetWalletCardsResponse> onSuccessListener,
@NonNull Consumer<GetWalletCardsError> 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<WalletServiceEvent> listener);
void addWalletServiceEventListener(@NonNull WalletServiceEventListener listener);
/**
* Unregister event listener
*/
void unregisterWalletServiceEventListener(Consumer<WalletServiceEvent> 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();
}

View File

@@ -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<ApiCaller> mRequestQueue;
private final Map<Consumer<WalletServiceEvent>, String> mEventListeners;
private final Map<WalletServiceEventListener, String> 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<GetWalletCardsResponse> onSuccessListener,
@NonNull Consumer<GetWalletCardsError> 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<WalletServiceEvent> 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<WalletServiceEvent> 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();

View File

@@ -45,8 +45,9 @@ import android.util.Log;
* android.permission.BIND_NFC_SERVICE permission.
* <li>The user explicitly selected the application as the default payment application in
* the Tap &amp; pay settings screen.
* <li>The application requires the {@code android.permission.BIND_QUICK_ACCESS_WALLET_SERVICE}
* permission in its manifest.
* <li>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.
* <li>The user explicitly enables it using Android Settings (the
* {@link Settings#ACTION_QUICK_ACCESS_WALLET_SETTINGS} intent can be used to launch it).
* </ol>
@@ -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.
* <li>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.
* <li>When the wallet is dismissed, the Android System will notify the service through
* {@link #onWalletDismissed}.
* </ol>
@@ -110,6 +116,7 @@ import android.util.Log;
* android:permission="android.permission.BIND_QUICK_ACCESS_WALLET_SERVICE">
* <intent-filter>
* <action android:name="android.service.quickaccesswallet.QuickAccessWalletService" />
* <category android:name="android.intent.category.DEFAULT"/>
* </intent-filter>
* <meta-data android:name="android.quickaccesswallet"
* android:resource="@xml/quickaccesswallet_configuration" />;
@@ -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.
* <p>
* 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.
* <p>
* 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}
* <p>
* <pre class="prettyprint">
* context.sendBroadcast(
* new Intent(ACTION_DISMISS_WALLET), Manifest.permission.BIND_QUICK_ACCESS_WALLET_SERVICE);
* </pre>
*/
@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();
}
/**

View File

@@ -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;
}

View File

@@ -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. <b>Note:
* </b> this card ID should <b>not</b> 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.
* <b>Note: </b> this message should <b>not</b> 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,