1 /* 2 * Copyright 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.service.quickaccesswallet; 18 19 import static android.service.quickaccesswallet.QuickAccessWalletService.ACTION_VIEW_WALLET; 20 import static android.service.quickaccesswallet.QuickAccessWalletService.ACTION_VIEW_WALLET_SETTINGS; 21 import static android.service.quickaccesswallet.QuickAccessWalletService.SERVICE_INTERFACE; 22 23 import android.annotation.CallbackExecutor; 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.app.ActivityManager; 27 import android.app.PendingIntent; 28 import android.content.ComponentName; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.ServiceConnection; 32 import android.content.pm.PackageManager; 33 import android.content.pm.PackageManager.NameNotFoundException; 34 import android.content.pm.ResolveInfo; 35 import android.graphics.drawable.Drawable; 36 import android.os.Handler; 37 import android.os.IBinder; 38 import android.os.Looper; 39 import android.os.RemoteException; 40 import android.os.UserHandle; 41 import android.provider.Settings; 42 import android.text.TextUtils; 43 import android.util.Log; 44 45 import com.android.internal.widget.LockPatternUtils; 46 47 import java.io.IOException; 48 import java.util.ArrayDeque; 49 import java.util.ArrayList; 50 import java.util.HashMap; 51 import java.util.Map; 52 import java.util.Queue; 53 import java.util.UUID; 54 import java.util.concurrent.Executor; 55 56 /** 57 * Implements {@link QuickAccessWalletClient}. The client connects, performs requests, waits for 58 * responses, and disconnects automatically one minute after the last call is performed. 59 * 60 * @hide 61 */ 62 public class QuickAccessWalletClientImpl implements QuickAccessWalletClient, ServiceConnection { 63 64 private static final String TAG = "QAWalletSClient"; 65 public static final String SETTING_KEY = "lockscreen_show_wallet"; 66 private final Handler mHandler; 67 private final Context mContext; 68 private final Queue<ApiCaller> mRequestQueue; 69 private final Map<WalletServiceEventListener, String> mEventListeners; 70 private final Executor mLifecycleExecutor; 71 private boolean mIsConnected; 72 /** Timeout for active service connections (1 minute) */ 73 private static final long SERVICE_CONNECTION_TIMEOUT_MS = 60 * 1000; 74 75 @Nullable 76 private IQuickAccessWalletService mService; 77 78 @Nullable 79 private final QuickAccessWalletServiceInfo mServiceInfo; 80 81 private static final int MSG_TIMEOUT_SERVICE = 5; 82 QuickAccessWalletClientImpl(@onNull Context context, @Nullable Executor bgExecutor)83 QuickAccessWalletClientImpl(@NonNull Context context, @Nullable Executor bgExecutor) { 84 mContext = context.getApplicationContext(); 85 mServiceInfo = QuickAccessWalletServiceInfo.tryCreate(context); 86 mHandler = new Handler(Looper.getMainLooper()); 87 mLifecycleExecutor = (bgExecutor == null) ? Runnable::run : bgExecutor; 88 mRequestQueue = new ArrayDeque<>(); 89 mEventListeners = new HashMap<>(1); 90 } 91 92 @Override isWalletServiceAvailable()93 public boolean isWalletServiceAvailable() { 94 return mServiceInfo != null; 95 } 96 97 @Override isWalletFeatureAvailable()98 public boolean isWalletFeatureAvailable() { 99 int currentUser = ActivityManager.getCurrentUser(); 100 return currentUser == UserHandle.USER_SYSTEM 101 && checkUserSetupComplete() 102 && !new LockPatternUtils(mContext).isUserInLockdown(currentUser); 103 } 104 105 @Override isWalletFeatureAvailableWhenDeviceLocked()106 public boolean isWalletFeatureAvailableWhenDeviceLocked() { 107 return checkSecureSetting(SETTING_KEY); 108 } 109 110 @Override getWalletCards( @onNull GetWalletCardsRequest request, @NonNull OnWalletCardsRetrievedCallback callback)111 public void getWalletCards( 112 @NonNull GetWalletCardsRequest request, 113 @NonNull OnWalletCardsRetrievedCallback callback) { 114 getWalletCards(mContext.getMainExecutor(), request, callback); 115 } 116 117 @Override getWalletCards( @onNull @allbackExecutor Executor executor, @NonNull GetWalletCardsRequest request, @NonNull OnWalletCardsRetrievedCallback callback)118 public void getWalletCards( 119 @NonNull @CallbackExecutor Executor executor, 120 @NonNull GetWalletCardsRequest request, 121 @NonNull OnWalletCardsRetrievedCallback callback) { 122 if (!isWalletServiceAvailable()) { 123 executor.execute( 124 () -> callback.onWalletCardRetrievalError(new GetWalletCardsError(null, null))); 125 return; 126 } 127 128 BaseCallbacks serviceCallback = new BaseCallbacks() { 129 @Override 130 public void onGetWalletCardsSuccess(GetWalletCardsResponse response) { 131 executor.execute(() -> callback.onWalletCardsRetrieved(response)); 132 } 133 134 @Override 135 public void onGetWalletCardsFailure(GetWalletCardsError error) { 136 executor.execute(() -> callback.onWalletCardRetrievalError(error)); 137 } 138 }; 139 140 executeApiCall(new ApiCaller("onWalletCardsRequested") { 141 @Override 142 public void performApiCall(IQuickAccessWalletService service) throws RemoteException { 143 service.onWalletCardsRequested(request, serviceCallback); 144 } 145 146 @Override 147 public void onApiError() { 148 serviceCallback.onGetWalletCardsFailure(new GetWalletCardsError(null, null)); 149 } 150 }); 151 } 152 153 @Override selectWalletCard(@onNull SelectWalletCardRequest request)154 public void selectWalletCard(@NonNull SelectWalletCardRequest request) { 155 if (!isWalletServiceAvailable()) { 156 return; 157 } 158 executeApiCall(new ApiCaller("onWalletCardSelected") { 159 @Override 160 public void performApiCall(IQuickAccessWalletService service) throws RemoteException { 161 service.onWalletCardSelected(request); 162 } 163 }); 164 } 165 166 @Override notifyWalletDismissed()167 public void notifyWalletDismissed() { 168 if (!isWalletServiceAvailable()) { 169 return; 170 } 171 executeApiCall(new ApiCaller("onWalletDismissed") { 172 @Override 173 public void performApiCall(IQuickAccessWalletService service) throws RemoteException { 174 service.onWalletDismissed(); 175 } 176 }); 177 } 178 179 @Override addWalletServiceEventListener(WalletServiceEventListener listener)180 public void addWalletServiceEventListener(WalletServiceEventListener listener) { 181 addWalletServiceEventListener(mContext.getMainExecutor(), listener); 182 } 183 184 @Override addWalletServiceEventListener( @onNull @allbackExecutor Executor executor, @NonNull WalletServiceEventListener listener)185 public void addWalletServiceEventListener( 186 @NonNull @CallbackExecutor Executor executor, 187 @NonNull WalletServiceEventListener listener) { 188 if (!isWalletServiceAvailable()) { 189 return; 190 } 191 BaseCallbacks callback = new BaseCallbacks() { 192 @Override 193 public void onWalletServiceEvent(WalletServiceEvent event) { 194 executor.execute(() -> listener.onWalletServiceEvent(event)); 195 } 196 }; 197 198 executeApiCall(new ApiCaller("registerListener") { 199 @Override 200 public void performApiCall(IQuickAccessWalletService service) throws RemoteException { 201 String listenerId = UUID.randomUUID().toString(); 202 WalletServiceEventListenerRequest request = 203 new WalletServiceEventListenerRequest(listenerId); 204 mEventListeners.put(listener, listenerId); 205 service.registerWalletServiceEventListener(request, callback); 206 } 207 }); 208 } 209 210 @Override removeWalletServiceEventListener(WalletServiceEventListener listener)211 public void removeWalletServiceEventListener(WalletServiceEventListener listener) { 212 if (!isWalletServiceAvailable()) { 213 return; 214 } 215 executeApiCall(new ApiCaller("unregisterListener") { 216 @Override 217 public void performApiCall(IQuickAccessWalletService service) throws RemoteException { 218 String listenerId = mEventListeners.remove(listener); 219 if (listenerId == null) { 220 return; 221 } 222 WalletServiceEventListenerRequest request = 223 new WalletServiceEventListenerRequest(listenerId); 224 service.unregisterWalletServiceEventListener(request); 225 } 226 }); 227 } 228 229 @Override close()230 public void close() throws IOException { 231 disconnect(); 232 } 233 234 @Override disconnect()235 public void disconnect() { 236 mHandler.post(() -> disconnectInternal(true)); 237 } 238 239 @Override 240 @Nullable createWalletIntent()241 public Intent createWalletIntent() { 242 if (mServiceInfo == null) { 243 return null; 244 } 245 String packageName = mServiceInfo.getComponentName().getPackageName(); 246 String walletActivity = mServiceInfo.getWalletActivity(); 247 return createIntent(walletActivity, packageName, ACTION_VIEW_WALLET); 248 } 249 250 @Override getWalletPendingIntent( @onNull @allbackExecutor Executor executor, @NonNull WalletPendingIntentCallback pendingIntentCallback)251 public void getWalletPendingIntent( 252 @NonNull @CallbackExecutor Executor executor, 253 @NonNull WalletPendingIntentCallback pendingIntentCallback) { 254 BaseCallbacks callbacks = new BaseCallbacks() { 255 @Override 256 public void onTargetActivityPendingIntentReceived(PendingIntent pendingIntent) { 257 executor.execute( 258 () -> pendingIntentCallback.onWalletPendingIntentRetrieved(pendingIntent)); 259 } 260 }; 261 executeApiCall(new ApiCaller("getTargetActivityPendingIntent") { 262 @Override 263 void performApiCall(IQuickAccessWalletService service) throws RemoteException { 264 service.onTargetActivityIntentRequested(callbacks); 265 } 266 }); 267 } 268 269 @Override 270 @Nullable createWalletSettingsIntent()271 public Intent createWalletSettingsIntent() { 272 if (mServiceInfo == null) { 273 return null; 274 } 275 String packageName = mServiceInfo.getComponentName().getPackageName(); 276 String settingsActivity = mServiceInfo.getSettingsActivity(); 277 return createIntent(settingsActivity, packageName, ACTION_VIEW_WALLET_SETTINGS); 278 } 279 280 @Nullable createIntent(@ullable String activityName, String packageName, String action)281 private Intent createIntent(@Nullable String activityName, String packageName, String action) { 282 PackageManager pm = mContext.getPackageManager(); 283 if (TextUtils.isEmpty(activityName)) { 284 activityName = queryActivityForAction(pm, packageName, action); 285 } 286 if (TextUtils.isEmpty(activityName)) { 287 return null; 288 } 289 ComponentName component = new ComponentName(packageName, activityName); 290 if (!isActivityEnabled(pm, component)) { 291 return null; 292 } 293 return new Intent(action).setComponent(component); 294 } 295 296 @Nullable queryActivityForAction(PackageManager pm, String packageName, String action)297 private static String queryActivityForAction(PackageManager pm, String packageName, 298 String action) { 299 Intent intent = new Intent(action).setPackage(packageName); 300 ResolveInfo resolveInfo = pm.resolveActivity(intent, 0); 301 if (resolveInfo == null 302 || resolveInfo.activityInfo == null 303 || !resolveInfo.activityInfo.exported) { 304 return null; 305 } 306 return resolveInfo.activityInfo.name; 307 } 308 isActivityEnabled(PackageManager pm, ComponentName component)309 private static boolean isActivityEnabled(PackageManager pm, ComponentName component) { 310 int setting = pm.getComponentEnabledSetting(component); 311 if (setting == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { 312 return true; 313 } 314 if (setting != PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) { 315 return false; 316 } 317 try { 318 return pm.getActivityInfo(component, 0).isEnabled(); 319 } catch (NameNotFoundException e) { 320 return false; 321 } 322 } 323 324 @Override 325 @Nullable getLogo()326 public Drawable getLogo() { 327 return mServiceInfo == null ? null : mServiceInfo.getWalletLogo(mContext); 328 } 329 330 @Nullable 331 @Override getTileIcon()332 public Drawable getTileIcon() { 333 return mServiceInfo == null ? null : mServiceInfo.getTileIcon(); 334 } 335 336 @Override 337 @Nullable getServiceLabel()338 public CharSequence getServiceLabel() { 339 return mServiceInfo == null ? null : mServiceInfo.getServiceLabel(mContext); 340 } 341 342 @Override 343 @Nullable getShortcutShortLabel()344 public CharSequence getShortcutShortLabel() { 345 return mServiceInfo == null ? null : mServiceInfo.getShortcutShortLabel(mContext); 346 } 347 348 @Override getShortcutLongLabel()349 public CharSequence getShortcutLongLabel() { 350 return mServiceInfo == null ? null : mServiceInfo.getShortcutLongLabel(mContext); 351 } 352 connect()353 private void connect() { 354 mHandler.post(this::connectInternal); 355 } 356 connectInternal()357 private void connectInternal() { 358 if (mServiceInfo == null) { 359 Log.w(TAG, "Wallet service unavailable"); 360 return; 361 } 362 if (mIsConnected) { 363 return; 364 } 365 mIsConnected = true; 366 Intent intent = new Intent(SERVICE_INTERFACE); 367 intent.setComponent(mServiceInfo.getComponentName()); 368 int flags = Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY; 369 mLifecycleExecutor.execute(() -> mContext.bindService(intent, this, flags)); 370 resetServiceConnectionTimeout(); 371 } 372 onConnectedInternal(IQuickAccessWalletService service)373 private void onConnectedInternal(IQuickAccessWalletService service) { 374 if (!mIsConnected) { 375 Log.w(TAG, "onConnectInternal but connection closed"); 376 mService = null; 377 return; 378 } 379 mService = service; 380 for (ApiCaller apiCaller : new ArrayList<>(mRequestQueue)) { 381 performApiCallInternal(apiCaller, mService); 382 mRequestQueue.remove(apiCaller); 383 } 384 } 385 386 /** 387 * Resets the idle timeout for this connection by removing any pending timeout messages and 388 * posting a new delayed message. 389 */ resetServiceConnectionTimeout()390 private void resetServiceConnectionTimeout() { 391 mHandler.removeMessages(MSG_TIMEOUT_SERVICE); 392 mHandler.postDelayed( 393 () -> disconnectInternal(true), 394 MSG_TIMEOUT_SERVICE, 395 SERVICE_CONNECTION_TIMEOUT_MS); 396 } 397 disconnectInternal(boolean clearEventListeners)398 private void disconnectInternal(boolean clearEventListeners) { 399 if (!mIsConnected) { 400 Log.w(TAG, "already disconnected"); 401 return; 402 } 403 if (clearEventListeners && !mEventListeners.isEmpty()) { 404 for (WalletServiceEventListener listener : mEventListeners.keySet()) { 405 removeWalletServiceEventListener(listener); 406 } 407 mHandler.post(() -> disconnectInternal(false)); 408 return; 409 } 410 mIsConnected = false; 411 mLifecycleExecutor.execute(() -> mContext.unbindService(/*conn=*/ this)); 412 mService = null; 413 mEventListeners.clear(); 414 mRequestQueue.clear(); 415 } 416 executeApiCall(ApiCaller apiCaller)417 private void executeApiCall(ApiCaller apiCaller) { 418 mHandler.post(() -> executeInternal(apiCaller)); 419 } 420 executeInternal(ApiCaller apiCaller)421 private void executeInternal(ApiCaller apiCaller) { 422 if (mIsConnected && mService != null) { 423 performApiCallInternal(apiCaller, mService); 424 } else { 425 mRequestQueue.add(apiCaller); 426 connect(); 427 } 428 } 429 performApiCallInternal(ApiCaller apiCaller, IQuickAccessWalletService service)430 private void performApiCallInternal(ApiCaller apiCaller, IQuickAccessWalletService service) { 431 if (service == null) { 432 apiCaller.onApiError(); 433 return; 434 } 435 try { 436 apiCaller.performApiCall(service); 437 resetServiceConnectionTimeout(); 438 } catch (RemoteException e) { 439 Log.w(TAG, "executeInternal error: " + apiCaller.mDesc, e); 440 apiCaller.onApiError(); 441 disconnect(); 442 } 443 } 444 445 private abstract static class ApiCaller { 446 private final String mDesc; 447 ApiCaller(String desc)448 private ApiCaller(String desc) { 449 this.mDesc = desc; 450 } 451 performApiCall(IQuickAccessWalletService service)452 abstract void performApiCall(IQuickAccessWalletService service) 453 throws RemoteException; 454 onApiError()455 void onApiError() { 456 Log.w(TAG, "api error: " + mDesc); 457 } 458 } 459 460 @Override // ServiceConnection onServiceConnected(ComponentName name, IBinder binder)461 public void onServiceConnected(ComponentName name, IBinder binder) { 462 IQuickAccessWalletService service = IQuickAccessWalletService.Stub.asInterface(binder); 463 mHandler.post(() -> onConnectedInternal(service)); 464 } 465 466 @Override // ServiceConnection onServiceDisconnected(ComponentName name)467 public void onServiceDisconnected(ComponentName name) { 468 // Do not disconnect, as we may later be re-connected 469 } 470 471 @Override // ServiceConnection onBindingDied(ComponentName name)472 public void onBindingDied(ComponentName name) { 473 // This is a recoverable error but the client will need to reconnect. 474 disconnect(); 475 } 476 477 @Override // ServiceConnection onNullBinding(ComponentName name)478 public void onNullBinding(ComponentName name) { 479 disconnect(); 480 } 481 checkSecureSetting(String name)482 private boolean checkSecureSetting(String name) { 483 return Settings.Secure.getInt(mContext.getContentResolver(), name, 0) == 1; 484 } 485 checkUserSetupComplete()486 private boolean checkUserSetupComplete() { 487 return Settings.Secure.getIntForUser( 488 mContext.getContentResolver(), 489 Settings.Secure.USER_SETUP_COMPLETE, 0, 490 UserHandle.USER_CURRENT) == 1; 491 } 492 493 private static class BaseCallbacks extends IQuickAccessWalletServiceCallbacks.Stub { onGetWalletCardsSuccess(GetWalletCardsResponse response)494 public void onGetWalletCardsSuccess(GetWalletCardsResponse response) { 495 throw new IllegalStateException(); 496 } 497 onGetWalletCardsFailure(GetWalletCardsError error)498 public void onGetWalletCardsFailure(GetWalletCardsError error) { 499 throw new IllegalStateException(); 500 } 501 onWalletServiceEvent(WalletServiceEvent event)502 public void onWalletServiceEvent(WalletServiceEvent event) { 503 throw new IllegalStateException(); 504 } 505 onTargetActivityPendingIntentReceived(PendingIntent pendingIntent)506 public void onTargetActivityPendingIntentReceived(PendingIntent pendingIntent) { 507 throw new IllegalStateException(); 508 } 509 } 510 } 511