Moved Field Classification score logic to ExtServices.
Bug: 70939974
Test: atest CtsAutoFillServiceTestCases:FieldsClassificationTest \
CtsAutoFillServiceTestCases:UserDataTest
Test: atest CtsAutoFillServiceTestCases
Change-Id: I75fd59b5d7530fcd7095b26f6e592d7459c7d235
diff --git a/core/java/android/service/autofill/AutofillFieldClassificationService.java b/core/java/android/service/autofill/AutofillFieldClassificationService.java
new file mode 100644
index 0000000..18f6dab
--- /dev/null
+++ b/core/java/android/service/autofill/AutofillFieldClassificationService.java
@@ -0,0 +1,284 @@
+/*
+ * 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;
+
+import static android.view.autofill.AutofillManager.EXTRA_AVAILABLE_ALGORITHMS;
+import static android.view.autofill.AutofillManager.EXTRA_DEFAULT_ALGORITHM;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.os.HandlerCaller;
+import com.android.internal.os.SomeArgs;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A service that calculates field classification scores.
+ *
+ * <p>A field classification score is a {@code float} representing how well an
+ * {@link AutofillValue} filled matches a expected value predicted by an autofill service
+ * —a full-match is {@code 1.0} (representing 100%), while a full mismatch is {@code 0.0}.
+ *
+ * <p>The exact score depends on the algorithm used to calculate it— the service must provide
+ * at least one default algorithm (which is used when the algorithm is not specified or is invalid),
+ * but it could provide more (in which case the algorithm name should be specifiied by the caller
+ * when calculating the scores).
+ *
+ * {@hide}
+ */
+@SystemApi
+public abstract class AutofillFieldClassificationService extends Service {
+
+ private static final String TAG = "AutofillFieldClassificationService";
+
+ private static final int MSG_GET_AVAILABLE_ALGORITHMS = 1;
+ private static final int MSG_GET_DEFAULT_ALGORITHM = 2;
+ private static final int MSG_GET_SCORES = 3;
+
+ /**
+ * The {@link Intent} action that must be declared as handled by a service
+ * in its manifest for the system to recognize it as a quota providing service.
+ */
+ public static final String SERVICE_INTERFACE =
+ "android.service.autofill.AutofillFieldClassificationService";
+
+ /** {@hide} **/
+ public static final String EXTRA_SCORES = "scores";
+
+ private AutofillFieldClassificationServiceWrapper mWrapper;
+
+ private final HandlerCaller.Callback mHandlerCallback = (msg) -> {
+ final int action = msg.what;
+ final Bundle data = new Bundle();
+ final RemoteCallback callback;
+ switch (action) {
+ case MSG_GET_AVAILABLE_ALGORITHMS:
+ callback = (RemoteCallback) msg.obj;
+ final List<String> availableAlgorithms = onGetAvailableAlgorithms();
+ String[] asArray = null;
+ if (availableAlgorithms != null) {
+ asArray = new String[availableAlgorithms.size()];
+ availableAlgorithms.toArray(asArray);
+ }
+ data.putStringArray(EXTRA_AVAILABLE_ALGORITHMS, asArray);
+ break;
+ case MSG_GET_DEFAULT_ALGORITHM:
+ callback = (RemoteCallback) msg.obj;
+ final String defaultAlgorithm = onGetDefaultAlgorithm();
+ data.putString(EXTRA_DEFAULT_ALGORITHM, defaultAlgorithm);
+ break;
+ case MSG_GET_SCORES:
+ final SomeArgs args = (SomeArgs) msg.obj;
+ callback = (RemoteCallback) args.arg1;
+ final String algorithmName = (String) args.arg2;
+ final Bundle algorithmArgs = (Bundle) args.arg3;
+ @SuppressWarnings("unchecked")
+ final List<AutofillValue> actualValues = ((List<AutofillValue>) args.arg4);
+ @SuppressWarnings("unchecked")
+ final String[] userDataValues = (String[]) args.arg5;
+ final Scores scores = onGetScores(algorithmName, algorithmArgs, actualValues,
+ Arrays.asList(userDataValues));
+ data.putParcelable(EXTRA_SCORES, scores);
+ break;
+ default:
+ Log.w(TAG, "Handling unknown message: " + action);
+ return;
+ }
+ callback.sendResult(data);
+ };
+
+ private final HandlerCaller mHandlerCaller = new HandlerCaller(null, Looper.getMainLooper(),
+ mHandlerCallback, true);
+
+ /** @hide */
+ public AutofillFieldClassificationService() {
+
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mWrapper = new AutofillFieldClassificationServiceWrapper();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mWrapper;
+ }
+
+ /**
+ * Gets the name of all available algorithms.
+ *
+ * @throws UnsupportedOperationException if not implemented by service.
+ */
+ // TODO(b/70939974): rename to onGetAvailableAlgorithms if not removed
+ @NonNull
+ public List<String> onGetAvailableAlgorithms() {
+ throw new UnsupportedOperationException("Must be implemented by external service");
+ }
+
+ /**
+ * Gets the default algorithm that's used when an algorithm is not specified or is invalid.
+ *
+ * @throws UnsupportedOperationException if not implemented by service.
+ */
+ @NonNull
+ public String onGetDefaultAlgorithm() {
+ throw new UnsupportedOperationException("Must be implemented by external service");
+ }
+
+ /**
+ * Calculates field classification scores in a batch.
+ *
+ * <p>See {@link AutofillFieldClassificationService} for more info about field classification
+ * scores.
+ *
+ * @param algorithm name of the algorithm to be used to calculate the scores. If invalid, the
+ * default algorithm will be used instead.
+ * @param args optional arguments to be passed to the algorithm.
+ * @param actualValues values entered by the user.
+ * @param userDataValues values predicted from the user data.
+ * @return the calculated scores and the algorithm used.
+ *
+ * {@hide}
+ */
+ @Nullable
+ @SystemApi
+ public Scores onGetScores(@Nullable String algorithm,
+ @Nullable Bundle args, @NonNull List<AutofillValue> actualValues,
+ @NonNull List<String> userDataValues) {
+ throw new UnsupportedOperationException("Must be implemented by external service");
+ }
+
+ private final class AutofillFieldClassificationServiceWrapper
+ extends IAutofillFieldClassificationService.Stub {
+
+ @Override
+ public void getAvailableAlgorithms(RemoteCallback callback) throws RemoteException {
+ mHandlerCaller.obtainMessageO(MSG_GET_AVAILABLE_ALGORITHMS, callback).sendToTarget();
+ }
+
+ @Override
+ public void getDefaultAlgorithm(RemoteCallback callback) throws RemoteException {
+ mHandlerCaller.obtainMessageO(MSG_GET_DEFAULT_ALGORITHM, callback).sendToTarget();
+ }
+
+ @Override
+ public void getScores(RemoteCallback callback, String algorithmName, Bundle algorithmArgs,
+ List<AutofillValue> actualValues, String[] userDataValues)
+ throws RemoteException {
+ // TODO(b/70939974): refactor to use PooledLambda
+ mHandlerCaller.obtainMessageOOOOO(MSG_GET_SCORES, callback, algorithmName,
+ algorithmArgs, actualValues, userDataValues).sendToTarget();
+ }
+ }
+
+
+ // TODO(b/70939974): it might be simpler to remove this class and return the float[][] directly,
+ // ignoring the request if the algorithm name is invalid.
+ /**
+ * Represents field classification scores used in a batch calculation.
+ *
+ * {@hide}
+ */
+ @SystemApi
+ public static final class Scores implements Parcelable {
+ private final String mAlgorithmName;
+ private final float[][] mScores;
+
+ /* @hide */
+ public Scores(String algorithmName, int size1, int size2) {
+ mAlgorithmName = algorithmName;
+ mScores = new float[size1][size2];
+ }
+
+ public Scores(Parcel parcel) {
+ mAlgorithmName = parcel.readString();
+ final int size1 = parcel.readInt();
+ final int size2 = parcel.readInt();
+ mScores = new float[size1][size2];
+ for (int i = 0; i < size1; i++) {
+ for (int j = 0; j < size2; j++) {
+ mScores[i][j] = parcel.readFloat();
+ }
+ }
+ }
+
+ /**
+ * Gets the name of algorithm used to calculate the score.
+ */
+ @NonNull
+ public String getAlgorithm() {
+ return mAlgorithmName;
+ }
+
+ /**
+ * Gets the resulting scores, with the 1st dimension representing actual values and the 2nd
+ * dimension values from {@link UserData}.
+ */
+ @NonNull
+ public float[][] getScores() {
+ return mScores;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeString(mAlgorithmName);
+ int size1 = mScores.length;
+ int size2 = mScores[0].length;
+ parcel.writeInt(size1);
+ parcel.writeInt(size2);
+ for (int i = 0; i < size1; i++) {
+ for (int j = 0; j < size2; j++) {
+ parcel.writeFloat(mScores[i][j]);
+ }
+ }
+ }
+
+ public static final Creator<Scores> CREATOR = new Creator<Scores>() {
+
+ @Override
+ public Scores createFromParcel(Parcel parcel) {
+ return new Scores(parcel);
+ }
+
+ @Override
+ public Scores[] newArray(int size) {
+ return new Scores[size];
+ }
+
+ };
+ }
+}
diff --git a/core/java/android/service/autofill/EditDistanceScorer.java b/core/java/android/service/autofill/EditDistanceScorer.java
deleted file mode 100644
index 97a3868..0000000
--- a/core/java/android/service/autofill/EditDistanceScorer.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * 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 android.service.autofill;
-
-import android.annotation.NonNull;
-import android.annotation.TestApi;
-import android.view.autofill.AutofillValue;
-
-/**
- * Helper used to calculate the classification score between an actual {@link AutofillValue} filled
- * by the user and the expected value predicted by an autofill service.
- */
-// TODO(b/70291841): explain algorithm once it's fully implemented
-/** @hide */
-@TestApi
-public final class EditDistanceScorer {
-
- private static final EditDistanceScorer sInstance = new EditDistanceScorer();
-
- /** @hide */
- public static final String NAME = "EDIT_DISTANCE";
-
- /**
- * Gets the singleton instance.
- */
- @TestApi
- /** @hide */
- public static EditDistanceScorer getInstance() {
- return sInstance;
- }
-
- private EditDistanceScorer() {
- }
-
- /**
- * Returns the classification score between an actual {@link AutofillValue} filled
- * by the user and the expected value predicted by an autofill service.
- *
- * <p>A full-match is {@code 1.0} (representing 100%), a full mismatch is {@code 0.0} and
- * partial mathces are something in between, typically using edit-distance algorithms.
- *
- * @hide
- */
- @TestApi
- public float getScore(@NonNull AutofillValue actualValue, @NonNull String userDataValue) {
- if (actualValue == null || !actualValue.isText() || userDataValue == null) return 0;
- // TODO(b/70291841): implement edit distance - currently it's returning either 0, 100%, or
- // partial match when number of chars match
- final String textValue = actualValue.getTextValue().toString();
- final int total = textValue.length();
- if (total != userDataValue.length()) return 0F;
-
- int matches = 0;
- for (int i = 0; i < total; i++) {
- if (Character.toLowerCase(textValue.charAt(i)) == Character
- .toLowerCase(userDataValue.charAt(i))) {
- matches++;
- }
- }
-
- return ((float) matches) / total;
- }
-}
diff --git a/core/java/android/service/autofill/IAutofillFieldClassificationService.aidl b/core/java/android/service/autofill/IAutofillFieldClassificationService.aidl
new file mode 100644
index 0000000..d8e829d
--- /dev/null
+++ b/core/java/android/service/autofill/IAutofillFieldClassificationService.aidl
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+import android.os.Bundle;
+import android.os.RemoteCallback;
+import android.view.autofill.AutofillValue;
+import java.util.List;
+
+/**
+ * Service used to calculate match scores for Autofill Field Classification.
+ *
+ * @hide
+ */
+oneway interface IAutofillFieldClassificationService {
+ void getAvailableAlgorithms(in RemoteCallback callback);
+ void getDefaultAlgorithm(in RemoteCallback callback);
+ void getScores(in RemoteCallback callback, String algorithmName, in Bundle algorithmArgs,
+ in List<AutofillValue> actualValues, in String[] userDataValues);
+}
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index 78b41c6..deb627f 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -33,6 +33,7 @@
import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcelable;
+import android.os.RemoteCallback;
import android.os.RemoteException;
import android.service.autofill.AutofillService;
import android.service.autofill.FillEventHistory;
@@ -53,9 +54,12 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
// TODO: use java.lang.ref.Cleaner once Android supports Java 9
import sun.misc.Cleaner;
@@ -169,11 +173,15 @@
public static final String EXTRA_CLIENT_STATE =
"android.view.autofill.extra.CLIENT_STATE";
-
/** @hide */
public static final String EXTRA_RESTORE_SESSION_TOKEN =
"android.view.autofill.extra.RESTORE_SESSION_TOKEN";
+ /** @hide */
+ public static final String EXTRA_AVAILABLE_ALGORITHMS = "available_algorithms";
+ /** @hide */
+ public static final String EXTRA_DEFAULT_ALGORITHM = "default_algorithm";
+
private static final String SESSION_ID_TAG = "android:sessionId";
private static final String STATE_TAG = "android:state";
private static final String LAST_AUTOFILLED_DATA_TAG = "android:lastAutoFilledData";
@@ -259,6 +267,12 @@
public static final int STATE_DISABLED_BY_SERVICE = 4;
/**
+ * Timeout in ms for calls to the field classification service.
+ * @hide
+ */
+ public static final int FC_SERVICE_TIMEOUT = 5000;
+
+ /**
* Makes an authentication id from a request id and a dataset id.
*
* @param requestId The request id.
@@ -1092,10 +1106,22 @@
* and it's ignored if the caller currently doesn't have an enabled autofill service for
* the user.
*/
+ // TODO(b/70939974): refactor this method to be "purely" sync by getting the info from the
+ // the ExtService manifest (instead of calling the service)
@Nullable
public String getDefaultFieldClassificationAlgorithm() {
+ final SyncRemoteCallbackListener<String> listener =
+ new SyncRemoteCallbackListener<String>() {
+
+ @Override
+ String getResult(Bundle result) {
+ return result == null ? null : result.getString(EXTRA_DEFAULT_ALGORITHM);
+ }
+ };
+
try {
- return mService.getDefaultFieldClassificationAlgorithm();
+ mService.getDefaultFieldClassificationAlgorithm(new RemoteCallback(listener));
+ return listener.getResult(FC_SERVICE_TIMEOUT);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
return null;
@@ -1107,17 +1133,32 @@
* <a href="AutofillService.html#FieldClassification">field classification</a>.
*
* <p><b>Note:</b> This method should only be called by an app providing an autofill service,
- * and it's ignored if the caller currently doesn't have an enabled autofill service for
- * the user.
- *
- * @return list of all algorithms currently available, or an empty list if the caller currently
- * does not have an enabled autofill service for the user.
+ * and it returns an empty list if the caller currently doesn't have an enabled autofill service
+ * for the user.
*/
+ // TODO(b/70939974): refactor this method to be "purely" sync by getting the info from the
+ // the ExtService manifest (instead of calling the service)
@NonNull
public List<String> getAvailableFieldClassificationAlgorithms() {
+ final SyncRemoteCallbackListener<List<String>> listener =
+ new SyncRemoteCallbackListener<List<String>>() {
+
+ @Override
+ List<String> getResult(Bundle result) {
+ List<String> algorithms = null;
+ if (result != null) {
+ final String[] asArray = result.getStringArray(EXTRA_AVAILABLE_ALGORITHMS);
+ if (asArray != null) {
+ algorithms = Arrays.asList(asArray);
+ }
+ }
+ return algorithms != null ? algorithms : Collections.emptyList();
+ }
+ };
+
try {
- final List<String> names = mService.getAvailableFieldClassificationAlgorithms();
- return names != null ? names : Collections.emptyList();
+ mService.getAvailableFieldClassificationAlgorithms(new RemoteCallback(listener));
+ return listener.getResult(FC_SERVICE_TIMEOUT);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
return null;
@@ -2196,4 +2237,36 @@
}
}
}
+
+ private abstract static class SyncRemoteCallbackListener<T>
+ implements RemoteCallback.OnResultListener {
+
+ private final CountDownLatch mLatch = new CountDownLatch(1);
+ private T mResult;
+
+ @Override
+ public void onResult(Bundle result) {
+ if (sVerbose) Log.w(TAG, "SyncRemoteCallbackListener.onResult(): " + result);
+ mResult = getResult(result);
+ mLatch.countDown();
+ }
+
+ T getResult(int timeoutMs) {
+ T result = null;
+ try {
+ if (mLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) {
+ result = mResult;
+ } else {
+ Log.w(TAG, "SyncRemoteCallbackListener not called in " + timeoutMs + "ms");
+ }
+ } catch (InterruptedException e) {
+ Log.w(TAG, "SyncRemoteCallbackListener interrupted: " + e);
+ Thread.currentThread().interrupt();
+ }
+ if (sVerbose) Log.w(TAG, "SyncRemoteCallbackListener: returning " + result);
+ return result;
+ }
+
+ abstract T getResult(Bundle result);
+ }
}
diff --git a/core/java/android/view/autofill/IAutoFillManager.aidl b/core/java/android/view/autofill/IAutoFillManager.aidl
index 1afa35e..41672e7 100644
--- a/core/java/android/view/autofill/IAutoFillManager.aidl
+++ b/core/java/android/view/autofill/IAutoFillManager.aidl
@@ -20,6 +20,7 @@
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IBinder;
+import android.os.RemoteCallback;
import android.service.autofill.FillEventHistory;
import android.service.autofill.UserData;
import android.view.autofill.AutofillId;
@@ -58,6 +59,6 @@
void setUserData(in UserData userData);
boolean isFieldClassificationEnabled();
ComponentName getAutofillServiceComponentName();
- List<String> getAvailableFieldClassificationAlgorithms();
- String getDefaultFieldClassificationAlgorithm();
+ void getAvailableFieldClassificationAlgorithms(in RemoteCallback callback);
+ void getDefaultFieldClassificationAlgorithm(in RemoteCallback callback);
}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index d2a22d0..547e83c 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -554,8 +554,6 @@
<protected-broadcast android:name="android.intent.action.DEVICE_LOCKED_CHANGED" />
<!-- Added in O -->
- <!-- TODO: temporary broadcast used by AutoFillManagerServiceImpl; will be removed -->
- <protected-broadcast android:name="com.android.internal.autofill.action.REQUEST_AUTOFILL" />
<protected-broadcast android:name="android.app.action.APPLICATION_DELEGATION_SCOPES_CHANGED" />
<protected-broadcast android:name="com.android.server.wm.ACTION_REVOKE_SYSTEM_ALERT_WINDOW_PERMISSION" />
<protected-broadcast android:name="android.media.tv.action.PARENTAL_CONTROLS_ENABLED_CHANGED" />
@@ -2684,6 +2682,13 @@
<permission android:name="android.permission.BIND_AUTOFILL_SERVICE"
android:protectionLevel="signature" />
+ <!-- Must be required by an {@link android.service.autofill.AutofillFieldClassificationService}
+ to ensure that only the system can bind to it.
+ @hide This is not a third-party API (intended for OEMs and system apps).
+ -->
+ <permission android:name="android.permission.BIND_AUTOFILL_FIELD_CLASSIFICATION_SERVICE"
+ android:protectionLevel="signature" />
+
<!-- Must be required by hotword enrollment application,
to ensure that only the system can interact with it.
@hide <p>Not for use by third-party applications.</p> -->