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