diff options
16 files changed, 985 insertions, 226 deletions
diff --git a/Android.bp b/Android.bp index defe655e5fd9..fa9e5a31704a 100644 --- a/Android.bp +++ b/Android.bp @@ -242,6 +242,7 @@ java_library {          "core/java/android/security/IKeystoreService.aidl",          "core/java/android/security/keymaster/IKeyAttestationApplicationIdProvider.aidl",          "core/java/android/service/autofill/IAutoFillService.aidl", +        "core/java/android/service/autofill/IAutofillFieldClassificationService.aidl",          "core/java/android/service/autofill/IFillCallback.aidl",          "core/java/android/service/autofill/ISaveCallback.aidl",          "core/java/android/service/carrier/ICarrierService.aidl", diff --git a/api/system-current.txt b/api/system-current.txt index ca5f66e90d29..a7c0fff498be 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -3855,6 +3855,28 @@ package android.security.keystore {  } +package android.service.autofill { + +  public abstract class AutofillFieldClassificationService extends android.app.Service { +    method public android.os.IBinder onBind(android.content.Intent); +    method public java.util.List<java.lang.String> onGetAvailableAlgorithms(); +    method public java.lang.String onGetDefaultAlgorithm(); +    method public android.service.autofill.AutofillFieldClassificationService.Scores onGetScores(java.lang.String, android.os.Bundle, java.util.List<android.view.autofill.AutofillValue>, java.util.List<java.lang.String>); +    field public static final java.lang.String SERVICE_INTERFACE = "android.service.autofill.AutofillFieldClassificationService"; +  } + +  public static final class AutofillFieldClassificationService.Scores implements android.os.Parcelable { +    ctor public AutofillFieldClassificationService.Scores(java.lang.String, int, int); +    ctor public AutofillFieldClassificationService.Scores(android.os.Parcel); +    method public int describeContents(); +    method public java.lang.String getAlgorithm(); +    method public float[][] getScores(); +    method public void writeToParcel(android.os.Parcel, int); +    field public static final android.os.Parcelable.Creator<android.service.autofill.AutofillFieldClassificationService.Scores> CREATOR; +  } + +} +  package android.service.notification {    public final class Adjustment implements android.os.Parcelable { diff --git a/api/test-current.txt b/api/test-current.txt index 6941731c29cd..8a20b0fb7348 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -563,11 +563,6 @@ package android.service.autofill {      method public void apply(android.service.autofill.ValueFinder, android.widget.RemoteViews, int) throws java.lang.Exception;    } -  public final class EditDistanceScorer { -    method public static android.service.autofill.EditDistanceScorer getInstance(); -    method public float getScore(android.view.autofill.AutofillValue, java.lang.String); -  } -    public final class FillResponse implements android.os.Parcelable {      method public int getFlags();    } diff --git a/core/java/android/service/autofill/AutofillFieldClassificationService.java b/core/java/android/service/autofill/AutofillFieldClassificationService.java new file mode 100644 index 000000000000..18f6dab9fc59 --- /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/IAutofillFieldClassificationService.aidl b/core/java/android/service/autofill/IAutofillFieldClassificationService.aidl new file mode 100644 index 000000000000..d8e829d8f67c --- /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 78b41c6f4c7b..deb627fb03e0 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -33,6 +33,7 @@ import android.metrics.LogMaker;  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.Retention;  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 final class AutofillManager {      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 final class AutofillManager {      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 @@ public final class AutofillManager {       * 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 @@ public final class AutofillManager {       * <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 @@ public final class AutofillManager {              }          }      } + +    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 1afa35e80a26..41672e7aeb9b 100644 --- a/core/java/android/view/autofill/IAutoFillManager.aidl +++ b/core/java/android/view/autofill/IAutoFillManager.aidl @@ -20,6 +20,7 @@ import android.content.ComponentName;  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 @@ interface IAutoFillManager {      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 d2a22d0794b6..547e83c144a4 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> --> diff --git a/packages/ExtServices/AndroidManifest.xml b/packages/ExtServices/AndroidManifest.xml index 291009ef7005..63d3623c468a 100644 --- a/packages/ExtServices/AndroidManifest.xml +++ b/packages/ExtServices/AndroidManifest.xml @@ -51,6 +51,13 @@              </intent-filter>          </service> +        <service android:name=".autofill.AutofillFieldClassificationServiceImpl" +             android:permission="android.permission.BIND_AUTOFILL_FIELD_CLASSIFICATION_SERVICE"> +            <intent-filter> +                <action android:name="android.service.autofill.AutofillFieldClassificationService" /> +            </intent-filter> +        </service> +          <library android:name="android.ext.services"/>      </application> diff --git a/packages/ExtServices/src/android/ext/services/autofill/AutofillFieldClassificationServiceImpl.java b/packages/ExtServices/src/android/ext/services/autofill/AutofillFieldClassificationServiceImpl.java new file mode 100644 index 000000000000..ea516a1db8b8 --- /dev/null +++ b/packages/ExtServices/src/android/ext/services/autofill/AutofillFieldClassificationServiceImpl.java @@ -0,0 +1,81 @@ +/* + * 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.ext.services.autofill; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Bundle; +import android.service.autofill.AutofillFieldClassificationService; +import android.util.Log; +import android.view.autofill.AutofillValue; + +import com.android.internal.util.ArrayUtils; + +import java.util.Arrays; +import java.util.List; + +public class AutofillFieldClassificationServiceImpl extends AutofillFieldClassificationService { + +    private static final String TAG = "AutofillFieldClassificationServiceImpl"; +    private static final boolean DEBUG = false; +    private static final List<String> sAvailableAlgorithms = Arrays.asList(EditDistanceScorer.NAME); + +    @Override +    public List<String> onGetAvailableAlgorithms() { +        return sAvailableAlgorithms; +    } + +    @Override +    public String onGetDefaultAlgorithm() { +        return EditDistanceScorer.NAME; +    } + +    @Nullable +    @Override +    public Scores onGetScores(@Nullable String algorithmName, +            @Nullable Bundle algorithmArgs, @NonNull List<AutofillValue> actualValues, +            @NonNull List<String> userDataValues) { +        if (ArrayUtils.isEmpty(actualValues) || ArrayUtils.isEmpty(userDataValues)) { +            Log.w(TAG, "getScores(): empty currentvalues (" + actualValues + ") or userValues (" +                    + userDataValues + ")"); +            // TODO(b/70939974): add unit test +            return null; +        } +        if (algorithmName != null && !algorithmName.equals(EditDistanceScorer.NAME)) { +            Log.w(TAG, "Ignoring invalid algorithm (" + algorithmName + ") and using " +                    + EditDistanceScorer.NAME + " instead"); +        } + +        final String actualAlgorithmName = EditDistanceScorer.NAME; +        final int actualValuesSize = actualValues.size(); +        final int userDataValuesSize = userDataValues.size(); +        if (DEBUG) { +            Log.d(TAG, "getScores() will return a " + actualValuesSize + "x" +                    + userDataValuesSize + " matrix for " + actualAlgorithmName); +        } +        final Scores scores = new Scores(actualAlgorithmName, actualValuesSize, userDataValuesSize); +        final float[][] scoresMatrix = scores.getScores(); + +        final EditDistanceScorer algorithm = EditDistanceScorer.getInstance(); +        for (int i = 0; i < actualValuesSize; i++) { +            for (int j = 0; j < userDataValuesSize; j++) { +                final float score = algorithm.getScore(actualValues.get(i), userDataValues.get(j)); +                scoresMatrix[i][j] = score; +            } +        } +        return scores; +    } +} diff --git a/core/java/android/service/autofill/EditDistanceScorer.java b/packages/ExtServices/src/android/ext/services/autofill/EditDistanceScorer.java index 97a386866665..d2e804af1b43 100644 --- a/core/java/android/service/autofill/EditDistanceScorer.java +++ b/packages/ExtServices/src/android/ext/services/autofill/EditDistanceScorer.java @@ -13,10 +13,9 @@   * See the License for the specific language governing permissions and   * limitations under the License.   */ -package android.service.autofill; +package android.ext.services.autofill;  import android.annotation.NonNull; -import android.annotation.TestApi;  import android.view.autofill.AutofillValue;  /** @@ -24,20 +23,15 @@ import android.view.autofill.AutofillValue;   * 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 { +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;      } @@ -52,9 +46,7 @@ public final class EditDistanceScorer {       * <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 diff --git a/packages/ExtServices/tests/src/android/ext/services/autofill/EditDistanceScorerTest.java b/packages/ExtServices/tests/src/android/ext/services/autofill/EditDistanceScorerTest.java new file mode 100644 index 000000000000..cc1571920e86 --- /dev/null +++ b/packages/ExtServices/tests/src/android/ext/services/autofill/EditDistanceScorerTest.java @@ -0,0 +1,77 @@ +/* + * 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.ext.services.autofill; + +import static com.google.common.truth.Truth.assertThat; + +import android.support.test.runner.AndroidJUnit4; +import android.view.autofill.AutofillValue; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class EditDistanceScorerTest { + +    private final EditDistanceScorer mScorer = EditDistanceScorer.getInstance(); + +    @Test +    public void testGetScore_nullValue() { +        assertFloat(mScorer.getScore(null, "D'OH!"), 0); +    } + +    @Test +    public void testGetScore_nonTextValue() { +        assertFloat(mScorer.getScore(AutofillValue.forToggle(true), "D'OH!"), 0); +    } + +    @Test +    public void testGetScore_nullUserData() { +        assertFloat(mScorer.getScore(AutofillValue.forText("D'OH!"), null), 0); +    } + +    @Test +    public void testGetScore_fullMatch() { +        assertFloat(mScorer.getScore(AutofillValue.forText("D'OH!"), "D'OH!"), 1); +    } + +    @Test +    public void testGetScore_fullMatchMixedCase() { +        assertFloat(mScorer.getScore(AutofillValue.forText("D'OH!"), "D'oH!"), 1); +    } + +    // TODO(b/70291841): might need to change it once it supports different sizes +    @Test +    public void testGetScore_mismatchDifferentSizes() { +        assertFloat(mScorer.getScore(AutofillValue.forText("One"), "MoreThanOne"), 0); +        assertFloat(mScorer.getScore(AutofillValue.forText("MoreThanOne"), "One"), 0); +    } + +    @Test +    public void testGetScore_partialMatch() { +        assertFloat(mScorer.getScore(AutofillValue.forText("Dude"), "Dxxx"), 0.25F); +        assertFloat(mScorer.getScore(AutofillValue.forText("Dude"), "DUxx"), 0.50F); +        assertFloat(mScorer.getScore(AutofillValue.forText("Dude"), "DUDx"), 0.75F); +        assertFloat(mScorer.getScore(AutofillValue.forText("Dxxx"), "Dude"), 0.25F); +        assertFloat(mScorer.getScore(AutofillValue.forText("DUxx"), "Dude"), 0.50F); +        assertFloat(mScorer.getScore(AutofillValue.forText("DUDx"), "Dude"), 0.75F); +    } + +    public static void assertFloat(float actualValue, float expectedValue) { +        assertThat(actualValue).isWithin(1.0e-10f).of(expectedValue); +    } +} diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java index cac7fedd0b00..03708375b311 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java @@ -44,6 +44,7 @@ import android.os.Build;  import android.os.Bundle;  import android.os.Handler;  import android.os.IBinder; +import android.os.RemoteCallback;  import android.os.RemoteException;  import android.os.ResultReceiver;  import android.os.ShellCallback; @@ -443,6 +444,8 @@ public final class AutofillManagerService extends SystemService {          }      } +    // TODO(b/70291841): add command to get field classification score +      private void setDebugLocked(boolean debug) {          com.android.server.autofill.Helper.sDebug = debug;          android.view.autofill.Helper.sDebug = debug; @@ -518,6 +521,8 @@ public final class AutofillManagerService extends SystemService {                  final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);                  if (service != null) {                      service.removeClientLocked(client); +                } else if (sVerbose) { +                    Slog.v(TAG, "removeClient(): no service for " + userId);                  }              }          } @@ -574,6 +579,8 @@ public final class AutofillManagerService extends SystemService {                  final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);                  if (service != null) {                      return service.getFillEventHistory(getCallingUid()); +                } else if (sVerbose) { +                    Slog.v(TAG, "getFillEventHistory(): no service for " + userId);                  }              } @@ -588,6 +595,8 @@ public final class AutofillManagerService extends SystemService {                  final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);                  if (service != null) {                      return service.getUserData(getCallingUid()); +                } else if (sVerbose) { +                    Slog.v(TAG, "getUserData(): no service for " + userId);                  }              } @@ -602,6 +611,8 @@ public final class AutofillManagerService extends SystemService {                  final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);                  if (service != null) {                      service.setUserData(getCallingUid(), userData); +                } else if (sVerbose) { +                    Slog.v(TAG, "setUserData(): no service for " + userId);                  }              }          } @@ -614,6 +625,8 @@ public final class AutofillManagerService extends SystemService {                  final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);                  if (service != null) {                      return service.isFieldClassificationEnabled(getCallingUid()); +                } else if (sVerbose) { +                    Slog.v(TAG, "isFieldClassificationEnabled(): no service for " + userId);                  }              } @@ -621,31 +634,39 @@ public final class AutofillManagerService extends SystemService {          }          @Override -        public String getDefaultFieldClassificationAlgorithm() throws RemoteException { +        public void getDefaultFieldClassificationAlgorithm(RemoteCallback callback) +                throws RemoteException {              final int userId = UserHandle.getCallingUserId();              synchronized (mLock) {                  final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);                  if (service != null) { -                    return service.getDefaultFieldClassificationAlgorithm(getCallingUid()); +                    service.getDefaultFieldClassificationAlgorithm(getCallingUid(), callback); +                } else { +                    if (sVerbose) { +                        Slog.v(TAG, "getDefaultFcAlgorithm(): no service for " + userId); +                    } +                    callback.sendResult(null);                  }              } - -            return null;          }          @Override -        public List<String> getAvailableFieldClassificationAlgorithms() throws RemoteException { +        public void getAvailableFieldClassificationAlgorithms(RemoteCallback callback) +                throws RemoteException {              final int userId = UserHandle.getCallingUserId();              synchronized (mLock) {                  final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);                  if (service != null) { -                    return service.getAvailableFieldClassificationAlgorithms(getCallingUid()); +                    service.getAvailableFieldClassificationAlgorithms(getCallingUid(), callback); +                } else { +                    if (sVerbose) { +                        Slog.v(TAG, "getAvailableFcAlgorithms(): no service for " + userId); +                    } +                    callback.sendResult(null);                  }              } - -            return null;          }          @Override @@ -656,6 +677,8 @@ public final class AutofillManagerService extends SystemService {                  final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);                  if (service != null) {                      return service.getServiceComponentName(); +                } else if (sVerbose) { +                    Slog.v(TAG, "getAutofillServiceComponentName(): no service for " + userId);                  }              } @@ -665,15 +688,17 @@ public final class AutofillManagerService extends SystemService {          @Override          public boolean restoreSession(int sessionId, IBinder activityToken, IBinder appCallback)                  throws RemoteException { +            final int userId = UserHandle.getCallingUserId();              activityToken = Preconditions.checkNotNull(activityToken, "activityToken");              appCallback = Preconditions.checkNotNull(appCallback, "appCallback");              synchronized (mLock) { -                final AutofillManagerServiceImpl service = mServicesCache.get( -                        UserHandle.getCallingUserId()); +                final AutofillManagerServiceImpl service = mServicesCache.get(userId);                  if (service != null) {                      return service.restoreSession(sessionId, getCallingUid(), activityToken,                              appCallback); +                } else if (sVerbose) { +                    Slog.v(TAG, "restoreSession(): no service for " + userId);                  }              } @@ -688,6 +713,8 @@ public final class AutofillManagerService extends SystemService {                  if (service != null) {                      service.updateSessionLocked(sessionId, getCallingUid(), autoFillId, bounds,                              value, action, flags); +                } else if (sVerbose) { +                    Slog.v(TAG, "updateSession(): no service for " + userId);                  }              }          } @@ -703,6 +730,8 @@ public final class AutofillManagerService extends SystemService {                  if (service != null) {                      restart = service.updateSessionLocked(sessionId, getCallingUid(), autoFillId,                              bounds, value, action, flags); +                } else if (sVerbose) { +                    Slog.v(TAG, "updateOrRestartSession(): no service for " + userId);                  }              }              if (restart) { @@ -720,6 +749,8 @@ public final class AutofillManagerService extends SystemService {                  final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);                  if (service != null) {                      service.finishSessionLocked(sessionId, getCallingUid()); +                } else if (sVerbose) { +                    Slog.v(TAG, "finishSession(): no service for " + userId);                  }              }          } @@ -730,6 +761,8 @@ public final class AutofillManagerService extends SystemService {                  final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);                  if (service != null) {                      service.cancelSessionLocked(sessionId, getCallingUid()); +                } else if (sVerbose) { +                    Slog.v(TAG, "cancelSession(): no service for " + userId);                  }              }          } @@ -740,6 +773,8 @@ public final class AutofillManagerService extends SystemService {                  final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);                  if (service != null) {                      service.disableOwnedAutofillServicesLocked(Binder.getCallingUid()); +                } else if (sVerbose) { +                    Slog.v(TAG, "cancelSession(): no service for " + userId);                  }              }          } @@ -755,8 +790,12 @@ public final class AutofillManagerService extends SystemService {          public boolean isServiceEnabled(int userId, String packageName) {              synchronized (mLock) {                  final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId); -                if (service == null) return false; -                return Objects.equals(packageName, service.getServicePackageName()); +                if (service != null) { +                    return Objects.equals(packageName, service.getServicePackageName()); +                } else if (sVerbose) { +                    Slog.v(TAG, "isServiceEnabled(): no service for " + userId); +                } +                return false;              }          } diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java index da74dba31416..a5bd59a9e77d 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java @@ -43,20 +43,15 @@ import android.os.Binder;  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.RemoteCallbackList;  import android.os.RemoteException;  import android.os.SystemClock;  import android.os.UserHandle;  import android.os.UserManager; -import android.os.Parcelable.Creator; -import android.os.RemoteCallback;  import android.provider.Settings;  import android.service.autofill.AutofillService;  import android.service.autofill.AutofillServiceInfo; -import android.service.autofill.Dataset; -import android.service.autofill.EditDistanceScorer;  import android.service.autofill.FieldClassification;  import android.service.autofill.FieldClassification.Match;  import android.service.autofill.FillEventHistory; @@ -69,8 +64,6 @@ import android.util.ArrayMap;  import android.util.ArraySet;  import android.util.DebugUtils;  import android.util.LocalLog; -import android.util.Log; -import android.util.Pair;  import android.util.Slog;  import android.util.SparseArray;  import android.util.TimeUtils; @@ -89,7 +82,6 @@ import com.android.server.autofill.ui.AutoFillUI;  import java.io.PrintWriter;  import java.util.ArrayList; -import java.util.Arrays;  import java.util.List;  import java.util.Random; @@ -124,137 +116,7 @@ final class AutofillManagerServiceImpl {      private final LocalLog mRequestsHistory;      private final LocalLog mUiLatencyHistory; - -    // TODO(b/70939974): temporary, will be moved to ExtServices -    static final class FieldClassificationAlgorithmService { - -        static final String EXTRA_SCORES = "scores"; - -        /** -         * Gets the name of all available algorithms. -         */ -        @NonNull -        public List<String> getAvailableAlgorithms() { -            return Arrays.asList(EditDistanceScorer.NAME); -        } - -        /** -         * Gets the default algorithm that's used when an algorithm is not specified or is invalid. -         */ -        @NonNull -        public String getDefaultAlgorithm() { -            return EditDistanceScorer.NAME; -        } - -        /** -         * Gets the field classification scores. -         * -         * @param algorithmName algorithm to be used. If invalid, the default algorithm will be used -         * instead. -         * @param algorithmArgs optional arguments to be passed to the algorithm. -         * @param currentValues values entered by the user. -         * @param userValues values from the user data. -         * @param callback returns a nullable bundle with the parcelable results on -         * {@link #EXTRA_SCORES}. -         */ -        @Nullable -        void getScores(@NonNull String algorithmName, @Nullable Bundle algorithmArgs, -                List<AutofillValue> currentValues, @NonNull String[] userValues, -                @NonNull RemoteCallback callback) { -            if (currentValues == null || userValues == null) { -                // TODO(b/70939974): use preconditions / add unit test -                throw new IllegalArgumentException("values cannot be null"); -            } -            if (currentValues.isEmpty() || userValues.length == 0) { -                Slog.w(TAG, "getScores(): empty currentvalues (" + currentValues -                        + ") or userValues (" + Arrays.toString(userValues) + ")"); -                // TODO(b/70939974): add unit test -                callback.sendResult(null); -            } -            String actualAlgorithName = algorithmName; -            if (!EditDistanceScorer.NAME.equals(algorithmName)) { -                Slog.w(TAG, "Ignoring invalid algorithm (" + algorithmName + ") and using " -                        + EditDistanceScorer.NAME + " instead"); -                actualAlgorithName = EditDistanceScorer.NAME; -            } -            final int currentValuesSize = currentValues.size(); -            if (sDebug) { -                Log.d(TAG, "getScores() will return a " + currentValuesSize + "x" -                        + userValues.length + " matrix for " + actualAlgorithName); -            } -            final FieldClassificationScores scores = new FieldClassificationScores( -                    actualAlgorithName, currentValuesSize, userValues.length); -            final EditDistanceScorer algorithm = EditDistanceScorer.getInstance(); -            for (int i = 0; i < currentValuesSize; i++) { -                for (int j = 0; j < userValues.length; j++) { -                    final float score = algorithm.getScore(currentValues.get(i), userValues[j]); -                    scores.scores[i][j] = score; -                } -            } -            final Bundle result = new Bundle(); -            result.putParcelable(EXTRA_SCORES, scores); -            callback.sendResult(result); -        } -    } - -    // TODO(b/70939974): temporary, will be moved to ExtServices -    public static final class FieldClassificationScores implements Parcelable { -        public final String algorithmName; -        public final float[][] scores; - -        public FieldClassificationScores(String algorithmName, int size1, int size2) { -            this.algorithmName = algorithmName; -            scores = new float[size1][size2]; -        } - -        public FieldClassificationScores(Parcel parcel) { -            algorithmName = parcel.readString(); -            final int size1 = parcel.readInt(); -            final int size2 = parcel.readInt(); -            scores = new float[size1][size2]; -            for (int i = 0; i < size1; i++) { -                for (int j = 0; j < size2; j++) { -                    scores[i][j] = parcel.readFloat(); -                } -            } -        } - -        @Override -        public int describeContents() { -            return 0; -        } - -        @Override -        public void writeToParcel(Parcel parcel, int flags) { -            parcel.writeString(algorithmName); -            int size1 = scores.length; -            int size2 = scores[0].length; -            parcel.writeInt(size1); -            parcel.writeInt(size2); -            for (int i = 0; i < size1; i++) { -                for (int j = 0; j < size2; j++) { -                    parcel.writeFloat(scores[i][j]); -                } -            } -        } - -        public static final Creator<FieldClassificationScores> CREATOR = new Creator<FieldClassificationScores>() { - -            @Override -            public FieldClassificationScores createFromParcel(Parcel parcel) { -                return new FieldClassificationScores(parcel); -            } - -            @Override -            public FieldClassificationScores[] newArray(int size) { -                return new FieldClassificationScores[size]; -            } - -        }; -    } - -    private final FieldClassificationAlgorithmService mFcService = -            new FieldClassificationAlgorithmService(); +    private final FieldClassificationStrategy mFieldClassificationStrategy;      /**       * Apps disabled by the service; key is package name, value is when they will be enabled again. @@ -324,6 +186,7 @@ final class AutofillManagerServiceImpl {          mUiLatencyHistory = uiLatencyHistory;          mUserId = userId;          mUi = ui; +        mFieldClassificationStrategy = new FieldClassificationStrategy(context, userId);          updateLocked(disabled);      } @@ -1089,10 +952,8 @@ final class AutofillManagerServiceImpl {              mUserData.dump(prefix2, pw);          } -        pw.print(prefix); pw.print("Available Field Classification algorithms: "); -        pw.println(mFcService.getAvailableAlgorithms()); -        pw.print(prefix); pw.print("Default Field Classification algorithm: "); -        pw.println(mFcService.getDefaultAlgorithm()); +        pw.print(prefix); pw.println("Field Classification strategy: "); +        mFieldClassificationStrategy.dump(prefix2, pw);      }      void destroySessionsLocked() { @@ -1288,26 +1149,26 @@ final class AutofillManagerServiceImpl {                  mUserId) == 1;      } -    FieldClassificationAlgorithmService getFieldClassificationService() { -        return mFcService; +    FieldClassificationStrategy getFieldClassificationStrategy() { +        return mFieldClassificationStrategy;      } -    List<String> getAvailableFieldClassificationAlgorithms(int callingUid) { +    void getAvailableFieldClassificationAlgorithms(int callingUid, RemoteCallback callback) {          synchronized (mLock) {              if (!isCalledByServiceLocked("getFCAlgorithms()", callingUid)) { -                return null; +                return;              }          } -        return mFcService.getAvailableAlgorithms(); +        mFieldClassificationStrategy.getAvailableAlgorithms(callback);      } -    String getDefaultFieldClassificationAlgorithm(int callingUid) { +    void getDefaultFieldClassificationAlgorithm(int callingUid, RemoteCallback callback) {          synchronized (mLock) {              if (!isCalledByServiceLocked("getDefaultFCAlgorithm()", callingUid)) { -                return null; +                return;              }          } -        return mFcService.getDefaultAlgorithm(); +        mFieldClassificationStrategy.getDefaultAlgorithm(callback);      }      @Override diff --git a/services/autofill/java/com/android/server/autofill/FieldClassificationStrategy.java b/services/autofill/java/com/android/server/autofill/FieldClassificationStrategy.java new file mode 100644 index 000000000000..7228f1d2a8ea --- /dev/null +++ b/services/autofill/java/com/android/server/autofill/FieldClassificationStrategy.java @@ -0,0 +1,279 @@ +/* + * 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 com.android.server.autofill; + +import static android.view.autofill.AutofillManager.EXTRA_AVAILABLE_ALGORITHMS; +import static android.view.autofill.AutofillManager.EXTRA_DEFAULT_ALGORITHM; +import static android.view.autofill.AutofillManager.FC_SERVICE_TIMEOUT; + +import static com.android.server.autofill.Helper.sDebug; +import static com.android.server.autofill.Helper.sVerbose; + +import android.Manifest; +import android.annotation.MainThread; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.os.Binder; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteCallback; +import android.os.RemoteException; +import android.os.UserHandle; +import android.service.autofill.AutofillFieldClassificationService; +import android.service.autofill.IAutofillFieldClassificationService; +import android.util.Log; +import android.util.Slog; +import android.view.autofill.AutofillValue; + +import com.android.internal.annotations.GuardedBy; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Strategy used to bridge the field classification algorithms provided by a service in an external + * package. + */ +//TODO(b/70291841): add unit tests ? +final class FieldClassificationStrategy { + +    private static final String TAG = "FieldClassificationStrategy"; + +    private final Context mContext; +    private final Object mLock = new Object(); +    private final int mUserId; + +    @GuardedBy("mLock") +    private ServiceConnection mServiceConnection; + +    @GuardedBy("mLock") +    private IAutofillFieldClassificationService mRemoteService; + +    @GuardedBy("mLock") +    private ArrayList<Command> mQueuedCommands; + +    public FieldClassificationStrategy(Context context, int userId) { +        mContext = context; +        mUserId = userId; +    } + +    private ComponentName getServiceComponentName() { +        final String packageName = +                mContext.getPackageManager().getServicesSystemSharedLibraryPackageName(); +        if (packageName == null) { +            Slog.w(TAG, "no external services package!"); +            return null; +        } + +        final Intent intent = new Intent(AutofillFieldClassificationService.SERVICE_INTERFACE); +        intent.setPackage(packageName); +        final ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent, +                PackageManager.GET_SERVICES | PackageManager.GET_META_DATA); +        if (resolveInfo == null || resolveInfo.serviceInfo == null) { +            Slog.w(TAG, "No valid components found."); +            return null; +        } +        final ServiceInfo serviceInfo = resolveInfo.serviceInfo; +        final ComponentName name = new ComponentName(serviceInfo.packageName, serviceInfo.name); + +        if (!Manifest.permission.BIND_AUTOFILL_FIELD_CLASSIFICATION_SERVICE +                .equals(serviceInfo.permission)) { +            Slog.w(TAG, name.flattenToShortString() + " does not require permission " +                    + Manifest.permission.BIND_AUTOFILL_FIELD_CLASSIFICATION_SERVICE); +            return null; +        } + +        if (sVerbose) Slog.v(TAG, "getServiceComponentName(): " + name); +        return name; +    } + +    /** +     * Run a command, starting the service connection if necessary. +     */ +    private void connectAndRun(@NonNull Command command) { +        synchronized (mLock) { +            if (mRemoteService != null) { +                try { +                    if (sVerbose) Slog.v(TAG, "running command right away"); +                    command.run(mRemoteService); +                } catch (RemoteException e) { +                    Slog.w(TAG, "exception calling service: " + e); +                } +                return; +            } else { +                if (sDebug) Slog.d(TAG, "service is null; queuing command"); +                if (mQueuedCommands == null) { +                    mQueuedCommands = new ArrayList<>(1); +                } +                mQueuedCommands.add(command); +                // If we're already connected, don't create a new connection, just leave - the +                // command will be run when the service connects +                if (mServiceConnection != null) return; +            } + +            if (sVerbose) Slog.v(TAG, "creating connection"); + +            // Create the connection +            mServiceConnection = new ServiceConnection() { +                @Override +                public void onServiceConnected(ComponentName name, IBinder service) { +                    if (sVerbose) Slog.v(TAG, "onServiceConnected(): " + name); +                    synchronized (mLock) { +                        mRemoteService = IAutofillFieldClassificationService.Stub +                                .asInterface(service); +                        if (mQueuedCommands != null) { +                            final int size = mQueuedCommands.size(); +                            if (sDebug) Slog.d(TAG, "running " + size + " queued commands"); +                            for (int i = 0; i < size; i++) { +                                final Command queuedCommand = mQueuedCommands.get(i); +                                try { +                                    if (sVerbose) Slog.v(TAG, "running queued command #" + i); +                                    queuedCommand.run(mRemoteService); +                                } catch (RemoteException e) { +                                    Slog.w(TAG, "exception calling " + name + ": " + e); +                                } +                            } +                            mQueuedCommands = null; +                        } else if (sDebug) Slog.d(TAG, "no queued commands"); +                    } +                } + +                @Override +                @MainThread +                public void onServiceDisconnected(ComponentName name) { +                    if (sVerbose) Slog.v(TAG, "onServiceDisconnected(): " + name); +                    synchronized (mLock) { +                        mRemoteService = null; +                    } +                } + +                @Override +                public void onBindingDied(ComponentName name) { +                    if (sVerbose) Slog.v(TAG, "onBindingDied(): " + name); +                    synchronized (mLock) { +                        mRemoteService = null; +                    } +                } + +                @Override +                public void onNullBinding(ComponentName name) { +                    if (sVerbose) Slog.v(TAG, "onNullBinding(): " + name); +                    synchronized (mLock) { +                        mRemoteService = null; +                    } +                } +            }; + +            final ComponentName component = getServiceComponentName(); +            if (sVerbose) Slog.v(TAG, "binding to: " + component); +            if (component != null) { +                final Intent intent = new Intent(); +                intent.setComponent(component); +                final long token = Binder.clearCallingIdentity(); +                try { +                    mContext.bindServiceAsUser(intent, mServiceConnection, Context.BIND_AUTO_CREATE, +                            UserHandle.of(mUserId)); +                    if (sVerbose) Slog.v(TAG, "bound"); +                } finally { +                    Binder.restoreCallingIdentity(token); +                } +            } +        } +    } + +    void getAvailableAlgorithms(RemoteCallback callback) { +        connectAndRun((service) -> service.getAvailableAlgorithms(callback)); +    } + +    void getDefaultAlgorithm(RemoteCallback callback) { +        connectAndRun((service) -> service.getDefaultAlgorithm(callback)); +    } + +    //TODO(b/70291841): rename this method (and all others in the chain) to something like +    // calculateScores() ? +    void getScores(RemoteCallback callback, @Nullable String algorithmName, +            @Nullable Bundle algorithmArgs, @NonNull List<AutofillValue> actualValues, +            @NonNull String[] userDataValues) { +        connectAndRun((service) -> service.getScores(callback, algorithmName, +                algorithmArgs, actualValues, userDataValues)); +    } + +    void dump(String prefix, PrintWriter pw) { +        final ComponentName impl = getServiceComponentName(); +        pw.print(prefix); pw.print("User ID: "); pw.println(mUserId); +        pw.print(prefix); pw.print("Queued commands: "); +        if (mQueuedCommands == null) { +            pw.println("N/A"); +        } else { +            pw.println(mQueuedCommands.size()); +        } +        pw.print(prefix); pw.print("Implementation: "); +        if (impl == null) { +            pw.println("N/A"); +            return; +        } +        pw.println(impl.flattenToShortString()); + +        final CountDownLatch latch = new CountDownLatch(2); + +        // Lock used to make sure lines don't overlap +        final Object lock = latch; + +        connectAndRun((service) -> service.getAvailableAlgorithms(new RemoteCallback((bundle) -> { +            synchronized (lock) { +                pw.print(prefix); pw.print("Available algorithms: "); +                pw.println(bundle.getStringArrayList(EXTRA_AVAILABLE_ALGORITHMS)); +            } +            latch.countDown(); +        }))); + +        connectAndRun((service) -> service.getDefaultAlgorithm(new RemoteCallback((bundle) -> { +            synchronized (lock) { +                pw.print(prefix); pw.print("Default algorithm: "); +                pw.println(bundle.getString(EXTRA_DEFAULT_ALGORITHM)); +            } +            latch.countDown(); +        }))); + +        try { +            if (!latch.await(FC_SERVICE_TIMEOUT, TimeUnit.MILLISECONDS)) { +                synchronized (lock) { +                    pw.print(prefix); pw.print("timeout ("); pw.print(FC_SERVICE_TIMEOUT); +                    pw.println("ms) waiting for service"); +                } +            } +        } catch (InterruptedException e) { +            synchronized (lock) { +                pw.print(prefix); pw.println("interrupted while waiting for service"); +            } +            Thread.currentThread().interrupt(); +        } +    } + +    private interface Command { +        void run(IAutofillFieldClassificationService service) throws RemoteException; +    } +} diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index f5d1336a0f6e..a0e23a152224 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -18,6 +18,7 @@ package com.android.server.autofill;  import static android.app.ActivityManagerInternal.ASSIST_KEY_RECEIVER_EXTRAS;  import static android.app.ActivityManagerInternal.ASSIST_KEY_STRUCTURE; +import static android.service.autofill.AutofillFieldClassificationService.EXTRA_SCORES;  import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST;  import static android.service.autofill.FillRequest.INVALID_REQUEST_ID;  import static android.view.autofill.AutofillManager.ACTION_START_SESSION; @@ -25,7 +26,6 @@ import static android.view.autofill.AutofillManager.ACTION_VALUE_CHANGED;  import static android.view.autofill.AutofillManager.ACTION_VIEW_ENTERED;  import static android.view.autofill.AutofillManager.ACTION_VIEW_EXITED; -import static com.android.server.autofill.AutofillManagerServiceImpl.FieldClassificationAlgorithmService.EXTRA_SCORES;  import static com.android.server.autofill.Helper.sDebug;  import static com.android.server.autofill.Helper.sPartitionMaxCount;  import static com.android.server.autofill.Helper.sVerbose; @@ -54,11 +54,11 @@ import android.os.Parcelable;  import android.os.RemoteCallback;  import android.os.RemoteException;  import android.os.SystemClock; +import android.service.autofill.AutofillFieldClassificationService.Scores;  import android.service.autofill.AutofillService;  import android.service.autofill.Dataset;  import android.service.autofill.FieldClassification;  import android.service.autofill.FieldClassification.Match; -import android.service.carrier.CarrierMessagingService.ResultCallback;  import android.service.autofill.FillContext;  import android.service.autofill.FillRequest;  import android.service.autofill.FillResponse; @@ -86,8 +86,6 @@ import com.android.internal.logging.MetricsLogger;  import com.android.internal.logging.nano.MetricsProto.MetricsEvent;  import com.android.internal.os.HandlerCaller;  import com.android.internal.util.ArrayUtils; -import com.android.server.autofill.AutofillManagerServiceImpl.FieldClassificationAlgorithmService; -import com.android.server.autofill.AutofillManagerServiceImpl.FieldClassificationScores;  import com.android.server.autofill.ui.AutoFillUI;  import com.android.server.autofill.ui.PendingUi; @@ -99,7 +97,6 @@ import java.util.Collections;  import java.util.List;  import java.util.Map;  import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference;  /**   * A session for a given activity. @@ -1101,10 +1098,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState          }          // Sets field classification scores -        final FieldClassificationAlgorithmService fcService = -                mService.getFieldClassificationService(); -        if (userData != null && fcService != null) { -            logFieldClassificationScoreLocked(fcService, ignoredDatasets, changedFieldIds, +        final FieldClassificationStrategy fcStrategy = mService.getFieldClassificationStrategy(); +        if (userData != null && fcStrategy != null) { +            logFieldClassificationScoreLocked(fcStrategy, ignoredDatasets, changedFieldIds,                      changedDatasetIds, manuallyFilledFieldIds, manuallyFilledDatasetIds,                      manuallyFilledIds, userData,                      mViewStates.values()); @@ -1121,7 +1117,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState       * {@code fieldId} based on its {@code currentValue} and {@code userData}.       */      private void logFieldClassificationScoreLocked( -            @NonNull AutofillManagerServiceImpl.FieldClassificationAlgorithmService fcService, +            @NonNull FieldClassificationStrategy fcStrategy,              @NonNull ArraySet<String> ignoredDatasets,              @NonNull ArrayList<AutofillId> changedFieldIds,              @NonNull ArrayList<String> changedDatasetIds, @@ -1161,6 +1157,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState              fieldIds[k++] = viewState.id;          } +        // Then use the results, asynchronously          final RemoteCallback callback = new RemoteCallback((result) -> {              if (result == null) {                  if (sDebug) Slog.d(TAG, "setFieldClassificationScore(): no results"); @@ -1170,35 +1167,46 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState                          mComponentName.getPackageName());                  return;              } -            final FieldClassificationScores matrix = result.getParcelable(EXTRA_SCORES); - -            // Then use the results. -            for (int i = 0; i < viewsSize; i++) { -                final AutofillId fieldId = fieldIds[i]; +            final Scores scores = result.getParcelable(EXTRA_SCORES); +            if (scores == null) { +                Slog.w(TAG, "No field classification score on " + result); +                return; +            } +            final float[][] scoresMatrix = scores.getScores(); -                ArrayList<Match> matches = null; -                for (int j = 0; j < userValues.length; j++) { -                    String remoteId = remoteIds[j]; -                    final String actualAlgorithm = matrix.algorithmName; -                    final float score = matrix.scores[i][j]; -                    if (score > 0) { -                        if (sVerbose) { -                            Slog.v(TAG, "adding score " + score + " at index " + j + " and id " -                                    + fieldId); +            int i = 0, j = 0; +            try { +                for (i = 0; i < viewsSize; i++) { +                    final AutofillId fieldId = fieldIds[i]; + +                    ArrayList<Match> matches = null; +                    for (j = 0; j < userValues.length; j++) { +                        String remoteId = remoteIds[j]; +                        final String actualAlgorithm = scores.getAlgorithm(); +                        final float score = scoresMatrix[i][j]; +                        if (score > 0) { +                            if (sVerbose) { +                                Slog.v(TAG, "adding score " + score + " at index " + j + " and id " +                                        + fieldId); +                            } +                            if (matches == null) { +                                matches = new ArrayList<>(userValues.length); +                            } +                            matches.add(new Match(remoteId, score, actualAlgorithm));                          } -                        if (matches == null) { -                            matches = new ArrayList<>(userValues.length); +                        else if (sVerbose) { +                            Slog.v(TAG, "skipping score 0 at index " + j + " and id " + fieldId);                          } -                        matches.add(new Match(remoteId, score, actualAlgorithm));                      } -                    else if (sVerbose) { -                        Slog.v(TAG, "skipping score 0 at index " + j + " and id " + fieldId); +                    if (matches != null) { +                        detectedFieldIds.add(fieldId); +                        detectedFieldClassifications.add(new FieldClassification(matches));                      }                  } -                if (matches != null) { -                    detectedFieldIds.add(fieldId); -                    detectedFieldClassifications.add(new FieldClassification(matches)); -                } +            } catch (ArrayIndexOutOfBoundsException e) { +                Slog.wtf(TAG, "Error accessing FC score at " + i + " x " + j + ": " +                        + Arrays.toString(scoresMatrix), e); +                return;              }              mService.logContextCommittedLocked(id, mClientState, mSelectedDatasetIds, @@ -1207,7 +1215,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState                      mComponentName.getPackageName());          }); -        fcService.getScores(algorithm, algorithmArgs, currentValues, userValues, callback); +        fcStrategy.getScores(callback, algorithm, algorithmArgs, currentValues, userValues);      }      /**  |