diff options
| author | 2018-01-05 17:04:10 -0800 | |
|---|---|---|
| committer | 2018-01-18 10:40:30 -0800 | |
| commit | bc055b0ef1c11337b8ec5f681097e7b51e84b9c4 (patch) | |
| tree | bc2348a03853306ec0d4b1358f188d843d0385c1 | |
| parent | d67e50eb805239fddedd5bbb5d21b8f78aba26e5 (diff) | |
Moved Field Classification score logic to ExtServices.
Bug: 70939974
Test: atest CtsAutoFillServiceTestCases:FieldsClassificationTest \
CtsAutoFillServiceTestCases:UserDataTest
Test: atest CtsAutoFillServiceTestCases
Change-Id: I75fd59b5d7530fcd7095b26f6e592d7459c7d235
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); } /** |