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 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 = null; 86 mHandler = new Handler(Looper.getMainLooper()); 87 mLifecycleExecutor = (bgExecutor == null) ? Runnable::run : bgExecutor; 88 mRequestQueue = new ArrayDeque<>(); 89 mEventListeners = new HashMap<>(1); 90 } 91 ensureServiceInfo()92 private QuickAccessWalletServiceInfo ensureServiceInfo() { 93 if (mServiceInfo == null) { 94 mServiceInfo = QuickAccessWalletServiceInfo.tryCreate(mContext); 95 } 96 97 return mServiceInfo; 98 } 99 100 @Override isWalletServiceAvailable()101 public boolean isWalletServiceAvailable() { 102 return ensureServiceInfo() != null; 103 } 104 105 @Override isWalletFeatureAvailable()106 public boolean isWalletFeatureAvailable() { 107 int currentUser = ActivityManager.getCurrentUser(); 108 return currentUser == UserHandle.USER_SYSTEM 109 && checkUserSetupComplete() 110 && !new LockPatternUtils(mContext).isUserInLockdown(currentUser); 111 } 112 113 @Override isWalletFeatureAvailableWhenDeviceLocked()114 public boolean isWalletFeatureAvailableWhenDeviceLocked() { 115 return checkSecureSetting(SETTING_KEY); 116 } 117 118 @Override getWalletCards( @onNull GetWalletCardsRequest request, @NonNull OnWalletCardsRetrievedCallback callback)119 public void getWalletCards( 120 @NonNull GetWalletCardsRequest request, 121 @NonNull OnWalletCardsRetrievedCallback callback) { 122 getWalletCards(mContext.getMainExecutor(), request, callback); 123 } 124 125 @Override getWalletCards( @onNull @allbackExecutor Executor executor, @NonNull GetWalletCardsRequest request, @NonNull OnWalletCardsRetrievedCallback callback)126 public void getWalletCards( 127 @NonNull @CallbackExecutor Executor executor, 128 @NonNull GetWalletCardsRequest request, 129 @NonNull OnWalletCardsRetrievedCallback callback) { 130 if (!isWalletServiceAvailable()) { 131 executor.execute( 132 () -> callback.onWalletCardRetrievalError(new GetWalletCardsError(null, null))); 133 return; 134 } 135 136 BaseCallbacks serviceCallback = new BaseCallbacks() { 137 @Override 138 public void onGetWalletCardsSuccess(GetWalletCardsResponse response) { 139 executor.execute(() -> callback.onWalletCardsRetrieved(response)); 140 } 141 142 @Override 143 public void onGetWalletCardsFailure(GetWalletCardsError error) { 144 executor.execute(() -> callback.onWalletCardRetrievalError(error)); 145 } 146 }; 147 148 executeApiCall(new ApiCaller("onWalletCardsRequested") { 149 @Override 150 public void performApiCall(IQuickAccessWalletService service) throws RemoteException { 151 service.onWalletCardsRequested(request, serviceCallback); 152 } 153 154 @Override 155 public void onApiError() { 156 serviceCallback.onGetWalletCardsFailure(new GetWalletCardsError(null, null)); 157 } 158 }); 159 } 160 161 @Override selectWalletCard(@onNull SelectWalletCardRequest request)162 public void selectWalletCard(@NonNull SelectWalletCardRequest request) { 163 if (!isWalletServiceAvailable()) { 164 return; 165 } 166 executeApiCall(new ApiCaller("onWalletCardSelected") { 167 @Override 168 public void performApiCall(IQuickAccessWalletService service) throws RemoteException { 169 service.onWalletCardSelected(request); 170 } 171 }); 172 } 173 174 @Override notifyWalletDismissed()175 public void notifyWalletDismissed() { 176 if (!isWalletServiceAvailable()) { 177 return; 178 } 179 executeApiCall(new ApiCaller("onWalletDismissed") { 180 @Override 181 public void performApiCall(IQuickAccessWalletService service) throws RemoteException { 182 service.onWalletDismissed(); 183 } 184 }); 185 } 186 187 @Override addWalletServiceEventListener(WalletServiceEventListener listener)188 public void addWalletServiceEventListener(WalletServiceEventListener listener) { 189 addWalletServiceEventListener(mContext.getMainExecutor(), listener); 190 } 191 192 @Override addWalletServiceEventListener( @onNull @allbackExecutor Executor executor, @NonNull WalletServiceEventListener listener)193 public void addWalletServiceEventListener( 194 @NonNull @CallbackExecutor Executor executor, 195 @NonNull WalletServiceEventListener listener) { 196 if (!isWalletServiceAvailable()) { 197 return; 198 } 199 BaseCallbacks callback = new BaseCallbacks() { 200 @Override 201 public void onWalletServiceEvent(WalletServiceEvent event) { 202 executor.execute(() -> listener.onWalletServiceEvent(event)); 203 } 204 }; 205 206 executeApiCall(new ApiCaller("registerListener") { 207 @Override 208 public void performApiCall(IQuickAccessWalletService service) throws RemoteException { 209 String listenerId = UUID.randomUUID().toString(); 210 WalletServiceEventListenerRequest request = 211 new WalletServiceEventListenerRequest(listenerId); 212 mEventListeners.put(listener, listenerId); 213 service.registerWalletServiceEventListener(request, callback); 214 } 215 }); 216 } 217 218 @Override removeWalletServiceEventListener(WalletServiceEventListener listener)219 public void removeWalletServiceEventListener(WalletServiceEventListener listener) { 220 if (!isWalletServiceAvailable()) { 221 return; 222 } 223 executeApiCall(new ApiCaller("unregisterListener") { 224 @Override 225 public void performApiCall(IQuickAccessWalletService service) throws RemoteException { 226 String listenerId = mEventListeners.remove(listener); 227 if (listenerId == null) { 228 return; 229 } 230 WalletServiceEventListenerRequest request = 231 new WalletServiceEventListenerRequest(listenerId); 232 service.unregisterWalletServiceEventListener(request); 233 } 234 }); 235 } 236 237 @Override close()238 public void close() throws IOException { 239 disconnect(); 240 } 241 242 @Override disconnect()243 public void disconnect() { 244 mHandler.post(() -> disconnectInternal(true)); 245 } 246 247 @Override 248 @Nullable createWalletIntent()249 public Intent createWalletIntent() { 250 QuickAccessWalletServiceInfo serviceInfo = ensureServiceInfo(); 251 if (serviceInfo == null) { 252 return null; 253 } 254 String packageName = serviceInfo.getComponentName().getPackageName(); 255 int userId = serviceInfo.getUserId(); 256 String walletActivity = serviceInfo.getWalletActivity(); 257 return createIntent(walletActivity, packageName, userId, ACTION_VIEW_WALLET); 258 } 259 260 @Override getWalletPendingIntent( @onNull @allbackExecutor Executor executor, @NonNull WalletPendingIntentCallback pendingIntentCallback)261 public void getWalletPendingIntent( 262 @NonNull @CallbackExecutor Executor executor, 263 @NonNull WalletPendingIntentCallback pendingIntentCallback) { 264 BaseCallbacks callbacks = new BaseCallbacks() { 265 @Override 266 public void onTargetActivityPendingIntentReceived(PendingIntent pendingIntent) { 267 executor.execute( 268 () -> pendingIntentCallback.onWalletPendingIntentRetrieved(pendingIntent)); 269 } 270 }; 271 executeApiCall(new ApiCaller("getTargetActivityPendingIntent") { 272 @Override 273 void performApiCall(IQuickAccessWalletService service) throws RemoteException { 274 service.onTargetActivityIntentRequested(callbacks); 275 } 276 }); 277 } 278 279 @Override getGestureTargetActivityPendingIntent( @onNull @allbackExecutor Executor executor, @NonNull GesturePendingIntentCallback gesturePendingIntentCallback)280 public void getGestureTargetActivityPendingIntent( 281 @NonNull @CallbackExecutor Executor executor, 282 @NonNull GesturePendingIntentCallback gesturePendingIntentCallback) { 283 BaseCallbacks callbacks = 284 new BaseCallbacks() { 285 @Override 286 public void onGestureTargetActivityPendingIntentReceived( 287 PendingIntent pendingIntent) { 288 if (!Flags.launchWalletOptionOnPowerDoubleTap()) { 289 return; 290 } 291 executor.execute( 292 () -> 293 gesturePendingIntentCallback 294 .onGesturePendingIntentRetrieved(pendingIntent)); 295 } 296 }; 297 298 executeApiCall( 299 new ApiCaller("getGestureTargetActivityPendingIntent") { 300 @Override 301 void performApiCall(IQuickAccessWalletService service) throws RemoteException { 302 service.onGestureTargetActivityIntentRequested(callbacks); 303 } 304 }); 305 } 306 307 @Override 308 @Nullable createWalletSettingsIntent()309 public Intent createWalletSettingsIntent() { 310 QuickAccessWalletServiceInfo serviceInfo = ensureServiceInfo(); 311 if (serviceInfo == null) { 312 return null; 313 } 314 String packageName = serviceInfo.getComponentName().getPackageName(); 315 String settingsActivity = serviceInfo.getSettingsActivity(); 316 return createIntent(settingsActivity, packageName, UserHandle.myUserId(), 317 ACTION_VIEW_WALLET_SETTINGS); 318 } 319 320 @Nullable createIntent(@ullable String activityName, String packageName, int userId, String action)321 private Intent createIntent(@Nullable String activityName, String packageName, 322 int userId, String action) { 323 Context userContext = mContext.createContextAsUser(UserHandle.of(userId), 0); 324 PackageManager pm = userContext.getPackageManager(); 325 if (TextUtils.isEmpty(activityName)) { 326 activityName = queryActivityForAction(pm, packageName, action); 327 } 328 if (TextUtils.isEmpty(activityName)) { 329 return null; 330 } 331 ComponentName component = new ComponentName(packageName, activityName); 332 if (!isActivityEnabled(pm, component)) { 333 return null; 334 } 335 return new Intent(action).setComponent(component); 336 } 337 338 @Nullable queryActivityForAction(PackageManager pm, String packageName, String action)339 private static String queryActivityForAction(PackageManager pm, String packageName, 340 String action) { 341 Intent intent = new Intent(action).setPackage(packageName); 342 ResolveInfo resolveInfo = pm.resolveActivity(intent, 0); 343 if (resolveInfo == null 344 || resolveInfo.activityInfo == null 345 || !resolveInfo.activityInfo.exported) { 346 return null; 347 } 348 return resolveInfo.activityInfo.name; 349 } 350 isActivityEnabled(PackageManager pm, ComponentName component)351 private static boolean isActivityEnabled(PackageManager pm, ComponentName component) { 352 int setting = pm.getComponentEnabledSetting(component); 353 if (setting == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { 354 return true; 355 } 356 if (setting != PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) { 357 return false; 358 } 359 try { 360 return pm.getActivityInfo(component, 0).isEnabled(); 361 } catch (NameNotFoundException e) { 362 return false; 363 } 364 } 365 366 @Override 367 @Nullable getLogo()368 public Drawable getLogo() { 369 QuickAccessWalletServiceInfo serviceInfo = ensureServiceInfo(); 370 return serviceInfo == null ? null : serviceInfo.getWalletLogo(mContext); 371 } 372 373 @Nullable 374 @Override getTileIcon()375 public Drawable getTileIcon() { 376 QuickAccessWalletServiceInfo serviceInfo = ensureServiceInfo(); 377 return serviceInfo == null ? null : serviceInfo.getTileIcon(); 378 } 379 380 @Nullable 381 @Override getUser()382 public UserHandle getUser() { 383 QuickAccessWalletServiceInfo serviceInfo = ensureServiceInfo(); 384 return serviceInfo == null ? null : UserHandle.of(serviceInfo.getUserId()); 385 } 386 387 @Override 388 @Nullable getServiceLabel()389 public CharSequence getServiceLabel() { 390 QuickAccessWalletServiceInfo serviceInfo = ensureServiceInfo(); 391 return serviceInfo == null ? null : serviceInfo.getServiceLabel(mContext); 392 } 393 394 @Override 395 @Nullable getShortcutShortLabel()396 public CharSequence getShortcutShortLabel() { 397 QuickAccessWalletServiceInfo serviceInfo = ensureServiceInfo(); 398 return serviceInfo == null ? null : serviceInfo.getShortcutShortLabel(mContext); 399 } 400 401 @Override getShortcutLongLabel()402 public CharSequence getShortcutLongLabel() { 403 QuickAccessWalletServiceInfo serviceInfo = ensureServiceInfo(); 404 return serviceInfo == null ? null : serviceInfo.getShortcutLongLabel(mContext); 405 } 406 connect()407 private void connect() { 408 mHandler.post(this::connectInternal); 409 } 410 connectInternal()411 private void connectInternal() { 412 QuickAccessWalletServiceInfo serviceInfo = ensureServiceInfo(); 413 if (serviceInfo == null) { 414 Log.w(TAG, "Wallet service unavailable"); 415 return; 416 } 417 if (mIsConnected) { 418 return; 419 } 420 mIsConnected = true; 421 Intent intent = new Intent(SERVICE_INTERFACE); 422 intent.setComponent(serviceInfo.getComponentName()); 423 int flags = Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY; 424 if (mServiceInfo == null) { 425 mLifecycleExecutor.execute(() -> mContext.bindService(intent, this, flags)); 426 } else { 427 mLifecycleExecutor.execute(() -> mContext.bindServiceAsUser(intent, this, flags, 428 UserHandle.of(mServiceInfo.getUserId()))); 429 } 430 resetServiceConnectionTimeout(); 431 } 432 onConnectedInternal(IQuickAccessWalletService service)433 private void onConnectedInternal(IQuickAccessWalletService service) { 434 if (!mIsConnected) { 435 Log.w(TAG, "onConnectInternal but connection closed"); 436 mService = null; 437 return; 438 } 439 mService = service; 440 for (ApiCaller apiCaller : new ArrayList<>(mRequestQueue)) { 441 performApiCallInternal(apiCaller, mService); 442 mRequestQueue.remove(apiCaller); 443 } 444 } 445 446 /** 447 * Resets the idle timeout for this connection by removing any pending timeout messages and 448 * posting a new delayed message. 449 */ resetServiceConnectionTimeout()450 private void resetServiceConnectionTimeout() { 451 mHandler.removeMessages(MSG_TIMEOUT_SERVICE); 452 mHandler.postDelayed( 453 () -> disconnectInternal(true), 454 MSG_TIMEOUT_SERVICE, 455 SERVICE_CONNECTION_TIMEOUT_MS); 456 } 457 disconnectInternal(boolean clearEventListeners)458 private void disconnectInternal(boolean clearEventListeners) { 459 if (!mIsConnected) { 460 Log.w(TAG, "already disconnected"); 461 return; 462 } 463 if (clearEventListeners && !mEventListeners.isEmpty()) { 464 for (WalletServiceEventListener listener : mEventListeners.keySet()) { 465 removeWalletServiceEventListener(listener); 466 } 467 mHandler.post(() -> disconnectInternal(false)); 468 return; 469 } 470 mIsConnected = false; 471 mLifecycleExecutor.execute(() -> mContext.unbindService(/*conn=*/ this)); 472 mService = null; 473 mEventListeners.clear(); 474 mRequestQueue.clear(); 475 } 476 executeApiCall(ApiCaller apiCaller)477 private void executeApiCall(ApiCaller apiCaller) { 478 mHandler.post(() -> executeInternal(apiCaller)); 479 } 480 executeInternal(ApiCaller apiCaller)481 private void executeInternal(ApiCaller apiCaller) { 482 if (mIsConnected && mService != null) { 483 performApiCallInternal(apiCaller, mService); 484 } else { 485 mRequestQueue.add(apiCaller); 486 connect(); 487 } 488 } 489 performApiCallInternal(ApiCaller apiCaller, IQuickAccessWalletService service)490 private void performApiCallInternal(ApiCaller apiCaller, IQuickAccessWalletService service) { 491 if (service == null) { 492 apiCaller.onApiError(); 493 return; 494 } 495 try { 496 apiCaller.performApiCall(service); 497 resetServiceConnectionTimeout(); 498 } catch (RemoteException e) { 499 Log.w(TAG, "executeInternal error: " + apiCaller.mDesc, e); 500 apiCaller.onApiError(); 501 disconnect(); 502 } 503 } 504 505 private abstract static class ApiCaller { 506 private final String mDesc; 507 ApiCaller(String desc)508 private ApiCaller(String desc) { 509 this.mDesc = desc; 510 } 511 performApiCall(IQuickAccessWalletService service)512 abstract void performApiCall(IQuickAccessWalletService service) 513 throws RemoteException; 514 onApiError()515 void onApiError() { 516 Log.w(TAG, "api error: " + mDesc); 517 } 518 } 519 520 @Override // ServiceConnection onServiceConnected(ComponentName name, IBinder binder)521 public void onServiceConnected(ComponentName name, IBinder binder) { 522 IQuickAccessWalletService service = IQuickAccessWalletService.Stub.asInterface(binder); 523 mHandler.post(() -> onConnectedInternal(service)); 524 } 525 526 @Override // ServiceConnection onServiceDisconnected(ComponentName name)527 public void onServiceDisconnected(ComponentName name) { 528 // Do not disconnect, as we may later be re-connected 529 } 530 531 @Override // ServiceConnection onBindingDied(ComponentName name)532 public void onBindingDied(ComponentName name) { 533 // This is a recoverable error but the client will need to reconnect. 534 disconnect(); 535 } 536 537 @Override // ServiceConnection onNullBinding(ComponentName name)538 public void onNullBinding(ComponentName name) { 539 disconnect(); 540 } 541 checkSecureSetting(String name)542 private boolean checkSecureSetting(String name) { 543 return Settings.Secure.getInt(mContext.getContentResolver(), name, 0) == 1; 544 } 545 checkUserSetupComplete()546 private boolean checkUserSetupComplete() { 547 return Settings.Secure.getIntForUser( 548 mContext.getContentResolver(), 549 Settings.Secure.USER_SETUP_COMPLETE, 0, 550 UserHandle.USER_CURRENT) == 1; 551 } 552 553 private static class BaseCallbacks extends IQuickAccessWalletServiceCallbacks.Stub { onGetWalletCardsSuccess(GetWalletCardsResponse response)554 public void onGetWalletCardsSuccess(GetWalletCardsResponse response) { 555 throw new IllegalStateException(); 556 } 557 onGetWalletCardsFailure(GetWalletCardsError error)558 public void onGetWalletCardsFailure(GetWalletCardsError error) { 559 throw new IllegalStateException(); 560 } 561 onWalletServiceEvent(WalletServiceEvent event)562 public void onWalletServiceEvent(WalletServiceEvent event) { 563 throw new IllegalStateException(); 564 } 565 onTargetActivityPendingIntentReceived(PendingIntent pendingIntent)566 public void onTargetActivityPendingIntentReceived(PendingIntent pendingIntent) { 567 throw new IllegalStateException(); 568 } 569 onGestureTargetActivityPendingIntentReceived(PendingIntent pendingIntent)570 public void onGestureTargetActivityPendingIntentReceived(PendingIntent pendingIntent) { 571 throw new IllegalStateException(); 572 } 573 } 574 } 575