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
+ * &mdash;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&mdash; 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> -->