/* * 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 mRequestQueue; private final Map 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(); } } }