/* * Copyright (C) 2017 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 com.android.server.autofill; import static android.view.autofill.AutofillManager.FLAG_MANUAL_REQUEST; import static android.view.autofill.AutofillManager.FLAG_START_SESSION; import static android.view.autofill.AutofillManager.FLAG_VALUE_CHANGED; import static android.view.autofill.AutofillManager.FLAG_VIEW_ENTERED; import static android.view.autofill.AutofillManager.FLAG_VIEW_EXITED; import static com.android.server.autofill.Helper.DEBUG; import static com.android.server.autofill.Helper.VERBOSE; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.assist.AssistStructure; import android.app.assist.AssistStructure.AutofillOverlay; import android.app.assist.AssistStructure.ViewNode; import android.app.assist.AssistStructure.WindowNode; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentSender; import android.graphics.Rect; import android.metrics.LogMaker; import android.os.Bundle; import android.os.IBinder; import android.os.Parcelable; import android.os.RemoteException; import android.service.autofill.AutofillService; import android.service.autofill.Dataset; import android.service.autofill.FillContext; import android.service.autofill.FillRequest; import android.service.autofill.FillResponse; import android.service.autofill.SaveInfo; import android.service.autofill.SaveRequest; import android.util.ArrayMap; import android.util.DebugUtils; import android.util.Slog; import android.util.SparseArray; import android.view.autofill.AutofillId; import android.view.autofill.AutofillManager; import android.view.autofill.AutofillValue; import android.view.autofill.IAutoFillManagerClient; import android.view.autofill.IAutofillWindowPresenter; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.os.HandlerCaller; import com.android.server.autofill.ui.AutoFillUI; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Map; import java.util.Map.Entry; /** * A session for a given activity. * *

This class manages the multiple {@link ViewState}s for each view it has, and keeps track * of the current {@link ViewState} to display the appropriate UI. * *

Although the autofill requests and callbacks are stateless from the service's point of * view, we need to keep state in the framework side for cases such as authentication. For * example, when service return a {@link FillResponse} that contains all the fields needed * to fill the activity but it requires authentication first, that response need to be held * until the user authenticates or it times out. */ // TODO(b/33197203): make sure sessions are removed (and tested by CTS): // - On all authentication scenarios. // - When user does not interact back after a while. // - When service is unbound. final class Session implements RemoteFillService.FillServiceCallbacks, ViewState.Listener, AutoFillUI.AutoFillUiCallback { private static final String TAG = "AutofillSession"; private final AutofillManagerServiceImpl mService; private final HandlerCaller mHandlerCaller; private final Object mLock; private final AutoFillUI mUi; private final MetricsLogger mMetricsLogger = new MetricsLogger(); /** Id of the session */ public final int id; /** uid the session is for */ public final int uid; @GuardedBy("mLock") @NonNull private IBinder mActivityToken; @GuardedBy("mLock") @NonNull private IBinder mWindowToken; /** Package name of the app that is auto-filled */ @NonNull private final String mPackageName; @GuardedBy("mLock") private final ArrayMap mViewStates = new ArrayMap<>(); /** * Id of the View currently being displayed. */ @GuardedBy("mLock") @Nullable private AutofillId mCurrentViewId; @GuardedBy("mLock") private IAutoFillManagerClient mClient; @GuardedBy("mLock") RemoteFillService mRemoteFillService; @GuardedBy("mLock") private SparseArray mResponses; /** * Response that requires a service authentitcation request. */ @GuardedBy("mLock") private FillResponse mResponseWaitingAuth; /** * Dataset that when tapped launched a service authentication request. */ @GuardedBy("mLock") private Dataset mDatasetWaitingAuth; /** * Assist structure sent by the app; it will be updated (sanitized, change values for save) * before sent to {@link AutofillService}. */ @GuardedBy("mLock") private AssistStructure mStructure; /** * Whether the client has an {@link android.view.autofill.AutofillManager.AutofillCallback}. */ private boolean mHasCallback; /** * Extras sent by service on {@code onFillRequest()} calls; the first non-null extra is saved * and used on subsequent {@code onFillRequest()} and {@code onSaveRequest()} calls. */ @GuardedBy("mLock") private Bundle mClientState; /** * Flags used to start the session. */ int mFlags; Session(@NonNull AutofillManagerServiceImpl service, @NonNull AutoFillUI ui, @NonNull Context context, @NonNull HandlerCaller handlerCaller, int userId, @NonNull Object lock, int sessionId, int uid, @NonNull IBinder activityToken, @Nullable IBinder windowToken, @NonNull IBinder client, boolean hasCallback, int flags, @NonNull ComponentName componentName, @NonNull String packageName) { id = sessionId; this.uid = uid; mService = service; mLock = lock; mUi = ui; mHandlerCaller = handlerCaller; mRemoteFillService = new RemoteFillService(context, componentName, userId, this); mActivityToken = activityToken; mWindowToken = windowToken; mHasCallback = hasCallback; mPackageName = packageName; mFlags = flags; mClient = IAutoFillManagerClient.Stub.asInterface(client); mMetricsLogger.action(MetricsEvent.AUTOFILL_SESSION_STARTED, mPackageName); } /** * Gets the currently registered activity token * * @return The activity token */ public IBinder getActivityTokenLocked() { return mActivityToken; } /** * Sets new window for this session. * * @param newWindow The window the Ui should be attached to. Can be {@code null} if no * further UI is needed. */ void switchWindow(@NonNull IBinder newWindow) { synchronized (mLock) { mWindowToken = newWindow; } } /** * Sets new activity and client for this session. * * @param newActivity The token of the new activity * @param newClient The client receiving autofill callbacks */ void switchActivity(@NonNull IBinder newActivity, @NonNull IBinder newClient) { synchronized (mLock) { mActivityToken = newActivity; mClient = IAutoFillManagerClient.Stub.asInterface(newClient); } } // FillServiceCallbacks @Override public void onFillRequestSuccess(@Nullable FillResponse response, @NonNull String servicePackageName, int requestId) { if (response == null) { if ((mFlags & FLAG_MANUAL_REQUEST) != 0) { getUiForShowing().showError(R.string.autofill_error_cannot_autofill); } // Nothing to be done, but need to notify client. notifyUnavailableToClient(); removeSelf(); return; } if ((response.getDatasets() == null || response.getDatasets().isEmpty()) && response.getAuthentication() == null) { // Response is "empty" from an UI point of view, need to notify client. notifyUnavailableToClient(); } synchronized (mLock) { if (response.getAuthentication() != null) { // TODO(b/33197203 , b/35707731): make sure it's ignored if there is one already mResponseWaitingAuth = response; } processResponseLocked(response, requestId); } final LogMaker log = (new LogMaker(MetricsEvent.AUTOFILL_REQUEST)) .setType(MetricsEvent.TYPE_SUCCESS) .setPackageName(mPackageName) .addTaggedData(MetricsEvent.FIELD_AUTOFILL_NUM_DATASETS, response.getDatasets() == null ? 0 : response.getDatasets().size()) .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE, servicePackageName); mMetricsLogger.write(log); } // FillServiceCallbacks @Override public void onFillRequestFailure(@Nullable CharSequence message, @NonNull String servicePackageName) { LogMaker log = (new LogMaker(MetricsEvent.AUTOFILL_REQUEST)) .setType(MetricsEvent.TYPE_FAILURE) .setPackageName(mPackageName) .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE, servicePackageName); mMetricsLogger.write(log); getUiForShowing().showError(message); removeSelf(); } // FillServiceCallbacks @Override public void onSaveRequestSuccess(@NonNull String servicePackageName) { LogMaker log = (new LogMaker( MetricsEvent.AUTOFILL_DATA_SAVE_REQUEST)) .setType(MetricsEvent.TYPE_SUCCESS) .setPackageName(mPackageName) .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE, servicePackageName); mMetricsLogger.write(log); // Nothing left to do... removeSelf(); } // FillServiceCallbacks @Override public void onSaveRequestFailure(@Nullable CharSequence message, @NonNull String servicePackageName) { LogMaker log = (new LogMaker( MetricsEvent.AUTOFILL_DATA_SAVE_REQUEST)) .setType(MetricsEvent.TYPE_FAILURE) .setPackageName(mPackageName) .addTaggedData(MetricsEvent.FIELD_AUTOFILL_SERVICE, servicePackageName); mMetricsLogger.write(log); getUiForShowing().showError(message); removeSelf(); } // FillServiceCallbacks @Override public void authenticate(IntentSender intent, Bundle extras) { final Intent fillInIntent; synchronized (mLock) { fillInIntent = createAuthFillInIntent(mStructure, extras); } mHandlerCaller.getHandler().post(() -> startAuthentication(intent, fillInIntent)); } // FillServiceCallbacks @Override public void onServiceDied(RemoteFillService service) { // TODO(b/33197203): implement } // AutoFillUiCallback @Override public void fill(Dataset dataset) { mHandlerCaller.getHandler().post(() -> autoFill(dataset)); } // AutoFillUiCallback @Override public void save() { mHandlerCaller.getHandler() .obtainMessage(AutofillManagerServiceImpl.MSG_SERVICE_SAVE, id, 0) .sendToTarget(); } // AutoFillUiCallback @Override public void cancelSave() { mHandlerCaller.getHandler().post(() -> removeSelf()); } // AutoFillUiCallback @Override public void requestShowFillUi(AutofillId id, int width, int height, IAutofillWindowPresenter presenter) { synchronized (mLock) { if (id.equals(mCurrentViewId)) { try { final ViewState view = mViewStates.get(id); mClient.requestShowFillUi(mWindowToken, id, width, height, view.getVirtualBounds(), presenter); } catch (RemoteException e) { Slog.e(TAG, "Error requesting to show fill UI", e); } } else { if (DEBUG) { Slog.d(TAG, "Do not show full UI on " + id + " as it is not the current view (" + mCurrentViewId + ") anymore"); } } } } // AutoFillUiCallback @Override public void requestHideFillUi(AutofillId id) { synchronized (mLock) { try { mClient.requestHideFillUi(mWindowToken, id); } catch (RemoteException e) { Slog.e(TAG, "Error requesting to hide fill UI", e); } } } // AutoFillUiCallback @Override public void startIntentSender(IntentSender intentSender) { synchronized (mLock) { removeSelfLocked(); } mHandlerCaller.getHandler().post(() -> { try { synchronized (mLock) { mClient.startIntentSender(intentSender); } } catch (RemoteException e) { Slog.e(TAG, "Error launching auth intent", e); } }); } public void setAuthenticationResultLocked(Bundle data) { if ((mResponseWaitingAuth == null && mDatasetWaitingAuth == null) || data == null) { removeSelf(); } else { final Parcelable result = data.getParcelable( AutofillManager.EXTRA_AUTHENTICATION_RESULT); if (result instanceof FillResponse) { mMetricsLogger.action(MetricsEvent.AUTOFILL_AUTHENTICATED, mPackageName); final int requestIndex = mResponses.indexOfValue(mResponseWaitingAuth); mResponseWaitingAuth = null; if (requestIndex >= 0) { final int requestId = mResponses.keyAt(requestIndex); processResponseLocked((FillResponse) result, requestId); } else { Slog.e(TAG, "Error cannot find id for auth response"); } } else if (result instanceof Dataset) { final Dataset dataset = (Dataset) result; for (int i = 0; i < mResponses.size(); i++) { final FillResponse response = mResponses.valueAt(i); final int index = response.getDatasets().indexOf(mDatasetWaitingAuth); if (index >= 0) { response.getDatasets().set(index, dataset); mDatasetWaitingAuth = null; autoFill(dataset); resetViewStatesLocked(dataset, ViewState.STATE_WAITING_DATASET_AUTH); return; } } } } } public void setHasCallback(boolean hasIt) { mHasCallback = hasIt; } public void setStructureLocked(AssistStructure structure) { mStructure = structure; } /** * Shows the save UI, when session can be saved. * * @return {@code true} if session is done, or {@code false} if it's pending user action. */ public boolean showSaveLocked() { if (mStructure == null) { Slog.wtf(TAG, "showSaveLocked(): no mStructure"); return true; } if (mResponses == null) { // Happens when the activity / session was finished before the service replied, or // when the service cannot autofill it (and returned a null response). if (DEBUG) { Slog.d(TAG, "showSaveLocked(): no responses on session"); } return true; } final int lastResponseIdx = getLastResponseIndex(); if (lastResponseIdx < 0) { Slog.d(TAG, "showSaveLocked(): mResponses=" + mResponses + ", mViewStates=" + mViewStates); return true; } final FillResponse response = mResponses.valueAt(lastResponseIdx); final SaveInfo saveInfo = response.getSaveInfo(); if (DEBUG) { Slog.d(TAG, "showSaveLocked(): mResponses=" + mResponses + ", mViewStates=" + mViewStates); } /* * The Save dialog is only shown if all conditions below are met: * * - saveInfo is not null * - autofillValue of all required ids is not null * - autofillValue of at least one id (required or optional) has changed. */ if (saveInfo == null) { return true; } final AutofillId[] requiredIds = saveInfo.getRequiredIds(); if (requiredIds == null || requiredIds.length == 0) { Slog.w(TAG, "showSaveLocked(): no required ids on saveInfo"); return true; } boolean allRequiredAreNotEmpty = true; boolean atLeastOneChanged = false; for (int i = 0; i < requiredIds.length; i++) { final AutofillId id = requiredIds[i]; final ViewState viewState = mViewStates.get(id); if (viewState == null) { Slog.w(TAG, "showSaveLocked(): no ViewState for required " + id); allRequiredAreNotEmpty = false; break; } final AutofillValue currentValue = viewState.getCurrentValue(); if (currentValue == null || currentValue.isEmpty()) { if (DEBUG) { Slog.d(TAG, "showSaveLocked(): empty value for required " + id ); } allRequiredAreNotEmpty = false; break; } final AutofillValue filledValue = viewState.getAutofilledValue(); if (!currentValue.equals(filledValue)) { if (DEBUG) { Slog.d(TAG, "showSaveLocked(): found a change on required " + id + ": " + filledValue + " => " + currentValue); } atLeastOneChanged = true; } } final AutofillId[] optionalIds = saveInfo.getOptionalIds(); if (allRequiredAreNotEmpty) { if (!atLeastOneChanged && optionalIds != null) { // No change on required ids yet, look for changes on optional ids. for (int i = 0; i < optionalIds.length; i++) { final AutofillId id = optionalIds[i]; final ViewState viewState = mViewStates.get(id); if (viewState == null) { Slog.w(TAG, "showSaveLocked(): no ViewState for optional " + id); continue; } if ((viewState.getState() & ViewState.STATE_CHANGED) != 0) { final AutofillValue currentValue = viewState.getCurrentValue(); final AutofillValue filledValue = viewState.getAutofilledValue(); if (currentValue != null && !currentValue.equals(filledValue)) { if (DEBUG) { Slog.d(TAG, "finishSessionLocked(): found a change on optional " + id + ": " + filledValue + " => " + currentValue); } atLeastOneChanged = true; break; } } } } if (atLeastOneChanged) { getUiForShowing().showSaveUi(mService.getServiceLabel(), saveInfo, mPackageName); return false; } } // Nothing changed... if (DEBUG) { Slog.d(TAG, "showSaveLocked(): with no changes, comes no responsibilities." + "allRequiredAreNotNull=" + allRequiredAreNotEmpty + ", atLeastOneChanged=" + atLeastOneChanged); } return true; } /** * Calls service when user requested save. */ void callSaveLocked() { if (DEBUG) { Slog.d(TAG, "callSaveLocked(): mViewStates=" + mViewStates); } for (Entry entry : mViewStates.entrySet()) { final AutofillValue value = entry.getValue().getCurrentValue(); if (value == null) { if (VERBOSE) { Slog.v(TAG, "callSaveLocked(): skipping " + entry.getKey()); } continue; } final AutofillId id = entry.getKey(); final ViewNode node = findViewNodeByIdLocked(id); if (node == null) { Slog.w(TAG, "callSaveLocked(): did not find node with id " + id); continue; } if (VERBOSE) { Slog.v(TAG, "callSaveLocked(): updating " + id + " to " + value); } node.updateAutofillValue(value); } // Sanitize structure before it's sent to service. mStructure.sanitizeForParceling(false); if (VERBOSE) { Slog.v(TAG, "Dumping " + mStructure + " before calling service.save()"); mStructure.dump(); } // TODO(b/33197203): Implement partitioning properly final int lastResponseIdx = getLastResponseIndex(); final int requestId = mResponses.keyAt(lastResponseIdx); final FillContext fillContext = new FillContext(requestId, mStructure); final ArrayList fillContexts = new ArrayList(1); fillContexts.add(fillContext); final SaveRequest saveRequest = new SaveRequest(fillContexts, mClientState); mRemoteFillService.onSaveRequest(saveRequest); } void updateLocked(AutofillId id, Rect virtualBounds, AutofillValue value, int flags) { ViewState viewState = mViewStates.get(id); if (viewState == null) { if ((flags & (FLAG_START_SESSION | FLAG_VALUE_CHANGED)) != 0) { if (DEBUG) { Slog.d(TAG, "Creating viewState for " + id + " on " + getFlagAsString(flags)); } viewState = new ViewState(this, id, value, this, ViewState.STATE_INITIAL); mViewStates.put(id, viewState); } else if ((flags & FLAG_VIEW_ENTERED) != 0) { viewState = startPartitionLocked(id, value); } else { if (VERBOSE) Slog.v(TAG, "Ignored " + getFlagAsString(flags) + " for " + id); return; } } if ((flags & FLAG_START_SESSION) != 0) { // View is triggering autofill. mCurrentViewId = viewState.id; viewState.update(value, virtualBounds); viewState.setState(ViewState.STATE_STARTED_SESSION); return; } if ((flags & FLAG_VALUE_CHANGED) != 0) { if (value != null && !value.equals(viewState.getCurrentValue())) { // Always update the internal state. viewState.setCurrentValue(value); // Must check if this update was caused by autofilling the view, in which // case we just update the value, but not the UI. final AutofillValue filledValue = viewState.getAutofilledValue(); if (value.equals(filledValue)) { return; } // Update the internal state... viewState.setState(ViewState.STATE_CHANGED); //..and the UI if (value.isText()) { getUiForShowing().filterFillUi(value.getTextValue().toString()); } else { getUiForShowing().filterFillUi(null); } } return; } if ((flags & FLAG_VIEW_ENTERED) != 0) { // Remove the UI if the ViewState has changed. if (mCurrentViewId != viewState.id) { mUi.hideFillUi(mCurrentViewId != null ? mCurrentViewId : null); mCurrentViewId = viewState.id; } // If the ViewState is ready to be displayed, onReady() will be called. viewState.update(value, virtualBounds); return; } if ((flags & FLAG_VIEW_EXITED) != 0) { if (mCurrentViewId == viewState.id) { mUi.hideFillUi(viewState.id); mCurrentViewId = null; } return; } Slog.w(TAG, "updateLocked(): unknown flags " + flags + ": " + getFlagAsString(flags)); } private ViewState startPartitionLocked(AutofillId id, AutofillValue value) { // TODO(b/33197203 , b/35707731): temporary workaround until partitioning supports auth if (mResponseWaitingAuth != null) { final ViewState viewState = new ViewState(this, id, value, this, ViewState.STATE_WAITING_RESPONSE_AUTH); mViewStates.put(id, viewState); return viewState; } if (DEBUG) { Slog.d(TAG, "Starting partition for view id " + id); } final ViewState newViewState = new ViewState(this, id, value, this,ViewState.STATE_STARTED_PARTITION); mViewStates.put(id, newViewState); // Must update value of nodes so: // - proper node is focused // - autofillValue is sent back to service when it was previously autofilled for (int i = 0; i < mViewStates.size(); i++) { final ViewState viewState = mViewStates.valueAt(i); final ViewNode node = findViewNodeByIdLocked(viewState.id); if (node == null) { Slog.w(TAG, "startPartitionLocked(): no node for " + viewState.id); continue; } final AutofillValue initialValue = viewState.getInitialValue(); final AutofillValue filledValue = viewState.getAutofilledValue(); final AutofillOverlay overlay = new AutofillOverlay(); if (filledValue != null && !filledValue.equals(initialValue)) { overlay.value = filledValue; } overlay.focused = id.equals(viewState.id); node.setAutofillOverlay(overlay); } FillRequest request = new FillRequest(mStructure, mClientState, 0); mRemoteFillService.onFillRequest(request); return newViewState; } @Override public void onFillReady(FillResponse response, AutofillId filledId, @Nullable AutofillValue value) { String filterText = null; if (value != null && value.isText()) { filterText = value.getTextValue().toString(); } getUiForShowing().showFillUi(filledId, response, filterText, mPackageName); } String getFlagAsString(int flag) { return DebugUtils.flagsToString(AutofillManager.class, "FLAG_", flag); } private void notifyUnavailableToClient() { synchronized (mLock) { if (mCurrentViewId == null) { // TODO(b/33197203): temporary sanity check; should never happen Slog.w(TAG, "notifyUnavailable(): mCurrentViewId is null"); return; } if (!mHasCallback) return; try { mClient.notifyNoFillUi(mWindowToken, mCurrentViewId); } catch (RemoteException e) { Slog.e(TAG, "Error notifying client no fill UI: windowToken=" + mWindowToken + " id=" + mCurrentViewId, e); } } } private void processResponseLocked(FillResponse response, int requestId) { if (DEBUG) { Slog.d(TAG, "processResponseLocked(mCurrentViewId=" + mCurrentViewId + "):" + response); } if (mResponses == null) { mResponses = new SparseArray<>(4); } mResponses.put(requestId, response); if (response != null) { mClientState = response.getClientState(); } setViewStatesLocked(response, ViewState.STATE_FILLABLE); if (mCurrentViewId == null) { return; } if ((mFlags & FLAG_MANUAL_REQUEST) != 0 && response.getDatasets() != null && response.getDatasets().size() == 1) { Slog.d(TAG, "autofilling manual request directly"); autoFill(response.getDatasets().get(0)); return; } // Updates the UI, if necessary. final ViewState currentView = mViewStates.get(mCurrentViewId); currentView.maybeCallOnFillReady(); } /** * Sets the state of all views in the given response. */ private void setViewStatesLocked(FillResponse response, int state) { final ArrayList datasets = response.getDatasets(); if (datasets != null) { for (int i = 0; i < datasets.size(); i++) { final Dataset dataset = datasets.get(i); if (dataset == null) { Slog.w(TAG, "Ignoring null dataset on " + datasets); continue; } setViewStatesLocked(response, dataset, state); } } } /** * Sets the state of all views in the given dataset and response. */ private void setViewStatesLocked(@Nullable FillResponse response, @NonNull Dataset dataset, int state) { final ArrayList ids = dataset.getFieldIds(); final ArrayList values = dataset.getFieldValues(); for (int j = 0; j < ids.size(); j++) { final AutofillId id = ids.get(j); ViewState viewState = mViewStates.get(id); if (viewState != null) { viewState.setState(state); } else { viewState = new ViewState(this, id, null, this, state); if (DEBUG) { // TODO(b/33197203): change to VERBOSE once stable Slog.d(TAG, "Adding autofillable view with id " + id + " and state " + state); } mViewStates.put(id, viewState); } if ((state & ViewState.STATE_AUTOFILLED) != 0) { viewState.setAutofilledValue(values.get(j)); } if (response != null) { viewState.setResponse(response); } } } /** * Resets the given state from all existing views in the given dataset. */ private void resetViewStatesLocked(@NonNull Dataset dataset, int state) { final ArrayList ids = dataset.getFieldIds(); for (int j = 0; j < ids.size(); j++) { final AutofillId id = ids.get(j); final ViewState viewState = mViewStates.get(id); if (viewState != null) { viewState.resetState(state); } } } void autoFill(Dataset dataset) { synchronized (mLock) { // Autofill it directly... if (dataset.getAuthentication() == null) { autoFillApp(dataset); return; } // ...or handle authentication. // TODO(b/33197203 , b/35707731): make sure it's ignored if there is one already mDatasetWaitingAuth = dataset; setViewStatesLocked(null, dataset, ViewState.STATE_WAITING_DATASET_AUTH); final Intent fillInIntent = createAuthFillInIntent(mStructure, null); startAuthentication(dataset.getAuthentication(), fillInIntent); } } CharSequence getServiceName() { return mService.getServiceName(); } FillResponse getResponseWaitingAuth() { return mResponseWaitingAuth; } private Intent createAuthFillInIntent(AssistStructure structure, Bundle extras) { final Intent fillInIntent = new Intent(); fillInIntent.putExtra(AutofillManager.EXTRA_ASSIST_STRUCTURE, structure); if (extras != null) { fillInIntent.putExtra(AutofillManager.EXTRA_DATA_EXTRAS, extras); } return fillInIntent; } private void startAuthentication(IntentSender intent, Intent fillInIntent) { try { synchronized (mLock) { mClient.authenticate(intent, fillInIntent); } } catch (RemoteException e) { Slog.e(TAG, "Error launching auth intent", e); } } void dumpLocked(String prefix, PrintWriter pw) { pw.print(prefix); pw.print("id: "); pw.println(id); pw.print(prefix); pw.print("uid: "); pw.println(uid); pw.print(prefix); pw.print("mActivityToken: "); pw.println(mActivityToken); pw.print(prefix); pw.print("mFlags: "); pw.println(mFlags); pw.print(prefix); pw.print("mResponses: "); pw.println(mResponses); pw.print(prefix); pw.print("mResponseWaitingAuth: "); pw.println(mResponseWaitingAuth); pw.print(prefix); pw.print("mDatasetWaitingAuth: "); pw.println(mDatasetWaitingAuth); pw.print(prefix); pw.print("mCurrentViewId: "); pw.println(mCurrentViewId); pw.print(prefix); pw.print("mViewStates size: "); pw.println(mViewStates.size()); final String prefix2 = prefix + " "; for (Map.Entry entry : mViewStates.entrySet()) { pw.print(prefix); pw.print("State for id "); pw.println(entry.getKey()); entry.getValue().dump(prefix2, pw); } if (VERBOSE) { pw.print(prefix); pw.print("mStructure: " ); // TODO(b/33197203): add method do dump AssistStructure on pw if (mStructure != null) { pw.println("look at logcat" ); mStructure.dump(); // dumps to logcat } else { pw.println("null"); } } pw.print(prefix); pw.print("mHasCallback: "); pw.println(mHasCallback); pw.print(prefix); pw.print("mClientState: "); pw.println( Helper.bundleToString(mClientState)); mRemoteFillService.dump(prefix, pw); } void autoFillApp(Dataset dataset) { synchronized (mLock) { try { if (DEBUG) { Slog.d(TAG, "autoFillApp(): the buck is on the app: " + dataset); } mClient.autofill(mWindowToken, dataset.getFieldIds(), dataset.getFieldValues()); setViewStatesLocked(null, dataset, ViewState.STATE_AUTOFILLED); } catch (RemoteException e) { Slog.w(TAG, "Error autofilling activity: " + e); } } } private AutoFillUI getUiForShowing() { synchronized (mLock) { mUi.setCallback(this); return mUi; } } private ViewNode findViewNodeByIdLocked(AutofillId id) { final int size = mStructure.getWindowNodeCount(); for (int i = 0; i < size; i++) { final WindowNode window = mStructure.getWindowNodeAt(i); final ViewNode root = window.getRootViewNode(); if (id.equals(root.getAutofillId())) { return root; } final ViewNode child = findViewNodeByIdLocked(root, id); if (child != null) { return child; } } return null; } private ViewNode findViewNodeByIdLocked(ViewNode parent, AutofillId id) { final int childrenSize = parent.getChildCount(); if (childrenSize > 0) { for (int i = 0; i < childrenSize; i++) { final ViewNode child = parent.getChildAt(i); if (id.equals(child.getAutofillId())) { return child; } final ViewNode grandChild = findViewNodeByIdLocked(child, id); if (grandChild != null && id.equals(grandChild.getAutofillId())) { return grandChild; } } } return null; } void destroyLocked() { mRemoteFillService.destroy(); mUi.setCallback(null); mMetricsLogger.action(MetricsEvent.AUTOFILL_SESSION_FINISHED, mPackageName); } void removeSelf() { synchronized (mLock) { removeSelfLocked(); } } void removeSelfLocked() { if (VERBOSE) { Slog.v(TAG, "removeSelfLocked()"); } destroyLocked(); mService.removeSessionLocked(id); } private int getLastResponseIndex() { // The response ids are monotonically increasing so // we just find the largest id which is the last. We // do not rely on the internal ordering in sparse // array to avoid - wow this stopped working!? int lastResponseIdx = -1; int lastResponseId = -1; final int responseCount = mResponses.size(); for (int i = 0; i < responseCount; i++) { if (mResponses.keyAt(i) > lastResponseId) { lastResponseIdx = i; } } return lastResponseIdx; } }