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