1 /*
2  * Copyright (C) 2018 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 package android.service.autofill.augmented;
17 
18 import static android.service.autofill.augmented.Helper.logResponse;
19 import static android.util.TimeUtils.formatDuration;
20 
21 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
22 
23 import android.annotation.CallSuper;
24 import android.annotation.IntDef;
25 import android.annotation.NonNull;
26 import android.annotation.Nullable;
27 import android.annotation.SystemApi;
28 import android.annotation.TestApi;
29 import android.app.Service;
30 import android.content.ComponentName;
31 import android.content.Intent;
32 import android.graphics.Rect;
33 import android.os.Build;
34 import android.os.CancellationSignal;
35 import android.os.Handler;
36 import android.os.IBinder;
37 import android.os.ICancellationSignal;
38 import android.os.Looper;
39 import android.os.RemoteException;
40 import android.os.SystemClock;
41 import android.service.autofill.augmented.PresentationParams.SystemPopupPresentationParams;
42 import android.util.Log;
43 import android.util.Pair;
44 import android.util.SparseArray;
45 import android.view.autofill.AutofillId;
46 import android.view.autofill.AutofillManager;
47 import android.view.autofill.AutofillValue;
48 import android.view.autofill.IAugmentedAutofillManagerClient;
49 import android.view.autofill.IAutofillWindowPresenter;
50 
51 import com.android.internal.annotations.GuardedBy;
52 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
53 
54 import java.io.FileDescriptor;
55 import java.io.PrintWriter;
56 import java.lang.annotation.Retention;
57 import java.lang.annotation.RetentionPolicy;
58 import java.util.ArrayList;
59 import java.util.List;
60 
61 /**
62  * A service used to augment the Autofill subsystem by potentially providing autofill data when the
63  * "standard" workflow failed (for example, because the standard AutofillService didn't have data).
64  *
65  * @hide
66  */
67 @SystemApi
68 @TestApi
69 public abstract class AugmentedAutofillService extends Service {
70 
71     private static final String TAG = AugmentedAutofillService.class.getSimpleName();
72 
73     static boolean sDebug = Build.IS_USER ? false : true;
74     static boolean sVerbose = false;
75 
76     /**
77      * The {@link Intent} that must be declared as handled by the service.
78      * To be supported, the service must also require the
79      * {@link android.Manifest.permission#BIND_AUGMENTED_AUTOFILL_SERVICE} permission so
80      * that other applications can not abuse it.
81      */
82     public static final String SERVICE_INTERFACE =
83             "android.service.autofill.augmented.AugmentedAutofillService";
84 
85     private Handler mHandler;
86 
87     private SparseArray<AutofillProxy> mAutofillProxies;
88 
89     // Used for metrics / debug only
90     private ComponentName mServiceComponentName;
91 
92     private final IAugmentedAutofillService mInterface = new IAugmentedAutofillService.Stub() {
93 
94         @Override
95         public void onConnected(boolean debug, boolean verbose) {
96             mHandler.sendMessage(obtainMessage(AugmentedAutofillService::handleOnConnected,
97                     AugmentedAutofillService.this, debug, verbose));
98         }
99 
100         @Override
101         public void onDisconnected() {
102             mHandler.sendMessage(obtainMessage(AugmentedAutofillService::handleOnDisconnected,
103                     AugmentedAutofillService.this));
104         }
105 
106         @Override
107         public void onFillRequest(int sessionId, IBinder client, int taskId,
108                 ComponentName componentName, AutofillId focusedId, AutofillValue focusedValue,
109                 long requestTime, IFillCallback callback) {
110             mHandler.sendMessage(obtainMessage(AugmentedAutofillService::handleOnFillRequest,
111                     AugmentedAutofillService.this, sessionId, client, taskId, componentName,
112                     focusedId, focusedValue, requestTime, callback));
113         }
114 
115         @Override
116         public void onDestroyAllFillWindowsRequest() {
117             mHandler.sendMessage(
118                     obtainMessage(AugmentedAutofillService::handleOnDestroyAllFillWindowsRequest,
119                             AugmentedAutofillService.this));
120         }
121     };
122 
123     @CallSuper
124     @Override
onCreate()125     public void onCreate() {
126         super.onCreate();
127         mHandler = new Handler(Looper.getMainLooper(), null, true);
128     }
129 
130     /** @hide */
131     @Override
onBind(Intent intent)132     public final IBinder onBind(Intent intent) {
133         mServiceComponentName = intent.getComponent();
134         if (SERVICE_INTERFACE.equals(intent.getAction())) {
135             return mInterface.asBinder();
136         }
137         Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent);
138         return null;
139     }
140 
141     @Override
onUnbind(Intent intent)142     public boolean onUnbind(Intent intent) {
143         mHandler.sendMessage(obtainMessage(AugmentedAutofillService::handleOnUnbind,
144                 AugmentedAutofillService.this));
145         return false;
146     }
147 
148     /**
149      * Called when the Android system connects to service.
150      *
151      * <p>You should generally do initialization here rather than in {@link #onCreate}.
152      */
onConnected()153     public void onConnected() {
154     }
155 
156     /**
157      * Asks the service to handle an "augmented" autofill request.
158      *
159      * <p>This method is called when the "stantard" autofill service cannot handle a request, which
160      * typically occurs when:
161      * <ul>
162      *   <li>Service does not recognize what should be autofilled.
163      *   <li>Service does not have data to fill the request.
164      *   <li>Service blacklisted that app (or activity) for autofill.
165      *   <li>App disabled itself for autofill.
166      * </ul>
167      *
168      * <p>Differently from the standard autofill workflow, on augmented autofill the service is
169      * responsible to generate the autofill UI and request the Android system to autofill the
170      * activity when the user taps an action in that UI (through the
171      * {@link FillController#autofill(List)} method).
172      *
173      * <p>The service <b>MUST</b> call {@link
174      * FillCallback#onSuccess(android.service.autofill.augmented.FillResponse)} as soon as possible,
175      * passing {@code null} when it cannot fulfill the request.
176      * @param request the request to handle.
177      * @param cancellationSignal signal for observing cancellation requests. The system will use
178      *     this to notify you that the fill result is no longer needed and you should stop
179      *     handling this fill request in order to save resources.
180      * @param controller object used to interact with the autofill system.
181      * @param callback object used to notify the result of the request. Service <b>must</b> call
182      * {@link FillCallback#onSuccess(android.service.autofill.augmented.FillResponse)}.
183      */
onFillRequest(@onNull FillRequest request, @NonNull CancellationSignal cancellationSignal, @NonNull FillController controller, @NonNull FillCallback callback)184     public void onFillRequest(@NonNull FillRequest request,
185             @NonNull CancellationSignal cancellationSignal, @NonNull FillController controller,
186             @NonNull FillCallback callback) {
187     }
188 
189     /**
190      * Called when the Android system disconnects from the service.
191      *
192      * <p> At this point this service may no longer be an active {@link AugmentedAutofillService}.
193      * It should not make calls on {@link AutofillManager} that requires the caller to be
194      * the current service.
195      */
onDisconnected()196     public void onDisconnected() {
197     }
198 
handleOnConnected(boolean debug, boolean verbose)199     private void handleOnConnected(boolean debug, boolean verbose) {
200         if (sDebug || debug) {
201             Log.d(TAG, "handleOnConnected(): debug=" + debug + ", verbose=" + verbose);
202         }
203         sDebug = debug;
204         sVerbose = verbose;
205         onConnected();
206     }
207 
handleOnDisconnected()208     private void handleOnDisconnected() {
209         onDisconnected();
210     }
211 
handleOnFillRequest(int sessionId, @NonNull IBinder client, int taskId, @NonNull ComponentName componentName, @NonNull AutofillId focusedId, @Nullable AutofillValue focusedValue, long requestTime, @NonNull IFillCallback callback)212     private void handleOnFillRequest(int sessionId, @NonNull IBinder client, int taskId,
213             @NonNull ComponentName componentName, @NonNull AutofillId focusedId,
214             @Nullable AutofillValue focusedValue, long requestTime,
215             @NonNull IFillCallback callback) {
216         if (mAutofillProxies == null) {
217             mAutofillProxies = new SparseArray<>();
218         }
219 
220         final ICancellationSignal transport = CancellationSignal.createTransport();
221         final CancellationSignal cancellationSignal = CancellationSignal.fromTransport(transport);
222         AutofillProxy proxy = mAutofillProxies.get(sessionId);
223         if (proxy == null) {
224             proxy = new AutofillProxy(sessionId, client, taskId, mServiceComponentName,
225                     componentName, focusedId, focusedValue, requestTime, callback,
226                     cancellationSignal);
227             mAutofillProxies.put(sessionId,  proxy);
228         } else {
229             // TODO(b/123099468): figure out if it's ok to reuse the proxy; add logging
230             if (sDebug) Log.d(TAG, "Reusing proxy for session " + sessionId);
231             proxy.update(focusedId, focusedValue, callback, cancellationSignal);
232         }
233 
234         try {
235             callback.onCancellable(transport);
236         } catch (RemoteException e) {
237             e.rethrowFromSystemServer();
238         }
239 
240         onFillRequest(new FillRequest(proxy), cancellationSignal, new FillController(proxy),
241                 new FillCallback(proxy));
242     }
243 
handleOnDestroyAllFillWindowsRequest()244     private void handleOnDestroyAllFillWindowsRequest() {
245         if (mAutofillProxies != null) {
246             final int size = mAutofillProxies.size();
247             for (int i = 0; i < size; i++) {
248                 final int sessionId = mAutofillProxies.keyAt(i);
249                 final AutofillProxy proxy = mAutofillProxies.valueAt(i);
250                 if (proxy == null) {
251                     // TODO(b/123100811): this might be fine, in which case we should logv it
252                     Log.w(TAG, "No proxy for session " + sessionId);
253                     return;
254                 }
255                 if (proxy.mCallback != null) {
256                     try {
257                         if (!proxy.mCallback.isCompleted()) {
258                             proxy.mCallback.cancel();
259                         }
260                     } catch (Exception e) {
261                         Log.e(TAG, "failed to check current pending request status", e);
262                     }
263                 }
264                 proxy.destroy();
265             }
266             mAutofillProxies.clear();
267         }
268     }
269 
handleOnUnbind()270     private void handleOnUnbind() {
271         if (mAutofillProxies == null) {
272             if (sDebug) Log.d(TAG, "onUnbind(): no proxy to destroy");
273             return;
274         }
275         final int size = mAutofillProxies.size();
276         if (sDebug) Log.d(TAG, "onUnbind(): destroying " + size + " proxies");
277         for (int i = 0; i < size; i++) {
278             final AutofillProxy proxy = mAutofillProxies.valueAt(i);
279             try {
280                 proxy.destroy();
281             } catch (Exception e) {
282                 Log.w(TAG, "error destroying " + proxy);
283             }
284         }
285         mAutofillProxies = null;
286     }
287 
288     @Override
289     /** @hide */
dump(FileDescriptor fd, PrintWriter pw, String[] args)290     protected final void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
291         pw.print("Service component: "); pw.println(
292                 ComponentName.flattenToShortString(mServiceComponentName));
293         if (mAutofillProxies != null) {
294             final int size = mAutofillProxies.size();
295             pw.print("Number proxies: "); pw.println(size);
296             for (int i = 0; i < size; i++) {
297                 final int sessionId = mAutofillProxies.keyAt(i);
298                 final AutofillProxy proxy = mAutofillProxies.valueAt(i);
299                 pw.print(i); pw.print(") SessionId="); pw.print(sessionId); pw.println(":");
300                 proxy.dump("  ", pw);
301             }
302         }
303         dump(pw, args);
304     }
305 
306     /**
307      * Implementation specific {@code dump}. The child class can override the method to provide
308      * additional information about the Service's state into the dumpsys output.
309      *
310      * @param pw The PrintWriter to which you should dump your state.  This will be closed for
311      * you after you return.
312      * @param args additional arguments to the dump request.
313      */
dump(@onNull PrintWriter pw, @SuppressWarnings("unused") @NonNull String[] args)314     protected void dump(@NonNull PrintWriter pw,
315             @SuppressWarnings("unused") @NonNull String[] args) {
316         pw.print(getClass().getName()); pw.println(": nothing to dump");
317     }
318 
319     /** @hide */
320     static final class AutofillProxy {
321 
322         static final int REPORT_EVENT_NO_RESPONSE = 1;
323         static final int REPORT_EVENT_UI_SHOWN = 2;
324         static final int REPORT_EVENT_UI_DESTROYED = 3;
325 
326         @IntDef(prefix = { "REPORT_EVENT_" }, value = {
327                 REPORT_EVENT_NO_RESPONSE,
328                 REPORT_EVENT_UI_SHOWN,
329                 REPORT_EVENT_UI_DESTROYED
330         })
331         @Retention(RetentionPolicy.SOURCE)
332         @interface ReportEvent{}
333 
334 
335         private final Object mLock = new Object();
336         private final IAugmentedAutofillManagerClient mClient;
337         private final int mSessionId;
338         public final int taskId;
339         public final ComponentName componentName;
340         // Used for metrics / debug only
341         private String mServicePackageName;
342         @GuardedBy("mLock")
343         private AutofillId mFocusedId;
344         @GuardedBy("mLock")
345         private AutofillValue mFocusedValue;
346         @GuardedBy("mLock")
347         private IFillCallback mCallback;
348 
349         /**
350          * Id of the last field that cause the Autofill UI to be shown.
351          *
352          * <p>Used to make sure the SmartSuggestionsParams is updated when a new fields is focused.
353          */
354         @GuardedBy("mLock")
355         private AutofillId mLastShownId;
356 
357         // Objects used to log metrics
358         private final long mFirstRequestTime;
359         private long mFirstOnSuccessTime;
360         private long mUiFirstShownTime;
361         private long mUiFirstDestroyedTime;
362 
363         @GuardedBy("mLock")
364         private SystemPopupPresentationParams mSmartSuggestion;
365 
366         @GuardedBy("mLock")
367         private FillWindow mFillWindow;
368 
369         private CancellationSignal mCancellationSignal;
370 
AutofillProxy(int sessionId, @NonNull IBinder client, int taskId, @NonNull ComponentName serviceComponentName, @NonNull ComponentName componentName, @NonNull AutofillId focusedId, @Nullable AutofillValue focusedValue, long requestTime, @NonNull IFillCallback callback, @NonNull CancellationSignal cancellationSignal)371         private AutofillProxy(int sessionId, @NonNull IBinder client, int taskId,
372                 @NonNull ComponentName serviceComponentName,
373                 @NonNull ComponentName componentName, @NonNull AutofillId focusedId,
374                 @Nullable AutofillValue focusedValue, long requestTime,
375                 @NonNull IFillCallback callback, @NonNull CancellationSignal cancellationSignal) {
376             mSessionId = sessionId;
377             mClient = IAugmentedAutofillManagerClient.Stub.asInterface(client);
378             mCallback = callback;
379             this.taskId = taskId;
380             this.componentName = componentName;
381             mServicePackageName = serviceComponentName.getPackageName();
382             mFocusedId = focusedId;
383             mFocusedValue = focusedValue;
384             mFirstRequestTime = requestTime;
385             mCancellationSignal = cancellationSignal;
386             // TODO(b/123099468): linkToDeath
387         }
388 
389         @NonNull
getSmartSuggestionParams()390         public SystemPopupPresentationParams getSmartSuggestionParams() {
391             synchronized (mLock) {
392                 if (mSmartSuggestion != null && mFocusedId.equals(mLastShownId)) {
393                     return mSmartSuggestion;
394                 }
395                 Rect rect;
396                 try {
397                     rect = mClient.getViewCoordinates(mFocusedId);
398                 } catch (RemoteException e) {
399                     Log.w(TAG, "Could not get coordinates for " + mFocusedId);
400                     return null;
401                 }
402                 if (rect == null) {
403                     if (sDebug) Log.d(TAG, "getViewCoordinates(" + mFocusedId + ") returned null");
404                     return null;
405                 }
406                 mSmartSuggestion = new SystemPopupPresentationParams(this, rect);
407                 mLastShownId = mFocusedId;
408                 return mSmartSuggestion;
409             }
410         }
411 
autofill(@onNull List<Pair<AutofillId, AutofillValue>> pairs)412         public void autofill(@NonNull List<Pair<AutofillId, AutofillValue>> pairs)
413                 throws RemoteException {
414             final int size = pairs.size();
415             final List<AutofillId> ids = new ArrayList<>(size);
416             final List<AutofillValue> values = new ArrayList<>(size);
417             for (int i = 0; i < size; i++) {
418                 final Pair<AutofillId, AutofillValue> pair = pairs.get(i);
419                 ids.add(pair.first);
420                 values.add(pair.second);
421             }
422             mClient.autofill(mSessionId, ids, values);
423         }
424 
setFillWindow(@onNull FillWindow fillWindow)425         public void setFillWindow(@NonNull FillWindow fillWindow) {
426             synchronized (mLock) {
427                 mFillWindow = fillWindow;
428             }
429         }
430 
getFillWindow()431         public FillWindow getFillWindow() {
432             synchronized (mLock) {
433                 return mFillWindow;
434             }
435         }
436 
requestShowFillUi(int width, int height, Rect anchorBounds, IAutofillWindowPresenter presenter)437         public void requestShowFillUi(int width, int height, Rect anchorBounds,
438                 IAutofillWindowPresenter presenter) throws RemoteException {
439             if (mCancellationSignal.isCanceled()) {
440                 if (sVerbose) {
441                     Log.v(TAG, "requestShowFillUi() not showing because request is cancelled");
442                 }
443                 return;
444             }
445             mClient.requestShowFillUi(mSessionId, mFocusedId, width, height, anchorBounds,
446                     presenter);
447         }
448 
requestHideFillUi()449         public void requestHideFillUi() throws RemoteException {
450             mClient.requestHideFillUi(mSessionId, mFocusedId);
451         }
452 
update(@onNull AutofillId focusedId, @NonNull AutofillValue focusedValue, @NonNull IFillCallback callback, @NonNull CancellationSignal cancellationSignal)453         private void update(@NonNull AutofillId focusedId, @NonNull AutofillValue focusedValue,
454                 @NonNull IFillCallback callback, @NonNull CancellationSignal cancellationSignal) {
455             synchronized (mLock) {
456                 mFocusedId = focusedId;
457                 mFocusedValue = focusedValue;
458                 if (mCallback != null) {
459                     try {
460                         if (!mCallback.isCompleted()) {
461                             mCallback.cancel();
462                         }
463                     } catch (RemoteException e) {
464                         Log.e(TAG, "failed to check current pending request status", e);
465                     }
466                     Log.d(TAG, "mCallback is updated.");
467                 }
468                 mCallback = callback;
469                 mCancellationSignal = cancellationSignal;
470             }
471         }
472 
473         @NonNull
getFocusedId()474         public AutofillId getFocusedId() {
475             synchronized (mLock) {
476                 return mFocusedId;
477             }
478         }
479 
480         @NonNull
getFocusedValue()481         public AutofillValue getFocusedValue() {
482             synchronized (mLock) {
483                 return mFocusedValue;
484             }
485         }
486 
487         // Used (mostly) for metrics.
report(@eportEvent int event)488         public void report(@ReportEvent int event) {
489             if (sVerbose) Log.v(TAG, "report(): " + event);
490             long duration = -1;
491             int type = MetricsEvent.TYPE_UNKNOWN;
492             switch (event) {
493                 case REPORT_EVENT_NO_RESPONSE:
494                     type = MetricsEvent.TYPE_SUCCESS;
495                     if (mFirstOnSuccessTime == 0) {
496                         mFirstOnSuccessTime = SystemClock.elapsedRealtime();
497                         duration = mFirstOnSuccessTime - mFirstRequestTime;
498                         if (sDebug) {
499                             Log.d(TAG, "Service responded nothing in " + formatDuration(duration));
500                         }
501                     }
502                     try {
503                         mCallback.onSuccess();
504                     } catch (RemoteException e) {
505                         Log.e(TAG, "Error reporting success: " + e);
506                     }
507                     break;
508                 case REPORT_EVENT_UI_SHOWN:
509                     type = MetricsEvent.TYPE_OPEN;
510                     if (mUiFirstShownTime == 0) {
511                         mUiFirstShownTime = SystemClock.elapsedRealtime();
512                         duration = mUiFirstShownTime - mFirstRequestTime;
513                         if (sDebug) Log.d(TAG, "UI shown in " + formatDuration(duration));
514                     }
515                     break;
516                 case REPORT_EVENT_UI_DESTROYED:
517                     type = MetricsEvent.TYPE_CLOSE;
518                     if (mUiFirstDestroyedTime == 0) {
519                         mUiFirstDestroyedTime = SystemClock.elapsedRealtime();
520                         duration =  mUiFirstDestroyedTime - mFirstRequestTime;
521                         if (sDebug) Log.d(TAG, "UI destroyed in " + formatDuration(duration));
522                     }
523                     break;
524                 default:
525                     Log.w(TAG, "invalid event reported: " + event);
526             }
527             logResponse(type, mServicePackageName, componentName, mSessionId, duration);
528         }
529 
dump(@onNull String prefix, @NonNull PrintWriter pw)530         public void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
531             pw.print(prefix); pw.print("sessionId: "); pw.println(mSessionId);
532             pw.print(prefix); pw.print("taskId: "); pw.println(taskId);
533             pw.print(prefix); pw.print("component: ");
534             pw.println(componentName.flattenToShortString());
535             pw.print(prefix); pw.print("focusedId: "); pw.println(mFocusedId);
536             if (mFocusedValue != null) {
537                 pw.print(prefix); pw.print("focusedValue: "); pw.println(mFocusedValue);
538             }
539             if (mLastShownId != null) {
540                 pw.print(prefix); pw.print("lastShownId: "); pw.println(mLastShownId);
541             }
542             pw.print(prefix); pw.print("client: "); pw.println(mClient);
543             final String prefix2 = prefix + "  ";
544             if (mFillWindow != null) {
545                 pw.print(prefix); pw.println("window:");
546                 mFillWindow.dump(prefix2, pw);
547             }
548             if (mSmartSuggestion != null) {
549                 pw.print(prefix); pw.println("smartSuggestion:");
550                 mSmartSuggestion.dump(prefix2, pw);
551             }
552             if (mFirstOnSuccessTime > 0) {
553                 final long responseTime = mFirstOnSuccessTime - mFirstRequestTime;
554                 pw.print(prefix); pw.print("response time: ");
555                 formatDuration(responseTime, pw); pw.println();
556             }
557 
558             if (mUiFirstShownTime > 0) {
559                 final long uiRenderingTime = mUiFirstShownTime - mFirstRequestTime;
560                 pw.print(prefix); pw.print("UI rendering time: ");
561                 formatDuration(uiRenderingTime, pw); pw.println();
562             }
563 
564             if (mUiFirstDestroyedTime > 0) {
565                 final long uiTotalTime = mUiFirstDestroyedTime - mFirstRequestTime;
566                 pw.print(prefix); pw.print("UI life time: ");
567                 formatDuration(uiTotalTime, pw); pw.println();
568             }
569         }
570 
destroy()571         private void destroy() {
572             synchronized (mLock) {
573                 if (mFillWindow != null) {
574                     if (sDebug) Log.d(TAG, "destroying window");
575                     mFillWindow.destroy();
576                     mFillWindow = null;
577                 }
578             }
579         }
580     }
581 }
582