diff --git a/core/java/android/service/autofill/Dataset.java b/core/java/android/service/autofill/Dataset.java index 0fa5e3e3b3237..d943bf9ac872b 100644 --- a/core/java/android/service/autofill/Dataset.java +++ b/core/java/android/service/autofill/Dataset.java @@ -18,6 +18,7 @@ package android.service.autofill; import static android.view.autofill.Helper.sDebug; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; @@ -33,6 +34,8 @@ import android.widget.RemoteViews; import com.android.internal.util.Preconditions; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Objects; import java.util.regex.Pattern; @@ -112,6 +115,55 @@ import java.util.regex.Pattern; * */ public final class Dataset implements Parcelable { + /** + * This dataset is picked because of unknown reason. + * @hide + */ + public static final int PICK_REASON_UNKNOWN = 0; + /** + * This dataset is picked because of autofill provider detection was chosen. + * @hide + */ + public static final int PICK_REASON_AUTOFILL_PROVIDER_DETECTION = 1; + /** + * This dataset is picked because of PCC detection was chosen. + * @hide + */ + public static final int PICK_REASON_PCC_DETECTION = 2; + /** + * This dataset is picked because of Framework detection was chosen. + * @hide + */ + public static final int PICK_REASON_FRAMEWORK_DETECTION = 3; + /** + * This dataset is picked because of Autofill Provider being a fallback. + * @hide + */ + public static final int PICK_REASON_AUTOFILL_PROVIDER_FALLBACK = 4; + /** + * This dataset is picked because of PCC detection being a fallback. + * @hide + */ + public static final int PICK_REASON_PCC_DETECTION_FALLBACK = 5; + /** + * This dataset is picked because of Framework detection being a fallback. + * @hide + */ + public static final int PICK_REASON_FRAMEWORK_FALLBACK = 6; + + @IntDef(prefix = { "PICK_REASON_" }, value = { + PICK_REASON_UNKNOWN, + PICK_REASON_AUTOFILL_PROVIDER_DETECTION, + PICK_REASON_PCC_DETECTION, + PICK_REASON_FRAMEWORK_DETECTION, + PICK_REASON_AUTOFILL_PROVIDER_FALLBACK, + PICK_REASON_PCC_DETECTION_FALLBACK, + PICK_REASON_FRAMEWORK_FALLBACK, + }) + @Retention(RetentionPolicy.SOURCE) + @interface DatasetEligibleReason{} + + private @DatasetEligibleReason int mEligibleReason; private final ArrayList mFieldIds; private final ArrayList mFieldValues; @@ -130,6 +182,67 @@ public final class Dataset implements Parcelable { private final IntentSender mAuthentication; @Nullable String mId; + /** + * Constructor to copy the dataset, but replaces the AutofillId with the given input. + * Useful to modify the field type, and provide autofillId. + * @hide + */ + public Dataset( + ArrayList fieldIds, + ArrayList fieldValues, + ArrayList fieldPresentations, + ArrayList fieldDialogPresentations, + ArrayList fieldInlinePresentations, + ArrayList fieldInlineTooltipPresentations, + ArrayList fieldFilters, + ArrayList autofillDatatypes, + ClipData fieldContent, + RemoteViews presentation, + RemoteViews dialogPresentation, + @Nullable InlinePresentation inlinePresentation, + @Nullable InlinePresentation inlineTooltipPresentation, + @Nullable String id, + IntentSender authentication) { + mFieldIds = fieldIds; + mFieldValues = fieldValues; + mFieldPresentations = fieldPresentations; + mFieldDialogPresentations = fieldDialogPresentations; + mFieldInlinePresentations = fieldInlinePresentations; + mFieldInlineTooltipPresentations = fieldInlineTooltipPresentations; + mAutofillDatatypes = autofillDatatypes; + mFieldFilters = fieldFilters; + mFieldContent = fieldContent; + mPresentation = presentation; + mDialogPresentation = dialogPresentation; + mInlinePresentation = inlinePresentation; + mInlineTooltipPresentation = inlineTooltipPresentation; + mAuthentication = authentication; + mId = id; + } + + /** + * Constructor to copy the dataset, but replaces the AutofillId with the given input. + * Useful to modify the field type, and provide autofillId. + * @hide + */ + public Dataset(Dataset dataset, ArrayList ids) { + mFieldIds = ids; + mFieldValues = dataset.mFieldValues; + mFieldPresentations = dataset.mFieldPresentations; + mFieldDialogPresentations = dataset.mFieldDialogPresentations; + mFieldInlinePresentations = dataset.mFieldInlinePresentations; + mFieldInlineTooltipPresentations = dataset.mFieldInlineTooltipPresentations; + mFieldFilters = dataset.mFieldFilters; + mFieldContent = dataset.mFieldContent; + mPresentation = dataset.mPresentation; + mDialogPresentation = dataset.mDialogPresentation; + mInlinePresentation = dataset.mInlinePresentation; + mInlineTooltipPresentation = dataset.mInlineTooltipPresentation; + mAuthentication = dataset.mAuthentication; + mId = dataset.mId; + mAutofillDatatypes = dataset.mAutofillDatatypes; + } + private Dataset(Builder builder) { mFieldIds = builder.mFieldIds; mFieldValues = builder.mFieldValues; @@ -291,6 +404,22 @@ public final class Dataset implements Parcelable { return mId; } + /** + * Sets the reason as to why this dataset is eligible + * @hide + */ + public void setEligibleReasonReason(@DatasetEligibleReason int eligibleReason) { + this.mEligibleReason = eligibleReason; + } + + /** + * Get the reason as to why this dataset is eligible. + * @hide + */ + public @DatasetEligibleReason int getEligibleReason() { + return mEligibleReason; + } + /** * A builder for {@link Dataset} objects. You must provide at least * one value for a field or set an authentication intent. @@ -1147,6 +1276,7 @@ public final class Dataset implements Parcelable { parcel.writeParcelable(mFieldContent, flags); parcel.writeParcelable(mAuthentication, flags); parcel.writeString(mId); + parcel.writeInt(mEligibleReason); } public static final @NonNull Creator CREATOR = new Creator() { @@ -1181,6 +1311,7 @@ public final class Dataset implements Parcelable { final IntentSender authentication = parcel.readParcelable(null, android.content.IntentSender.class); final String datasetId = parcel.readString(); + final int eligibleReason = parcel.readInt(); // Always go through the builder to ensure the data ingested by // the system obeys the contract of the builder to avoid attacks @@ -1243,7 +1374,9 @@ public final class Dataset implements Parcelable { } builder.setAuthentication(authentication); builder.setId(datasetId); - return builder.build(); + Dataset dataset = builder.build(); + dataset.mEligibleReason = eligibleReason; + return dataset; } @Override diff --git a/core/java/android/service/autofill/FillResponse.java b/core/java/android/service/autofill/FillResponse.java index fa7ace3bbe0d7..c962bf1954bf4 100644 --- a/core/java/android/service/autofill/FillResponse.java +++ b/core/java/android/service/autofill/FillResponse.java @@ -117,6 +117,80 @@ public final class FillResponse implements Parcelable { private final boolean mShowSaveDialogIcon; private final @Nullable FieldClassification[] mDetectedFieldTypes; + /** + * Creates a shollow copy of the provided FillResponse. + * + * @hide + */ + public static FillResponse shallowCopy(FillResponse r, List datasets) { + return new FillResponse( + (datasets != null) ? new ParceledListSlice<>(datasets) : null, + r.mSaveInfo, + r.mClientState, + r.mPresentation, + r.mInlinePresentation, + r.mInlineTooltipPresentation, + r.mDialogPresentation, + r.mDialogHeader, + r.mHeader, + r.mFooter, + r.mAuthentication, + r.mAuthenticationIds, + r.mIgnoredIds, + r.mFillDialogTriggerIds, + r.mDisableDuration, + r.mFieldClassificationIds, + r.mFlags, + r.mRequestId, + r.mUserData, + r.mCancelIds, + r.mSupportsInlineSuggestions, + r.mIconResourceId, + r.mServiceDisplayNameResourceId, + r.mShowFillDialogIcon, + r.mShowSaveDialogIcon, + r.mDetectedFieldTypes); + } + + private FillResponse(ParceledListSlice datasets, SaveInfo saveInfo, Bundle clientState, + RemoteViews presentation, InlinePresentation inlinePresentation, + InlinePresentation inlineTooltipPresentation, RemoteViews dialogPresentation, + RemoteViews dialogHeader, RemoteViews header, RemoteViews footer, + IntentSender authentication, AutofillId[] authenticationIds, AutofillId[] ignoredIds, + AutofillId[] fillDialogTriggerIds, long disableDuration, + AutofillId[] fieldClassificationIds, int flags, int requestId, UserData userData, + int[] cancelIds, boolean supportsInlineSuggestions, int iconResourceId, + int serviceDisplayNameResourceId, boolean showFillDialogIcon, + boolean showSaveDialogIcon, + FieldClassification[] detectedFieldTypes) { + mDatasets = datasets; + mSaveInfo = saveInfo; + mClientState = clientState; + mPresentation = presentation; + mInlinePresentation = inlinePresentation; + mInlineTooltipPresentation = inlineTooltipPresentation; + mDialogPresentation = dialogPresentation; + mDialogHeader = dialogHeader; + mHeader = header; + mFooter = footer; + mAuthentication = authentication; + mAuthenticationIds = authenticationIds; + mIgnoredIds = ignoredIds; + mFillDialogTriggerIds = fillDialogTriggerIds; + mDisableDuration = disableDuration; + mFieldClassificationIds = fieldClassificationIds; + mFlags = flags; + mRequestId = requestId; + mUserData = userData; + mCancelIds = cancelIds; + mSupportsInlineSuggestions = supportsInlineSuggestions; + mIconResourceId = iconResourceId; + mServiceDisplayNameResourceId = serviceDisplayNameResourceId; + mShowFillDialogIcon = showFillDialogIcon; + mShowSaveDialogIcon = showSaveDialogIcon; + mDetectedFieldTypes = detectedFieldTypes; + } + private FillResponse(@NonNull Builder builder) { mDatasets = (builder.mDatasets != null) ? new ParceledListSlice<>(builder.mDatasets) : null; mSaveInfo = builder.mSaveInfo; @@ -673,6 +747,15 @@ public final class FillResponse implements Parcelable { return this; } + /** + * @hide + */ + @NonNull + public Builder setDatasets(ArrayList dataset) { + mDatasets = dataset; + return this; + } + /** * Sets the {@link SaveInfo} associated with this response. * diff --git a/core/java/android/view/autofill/AutofillFeatureFlags.java b/core/java/android/view/autofill/AutofillFeatureFlags.java index 864ba92ca887c..6d78e60e93581 100644 --- a/core/java/android/view/autofill/AutofillFeatureFlags.java +++ b/core/java/android/view/autofill/AutofillFeatureFlags.java @@ -81,12 +81,6 @@ public class AutofillFeatureFlags { public static final String DEVICE_CONFIG_AUTOFILL_DIALOG_ENABLED = "autofill_dialog_enabled"; - /** - * Indicates that PCC Autofill detection feature is enabled or not. - */ - public static final String DEVICE_CONFIG_AUTOFILL_PCC_FEATURE_PROVIDER_HINTS = - "pcc_classification_hints"; - /** * Sets the autofill hints allowed list for the fields that can trigger the fill dialog * feature at Activity starting. @@ -190,6 +184,12 @@ public class AutofillFeatureFlags { */ public static final String DEVICE_CONFIG_PREFER_PROVIDER_OVER_PCC = "prefer_provider_over_pcc"; + /** + * Indicates the Autofill Hints that would be requested by the service from the Autofill + * Provider. + */ + public static final String DEVICE_CONFIG_AUTOFILL_PCC_FEATURE_PROVIDER_HINTS = + "pcc_classification_hints"; /** * Use data from secondary source if primary not present . @@ -212,11 +212,9 @@ public class AutofillFeatureFlags { "autofill_inline_tooltip_first_show_delay"; private static final String DIALOG_HINTS_DELIMITER = ":"; - private static final String PCC_HINTS_DELIMITER = ","; private static final boolean DEFAULT_HAS_FILL_DIALOG_UI_FEATURE = false; private static final String DEFAULT_FILL_DIALOG_ENABLED_HINTS = ""; - private static final String DEFAULT_PCC_FEATURE_PROVIDER_HINTS = ""; // CREDENTIAL MANAGER DEFAULTS @@ -249,25 +247,6 @@ public class AutofillFeatureFlags { DEFAULT_HAS_FILL_DIALOG_UI_FEATURE); } - /** - * The list of datatypes that is supported by framework - * detection. - * - * @hide - */ - public static String[] getTypeHintsForProvider() { - final String typeHints = DeviceConfig.getString( - DeviceConfig.NAMESPACE_AUTOFILL, - DEVICE_CONFIG_AUTOFILL_PCC_FEATURE_PROVIDER_HINTS, - DEFAULT_PCC_FEATURE_PROVIDER_HINTS); - if (TextUtils.isEmpty(typeHints)) { - return new String[0]; - } - - return ArrayUtils.filter(typeHints.split(PCC_HINTS_DELIMITER), String[]::new, - (str) -> !TextUtils.isEmpty(str)); - } - /** * Gets fill dialog enabled hints. * diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java index b991a026faa1e..5a7fbc57ba115 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java @@ -150,6 +150,12 @@ public final class AutofillManagerService @NonNull final FrameworkResourcesServiceNameResolver mAugmentedAutofillResolver; + /** + * Object used to set the name of the field classification service. + */ + @NonNull + final FrameworkResourcesServiceNameResolver mFieldClassificationResolver; + private final AutoFillUI mUi; private final LocalLog mRequestsHistory = new LocalLog(20); @@ -245,6 +251,15 @@ public final class AutofillManagerService mAugmentedAutofillResolver.setOnTemporaryServiceNameChangedCallback( (u, s, t) -> onAugmentedServiceNameChanged(u, s, t)); + mFieldClassificationResolver = new FrameworkResourcesServiceNameResolver(getContext(), + com.android.internal.R.string.config_defaultFieldClassificationService); + if (sVerbose) { + Slog.v(TAG, "Resolving FieldClassificationService to serviceName: " + + mFieldClassificationResolver.readServiceName(0)); + } + mFieldClassificationResolver.setOnTemporaryServiceNameChangedCallback( + (u, s, t) -> onFieldClassificationServiceNameChanged(u, s, t)); + if (mSupportedSmartSuggestionModes != AutofillManager.FLAG_SMART_SUGGESTION_OFF) { final List users = getSupportedUsers(); for (int i = 0; i < users.size(); i++) { @@ -358,6 +373,20 @@ public final class AutofillManagerService } } + private void onFieldClassificationServiceNameChanged( + @UserIdInt int userId, @Nullable String serviceName, boolean isTemporary) { + synchronized (mLock) { + final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId); + if (service == null) { + // If we cannot get the service from the services cache, it will call + // updateRemoteAugmentedAutofillService() finally. Skip call this update again. + getServiceForUserLocked(userId); + } else { + service.updateRemoteFieldClassificationService(); + } + } + } + @Override // from AbstractMasterSystemService protected AutofillManagerServiceImpl newServiceLocked(@UserIdInt int resolvedUserId, boolean disabled) { diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java index cc29109d85c50..76e6974e14a6b 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java @@ -161,6 +161,17 @@ final class AutofillManagerServiceImpl /** When was {@link PruneTask} last executed? */ private long mLastPrune = 0; + /** + * Reference to the {@link RemoteFieldClassificationService}, is set on demand. + */ + @GuardedBy("mLock") + @Nullable + private RemoteFieldClassificationService mRemoteFieldClassificationService; + + @GuardedBy("mLock") + @Nullable + private ServiceInfo mRemoteFieldClassificationServiceInfo; + /** * Reference to the {@link RemoteAugmentedAutofillService}, is set on demand. */ @@ -1051,10 +1062,11 @@ final class AutofillManagerServiceImpl } pw.print(prefix); pw.print("Default component: "); pw.println(getContext() .getString(R.string.config_defaultAutofillService)); + pw.println(); - pw.print(prefix); pw.println("mAugmentedAutofillNamer: "); - pw.print(prefix2); mMaster.mAugmentedAutofillResolver.dumpShort(pw, mUserId); pw.println(); - + pw.print(prefix); pw.println("mAugmentedAutofillName: "); + pw.print(prefix2); mMaster.mAugmentedAutofillResolver.dumpShort(pw, mUserId); + pw.println(); if (mRemoteAugmentedAutofillService != null) { pw.print(prefix); pw.println("RemoteAugmentedAutofillService: "); mRemoteAugmentedAutofillService.dump(prefix2, pw); @@ -1063,6 +1075,27 @@ final class AutofillManagerServiceImpl pw.print(prefix); pw.print("RemoteAugmentedAutofillServiceInfo: "); pw.println(mRemoteAugmentedAutofillServiceInfo); } + pw.println(); + + pw.print(prefix); pw.println("mFieldClassificationService for system detection"); + pw.print(prefix2); pw.print("Default component: "); pw.println(getContext() + .getString(R.string.config_defaultFieldClassificationService)); + pw.print(prefix2); mMaster.mFieldClassificationResolver.dumpShort(pw, mUserId); + pw.println(); + + if (mRemoteFieldClassificationService != null) { + pw.print(prefix); pw.println("RemoteFieldClassificationService: "); + mRemoteFieldClassificationService.dump(prefix2, pw); + } else { + pw.print(prefix); pw.println("mRemoteFieldClassificationService: null"); + } + if (mRemoteFieldClassificationServiceInfo != null) { + pw.print(prefix); pw.print("RemoteFieldClassificationServiceInfo: "); + pw.println(mRemoteFieldClassificationServiceInfo); + } else { + pw.print(prefix); pw.println("mRemoteFieldClassificationServiceInfo: null"); + } + pw.println(); pw.print(prefix); pw.print("Field classification enabled: "); pw.println(isFieldClassificationEnabledLocked()); @@ -1629,6 +1662,95 @@ final class AutofillManagerServiceImpl } } + @GuardedBy("mLock") + @Nullable RemoteFieldClassificationService getRemoteFieldClassificationServiceLocked() { + if (mRemoteFieldClassificationService == null) { + final String serviceName = mMaster.mFieldClassificationResolver.getServiceName(mUserId); + if (serviceName == null) { + if (mMaster.verbose) { + Slog.v(TAG, "getRemoteFieldClassificationServiceLocked(): not set"); + } + return null; + } + if (sVerbose) { + Slog.v(TAG, "getRemoteFieldClassificationServiceLocked serviceName: " + + serviceName); + } + boolean sTemporaryFieldDetectionService = + mMaster.mFieldClassificationResolver.isTemporary(mUserId); + final Pair pair = RemoteFieldClassificationService + .getComponentName(serviceName, mUserId, sTemporaryFieldDetectionService); + if (pair == null) { + Slog.w(TAG, "RemoteFieldClassificationService.getComponentName returned null " + + "with serviceName: " + serviceName); + return null; + } + + mRemoteFieldClassificationServiceInfo = pair.first; + final ComponentName componentName = pair.second; + if (sVerbose) { + Slog.v(TAG, "getRemoteFieldClassificationServiceLocked(): " + componentName); + } + final int serviceUid = mRemoteFieldClassificationServiceInfo.applicationInfo.uid; + mRemoteFieldClassificationService = new RemoteFieldClassificationService(getContext(), + componentName, serviceUid, mUserId); + } + + return mRemoteFieldClassificationService; + } + + @GuardedBy("mLock") + @Nullable RemoteFieldClassificationService + getRemoteFieldClassificationServiceIfCreatedLocked() { + return mRemoteFieldClassificationService; + } + + /** + * Called when the {@link AutofillManagerService#mAugmentedAutofillResolver} + * changed (among other places). + */ + void updateRemoteFieldClassificationService() { + synchronized (mLock) { + if (mRemoteFieldClassificationService != null) { + if (sVerbose) { + Slog.v(TAG, "updateRemoteFieldClassificationService(): " + + "destroying old remote service"); + } + mRemoteFieldClassificationService.unbind(); + + mRemoteFieldClassificationService = null; + mRemoteFieldClassificationServiceInfo = null; + } + + final boolean available = isFieldClassificationServiceAvailableLocked(); + if (sVerbose) Slog.v(TAG, "updateRemoteFieldClassificationService(): " + available); + + if (available) { + mRemoteFieldClassificationService = getRemoteFieldClassificationServiceLocked(); + } + } + } + + private boolean isFieldClassificationServiceAvailableLocked() { + if (mMaster.verbose) { + Slog.v(TAG, "isAugmentedAutofillService(): " + + "setupCompleted=" + isSetupCompletedLocked() + + ", disabled=" + isDisabledByUserRestrictionsLocked() + + ", augmentedService=" + + mMaster.mAugmentedAutofillResolver.getServiceName(mUserId)); + } + if (!isSetupCompletedLocked() || isDisabledByUserRestrictionsLocked() + || mMaster.mAugmentedAutofillResolver.getServiceName(mUserId) == null) { + return false; + } + return true; + } + + boolean isRemoteClassificationServiceForUserLocked(int callingUid) { + return mRemoteFieldClassificationServiceInfo != null + && mRemoteFieldClassificationServiceInfo.applicationInfo.uid == callingUid; + } + @Override public String toString() { return "AutofillManagerServiceImpl: [userId=" + mUserId diff --git a/services/autofill/java/com/android/server/autofill/RemoteFieldClassificationService.java b/services/autofill/java/com/android/server/autofill/RemoteFieldClassificationService.java new file mode 100644 index 0000000000000..99a2291016e9f --- /dev/null +++ b/services/autofill/java/com/android/server/autofill/RemoteFieldClassificationService.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2020 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 com.android.server.autofill.Helper.sDebug; +import static com.android.server.autofill.Helper.sVerbose; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.app.AppGlobals; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.os.ICancellationSignal; +import android.os.RemoteException; +import android.service.assist.classification.FieldClassificationRequest; +import android.service.assist.classification.FieldClassificationResponse; +import android.service.assist.classification.FieldClassificationService; +import android.service.assist.classification.IFieldClassificationCallback; +import android.service.assist.classification.IFieldClassificationService; +import android.util.Log; +import android.util.Pair; +import android.util.Slog; + +import com.android.internal.infra.AbstractRemoteService; +import com.android.internal.infra.ServiceConnector; + +/** + * Class responsible for connection with the Remote {@link FieldClassificationService}. + * This class is instantiated when {@link AutofillManagerServiceImpl} is established. + * The connection is supposed to be bounded forever, as such, this class persists beyond + * Autofill {@link Session}'s lifecycle. As such, it can't contain information relevant to Session. + * This design is completely different from {@link RemoteFillService}. + */ +final class RemoteFieldClassificationService + extends ServiceConnector.Impl { + + private static final String TAG = + "Autofill" + RemoteFieldClassificationService.class.getSimpleName(); + + // Bind forever. + private static final long TIMEOUT_IDLE_UNBIND_MS = + AbstractRemoteService.PERMANENT_BOUND_TIMEOUT_MS; + private final ComponentName mComponentName; + + public interface FieldClassificationServiceCallbacks { + void onClassificationRequestSuccess(@NonNull FieldClassificationResponse response); + void onClassificationRequestFailure(int requestId, @Nullable CharSequence message); + void onClassificationRequestTimeout(int requestId); + void onServiceDied(@NonNull RemoteFieldClassificationService service); + } + + RemoteFieldClassificationService(Context context, ComponentName serviceName, + int serviceUid, int userId) { + super(context, + // TODO(b/266379948): Update service + new Intent(FieldClassificationService.SERVICE_INTERFACE).setComponent(serviceName), + /* bindingFlags= */ 0, userId, IFieldClassificationService.Stub::asInterface); + mComponentName = serviceName; + if (sDebug) { + Slog.d(TAG, "About to connect to serviceName: " + serviceName); + } + // Bind right away. + connect(); + } + + @Nullable + static Pair getComponentName(@NonNull String serviceName, + @UserIdInt int userId, boolean isTemporary) { + int flags = PackageManager.GET_META_DATA; + if (!isTemporary) { + flags |= PackageManager.MATCH_SYSTEM_ONLY; + } + + final ComponentName serviceComponent; + ServiceInfo serviceInfo = null; + try { + serviceComponent = ComponentName.unflattenFromString(serviceName); + serviceInfo = AppGlobals.getPackageManager().getServiceInfo(serviceComponent, flags, + userId); + if (serviceInfo == null) { + Slog.e(TAG, "Bad service name for flags " + flags + ": " + serviceName); + return null; + } + } catch (Exception e) { + Slog.e(TAG, "Error getting service info for '" + serviceName + "': " + e); + return null; + } + return new Pair<>(serviceInfo, serviceComponent); + } + + public ComponentName getComponentName() { + return mComponentName; + } + + @Override // from ServiceConnector.Impl + protected void onServiceConnectionStatusChanged(IFieldClassificationService service, + boolean connected) { + try { + if (connected) { + service.onConnected(false, false); + } else { + service.onDisconnected(); + } + } catch (Exception e) { + Slog.w(TAG, + "Exception calling onServiceConnectionStatusChanged(" + connected + "): ", e); + } + } + + @Override // from AbstractRemoteService + protected long getAutoDisconnectTimeoutMs() { + return TIMEOUT_IDLE_UNBIND_MS; + } + + public void onFieldClassificationRequest(@NonNull FieldClassificationRequest request, + FieldClassificationServiceCallbacks fieldClassificationServiceCallbacks) { + + if (sVerbose) { + Slog.v(TAG, "onFieldClassificationRequest request:" + request); + } + + run( + (s) -> + s.onFieldClassificationRequest( + request, + new IFieldClassificationCallback.Stub() { + @Override + public void onCancellable(ICancellationSignal cancellation) { + if (sDebug) { + Log.d(TAG, "onCancellable"); + } + } + + @Override + public void onSuccess(FieldClassificationResponse response) { + if (sDebug) { + Log.d(TAG, "onSuccess Response: " + response); + } + fieldClassificationServiceCallbacks + .onClassificationRequestSuccess(response); + } + + @Override + public void onFailure() { + if (sDebug) { + Log.d(TAG, "onFailure"); + } + } + + @Override + public boolean isCompleted() throws RemoteException { + return false; + } + + @Override + public void cancel() throws RemoteException {} + })); + } +} diff --git a/services/autofill/java/com/android/server/autofill/RemoteFillService.java b/services/autofill/java/com/android/server/autofill/RemoteFillService.java index 94872b09cd36a..4688658bf1c33 100644 --- a/services/autofill/java/com/android/server/autofill/RemoteFillService.java +++ b/services/autofill/java/com/android/server/autofill/RemoteFillService.java @@ -135,6 +135,9 @@ final class RemoteFillService extends ServiceConnector.Impl { } public void onFillRequest(@NonNull FillRequest request) { + if (sVerbose) { + Slog.v(TAG, "onFillRequest:" + request); + } AtomicReference cancellationSink = new AtomicReference<>(); AtomicReference> futureRef = new AtomicReference<>(); diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index c5c92885406f4..b55f16bc9ae45 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -88,6 +88,8 @@ import android.os.Process; import android.os.RemoteCallback; import android.os.RemoteException; import android.os.SystemClock; +import android.service.assist.classification.FieldClassificationRequest; +import android.service.assist.classification.FieldClassificationResponse; import android.service.autofill.AutofillFieldClassificationService.Scores; import android.service.autofill.AutofillService; import android.service.autofill.CompositeUserData; @@ -125,6 +127,7 @@ import android.view.autofill.AutofillValue; import android.view.autofill.IAutoFillManagerClient; import android.view.autofill.IAutofillWindowPresenter; import android.view.inputmethod.InlineSuggestionsRequest; +import android.widget.RemoteViews; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; @@ -146,6 +149,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Function; @@ -163,7 +167,8 @@ import java.util.function.Function; * until the user authenticates or it times out. */ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState.Listener, - AutoFillUI.AutoFillUiCallback, ValueFinder { + AutoFillUI.AutoFillUiCallback, ValueFinder, + RemoteFieldClassificationService.FieldClassificationServiceCallbacks { private static final String TAG = "AutofillSession"; private static final String ACTION_DELAYED_FILL = @@ -185,6 +190,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState private static AtomicInteger sIdCounter = new AtomicInteger(2); + private static AtomicInteger sIdCounterForPcc = new AtomicInteger(2); + @GuardedBy("mLock") private @SessionState int mSessionState = STATE_UNKNOWN; @@ -395,6 +402,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState @Nullable private ClientSuggestionsSession mClientSuggestionsSession; + private final ClassificationState mClassificationState = new ClassificationState(); + // TODO(b/216576510): Share one BroadcastReceiver between all Sessions instead of creating a // new one per Session. private final BroadcastReceiver mDelayedFillBroadcastReceiver = @@ -729,24 +738,21 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState * Assist Data Receiver for PCC */ private final class PccAssistDataReceiverImpl extends IAssistDataReceiver.Stub { - // TODO: Uncomment lines below after field classification service definition merged - // @GuardedBy("mLock") - // private FieldClassificationRequest mPendingFieldClassifitacionRequest; - // @GuardedBy("mLock") - // private FieldClassificationRequest mLastFieldClassifitacionRequest; @GuardedBy("mLock") void maybeRequestFieldClassificationFromServiceLocked() { - // TODO: Uncomment lines below after field classification service definition merged - // if (mPendingFieldClassifitacionRequest == null) { - // return; - // } - // mLastFieldClassifitacionRequest = mPendingFieldClassifitacionRequest; - // - // mRemoteFieldClassificationService.onFieldClassificationRequest( - // mPendingFieldClassifitacionRequest); - // - // mPendingFieldClassifitacionRequest = null; + if (mClassificationState.mPendingFieldClassificationRequest == null) { + Log.w(TAG, "Received AssistData without pending classification request"); + return; + } + + RemoteFieldClassificationService remoteFieldClassificationService = + mService.getRemoteFieldClassificationServiceLocked(); + if (remoteFieldClassificationService != null) { + remoteFieldClassificationService.onFieldClassificationRequest( + mClassificationState.mPendingFieldClassificationRequest, Session.this); + } + mClassificationState.onFieldClassificationRequestSent(); } @Override @@ -793,12 +799,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState ids.get(i).setSessionId(Session.this.id); } - // TODO: Uncomment lines below after field classification service definition merged - // FieldClassificationRequest request = new FieldClassificationRequest(structure); - // - // mPendingFieldClassifitacionRequest = request; - // - // maybeRequestFieldClassificationFromServiceLocked(); + mClassificationState.onAssistStructureReceived(structure); + + maybeRequestFieldClassificationFromServiceLocked(); } } @@ -1177,9 +1180,12 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState int requestId; // TODO(b/158623971): Update this to prevent possible overflow do { - requestId = sIdCounter.getAndIncrement(); + requestId = sIdCounterForPcc.getAndIncrement(); } while (requestId == INVALID_REQUEST_ID); + if (sVerbose) { + Slog.v(TAG, "request id is " + requestId + ", requesting assist structure for pcc"); + } // Call requestAutofilLData try { final Bundle receiverExtras = new Bundle(); @@ -1187,8 +1193,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState final long identity = Binder.clearCallingIdentity(); try { if (!ActivityTaskManager.getService().requestAutofillData(mPccAssistReceiver, - receiverExtras, mActivityToken, flags)) { - Slog.w(TAG, "failed to request autofill data for pcc: " + mActivityToken); + receiverExtras, mActivityToken, flags)) { + Slog.w(TAG, "failed to request autofill data for " + mActivityToken); } } finally { Binder.restoreCallingIdentity(identity); @@ -1386,6 +1392,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return; } + // TODO: Check if this is required. We can still present datasets to the user even if + // traditional field classification is disabled. fieldClassificationIds = response.getFieldClassificationIds(); if (!mSessionFlags.mClientSuggestionsEnabled && fieldClassificationIds != null && !mService.isFieldClassificationEnabledLocked()) { @@ -1470,11 +1478,231 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } + // TODO(b/266379948): Ideally wait for PCC request to finish for a while more + // (say 100ms) before proceeding further on. + synchronized (mLock) { + response = getEffectiveFillResponse(response); processResponseLocked(response, null, requestFlags); } } + private FillResponse getEffectiveFillResponse(FillResponse response) { + // TODO(b/266379948): label dataset source + if (!mService.getMaster().isPccClassificationEnabled()) return response; + synchronized (mLock) { + if (mClassificationState.mState != ClassificationState.STATE_RESPONSE + || mClassificationState.mLastFieldClassificationResponse == null) { + return response; + } + if (!mClassificationState.processResponse()) return response; + } + boolean preferAutofillProvider = mService.getMaster().preferProviderOverPcc(); + boolean shouldUseFallback = mService.getMaster().shouldUsePccFallback(); + if (preferAutofillProvider && !shouldUseFallback) { + return response; + } + + DatasetComputationContainer autofillProviderContainer = new DatasetComputationContainer(); + DatasetComputationContainer detectionPccContainer = new DatasetComputationContainer(); + + computeDatasetsForProviderAndUpdateContainer(response, autofillProviderContainer); + computeDatasetsForPccAndUpdateContainer(response, detectionPccContainer); + + DatasetComputationContainer resultContainer; + if (preferAutofillProvider) { + resultContainer = autofillProviderContainer; + if (shouldUseFallback) { + // add PCC datasets that are not detected by provider. + addFallbackDatasets(autofillProviderContainer, detectionPccContainer); + } + } else { + resultContainer = detectionPccContainer; + if (shouldUseFallback) { + // add Provider's datasets that are not detected by PCC. + addFallbackDatasets(detectionPccContainer, autofillProviderContainer); + } + } + // Create FillResponse with effectiveDatasets, and all the rest value from the original + // response. + return FillResponse.shallowCopy(response, new ArrayList<>(resultContainer.mDatasets)); + } + + /** + * A private class to hold & compute datasets to be shown + */ + private static class DatasetComputationContainer { + // List of all autofill ids that have a corresponding datasets + Set mAutofillIds = new ArraySet<>(); + // Set of datasets. Kept separately, to be able to be used directly for composing + // FillResponse. + Set mDatasets = new ArraySet<>(); + ArrayMap> mAutofillIdToDatasetMap = new ArrayMap<>(); + } + + // Adds fallback datasets to the first container. + // This function will destruct and modify c2 container. + private void addFallbackDatasets( + DatasetComputationContainer c1, DatasetComputationContainer c2) { + for (AutofillId id : c2.mAutofillIds) { + if (!c1.mAutofillIds.contains(id)) { + + // Since c2 could be modified in a previous iteration, it's possible that all + // datasets corresponding to it have been evaluated, and it's map no longer has + // any more datasets left. Early return in this case. + if (c2.mAutofillIdToDatasetMap.get(id).isEmpty()) return; + + // For AutofillId id, do the following + // 1. Add all the datasets corresponding to it to c1's dataset, and update c1 + // properly. + // 2. All the datasets that were added should be removed from the other autofill + // ids that were in this dataset. This prevents us from revisiting those datasets. + // Although we are using Sets, and that'd avoid re-adding them, using this logic + // for now to keep safe. TODO(b/266379948): Revisit this logic. + + Set datasets = c2.mAutofillIdToDatasetMap.get(id); + Set copyDatasets = new ArraySet<>(datasets); + c1.mAutofillIds.add(id); + c1.mAutofillIdToDatasetMap.put(id, copyDatasets); + c1.mDatasets.addAll(copyDatasets); + + for (Dataset dataset : datasets) { + for (AutofillId currentId : dataset.getFieldIds()) { + if (currentId.equals(id)) continue; + // For this id, we need to remove the dataset from it's map. + c2.mAutofillIdToDatasetMap.get(currentId).remove(dataset); + } + } + } + } + } + + private void computeDatasetsForProviderAndUpdateContainer( + FillResponse response, DatasetComputationContainer container) { + List datasets = response.getDatasets(); + if (datasets == null) return; + ArrayMap> autofillIdToDatasetMap = new ArrayMap<>(); + Set eligibleDatasets = new ArraySet<>(); + Set eligibleAutofillIds = new ArraySet<>(); + for (Dataset dataset : response.getDatasets()) { + if (dataset.getFieldIds() == null) continue; + if (dataset.getAutofillDatatypes() != null + && dataset.getAutofillDatatypes().size() > 0) { + continue; + } + eligibleDatasets.add(dataset); + for (AutofillId id : dataset.getFieldIds()) { + eligibleAutofillIds.add(id); + Set datasetForIds = autofillIdToDatasetMap.get(id); + if (datasetForIds == null) { + datasetForIds = new ArraySet<>(); + } + datasetForIds.add(dataset); + autofillIdToDatasetMap.put(id, datasetForIds); + } + } + container.mAutofillIdToDatasetMap = autofillIdToDatasetMap; + container.mDatasets = eligibleDatasets; + container.mAutofillIds = eligibleAutofillIds; + } + + private void computeDatasetsForPccAndUpdateContainer( + FillResponse response, DatasetComputationContainer container) { + List datasets = response.getDatasets(); + if (datasets == null) return; + + synchronized (mLock) { + ArrayMap> hintsToAutofillIdMap = + mClassificationState.mHintsToAutofillIdMap; + + ArrayMap> groupHintsToAutofillIdMap = + mClassificationState.mGroupHintsToAutofillIdMap; + + ArrayMap> map = new ArrayMap<>(); + + Set eligibleDatasets = new ArraySet<>(); + Set eligibleAutofillIds = new ArraySet<>(); + + for (int i = 0; i < datasets.size(); i++) { + Dataset dataset = datasets.get(i); + if (dataset.getAutofillDatatypes() == null) continue; + if (dataset.getFieldIds() != null && dataset.getFieldIds().size() > 0) continue; + + ArrayList fieldIds = new ArrayList<>(); + ArrayList fieldValues = new ArrayList<>(); + ArrayList fieldPresentations = new ArrayList<>(); + ArrayList fieldDialogPresentations = new ArrayList<>(); + ArrayList fieldInlinePresentations = new ArrayList<>(); + ArrayList fieldInlineTooltipPresentations = new ArrayList<>(); + ArrayList fieldFilters = new ArrayList<>(); + + for (int j = 0; j < dataset.getAutofillDatatypes().size(); j++) { + String hint = dataset.getAutofillDatatypes().get(j); + + if (hintsToAutofillIdMap.containsKey(hint)) { + ArrayList tempIds = + new ArrayList<>(hintsToAutofillIdMap.get(hint)); + + for (AutofillId autofillId : tempIds) { + eligibleAutofillIds.add(autofillId); + // For each of the field, copy over values. + fieldIds.add(autofillId); + fieldValues.add(dataset.getFieldValues().get(j)); + // TODO(b/266379948): might need to make it more efficient by not + // copying over value if it didn't exist. This would require creating + // a getter for the presentations arraylist. + fieldPresentations.add(dataset.getFieldPresentation(j)); + fieldDialogPresentations.add(dataset.getFieldDialogPresentation(j)); + fieldInlinePresentations.add(dataset.getFieldInlinePresentation(j)); + fieldInlineTooltipPresentations.add( + dataset.getFieldInlineTooltipPresentation(j)); + fieldFilters.add(dataset.getFilter(j)); + } + + Dataset newDataset = + new Dataset( + fieldIds, + fieldValues, + fieldPresentations, + fieldDialogPresentations, + fieldInlinePresentations, + fieldInlineTooltipPresentations, + fieldFilters, + new ArrayList<>(), + dataset.getFieldContent(), + null, + null, + null, + null, + dataset.getId(), + dataset.getAuthentication()); + eligibleDatasets.add(newDataset); + + // Associate this dataset with all the ids that are represented with it. + Set newDatasets; + for (AutofillId autofillId : tempIds) { + if (map.containsKey(autofillId)) { + newDatasets = map.get(autofillId); + } else { + newDatasets = new ArraySet<>(); + } + newDatasets.add(newDataset); + map.put(autofillId, newDatasets); + } + } + // TODO(b/266379948): handle the case: + // groupHintsToAutofillIdMap.containsKey(hint)) + // but the autofill id not being applicable to other hints. + // TODO(b/266379948): also handle the case where there could be more types in + // the dataset, provided by the provider, however, they aren't applicable. + } + } + container.mAutofillIds = eligibleAutofillIds; + container.mDatasets = eligibleDatasets; + container.mAutofillIdToDatasetMap = map; + } + } + @GuardedBy("mLock") private void processNullResponseOrFallbackLocked(int requestId, int flags) { if (!mSessionFlags.mClientSuggestionsEnabled) { @@ -4579,6 +4807,189 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } + /** + * Class maintaining the state of the requests to + * {@link android.service.assist.classification.FieldClassificationService}. + */ + private static final class ClassificationState { + + /** + * Initial state indicating that the request for classification hasn't been triggered yet. + */ + private static final int STATE_INITIAL = 1; + /** + * Assist request has been triggered, but awaiting response. + */ + private static final int STATE_PENDING_ASSIST_REQUEST = 2; + /** + * Classification request has been triggered, but awaiting response. + */ + private static final int STATE_PENDING_REQUEST = 3; + /** + * Classification response has been received. + */ + private static final int STATE_RESPONSE = 4; + /** + * Classification state has been invalidated, and the last response may no longer be valid. + * This could occur due to various reasons like views changing their layouts, becoming + * visible or invisible, thereby rendering previous response potentially inaccurate or + * incomplete. + */ + private static final int STATE_INVALIDATED = 5; + + @IntDef(prefix = { "STATE_" }, value = { + STATE_INITIAL, + STATE_PENDING_ASSIST_REQUEST, + STATE_PENDING_REQUEST, + STATE_RESPONSE, + STATE_INVALIDATED + }) + @Retention(RetentionPolicy.SOURCE) + @interface ClassificationRequestState{} + + @GuardedBy("mLock") + private @ClassificationRequestState int mState = STATE_INITIAL; + + @GuardedBy("mLock") + private FieldClassificationRequest mPendingFieldClassificationRequest; + + @GuardedBy("mLock") + private FieldClassificationResponse mLastFieldClassificationResponse; + + @GuardedBy("mLock") + private ArrayMap> mClassificationHintsMap; + + @GuardedBy("mLock") + private ArrayMap> mClassificationGroupHintsMap; + + @GuardedBy("mLock") + private ArrayMap> mClassificationCombinedHintsMap; + + /** + * Typically, there would be a 1:1 mapping. However, in certain cases, we may have a hint + * being applicable to many types. An example of this being new/change password forms, + * where you need to confirm the passward twice. + */ + @GuardedBy("mLock") + private ArrayMap> mHintsToAutofillIdMap; + + /** + * Group hints are expected to have a 1:many mapping. For example, different credit card + * fields (creditCardNumber, expiry, cvv) will all map to the same group hints. + */ + @GuardedBy("mLock") + private ArrayMap> mGroupHintsToAutofillIdMap; + + @GuardedBy("mLock") + private String stateToString() { + switch (mState) { + case STATE_INITIAL: + return "STATE_INITIAL"; + case STATE_PENDING_ASSIST_REQUEST: + return "STATE_PENDING_ASSIST_REQUEST"; + case STATE_PENDING_REQUEST: + return "STATE_PENDING_REQUEST"; + case STATE_RESPONSE: + return "STATE_RESPONSE"; + case STATE_INVALIDATED: + return "STATE_INVALIDATED"; + default: + return "UNKNOWN_CLASSIFICATION_STATE_" + mState; + } + } + + /** + * Process the response received. + * @return true if the response was processed, false otherwise. If there wasn't any + * response, yet this function was called, it would return false. + */ + @GuardedBy("mLock") + private boolean processResponse() { + if (mClassificationHintsMap != null && !mClassificationHintsMap.isEmpty()) { + // Already processed, so return + return true; + } + + FieldClassificationResponse response = mLastFieldClassificationResponse; + if (response == null) return false; + + mClassificationHintsMap = new ArrayMap<>(); + mClassificationGroupHintsMap = new ArrayMap<>(); + mHintsToAutofillIdMap = new ArrayMap<>(); + mGroupHintsToAutofillIdMap = new ArrayMap<>(); + Set classifications = + response.getClassifications(); + + for (android.service.assist.classification.FieldClassification classification : + classifications) { + AutofillId id = classification.getAutofillId(); + Set hintDetections = classification.getHints(); + Set groupHintsDetections = classification.getGroupHints(); + ArraySet combinedHints = new ArraySet<>(hintDetections); + mClassificationHintsMap.put(id, hintDetections); + if (groupHintsDetections != null) { + mClassificationGroupHintsMap.put(id, groupHintsDetections); + combinedHints.addAll(groupHintsDetections); + } + mClassificationCombinedHintsMap.put(id, combinedHints); + + processDetections(hintDetections, id, mHintsToAutofillIdMap); + processDetections(groupHintsDetections, id, mGroupHintsToAutofillIdMap); + } + return true; + } + + @GuardedBy("mLock") + private static void processDetections(Set detections, AutofillId id, + ArrayMap> currentMap) { + for (String detection : detections) { + Set autofillIds; + if (currentMap.containsKey(detection)) { + autofillIds = currentMap.get(detection); + } else { + autofillIds = new ArraySet<>(); + } + autofillIds.add(id); + currentMap.put(detection, autofillIds); + } + } + + @GuardedBy("mLock") + private void invalidateState() { + mState = STATE_INVALIDATED; + } + + @GuardedBy("mLock") + private void updatePendingAssistData() { + mState = STATE_PENDING_ASSIST_REQUEST; + } + + @GuardedBy("mLock") + private void updatePendingRequest() { + mState = STATE_PENDING_REQUEST; + } + + @GuardedBy("mLock") + private void updateResponseReceived(FieldClassificationResponse response) { + mState = STATE_RESPONSE; + mLastFieldClassificationResponse = response; + mPendingFieldClassificationRequest = null; + processResponse(); + } + + @GuardedBy("mLock") + private void onAssistStructureReceived(AssistStructure structure) { + mState = STATE_PENDING_REQUEST; + mPendingFieldClassificationRequest = new FieldClassificationRequest(structure); + } + + @GuardedBy("mLock") + private void onFieldClassificationRequestSent() { + mState = STATE_PENDING_REQUEST; + mPendingFieldClassificationRequest = null; + } + } + @Override public String toString() { return "Session: [id=" + id + ", component=" + mComponentName @@ -5088,4 +5499,28 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState ServiceInfo serviceInfo = mService.getServiceInfo(); return serviceInfo == null ? Process.INVALID_UID : serviceInfo.applicationInfo.uid; } + + // DetectionServiceCallbacks + public void onClassificationRequestSuccess(@Nullable FieldClassificationResponse response) { + mClassificationState.updateResponseReceived(response); + } + + public void onClassificationRequestFailure(int requestId, @Nullable CharSequence message) { + + } + + public void onClassificationRequestTimeout(int requestId) { + + } + + @Override + public void onServiceDied(@NonNull RemoteFieldClassificationService service) { + Slog.w(TAG, "removing session because service died"); + synchronized (mLock) { + // TODO(b/266379948) + // forceRemoveFromServiceLocked(); + } + } + // DetectionServiceCallbacks end + }