content: /*
   * 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 com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.SystemApi;
  import android.annotation.TestApi;
  import android.app.Service;
  import android.content.Intent;
  import android.os.Bundle;
  import android.os.Handler;
  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 java.util.Arrays;
  import java.util.List;
  import java.util.Map;
  
  /**
   * A service that calculates field classification scores.
   *
   * <p>A field classification score is a {@code float} representing how well an
   * {@link AutofillValue} filled matches a expected value predicted by an autofill service
   * &mdash;a full match is {@code 1.0} (representing 100%), while a full mismatch is {@code 0.0}.
   *
   * <p>The exact score depends on the algorithm used to calculate it&mdash;the service must provide
   * at least one default algorithm (which is used when the algorithm is not specified or is invalid),
   * but it could provide more (in which case the algorithm name should be specified by the caller
   * when calculating the scores).
   *
   * {@hide}
   */
  @SystemApi
  @TestApi
  public abstract class AutofillFieldClassificationService extends Service {
  
      private static final String TAG = "AutofillFieldClassificationService";
  
      /**
       * 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";
  
      /**
       * Manifest metadata key for the resource string containing the name of the default field
       * classification algorithm.
       */
      public static final String SERVICE_META_DATA_KEY_DEFAULT_ALGORITHM =
              "android.autofill.field_classification.default_algorithm";
      /**
       * Manifest metadata key for the resource string array containing the names of all field
       * classification algorithms provided by the service.
       */
      public static final String SERVICE_META_DATA_KEY_AVAILABLE_ALGORITHMS =
              "android.autofill.field_classification.available_algorithms";
  
      /**
       * Field classification algorithm that computes the edit distance between two Strings.
       *
       * <p>Service implementation must provide this algorithm.</p>
       */
      public static final String REQUIRED_ALGORITHM_EDIT_DISTANCE = "EDIT_DISTANCE";
  
      /**
       * Field classification algorithm that computes whether the last four digits between two
       * Strings match exactly.
       *
       * <p>Service implementation must provide this algorithm.</p>
       */
      public static final String REQUIRED_ALGORITHM_EXACT_MATCH = "EXACT_MATCH";
  
      /** {@hide} **/
      public static final String EXTRA_SCORES = "scores";
  
      private AutofillFieldClassificationServiceWrapper mWrapper;
  
      private void calculateScores(RemoteCallback callback, List<AutofillValue> actualValues,
              String[] userDataValues, String[] categoryIds, String defaultAlgorithm,
              Bundle defaultArgs, Map algorithms, Map args) {
          final Bundle data = new Bundle();
          final float[][] scores = onCalculateScores(actualValues, Arrays.asList(userDataValues),
                  Arrays.asList(categoryIds), defaultAlgorithm, defaultArgs, algorithms, args);
          if (scores != null) {
              data.putParcelable(EXTRA_SCORES, new Scores(scores));
          }
          callback.sendResult(data);
      }
  
      private final Handler mHandler = new Handler(Looper.getMainLooper(), null, true);
  
      /** @hide */
      public AutofillFieldClassificationService() {
  
      }
  
      @Override
      public void onCreate() {
          super.onCreate();
          mWrapper = new AutofillFieldClassificationServiceWrapper();
      }
  
      @Override
      public IBinder onBind(Intent intent) {
          return mWrapper;
      }
  
      /**
       * Calculates field classification scores in a batch.
       *
       * <p>A field classification score is a {@code float} representing how well an
       * {@link AutofillValue} filled matches a expected value predicted by an autofill service
       * &mdash;a full match is {@code 1.0} (representing 100%), while a full mismatch is {@code 0.0}.
       *
       * <p>The exact score depends on the algorithm used to calculate it&mdash;the service must
       * provide at least one default algorithm (which is used when the algorithm is not specified
       * or is invalid), but it could provide more (in which case the algorithm name should be
       * specified by the caller when calculating the scores).
       *
       * <p>For example, if the service provides an algorithm named {@code EXACT_MATCH} that
       * returns {@code 1.0} if all characters match or {@code 0.0} otherwise, a call to:
       *
       * <pre>
       * service.onGetScores("EXACT_MATCH", null,
       *   Arrays.asList(AutofillValue.forText("email1"), AutofillValue.forText("PHONE1")),
       *   Arrays.asList("email1", "phone1"));
       * </pre>
       *
       * <p>Returns:
       *
       * <pre>
       * [
       *   [1.0, 0.0], // "email1" compared against ["email1", "phone1"]
       *   [0.0, 0.0]  // "PHONE1" compared against ["email1", "phone1"]
       * ];
       * </pre>
       *
       * <p>If the same algorithm allows the caller to specify whether the comparisons should be
       * case sensitive by passing a boolean option named {@code "case_sensitive"}, then a call to:
       *
       * <pre>
       * Bundle algorithmOptions = new Bundle();
       * algorithmOptions.putBoolean("case_sensitive", false);
       *
       * service.onGetScores("EXACT_MATCH", algorithmOptions,
       *   Arrays.asList(AutofillValue.forText("email1"), AutofillValue.forText("PHONE1")),
       *   Arrays.asList("email1", "phone1"));
       * </pre>
       *
       * <p>Returns:
       *
       * <pre>
       * [
       *   [1.0, 0.0], // "email1" compared against ["email1", "phone1"]
       *   [0.0, 1.0]  // "PHONE1" compared against ["email1", "phone1"]
       * ];
       * </pre>
       *
       * @param algorithm name of the algorithm to be used to calculate the scores. If invalid or
       * {@code null}, the default algorithm is used instead.
       * @param algorithmOptions 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 of {@code actualValues} x {@code userDataValues}.
       *
       * {@hide}
       *
       * @deprecated Use {@link AutofillFieldClassificationService#onCalculateScores} instead.
       */
      @Nullable
      @SystemApi
      @Deprecated
      public float[][] onGetScores(@Nullable String algorithm,
              @Nullable Bundle algorithmOptions, @NonNull List<AutofillValue> actualValues,
              @NonNull List<String> userDataValues) {
          Log.e(TAG, "service implementation (" + getClass() + " does not implement onGetScores()");
          return null;
      }
  
      /**
       * Calculates field classification scores in a batch.
       *
       * <p>A field classification score is a {@code float} representing how well an
       * {@link AutofillValue} matches a expected value predicted by an autofill service
       * &mdash;a full match is {@code 1.0} (representing 100%), while a full mismatch is {@code 0.0}.
       *
       * <p>The exact score depends on the algorithm used to calculate it&mdash;the service must
       * provide at least one default algorithm (which is used when the algorithm is not specified
       * or is invalid), but it could provide more (in which case the algorithm name should be
       * specified by the caller when calculating the scores).
       *
       * <p>For example, if the service provides an algorithm named {@code EXACT_MATCH} that
       * returns {@code 1.0} if all characters match or {@code 0.0} otherwise, a call to:
       *
       * <pre>
       * HashMap algorithms = new HashMap<>();
       * algorithms.put("email", "EXACT_MATCH");
       * algorithms.put("phone", "EXACT_MATCH");
       *
       * HashMap args = new HashMap<>();
       * args.put("email", null);
       * args.put("phone", null);
       *
       * service.onCalculateScores(Arrays.asList(AutofillValue.forText("email1"),
       * AutofillValue.forText("PHONE1")), Arrays.asList("email1", "phone1"),
       * Array.asList("email", "phone"), algorithms, args);
       * </pre>
       *
       * <p>Returns:
       *
       * <pre>
       * [
       *   [1.0, 0.0], // "email1" compared against ["email1", "phone1"]
       *   [0.0, 0.0]  // "PHONE1" compared against ["email1", "phone1"]
       * ];
       * </pre>
       *
       * <p>If the same algorithm allows the caller to specify whether the comparisons should be
       * case sensitive by passing a boolean option named {@code "case_sensitive"}, then a call to:
       *
       * <pre>
       * Bundle algorithmOptions = new Bundle();
       * algorithmOptions.putBoolean("case_sensitive", false);
       * args.put("phone", algorithmOptions);
       *
       * service.onCalculateScores(Arrays.asList(AutofillValue.forText("email1"),
       * AutofillValue.forText("PHONE1")), Arrays.asList("email1", "phone1"),
       * Array.asList("email", "phone"), algorithms, args);
       * </pre>
       *
       * <p>Returns:
       *
       * <pre>
       * [
       *   [1.0, 0.0], // "email1" compared against ["email1", "phone1"]
       *   [0.0, 1.0]  // "PHONE1" compared against ["email1", "phone1"]
       * ];
       * </pre>
       *
       * @param actualValues values entered by the user.
       * @param userDataValues values predicted from the user data.
       * @param categoryIds category Ids correspoinding to userDataValues
       * @param defaultAlgorithm default field classification algorithm
       * @param algorithms array of field classification algorithms
       * @return the calculated scores of {@code actualValues} x {@code userDataValues}.
       *
       * {@hide}
       */
      @Nullable
      @SystemApi
      public float[][] onCalculateScores(@NonNull List<AutofillValue> actualValues,
              @NonNull List<String> userDataValues, @NonNull List<String> categoryIds,
              @Nullable String defaultAlgorithm, @Nullable Bundle defaultArgs,
              @Nullable Map algorithms, @Nullable Map args) {
          Log.e(TAG, "service implementation (" + getClass()
                  + " does not implement onCalculateScore()");
          return null;
      }
  
      private final class AutofillFieldClassificationServiceWrapper
              extends IAutofillFieldClassificationService.Stub {
          @Override
          public void calculateScores(RemoteCallback callback, List<AutofillValue> actualValues,
                  String[] userDataValues, String[] categoryIds, String defaultAlgorithm,
                  Bundle defaultArgs, Map algorithms, Map args)
                  throws RemoteException {
              mHandler.sendMessage(obtainMessage(
                      AutofillFieldClassificationService::calculateScores,
                      AutofillFieldClassificationService.this,
                      callback, actualValues, userDataValues, categoryIds, defaultAlgorithm,
                      defaultArgs, algorithms, args));
          }
      }
  
      /**
       * Helper class used to encapsulate a float[][] in a Parcelable.
       *
       * {@hide}
       */
      public static final class Scores implements Parcelable {
          @NonNull
          public final float[][] scores;
  
          private Scores(Parcel parcel) {
              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();
                  }
              }
          }
  
          private Scores(@NonNull float[][] scores) {
              this.scores = scores;
          }
  
          @Override
          public String toString() {
              final int size1 = scores.length;
              final int size2 = size1 > 0 ? scores[0].length : 0;
              final StringBuilder builder = new StringBuilder("Scores [")
                      .append(size1).append("x").append(size2).append("] ");
              for (int i = 0; i < size1; i++) {
                  builder.append(i).append(": ").append(Arrays.toString(scores[i])).append(' ');
              }
              return builder.toString();
          }
  
          @Override
          public int describeContents() {
              return 0;
          }
  
          @Override
          public void writeToParcel(Parcel parcel, int flags) {
              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 @android.annotation.NonNull 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];
              }
          };
      }
  }
  