• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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