701 lines
28 KiB
Java
701 lines
28 KiB
Java
/*
|
|
* Copyright (C) 2018 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
package android.service.autofill.augmented;
|
|
|
|
import static android.service.autofill.augmented.Helper.logResponse;
|
|
import static android.util.TimeUtils.formatDuration;
|
|
|
|
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
|
|
|
|
import android.annotation.CallSuper;
|
|
import android.annotation.IntDef;
|
|
import android.annotation.NonNull;
|
|
import android.annotation.Nullable;
|
|
import android.annotation.SystemApi;
|
|
import android.app.Service;
|
|
import android.app.assist.AssistStructure.ViewNode;
|
|
import android.app.assist.AssistStructure.ViewNodeParcelable;
|
|
import android.content.ComponentName;
|
|
import android.content.Intent;
|
|
import android.graphics.Rect;
|
|
import android.os.BaseBundle;
|
|
import android.os.Build;
|
|
import android.os.Bundle;
|
|
import android.os.CancellationSignal;
|
|
import android.os.Handler;
|
|
import android.os.IBinder;
|
|
import android.os.ICancellationSignal;
|
|
import android.os.Looper;
|
|
import android.os.RemoteException;
|
|
import android.os.SystemClock;
|
|
import android.service.autofill.Dataset;
|
|
import android.service.autofill.FillEventHistory;
|
|
import android.service.autofill.augmented.PresentationParams.SystemPopupPresentationParams;
|
|
import android.util.Log;
|
|
import android.util.Pair;
|
|
import android.util.SparseArray;
|
|
import android.view.autofill.AutofillId;
|
|
import android.view.autofill.AutofillManager;
|
|
import android.view.autofill.AutofillValue;
|
|
import android.view.autofill.IAugmentedAutofillManagerClient;
|
|
import android.view.autofill.IAutofillWindowPresenter;
|
|
import android.view.inputmethod.InlineSuggestionsRequest;
|
|
|
|
import com.android.internal.annotations.GuardedBy;
|
|
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
|
|
|
|
import java.io.FileDescriptor;
|
|
import java.io.PrintWriter;
|
|
import java.lang.annotation.Retention;
|
|
import java.lang.annotation.RetentionPolicy;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* A service used to augment the Autofill subsystem by potentially providing autofill data when the
|
|
* "standard" workflow failed (for example, because the standard AutofillService didn't have data).
|
|
*
|
|
* @hide
|
|
*/
|
|
@SystemApi
|
|
public abstract class AugmentedAutofillService extends Service {
|
|
|
|
private static final String TAG = AugmentedAutofillService.class.getSimpleName();
|
|
|
|
static boolean sDebug = Build.IS_USER ? false : true;
|
|
static boolean sVerbose = false;
|
|
|
|
/**
|
|
* The {@link Intent} that must be declared as handled by the service.
|
|
* To be supported, the service must also require the
|
|
* {@link android.Manifest.permission#BIND_AUGMENTED_AUTOFILL_SERVICE} permission so
|
|
* that other applications can not abuse it.
|
|
*/
|
|
public static final String SERVICE_INTERFACE =
|
|
"android.service.autofill.augmented.AugmentedAutofillService";
|
|
|
|
private Handler mHandler;
|
|
|
|
private SparseArray<AutofillProxy> mAutofillProxies;
|
|
|
|
private AutofillProxy mAutofillProxyForLastRequest;
|
|
|
|
// Used for metrics / debug only
|
|
private ComponentName mServiceComponentName;
|
|
|
|
private final class AugmentedAutofillServiceImpl extends IAugmentedAutofillService.Stub {
|
|
|
|
@Override
|
|
public void onConnected(boolean debug, boolean verbose) {
|
|
mHandler.sendMessage(obtainMessage(AugmentedAutofillService::handleOnConnected,
|
|
AugmentedAutofillService.this, debug, verbose));
|
|
}
|
|
|
|
@Override
|
|
public void onDisconnected() {
|
|
mHandler.sendMessage(obtainMessage(AugmentedAutofillService::handleOnDisconnected,
|
|
AugmentedAutofillService.this));
|
|
}
|
|
|
|
@Override
|
|
public void onFillRequest(int sessionId, IBinder client, int taskId,
|
|
ComponentName componentName, AutofillId focusedId, AutofillValue focusedValue,
|
|
long requestTime, @Nullable InlineSuggestionsRequest inlineSuggestionsRequest,
|
|
IFillCallback callback) {
|
|
mHandler.sendMessage(obtainMessage(AugmentedAutofillService::handleOnFillRequest,
|
|
AugmentedAutofillService.this, sessionId, client, taskId, componentName,
|
|
focusedId, focusedValue, requestTime, inlineSuggestionsRequest, callback));
|
|
}
|
|
|
|
@Override
|
|
public void onDestroyAllFillWindowsRequest() {
|
|
mHandler.sendMessage(
|
|
obtainMessage(AugmentedAutofillService::handleOnDestroyAllFillWindowsRequest,
|
|
AugmentedAutofillService.this));
|
|
}
|
|
};
|
|
|
|
@CallSuper
|
|
@Override
|
|
public void onCreate() {
|
|
super.onCreate();
|
|
mHandler = new Handler(Looper.getMainLooper(), null, true);
|
|
BaseBundle.setShouldDefuse(true);
|
|
}
|
|
|
|
/** @hide */
|
|
@Override
|
|
public final IBinder onBind(Intent intent) {
|
|
mServiceComponentName = intent.getComponent();
|
|
if (SERVICE_INTERFACE.equals(intent.getAction())) {
|
|
return new AugmentedAutofillServiceImpl();
|
|
}
|
|
Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent);
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public boolean onUnbind(Intent intent) {
|
|
mHandler.sendMessage(obtainMessage(AugmentedAutofillService::handleOnUnbind,
|
|
AugmentedAutofillService.this));
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Called when the Android system connects to service.
|
|
*
|
|
* <p>You should generally do initialization here rather than in {@link #onCreate}.
|
|
*/
|
|
public void onConnected() {
|
|
}
|
|
|
|
/**
|
|
* The child class of the service can call this method to initiate a new Autofill flow. If all
|
|
* conditions are met, it will make a request to the client app process to explicitly cancel
|
|
* the current autofill session and create a new session. For example, an augmented autofill
|
|
* service may notice some events which make it think a good time to provide updated
|
|
* augmented autofill suggestions.
|
|
*
|
|
* <p> The request would be respected only if the previous augmented autofill request was
|
|
* made for the same {@code activityComponent} and {@code autofillId}, and the field is
|
|
* currently on focus.
|
|
*
|
|
* <p> The request would cancel the current session and start a new autofill flow.
|
|
* It doesn't guarantee that the {@link AutofillManager} will proceed with the request.
|
|
*
|
|
* @param activityComponent the client component for which the autofill is requested for
|
|
* @param autofillId the client field id for which the autofill is requested for
|
|
* @return true if the request makes the {@link AutofillManager} start a new Autofill flow,
|
|
* false otherwise.
|
|
*/
|
|
public final boolean requestAutofill(@NonNull ComponentName activityComponent,
|
|
@NonNull AutofillId autofillId) {
|
|
final AutofillProxy proxy = mAutofillProxyForLastRequest;
|
|
if (proxy == null || !proxy.mComponentName.equals(activityComponent)
|
|
|| !proxy.mFocusedId.equals(autofillId)) {
|
|
return false;
|
|
}
|
|
try {
|
|
return proxy.requestAutofill();
|
|
} catch (RemoteException e) {
|
|
e.rethrowFromSystemServer();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Asks the service to handle an "augmented" autofill request.
|
|
*
|
|
* <p>This method is called when the "stantard" autofill service cannot handle a request, which
|
|
* typically occurs when:
|
|
* <ul>
|
|
* <li>Service does not recognize what should be autofilled.
|
|
* <li>Service does not have data to fill the request.
|
|
* <li>Service denylisted that app (or activity) for autofill.
|
|
* <li>App disabled itself for autofill.
|
|
* </ul>
|
|
*
|
|
* <p>Differently from the standard autofill workflow, on augmented autofill the service is
|
|
* responsible to generate the autofill UI and request the Android system to autofill the
|
|
* activity when the user taps an action in that UI (through the
|
|
* {@link FillController#autofill(List)} method).
|
|
*
|
|
* <p>The service <b>MUST</b> call {@link
|
|
* FillCallback#onSuccess(android.service.autofill.augmented.FillResponse)} as soon as possible,
|
|
* passing {@code null} when it cannot fulfill the request.
|
|
* @param request the request to handle.
|
|
* @param cancellationSignal signal for observing cancellation requests. The system will use
|
|
* this to notify you that the fill result is no longer needed and you should stop
|
|
* handling this fill request in order to save resources.
|
|
* @param controller object used to interact with the autofill system.
|
|
* @param callback object used to notify the result of the request. Service <b>must</b> call
|
|
* {@link FillCallback#onSuccess(android.service.autofill.augmented.FillResponse)}.
|
|
*/
|
|
public void onFillRequest(@NonNull FillRequest request,
|
|
@NonNull CancellationSignal cancellationSignal, @NonNull FillController controller,
|
|
@NonNull FillCallback callback) {
|
|
}
|
|
|
|
/**
|
|
* Called when the Android system disconnects from the service.
|
|
*
|
|
* <p> At this point this service may no longer be an active {@link AugmentedAutofillService}.
|
|
* It should not make calls on {@link AutofillManager} that requires the caller to be
|
|
* the current service.
|
|
*/
|
|
public void onDisconnected() {
|
|
}
|
|
|
|
private void handleOnConnected(boolean debug, boolean verbose) {
|
|
if (sDebug || debug) {
|
|
Log.d(TAG, "handleOnConnected(): debug=" + debug + ", verbose=" + verbose);
|
|
}
|
|
sDebug = debug;
|
|
sVerbose = verbose;
|
|
onConnected();
|
|
}
|
|
|
|
private void handleOnDisconnected() {
|
|
onDisconnected();
|
|
}
|
|
|
|
private void handleOnFillRequest(int sessionId, @NonNull IBinder client, int taskId,
|
|
@NonNull ComponentName componentName, @NonNull AutofillId focusedId,
|
|
@Nullable AutofillValue focusedValue, long requestTime,
|
|
@Nullable InlineSuggestionsRequest inlineSuggestionsRequest,
|
|
@NonNull IFillCallback callback) {
|
|
if (mAutofillProxies == null) {
|
|
mAutofillProxies = new SparseArray<>();
|
|
}
|
|
|
|
final ICancellationSignal transport = CancellationSignal.createTransport();
|
|
final CancellationSignal cancellationSignal = CancellationSignal.fromTransport(transport);
|
|
AutofillProxy proxy = mAutofillProxies.get(sessionId);
|
|
if (proxy == null) {
|
|
proxy = new AutofillProxy(sessionId, client, taskId, mServiceComponentName,
|
|
componentName, focusedId, focusedValue, requestTime, callback,
|
|
cancellationSignal);
|
|
mAutofillProxies.put(sessionId, proxy);
|
|
} else {
|
|
// TODO(b/123099468): figure out if it's ok to reuse the proxy; add logging
|
|
if (sDebug) Log.d(TAG, "Reusing proxy for session " + sessionId);
|
|
proxy.update(focusedId, focusedValue, callback, cancellationSignal);
|
|
}
|
|
|
|
try {
|
|
callback.onCancellable(transport);
|
|
} catch (RemoteException e) {
|
|
e.rethrowFromSystemServer();
|
|
}
|
|
mAutofillProxyForLastRequest = proxy;
|
|
onFillRequest(new FillRequest(proxy, inlineSuggestionsRequest), cancellationSignal,
|
|
new FillController(proxy), new FillCallback(proxy));
|
|
}
|
|
|
|
private void handleOnDestroyAllFillWindowsRequest() {
|
|
if (mAutofillProxies != null) {
|
|
final int size = mAutofillProxies.size();
|
|
for (int i = 0; i < size; i++) {
|
|
final int sessionId = mAutofillProxies.keyAt(i);
|
|
final AutofillProxy proxy = mAutofillProxies.valueAt(i);
|
|
if (proxy == null) {
|
|
// TODO(b/123100811): this might be fine, in which case we should logv it
|
|
Log.w(TAG, "No proxy for session " + sessionId);
|
|
return;
|
|
}
|
|
if (proxy.mCallback != null) {
|
|
try {
|
|
if (!proxy.mCallback.isCompleted()) {
|
|
proxy.mCallback.cancel();
|
|
}
|
|
} catch (Exception e) {
|
|
Log.e(TAG, "failed to check current pending request status", e);
|
|
}
|
|
}
|
|
proxy.destroy();
|
|
}
|
|
mAutofillProxies.clear();
|
|
mAutofillProxyForLastRequest = null;
|
|
}
|
|
}
|
|
|
|
private void handleOnUnbind() {
|
|
if (mAutofillProxies == null) {
|
|
if (sDebug) Log.d(TAG, "onUnbind(): no proxy to destroy");
|
|
return;
|
|
}
|
|
final int size = mAutofillProxies.size();
|
|
if (sDebug) Log.d(TAG, "onUnbind(): destroying " + size + " proxies");
|
|
for (int i = 0; i < size; i++) {
|
|
final AutofillProxy proxy = mAutofillProxies.valueAt(i);
|
|
try {
|
|
proxy.destroy();
|
|
} catch (Exception e) {
|
|
Log.w(TAG, "error destroying " + proxy);
|
|
}
|
|
}
|
|
mAutofillProxies = null;
|
|
mAutofillProxyForLastRequest = null;
|
|
}
|
|
|
|
@Override
|
|
protected final void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
|
|
pw.print("Service component: "); pw.println(
|
|
ComponentName.flattenToShortString(mServiceComponentName));
|
|
if (mAutofillProxies != null) {
|
|
final int size = mAutofillProxies.size();
|
|
pw.print("Number proxies: "); pw.println(size);
|
|
for (int i = 0; i < size; i++) {
|
|
final int sessionId = mAutofillProxies.keyAt(i);
|
|
final AutofillProxy proxy = mAutofillProxies.valueAt(i);
|
|
pw.print(i); pw.print(") SessionId="); pw.print(sessionId); pw.println(":");
|
|
proxy.dump(" ", pw);
|
|
}
|
|
}
|
|
dump(pw, args);
|
|
}
|
|
|
|
/**
|
|
* Implementation specific {@code dump}. The child class can override the method to provide
|
|
* additional information about the Service's state into the dumpsys output.
|
|
*
|
|
* @param pw The PrintWriter to which you should dump your state. This will be closed for
|
|
* you after you return.
|
|
* @param args additional arguments to the dump request.
|
|
*/
|
|
protected void dump(@NonNull PrintWriter pw,
|
|
@SuppressWarnings("unused") @NonNull String[] args) {
|
|
pw.print(getClass().getName()); pw.println(": nothing to dump");
|
|
}
|
|
|
|
/**
|
|
* Gets the inline augmented autofill events that happened after the last
|
|
* {@link #onFillRequest(FillRequest, CancellationSignal, FillController, FillCallback)} call.
|
|
*
|
|
* <p>The history is not persisted over reboots, and it's cleared every time the service
|
|
* replies to a
|
|
* {@link #onFillRequest(FillRequest, CancellationSignal, FillController, FillCallback)}
|
|
* by calling {@link FillCallback#onSuccess(FillResponse)}. Hence, the service should call
|
|
* {@link #getFillEventHistory() before finishing the {@link FillCallback}.
|
|
*
|
|
* <p>Also note that the events from the dropdown suggestion UI is not stored in the history
|
|
* since the service owns the UI.
|
|
*
|
|
* @return The history or {@code null} if there are no events.
|
|
*/
|
|
@Nullable public final FillEventHistory getFillEventHistory() {
|
|
final AutofillManager afm = getSystemService(AutofillManager.class);
|
|
|
|
if (afm == null) {
|
|
return null;
|
|
} else {
|
|
return afm.getFillEventHistory();
|
|
}
|
|
}
|
|
|
|
/** @hide */
|
|
static final class AutofillProxy {
|
|
|
|
static final int REPORT_EVENT_NO_RESPONSE = 1;
|
|
static final int REPORT_EVENT_UI_SHOWN = 2;
|
|
static final int REPORT_EVENT_UI_DESTROYED = 3;
|
|
static final int REPORT_EVENT_INLINE_RESPONSE = 4;
|
|
|
|
@IntDef(prefix = { "REPORT_EVENT_" }, value = {
|
|
REPORT_EVENT_NO_RESPONSE,
|
|
REPORT_EVENT_UI_SHOWN,
|
|
REPORT_EVENT_UI_DESTROYED,
|
|
REPORT_EVENT_INLINE_RESPONSE
|
|
})
|
|
@Retention(RetentionPolicy.SOURCE)
|
|
@interface ReportEvent{}
|
|
|
|
|
|
private final Object mLock = new Object();
|
|
private final IAugmentedAutofillManagerClient mClient;
|
|
private final int mSessionId;
|
|
public final int mTaskId;
|
|
public final ComponentName mComponentName;
|
|
// Used for metrics / debug only
|
|
private String mServicePackageName;
|
|
@GuardedBy("mLock")
|
|
private AutofillId mFocusedId;
|
|
@GuardedBy("mLock")
|
|
private AutofillValue mFocusedValue;
|
|
@GuardedBy("mLock")
|
|
private ViewNode mFocusedViewNode;
|
|
@GuardedBy("mLock")
|
|
private IFillCallback mCallback;
|
|
|
|
/**
|
|
* Id of the last field that cause the Autofill UI to be shown.
|
|
*
|
|
* <p>Used to make sure the SmartSuggestionsParams is updated when a new fields is focused.
|
|
*/
|
|
@GuardedBy("mLock")
|
|
private AutofillId mLastShownId;
|
|
|
|
// Objects used to log metrics
|
|
private final long mFirstRequestTime;
|
|
private long mFirstOnSuccessTime;
|
|
private long mUiFirstShownTime;
|
|
private long mUiFirstDestroyedTime;
|
|
|
|
@GuardedBy("mLock")
|
|
private SystemPopupPresentationParams mSmartSuggestion;
|
|
|
|
@GuardedBy("mLock")
|
|
private FillWindow mFillWindow;
|
|
|
|
private CancellationSignal mCancellationSignal;
|
|
|
|
private 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) {
|
|
mSessionId = sessionId;
|
|
mClient = IAugmentedAutofillManagerClient.Stub.asInterface(client);
|
|
mCallback = callback;
|
|
mTaskId = taskId;
|
|
mComponentName = componentName;
|
|
mServicePackageName = serviceComponentName.getPackageName();
|
|
mFocusedId = focusedId;
|
|
mFocusedValue = focusedValue;
|
|
mFirstRequestTime = requestTime;
|
|
mCancellationSignal = cancellationSignal;
|
|
// TODO(b/123099468): linkToDeath
|
|
}
|
|
|
|
@NonNull
|
|
public SystemPopupPresentationParams getSmartSuggestionParams() {
|
|
synchronized (mLock) {
|
|
if (mSmartSuggestion != null && mFocusedId.equals(mLastShownId)) {
|
|
return mSmartSuggestion;
|
|
}
|
|
Rect rect;
|
|
try {
|
|
rect = mClient.getViewCoordinates(mFocusedId);
|
|
} catch (RemoteException e) {
|
|
Log.w(TAG, "Could not get coordinates for " + mFocusedId);
|
|
return null;
|
|
}
|
|
if (rect == null) {
|
|
if (sDebug) Log.d(TAG, "getViewCoordinates(" + mFocusedId + ") returned null");
|
|
return null;
|
|
}
|
|
mSmartSuggestion = new SystemPopupPresentationParams(this, rect);
|
|
mLastShownId = mFocusedId;
|
|
return mSmartSuggestion;
|
|
}
|
|
}
|
|
|
|
public void autofill(@NonNull List<Pair<AutofillId, AutofillValue>> pairs)
|
|
throws RemoteException {
|
|
final int size = pairs.size();
|
|
final List<AutofillId> ids = new ArrayList<>(size);
|
|
final List<AutofillValue> values = new ArrayList<>(size);
|
|
for (int i = 0; i < size; i++) {
|
|
final Pair<AutofillId, AutofillValue> pair = pairs.get(i);
|
|
ids.add(pair.first);
|
|
values.add(pair.second);
|
|
}
|
|
final boolean hideHighlight = size == 1 && ids.get(0).equals(mFocusedId);
|
|
mClient.autofill(mSessionId, ids, values, hideHighlight);
|
|
}
|
|
|
|
public void setFillWindow(@NonNull FillWindow fillWindow) {
|
|
synchronized (mLock) {
|
|
mFillWindow = fillWindow;
|
|
}
|
|
}
|
|
|
|
public FillWindow getFillWindow() {
|
|
synchronized (mLock) {
|
|
return mFillWindow;
|
|
}
|
|
}
|
|
|
|
public void requestShowFillUi(int width, int height, Rect anchorBounds,
|
|
IAutofillWindowPresenter presenter) throws RemoteException {
|
|
if (mCancellationSignal.isCanceled()) {
|
|
if (sVerbose) {
|
|
Log.v(TAG, "requestShowFillUi() not showing because request is cancelled");
|
|
}
|
|
return;
|
|
}
|
|
mClient.requestShowFillUi(mSessionId, mFocusedId, width, height, anchorBounds,
|
|
presenter);
|
|
}
|
|
|
|
public void requestHideFillUi() throws RemoteException {
|
|
mClient.requestHideFillUi(mSessionId, mFocusedId);
|
|
}
|
|
|
|
|
|
private boolean requestAutofill() throws RemoteException {
|
|
return mClient.requestAutofill(mSessionId, mFocusedId);
|
|
}
|
|
|
|
private void update(@NonNull AutofillId focusedId, @NonNull AutofillValue focusedValue,
|
|
@NonNull IFillCallback callback, @NonNull CancellationSignal cancellationSignal) {
|
|
synchronized (mLock) {
|
|
mFocusedId = focusedId;
|
|
mFocusedValue = focusedValue;
|
|
mFocusedViewNode = null;
|
|
if (mCallback != null) {
|
|
try {
|
|
if (!mCallback.isCompleted()) {
|
|
mCallback.cancel();
|
|
}
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "failed to check current pending request status", e);
|
|
}
|
|
Log.d(TAG, "mCallback is updated.");
|
|
}
|
|
mCallback = callback;
|
|
mCancellationSignal = cancellationSignal;
|
|
}
|
|
}
|
|
|
|
@NonNull
|
|
public AutofillId getFocusedId() {
|
|
synchronized (mLock) {
|
|
return mFocusedId;
|
|
}
|
|
}
|
|
|
|
@NonNull
|
|
public AutofillValue getFocusedValue() {
|
|
synchronized (mLock) {
|
|
return mFocusedValue;
|
|
}
|
|
}
|
|
|
|
void reportResult(@Nullable List<Dataset> inlineSuggestionsData,
|
|
@Nullable Bundle clientState, boolean showingFillWindow) {
|
|
try {
|
|
mCallback.onSuccess(inlineSuggestionsData, clientState, showingFillWindow);
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "Error calling back with the inline suggestions data: " + e);
|
|
}
|
|
}
|
|
|
|
@Nullable
|
|
public ViewNode getFocusedViewNode() {
|
|
synchronized (mLock) {
|
|
if (mFocusedViewNode == null) {
|
|
try {
|
|
final ViewNodeParcelable viewNodeParcelable = mClient.getViewNodeParcelable(
|
|
mFocusedId);
|
|
if (viewNodeParcelable != null) {
|
|
mFocusedViewNode = viewNodeParcelable.getViewNode();
|
|
}
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "Error getting the ViewNode of the focused view: " + e);
|
|
return null;
|
|
}
|
|
}
|
|
return mFocusedViewNode;
|
|
}
|
|
}
|
|
|
|
void logEvent(@ReportEvent int event) {
|
|
if (sVerbose) Log.v(TAG, "returnAndLogResult(): " + event);
|
|
long duration = -1;
|
|
int type = MetricsEvent.TYPE_UNKNOWN;
|
|
|
|
switch (event) {
|
|
case REPORT_EVENT_NO_RESPONSE: {
|
|
type = MetricsEvent.TYPE_SUCCESS;
|
|
if (mFirstOnSuccessTime == 0) {
|
|
mFirstOnSuccessTime = SystemClock.elapsedRealtime();
|
|
duration = mFirstOnSuccessTime - mFirstRequestTime;
|
|
if (sDebug) {
|
|
Log.d(TAG, "Service responded nothing in " + formatDuration(duration));
|
|
}
|
|
}
|
|
} break;
|
|
|
|
case REPORT_EVENT_INLINE_RESPONSE: {
|
|
// TODO: Define a constant and log this event
|
|
// type = MetricsEvent.TYPE_SUCCESS_INLINE;
|
|
if (mFirstOnSuccessTime == 0) {
|
|
mFirstOnSuccessTime = SystemClock.elapsedRealtime();
|
|
duration = mFirstOnSuccessTime - mFirstRequestTime;
|
|
if (sDebug) {
|
|
Log.d(TAG, "Inline response in " + formatDuration(duration));
|
|
}
|
|
}
|
|
} break;
|
|
|
|
case REPORT_EVENT_UI_SHOWN: {
|
|
type = MetricsEvent.TYPE_OPEN;
|
|
if (mUiFirstShownTime == 0) {
|
|
mUiFirstShownTime = SystemClock.elapsedRealtime();
|
|
duration = mUiFirstShownTime - mFirstRequestTime;
|
|
if (sDebug) Log.d(TAG, "UI shown in " + formatDuration(duration));
|
|
}
|
|
} break;
|
|
|
|
case REPORT_EVENT_UI_DESTROYED: {
|
|
type = MetricsEvent.TYPE_CLOSE;
|
|
if (mUiFirstDestroyedTime == 0) {
|
|
mUiFirstDestroyedTime = SystemClock.elapsedRealtime();
|
|
duration = mUiFirstDestroyedTime - mFirstRequestTime;
|
|
if (sDebug) Log.d(TAG, "UI destroyed in " + formatDuration(duration));
|
|
}
|
|
} break;
|
|
|
|
default:
|
|
Log.w(TAG, "invalid event reported: " + event);
|
|
}
|
|
logResponse(type, mServicePackageName, mComponentName, mSessionId, duration);
|
|
}
|
|
|
|
public void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
|
|
pw.print(prefix); pw.print("sessionId: "); pw.println(mSessionId);
|
|
pw.print(prefix); pw.print("taskId: "); pw.println(mTaskId);
|
|
pw.print(prefix); pw.print("component: ");
|
|
pw.println(mComponentName.flattenToShortString());
|
|
pw.print(prefix); pw.print("focusedId: "); pw.println(mFocusedId);
|
|
if (mFocusedValue != null) {
|
|
pw.print(prefix); pw.print("focusedValue: "); pw.println(mFocusedValue);
|
|
}
|
|
if (mLastShownId != null) {
|
|
pw.print(prefix); pw.print("lastShownId: "); pw.println(mLastShownId);
|
|
}
|
|
pw.print(prefix); pw.print("client: "); pw.println(mClient);
|
|
final String prefix2 = prefix + " ";
|
|
if (mFillWindow != null) {
|
|
pw.print(prefix); pw.println("window:");
|
|
mFillWindow.dump(prefix2, pw);
|
|
}
|
|
if (mSmartSuggestion != null) {
|
|
pw.print(prefix); pw.println("smartSuggestion:");
|
|
mSmartSuggestion.dump(prefix2, pw);
|
|
}
|
|
if (mFirstOnSuccessTime > 0) {
|
|
final long responseTime = mFirstOnSuccessTime - mFirstRequestTime;
|
|
pw.print(prefix); pw.print("response time: ");
|
|
formatDuration(responseTime, pw); pw.println();
|
|
}
|
|
|
|
if (mUiFirstShownTime > 0) {
|
|
final long uiRenderingTime = mUiFirstShownTime - mFirstRequestTime;
|
|
pw.print(prefix); pw.print("UI rendering time: ");
|
|
formatDuration(uiRenderingTime, pw); pw.println();
|
|
}
|
|
|
|
if (mUiFirstDestroyedTime > 0) {
|
|
final long uiTotalTime = mUiFirstDestroyedTime - mFirstRequestTime;
|
|
pw.print(prefix); pw.print("UI life time: ");
|
|
formatDuration(uiTotalTime, pw); pw.println();
|
|
}
|
|
}
|
|
|
|
private void destroy() {
|
|
synchronized (mLock) {
|
|
if (mFillWindow != null) {
|
|
if (sDebug) Log.d(TAG, "destroying window");
|
|
mFillWindow.destroy();
|
|
mFillWindow = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|