511 lines
17 KiB
Java
511 lines
17 KiB
Java
/*
|
|
* Copyright 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 static android.service.quickaccesswallet.QuickAccessWalletService.ACTION_VIEW_WALLET;
|
|
import static android.service.quickaccesswallet.QuickAccessWalletService.ACTION_VIEW_WALLET_SETTINGS;
|
|
import static android.service.quickaccesswallet.QuickAccessWalletService.SERVICE_INTERFACE;
|
|
|
|
import android.annotation.CallbackExecutor;
|
|
import android.annotation.NonNull;
|
|
import android.annotation.Nullable;
|
|
import android.app.ActivityManager;
|
|
import android.app.PendingIntent;
|
|
import android.content.ComponentName;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.ServiceConnection;
|
|
import android.content.pm.PackageManager;
|
|
import android.content.pm.PackageManager.NameNotFoundException;
|
|
import android.content.pm.ResolveInfo;
|
|
import android.graphics.drawable.Drawable;
|
|
import android.os.Handler;
|
|
import android.os.IBinder;
|
|
import android.os.Looper;
|
|
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.io.IOException;
|
|
import java.util.ArrayDeque;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
import java.util.Queue;
|
|
import java.util.UUID;
|
|
import java.util.concurrent.Executor;
|
|
|
|
/**
|
|
* Implements {@link QuickAccessWalletClient}. The client connects, performs requests, waits for
|
|
* responses, and disconnects automatically one minute after the last call is performed.
|
|
*
|
|
* @hide
|
|
*/
|
|
public class QuickAccessWalletClientImpl implements QuickAccessWalletClient, ServiceConnection {
|
|
|
|
private static final String TAG = "QAWalletSClient";
|
|
public static final String SETTING_KEY = "lockscreen_show_wallet";
|
|
private final Handler mHandler;
|
|
private final Context mContext;
|
|
private final Queue<ApiCaller> mRequestQueue;
|
|
private final Map<WalletServiceEventListener, String> mEventListeners;
|
|
private final Executor mLifecycleExecutor;
|
|
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_TIMEOUT_SERVICE = 5;
|
|
|
|
QuickAccessWalletClientImpl(@NonNull Context context, @Nullable Executor bgExecutor) {
|
|
mContext = context.getApplicationContext();
|
|
mServiceInfo = QuickAccessWalletServiceInfo.tryCreate(context);
|
|
mHandler = new Handler(Looper.getMainLooper());
|
|
mLifecycleExecutor = (bgExecutor == null) ? Runnable::run : bgExecutor;
|
|
mRequestQueue = new ArrayDeque<>();
|
|
mEventListeners = new HashMap<>(1);
|
|
}
|
|
|
|
@Override
|
|
public boolean isWalletServiceAvailable() {
|
|
return mServiceInfo != null;
|
|
}
|
|
|
|
@Override
|
|
public boolean isWalletFeatureAvailable() {
|
|
int currentUser = ActivityManager.getCurrentUser();
|
|
return currentUser == UserHandle.USER_SYSTEM
|
|
&& checkUserSetupComplete()
|
|
&& !new LockPatternUtils(mContext).isUserInLockdown(currentUser);
|
|
}
|
|
|
|
@Override
|
|
public boolean isWalletFeatureAvailableWhenDeviceLocked() {
|
|
return checkSecureSetting(SETTING_KEY);
|
|
}
|
|
|
|
@Override
|
|
public void getWalletCards(
|
|
@NonNull GetWalletCardsRequest request,
|
|
@NonNull OnWalletCardsRetrievedCallback callback) {
|
|
getWalletCards(mContext.getMainExecutor(), request, callback);
|
|
}
|
|
|
|
@Override
|
|
public void getWalletCards(
|
|
@NonNull @CallbackExecutor Executor executor,
|
|
@NonNull GetWalletCardsRequest request,
|
|
@NonNull OnWalletCardsRetrievedCallback callback) {
|
|
if (!isWalletServiceAvailable()) {
|
|
executor.execute(
|
|
() -> callback.onWalletCardRetrievalError(new GetWalletCardsError(null, null)));
|
|
return;
|
|
}
|
|
|
|
BaseCallbacks serviceCallback = new BaseCallbacks() {
|
|
@Override
|
|
public void onGetWalletCardsSuccess(GetWalletCardsResponse response) {
|
|
executor.execute(() -> callback.onWalletCardsRetrieved(response));
|
|
}
|
|
|
|
@Override
|
|
public void onGetWalletCardsFailure(GetWalletCardsError error) {
|
|
executor.execute(() -> 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) {
|
|
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;
|
|
}
|
|
executeApiCall(new ApiCaller("onWalletDismissed") {
|
|
@Override
|
|
public void performApiCall(IQuickAccessWalletService service) throws RemoteException {
|
|
service.onWalletDismissed();
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void addWalletServiceEventListener(WalletServiceEventListener listener) {
|
|
addWalletServiceEventListener(mContext.getMainExecutor(), listener);
|
|
}
|
|
|
|
@Override
|
|
public void addWalletServiceEventListener(
|
|
@NonNull @CallbackExecutor Executor executor,
|
|
@NonNull WalletServiceEventListener listener) {
|
|
if (!isWalletServiceAvailable()) {
|
|
return;
|
|
}
|
|
BaseCallbacks callback = new BaseCallbacks() {
|
|
@Override
|
|
public void onWalletServiceEvent(WalletServiceEvent event) {
|
|
executor.execute(() -> 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;
|
|
}
|
|
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 close() throws IOException {
|
|
disconnect();
|
|
}
|
|
|
|
@Override
|
|
public void disconnect() {
|
|
mHandler.post(() -> disconnectInternal(true));
|
|
}
|
|
|
|
@Override
|
|
@Nullable
|
|
public Intent createWalletIntent() {
|
|
if (mServiceInfo == null) {
|
|
return null;
|
|
}
|
|
String packageName = mServiceInfo.getComponentName().getPackageName();
|
|
String walletActivity = mServiceInfo.getWalletActivity();
|
|
return createIntent(walletActivity, packageName, ACTION_VIEW_WALLET);
|
|
}
|
|
|
|
@Override
|
|
public void getWalletPendingIntent(
|
|
@NonNull @CallbackExecutor Executor executor,
|
|
@NonNull WalletPendingIntentCallback pendingIntentCallback) {
|
|
BaseCallbacks callbacks = new BaseCallbacks() {
|
|
@Override
|
|
public void onTargetActivityPendingIntentReceived(PendingIntent pendingIntent) {
|
|
executor.execute(
|
|
() -> pendingIntentCallback.onWalletPendingIntentRetrieved(pendingIntent));
|
|
}
|
|
};
|
|
executeApiCall(new ApiCaller("getTargetActivityPendingIntent") {
|
|
@Override
|
|
void performApiCall(IQuickAccessWalletService service) throws RemoteException {
|
|
service.onTargetActivityIntentRequested(callbacks);
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
@Nullable
|
|
public Intent createWalletSettingsIntent() {
|
|
if (mServiceInfo == null) {
|
|
return null;
|
|
}
|
|
String packageName = mServiceInfo.getComponentName().getPackageName();
|
|
String settingsActivity = mServiceInfo.getSettingsActivity();
|
|
return createIntent(settingsActivity, packageName, ACTION_VIEW_WALLET_SETTINGS);
|
|
}
|
|
|
|
@Nullable
|
|
private Intent createIntent(@Nullable String activityName, String packageName, String action) {
|
|
PackageManager pm = mContext.getPackageManager();
|
|
if (TextUtils.isEmpty(activityName)) {
|
|
activityName = queryActivityForAction(pm, packageName, action);
|
|
}
|
|
if (TextUtils.isEmpty(activityName)) {
|
|
return null;
|
|
}
|
|
ComponentName component = new ComponentName(packageName, activityName);
|
|
if (!isActivityEnabled(pm, component)) {
|
|
return null;
|
|
}
|
|
return new Intent(action).setComponent(component);
|
|
}
|
|
|
|
@Nullable
|
|
private static String queryActivityForAction(PackageManager pm, String packageName,
|
|
String action) {
|
|
Intent intent = new Intent(action).setPackage(packageName);
|
|
ResolveInfo resolveInfo = pm.resolveActivity(intent, 0);
|
|
if (resolveInfo == null
|
|
|| resolveInfo.activityInfo == null
|
|
|| !resolveInfo.activityInfo.exported) {
|
|
return null;
|
|
}
|
|
return resolveInfo.activityInfo.name;
|
|
}
|
|
|
|
private static boolean isActivityEnabled(PackageManager pm, ComponentName component) {
|
|
int setting = pm.getComponentEnabledSetting(component);
|
|
if (setting == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
|
|
return true;
|
|
}
|
|
if (setting != PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) {
|
|
return false;
|
|
}
|
|
try {
|
|
return pm.getActivityInfo(component, 0).isEnabled();
|
|
} catch (NameNotFoundException e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
@Nullable
|
|
public Drawable getLogo() {
|
|
return mServiceInfo == null ? null : mServiceInfo.getWalletLogo(mContext);
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public Drawable getTileIcon() {
|
|
return mServiceInfo == null ? null : mServiceInfo.getTileIcon();
|
|
}
|
|
|
|
@Override
|
|
@Nullable
|
|
public CharSequence getServiceLabel() {
|
|
return mServiceInfo == null ? null : mServiceInfo.getServiceLabel(mContext);
|
|
}
|
|
|
|
@Override
|
|
@Nullable
|
|
public CharSequence getShortcutShortLabel() {
|
|
return mServiceInfo == null ? null : mServiceInfo.getShortcutShortLabel(mContext);
|
|
}
|
|
|
|
@Override
|
|
public CharSequence getShortcutLongLabel() {
|
|
return mServiceInfo == null ? null : mServiceInfo.getShortcutLongLabel(mContext);
|
|
}
|
|
|
|
private void connect() {
|
|
mHandler.post(this::connectInternal);
|
|
}
|
|
|
|
private void connectInternal() {
|
|
if (mServiceInfo == null) {
|
|
Log.w(TAG, "Wallet service unavailable");
|
|
return;
|
|
}
|
|
if (mIsConnected) {
|
|
return;
|
|
}
|
|
mIsConnected = true;
|
|
Intent intent = new Intent(SERVICE_INTERFACE);
|
|
intent.setComponent(mServiceInfo.getComponentName());
|
|
int flags = Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY;
|
|
mLifecycleExecutor.execute(() -> mContext.bindService(intent, this, flags));
|
|
resetServiceConnectionTimeout();
|
|
}
|
|
|
|
private void onConnectedInternal(IQuickAccessWalletService service) {
|
|
if (!mIsConnected) {
|
|
Log.w(TAG, "onConnectInternal but connection closed");
|
|
mService = null;
|
|
return;
|
|
}
|
|
mService = service;
|
|
for (ApiCaller apiCaller : new ArrayList<>(mRequestQueue)) {
|
|
performApiCallInternal(apiCaller, mService);
|
|
mRequestQueue.remove(apiCaller);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resets the idle timeout for this connection by removing any pending timeout messages and
|
|
* posting a new delayed message.
|
|
*/
|
|
private void resetServiceConnectionTimeout() {
|
|
mHandler.removeMessages(MSG_TIMEOUT_SERVICE);
|
|
mHandler.postDelayed(
|
|
() -> disconnectInternal(true),
|
|
MSG_TIMEOUT_SERVICE,
|
|
SERVICE_CONNECTION_TIMEOUT_MS);
|
|
}
|
|
|
|
private void disconnectInternal(boolean clearEventListeners) {
|
|
if (!mIsConnected) {
|
|
Log.w(TAG, "already disconnected");
|
|
return;
|
|
}
|
|
if (clearEventListeners && !mEventListeners.isEmpty()) {
|
|
for (WalletServiceEventListener listener : mEventListeners.keySet()) {
|
|
removeWalletServiceEventListener(listener);
|
|
}
|
|
mHandler.post(() -> disconnectInternal(false));
|
|
return;
|
|
}
|
|
mIsConnected = false;
|
|
mLifecycleExecutor.execute(() -> mContext.unbindService(/*conn=*/ this));
|
|
mService = null;
|
|
mEventListeners.clear();
|
|
mRequestQueue.clear();
|
|
}
|
|
|
|
private void executeApiCall(ApiCaller apiCaller) {
|
|
mHandler.post(() -> executeInternal(apiCaller));
|
|
}
|
|
|
|
private void executeInternal(ApiCaller apiCaller) {
|
|
if (mIsConnected && mService != null) {
|
|
performApiCallInternal(apiCaller, mService);
|
|
} else {
|
|
mRequestQueue.add(apiCaller);
|
|
connect();
|
|
}
|
|
}
|
|
|
|
private void performApiCallInternal(ApiCaller apiCaller, IQuickAccessWalletService service) {
|
|
if (service == null) {
|
|
apiCaller.onApiError();
|
|
return;
|
|
}
|
|
try {
|
|
apiCaller.performApiCall(service);
|
|
resetServiceConnectionTimeout();
|
|
} catch (RemoteException e) {
|
|
Log.w(TAG, "executeInternal error: " + apiCaller.mDesc, e);
|
|
apiCaller.onApiError();
|
|
disconnect();
|
|
}
|
|
}
|
|
|
|
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: " + mDesc);
|
|
}
|
|
}
|
|
|
|
@Override // ServiceConnection
|
|
public void onServiceConnected(ComponentName name, IBinder binder) {
|
|
IQuickAccessWalletService service = IQuickAccessWalletService.Stub.asInterface(binder);
|
|
mHandler.post(() -> onConnectedInternal(service));
|
|
}
|
|
|
|
@Override // ServiceConnection
|
|
public void onServiceDisconnected(ComponentName name) {
|
|
// Do not disconnect, as we may later be re-connected
|
|
}
|
|
|
|
@Override // ServiceConnection
|
|
public void onBindingDied(ComponentName name) {
|
|
// This is a recoverable error but the client will need to reconnect.
|
|
disconnect();
|
|
}
|
|
|
|
@Override // ServiceConnection
|
|
public void onNullBinding(ComponentName name) {
|
|
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();
|
|
}
|
|
|
|
public void onGetWalletCardsFailure(GetWalletCardsError error) {
|
|
throw new IllegalStateException();
|
|
}
|
|
|
|
public void onWalletServiceEvent(WalletServiceEvent event) {
|
|
throw new IllegalStateException();
|
|
}
|
|
|
|
public void onTargetActivityPendingIntentReceived(PendingIntent pendingIntent) {
|
|
throw new IllegalStateException();
|
|
}
|
|
}
|
|
}
|