blob: 2d0faad8cb13dc9d629773f4a6a7dc257fe2390d [file] [log] [blame]
/*
* 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.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.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
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 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) {
mContext = context.getApplicationContext();
mServiceInfo = QuickAccessWalletServiceInfo.tryCreate(context);
mHandler = new Handler(Looper.getMainLooper());
mRequestQueue = new LinkedList<>();
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
@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;
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;
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();
}
}
}