blob: 37a8703ddebe65b9206bbaa0aaa049face8a7579 [file] [log] [blame]
Sean Pontd00aefb2020-01-07 12:05:09 -08001/*
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
17package android.service.quickaccesswallet;
18
19import static android.service.quickaccesswallet.QuickAccessWalletService.SERVICE_INTERFACE;
20
21import android.annotation.NonNull;
22import android.annotation.Nullable;
Sean Pont72fc25f2020-02-11 19:02:44 -080023import android.app.ActivityManager;
Sean Pontd00aefb2020-01-07 12:05:09 -080024import android.content.ComponentName;
25import android.content.Context;
26import android.content.Intent;
27import android.content.ServiceConnection;
28import android.os.Handler;
29import android.os.IBinder;
30import android.os.Looper;
Sean Pontd00aefb2020-01-07 12:05:09 -080031import android.os.RemoteException;
Sean Pont72fc25f2020-02-11 19:02:44 -080032import android.os.UserHandle;
33import android.provider.Settings;
Sean Pontd00aefb2020-01-07 12:05:09 -080034import android.text.TextUtils;
35import android.util.Log;
36
Sean Pont72fc25f2020-02-11 19:02:44 -080037import com.android.internal.widget.LockPatternUtils;
38
Sean Pontd00aefb2020-01-07 12:05:09 -080039import java.util.ArrayList;
40import java.util.HashMap;
41import java.util.LinkedList;
42import java.util.Map;
43import java.util.Queue;
44import java.util.UUID;
Sean Pontd00aefb2020-01-07 12:05:09 -080045
46/**
Sean Pont72fc25f2020-02-11 19:02:44 -080047 * Implements {@link QuickAccessWalletClient}. The client connects, performs requests, waits for
48 * responses, and disconnects automatically after a short period of time. The client may
Sean Pontd00aefb2020-01-07 12:05:09 -080049 * @hide
50 */
Sean Pont72fc25f2020-02-11 19:02:44 -080051public class QuickAccessWalletClientImpl implements QuickAccessWalletClient, ServiceConnection {
Sean Pontd00aefb2020-01-07 12:05:09 -080052
53 private static final String TAG = "QAWalletSClient";
54 private final Handler mHandler;
55 private final Context mContext;
56 private final Queue<ApiCaller> mRequestQueue;
Sean Pont72fc25f2020-02-11 19:02:44 -080057 private final Map<WalletServiceEventListener, String> mEventListeners;
Sean Pontd00aefb2020-01-07 12:05:09 -080058 private boolean mIsConnected;
Sean Pont72fc25f2020-02-11 19:02:44 -080059 /**
60 * Timeout for active service connections (1 minute)
61 */
62 private static final long SERVICE_CONNECTION_TIMEOUT_MS = 60 * 1000;
Sean Pontd00aefb2020-01-07 12:05:09 -080063 @Nullable
64 private IQuickAccessWalletService mService;
65
Sean Pontd00aefb2020-01-07 12:05:09 -080066 @Nullable
67 private final QuickAccessWalletServiceInfo mServiceInfo;
68
Sean Pont72fc25f2020-02-11 19:02:44 -080069 private static final int MSG_TIMEOUT_SERVICE = 5;
Sean Pontd00aefb2020-01-07 12:05:09 -080070
71 QuickAccessWalletClientImpl(@NonNull Context context) {
72 mContext = context.getApplicationContext();
73 mServiceInfo = QuickAccessWalletServiceInfo.tryCreate(context);
Sean Pont72fc25f2020-02-11 19:02:44 -080074 mHandler = new Handler(Looper.getMainLooper());
Sean Pontd00aefb2020-01-07 12:05:09 -080075 mRequestQueue = new LinkedList<>();
76 mEventListeners = new HashMap<>(1);
77 }
78
79 @Override
Sean Pont72fc25f2020-02-11 19:02:44 -080080 public boolean isWalletServiceAvailable() {
81 boolean available = mServiceInfo != null;
82 Log.i(TAG, "isWalletServiceAvailable: " + available);
83 return available;
84 }
85
86 @Override
87 public boolean isWalletFeatureAvailable() {
88 int currentUser = ActivityManager.getCurrentUser();
89 return checkUserSetupComplete()
90 && checkSecureSetting(Settings.Secure.GLOBAL_ACTIONS_PANEL_ENABLED)
91 && !new LockPatternUtils(mContext).isUserInLockdown(currentUser);
92 }
93
94 @Override
95 public boolean isWalletFeatureAvailableWhenDeviceLocked() {
96 return checkSecureSetting(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS)
97 && checkSecureSetting(Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS);
98 }
99
100 @Override
101 public void getWalletCards(
102 @NonNull GetWalletCardsRequest request,
103 @NonNull OnWalletCardsRetrievedCallback callback) {
104
105 Log.i(TAG, "getWalletCards");
106
107 if (!isWalletServiceAvailable()) {
108 callback.onWalletCardRetrievalError(new GetWalletCardsError(null, null));
109 return;
Sean Pontd00aefb2020-01-07 12:05:09 -0800110 }
Sean Pont72fc25f2020-02-11 19:02:44 -0800111
112 BaseCallbacks serviceCallback = new BaseCallbacks() {
113 @Override
114 public void onGetWalletCardsSuccess(GetWalletCardsResponse response) {
115 mHandler.post(() -> callback.onWalletCardsRetrieved(response));
116 }
117
118 @Override
119 public void onGetWalletCardsFailure(GetWalletCardsError error) {
120 mHandler.post(() -> callback.onWalletCardRetrievalError(error));
121 }
122 };
123
124 executeApiCall(new ApiCaller("onWalletCardsRequested") {
125 @Override
126 public void performApiCall(IQuickAccessWalletService service) throws RemoteException {
127 service.onWalletCardsRequested(request, serviceCallback);
128 }
129
130 @Override
131 public void onApiError() {
132 serviceCallback.onGetWalletCardsFailure(new GetWalletCardsError(null, null));
133 }
134 });
135 }
136
137 @Override
138 public void selectWalletCard(@NonNull SelectWalletCardRequest request) {
139 Log.i(TAG, "selectWalletCard");
140 if (!isWalletServiceAvailable()) {
141 return;
142 }
143 executeApiCall(new ApiCaller("onWalletCardSelected") {
144 @Override
145 public void performApiCall(IQuickAccessWalletService service) throws RemoteException {
146 service.onWalletCardSelected(request);
147 }
148 });
149 }
150
151 @Override
152 public void notifyWalletDismissed() {
153 if (!isWalletServiceAvailable()) {
154 return;
155 }
156 Log.i(TAG, "notifyWalletDismissed");
157 executeApiCall(new ApiCaller("onWalletDismissed") {
158 @Override
159 public void performApiCall(IQuickAccessWalletService service) throws RemoteException {
160 service.onWalletDismissed();
161 }
162 });
163 }
164
165 @Override
166 public void addWalletServiceEventListener(WalletServiceEventListener listener) {
167 if (!isWalletServiceAvailable()) {
168 return;
169 }
170 Log.i(TAG, "registerWalletServiceEventListener");
171 BaseCallbacks callback = new BaseCallbacks() {
172 @Override
173 public void onWalletServiceEvent(WalletServiceEvent event) {
174 Log.i(TAG, "onWalletServiceEvent");
175 mHandler.post(() -> listener.onWalletServiceEvent(event));
176 }
177 };
178
179 executeApiCall(new ApiCaller("registerListener") {
180 @Override
181 public void performApiCall(IQuickAccessWalletService service) throws RemoteException {
182 String listenerId = UUID.randomUUID().toString();
183 WalletServiceEventListenerRequest request =
184 new WalletServiceEventListenerRequest(listenerId);
185 mEventListeners.put(listener, listenerId);
186 service.registerWalletServiceEventListener(request, callback);
187 }
188 });
189 }
190
191 @Override
192 public void removeWalletServiceEventListener(WalletServiceEventListener listener) {
193 if (!isWalletServiceAvailable()) {
194 return;
195 }
196 Log.i(TAG, "unregisterWalletServiceEventListener");
197 executeApiCall(new ApiCaller("unregisterListener") {
198 @Override
199 public void performApiCall(IQuickAccessWalletService service) throws RemoteException {
200 String listenerId = mEventListeners.remove(listener);
201 if (listenerId == null) {
202 return;
203 }
204 WalletServiceEventListenerRequest request =
205 new WalletServiceEventListenerRequest(listenerId);
206 service.unregisterWalletServiceEventListener(request);
207 }
208 });
209 }
210
211 @Override
212 public void disconnect() {
213 Log.i(TAG, "disconnect");
214 mHandler.post(() -> disconnectInternal(true));
215 }
216
217 @Override
218 @Nullable
219 public Intent createWalletIntent() {
220 if (mServiceInfo == null || TextUtils.isEmpty(mServiceInfo.getWalletActivity())) {
221 return null;
222 }
223 return new Intent(QuickAccessWalletService.ACTION_VIEW_WALLET)
224 .setComponent(
225 new ComponentName(
226 mServiceInfo.getComponentName().getPackageName(),
227 mServiceInfo.getWalletActivity()));
228 }
229
230 @Override
231 @Nullable
232 public Intent createWalletSettingsIntent() {
233 if (mServiceInfo == null || TextUtils.isEmpty(mServiceInfo.getSettingsActivity())) {
234 return null;
235 }
236 return new Intent(QuickAccessWalletService.ACTION_VIEW_WALLET_SETTINGS)
237 .setComponent(
238 new ComponentName(
239 mServiceInfo.getComponentName().getPackageName(),
240 mServiceInfo.getSettingsActivity()));
Sean Pontd00aefb2020-01-07 12:05:09 -0800241 }
242
243 private void connect() {
Sean Pont72fc25f2020-02-11 19:02:44 -0800244 Log.i(TAG, "connect");
245 mHandler.post(this::connectInternal);
Sean Pontd00aefb2020-01-07 12:05:09 -0800246 }
247
248 private void connectInternal() {
Sean Pont72fc25f2020-02-11 19:02:44 -0800249 Log.i(TAG, "connectInternal");
Sean Pontd00aefb2020-01-07 12:05:09 -0800250 if (mServiceInfo == null) {
251 Log.w(TAG, "Wallet service unavailable");
252 return;
253 }
254 if (mIsConnected) {
255 Log.w(TAG, "already connected");
256 return;
257 }
258 mIsConnected = true;
259 Intent intent = new Intent(SERVICE_INTERFACE);
260 intent.setComponent(mServiceInfo.getComponentName());
261 int flags = Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY;
262 mContext.bindService(intent, this, flags);
Sean Pont72fc25f2020-02-11 19:02:44 -0800263 resetServiceConnectionTimeout();
Sean Pontd00aefb2020-01-07 12:05:09 -0800264 }
265
266 private void onConnectedInternal(IQuickAccessWalletService service) {
Sean Pont72fc25f2020-02-11 19:02:44 -0800267 Log.i(TAG, "onConnectedInternal");
Sean Pontd00aefb2020-01-07 12:05:09 -0800268 if (!mIsConnected) {
269 Log.w(TAG, "onConnectInternal but connection closed");
270 mService = null;
271 return;
272 }
273 mService = service;
Sean Pont72fc25f2020-02-11 19:02:44 -0800274 Log.i(TAG, "onConnectedInternal success: request queue size " + mRequestQueue.size());
Sean Pontd00aefb2020-01-07 12:05:09 -0800275 for (ApiCaller apiCaller : new ArrayList<>(mRequestQueue)) {
276 try {
277 apiCaller.performApiCall(mService);
278 } catch (RemoteException e) {
279 Log.e(TAG, "onConnectedInternal error", e);
280 apiCaller.onApiError();
281 disconnect();
282 break;
283 }
284 mRequestQueue.remove(apiCaller);
285 }
286 }
287
Sean Pont72fc25f2020-02-11 19:02:44 -0800288 /**
289 * Resets the idle timeout for this connection by removing any pending timeout messages and
290 * posting a new delayed message.
291 */
292 private void resetServiceConnectionTimeout() {
293 Log.i(TAG, "resetServiceConnectionTimeout");
294 mHandler.removeMessages(MSG_TIMEOUT_SERVICE);
295 mHandler.postDelayed(
296 () -> disconnectInternal(true),
297 MSG_TIMEOUT_SERVICE,
298 SERVICE_CONNECTION_TIMEOUT_MS);
Sean Pontd00aefb2020-01-07 12:05:09 -0800299 }
300
Sean Pont72fc25f2020-02-11 19:02:44 -0800301 private void disconnectInternal(boolean clearEventListeners) {
302 Log.i(TAG, "disconnectInternal: " + clearEventListeners);
Sean Pontd00aefb2020-01-07 12:05:09 -0800303 if (!mIsConnected) {
304 Log.w(TAG, "already disconnected");
305 return;
306 }
Sean Pont72fc25f2020-02-11 19:02:44 -0800307 if (clearEventListeners && !mEventListeners.isEmpty()) {
308 Log.i(TAG, "disconnectInternal: clear event listeners");
309 for (WalletServiceEventListener listener : mEventListeners.keySet()) {
310 removeWalletServiceEventListener(listener);
311 }
312 mHandler.post(() -> disconnectInternal(false));
313 return;
314 }
Sean Pontd00aefb2020-01-07 12:05:09 -0800315 mIsConnected = false;
316 mContext.unbindService(/*conn=*/this);
317 mService = null;
318 mEventListeners.clear();
319 mRequestQueue.clear();
320 }
321
Sean Pont72fc25f2020-02-11 19:02:44 -0800322 private void executeApiCall(ApiCaller apiCaller) {
323 Log.i(TAG, "execute: " + apiCaller.mDesc);
324 mHandler.post(() -> executeInternal(apiCaller));
Sean Pontd00aefb2020-01-07 12:05:09 -0800325 }
326
327 private void executeInternal(ApiCaller apiCall) {
Sean Pont72fc25f2020-02-11 19:02:44 -0800328 Log.i(TAG, "executeInternal: " + apiCall.mDesc);
Sean Pontd00aefb2020-01-07 12:05:09 -0800329 if (mIsConnected && mService != null) {
330 try {
331 apiCall.performApiCall(mService);
Sean Pont72fc25f2020-02-11 19:02:44 -0800332 Log.i(TAG, "executeInternal success: " + apiCall.mDesc);
333 resetServiceConnectionTimeout();
Sean Pontd00aefb2020-01-07 12:05:09 -0800334 } catch (RemoteException e) {
Sean Pont72fc25f2020-02-11 19:02:44 -0800335 Log.w(TAG, "executeInternal error: " + apiCall.mDesc, e);
Sean Pontd00aefb2020-01-07 12:05:09 -0800336 apiCall.onApiError();
337 disconnect();
338 }
339 } else {
Sean Pont72fc25f2020-02-11 19:02:44 -0800340 Log.i(TAG, "executeInternal: queued" + apiCall.mDesc);
Sean Pontd00aefb2020-01-07 12:05:09 -0800341 mRequestQueue.add(apiCall);
342 connect();
343 }
344 }
345
Sean Pontd00aefb2020-01-07 12:05:09 -0800346 private abstract static class ApiCaller {
Sean Pont72fc25f2020-02-11 19:02:44 -0800347 private final String mDesc;
348
349 private ApiCaller(String desc) {
350 this.mDesc = desc;
351 }
352
Sean Pontd00aefb2020-01-07 12:05:09 -0800353 abstract void performApiCall(IQuickAccessWalletService service) throws RemoteException;
354
355 void onApiError() {
Sean Pont72fc25f2020-02-11 19:02:44 -0800356 Log.w(TAG, "api error: " + mDesc);
Sean Pontd00aefb2020-01-07 12:05:09 -0800357 }
358 }
359
Sean Pont72fc25f2020-02-11 19:02:44 -0800360 @Override // ServiceConnection
Sean Pontd00aefb2020-01-07 12:05:09 -0800361 public void onServiceConnected(ComponentName name, IBinder binder) {
Sean Pont72fc25f2020-02-11 19:02:44 -0800362 Log.i(TAG, "onServiceConnected: " + name);
Sean Pontd00aefb2020-01-07 12:05:09 -0800363 IQuickAccessWalletService service = IQuickAccessWalletService.Stub.asInterface(binder);
Sean Pont72fc25f2020-02-11 19:02:44 -0800364 mHandler.post(() -> onConnectedInternal(service));
Sean Pontd00aefb2020-01-07 12:05:09 -0800365 }
366
Sean Pont72fc25f2020-02-11 19:02:44 -0800367 @Override // ServiceConnection
Sean Pontd00aefb2020-01-07 12:05:09 -0800368 public void onServiceDisconnected(ComponentName name) {
369 // Do not disconnect, as we may later be re-connected
370 Log.w(TAG, "onServiceDisconnected");
371 }
372
Sean Pont72fc25f2020-02-11 19:02:44 -0800373 @Override // ServiceConnection
Sean Pontd00aefb2020-01-07 12:05:09 -0800374 public void onBindingDied(ComponentName name) {
375 // This is a recoverable error but the client will need to reconnect.
376 Log.w(TAG, "onBindingDied");
377 disconnect();
378 }
379
Sean Pont72fc25f2020-02-11 19:02:44 -0800380 @Override // ServiceConnection
Sean Pontd00aefb2020-01-07 12:05:09 -0800381 public void onNullBinding(ComponentName name) {
382 Log.w(TAG, "onNullBinding");
383 disconnect();
384 }
385
Sean Pont72fc25f2020-02-11 19:02:44 -0800386 private boolean checkSecureSetting(String name) {
387 return Settings.Secure.getInt(mContext.getContentResolver(), name, 0) == 1;
388 }
389
390 private boolean checkUserSetupComplete() {
391 return Settings.Secure.getIntForUser(
392 mContext.getContentResolver(),
393 Settings.Secure.USER_SETUP_COMPLETE, 0,
394 UserHandle.USER_CURRENT) == 1;
395 }
396
Sean Pontd00aefb2020-01-07 12:05:09 -0800397 private static class BaseCallbacks extends IQuickAccessWalletServiceCallbacks.Stub {
398 public void onGetWalletCardsSuccess(GetWalletCardsResponse response) {
399 throw new IllegalStateException();
400 }
401
402 public void onGetWalletCardsFailure(GetWalletCardsError error) {
403 throw new IllegalStateException();
404 }
405
406 public void onWalletServiceEvent(WalletServiceEvent event) {
407 throw new IllegalStateException();
408 }
409 }
410}