Merge "Flag to configure model parameters" into qt-dev
diff --git a/core/java/android/view/textclassifier/ActionsModelParamsSupplier.java b/core/java/android/view/textclassifier/ActionsModelParamsSupplier.java
new file mode 100644
index 0000000..6b90588
--- /dev/null
+++ b/core/java/android/view/textclassifier/ActionsModelParamsSupplier.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2019 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.view.textclassifier;
+
+import android.annotation.Nullable;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.KeyValueListParser;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+
+import java.lang.ref.WeakReference;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+/**
+ * Parses the {@link Settings.Global#TEXT_CLASSIFIER_ACTION_MODEL_PARAMS} flag.
+ *
+ * @hide
+ */
+public final class ActionsModelParamsSupplier implements
+        Supplier<ActionsModelParamsSupplier.ActionsModelParams> {
+    private static final String TAG = TextClassifier.DEFAULT_LOG_TAG;
+
+    @VisibleForTesting
+    static final String KEY_REQUIRED_MODEL_VERSION = "required_model_version";
+    @VisibleForTesting
+    static final String KEY_REQUIRED_LOCALES = "required_locales";
+    @VisibleForTesting
+    static final String KEY_SERIALIZED_PRECONDITIONS = "serialized_preconditions";
+
+    private final Context mAppContext;
+    private final SettingsObserver mSettingsObserver;
+
+    private final Object mLock = new Object();
+    private final Runnable mOnChangedListener;
+    @Nullable
+    @GuardedBy("mLock")
+    private ActionsModelParams mActionsModelParams;
+    @GuardedBy("mLock")
+    private boolean mParsed = true;
+
+    public ActionsModelParamsSupplier(Context context, @Nullable Runnable onChangedListener) {
+        mAppContext = Preconditions.checkNotNull(context).getApplicationContext();
+        mOnChangedListener = onChangedListener == null ? () -> {} : onChangedListener;
+        mSettingsObserver = new SettingsObserver(mAppContext, () -> {
+            synchronized (mLock) {
+                Log.v(TAG, "Settings.Global.TEXT_CLASSIFIER_ACTION_MODEL_PARAMS is updated");
+                mParsed = true;
+                mOnChangedListener.run();
+            }
+        });
+    }
+
+    /**
+     * Returns the parsed actions params or {@link ActionsModelParams#INVALID} if the value is
+     * invalid.
+     */
+    @Override
+    public ActionsModelParams get() {
+        synchronized (mLock) {
+            if (mParsed) {
+                mActionsModelParams = parse(mAppContext.getContentResolver());
+                mParsed = false;
+            }
+        }
+        return mActionsModelParams;
+    }
+
+    private ActionsModelParams parse(ContentResolver contentResolver) {
+        String settingStr = Settings.Global.getString(contentResolver,
+                Settings.Global.TEXT_CLASSIFIER_ACTION_MODEL_PARAMS);
+        if (TextUtils.isEmpty(settingStr)) {
+            return ActionsModelParams.INVALID;
+        }
+        try {
+            KeyValueListParser keyValueListParser = new KeyValueListParser(',');
+            keyValueListParser.setString(settingStr);
+            int version = keyValueListParser.getInt(KEY_REQUIRED_MODEL_VERSION, -1);
+            if (version == -1) {
+                Log.w(TAG, "ActionsModelParams.Parse, invalid model version");
+                return ActionsModelParams.INVALID;
+            }
+            String locales = keyValueListParser.getString(KEY_REQUIRED_LOCALES, null);
+            if (locales == null) {
+                Log.w(TAG, "ActionsModelParams.Parse, invalid locales");
+                return ActionsModelParams.INVALID;
+            }
+            String serializedPreconditionsStr =
+                    keyValueListParser.getString(KEY_SERIALIZED_PRECONDITIONS, null);
+            if (serializedPreconditionsStr == null) {
+                Log.w(TAG, "ActionsModelParams.Parse, invalid preconditions");
+                return ActionsModelParams.INVALID;
+            }
+            byte[] serializedPreconditions =
+                    Base64.decode(serializedPreconditionsStr, Base64.NO_WRAP);
+            return new ActionsModelParams(version, locales, serializedPreconditions);
+        } catch (Throwable t) {
+            Log.e(TAG, "Invalid TEXT_CLASSIFIER_ACTION_MODEL_PARAMS, ignore", t);
+        }
+        return ActionsModelParams.INVALID;
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            mAppContext.getContentResolver().unregisterContentObserver(mSettingsObserver);
+        } finally {
+            super.finalize();
+        }
+    }
+
+    /**
+     * Represents the parsed result.
+     */
+    public static final class ActionsModelParams {
+
+        public static final ActionsModelParams INVALID =
+                new ActionsModelParams(-1, "", new byte[0]);
+
+        /**
+         * The required model version to apply {@code mSerializedPreconditions}.
+         */
+        private final int mRequiredModelVersion;
+
+        /**
+         * The required model locales to apply {@code mSerializedPreconditions}.
+         */
+        private final String mRequiredModelLocales;
+
+        /**
+         * The serialized params that will be applied to the model file, if all requirements are
+         * met. Do not modify.
+         */
+        private final byte[] mSerializedPreconditions;
+
+        public ActionsModelParams(int requiredModelVersion, String requiredModelLocales,
+                byte[] serializedPreconditions) {
+            mRequiredModelVersion = requiredModelVersion;
+            mRequiredModelLocales = Preconditions.checkNotNull(requiredModelLocales);
+            mSerializedPreconditions = Preconditions.checkNotNull(serializedPreconditions);
+        }
+
+        /**
+         * Returns the serialized preconditions. Returns {@code null} if the the model in use does
+         * not meet all the requirements listed in the {@code ActionsModelParams} or the params
+         * are invalid.
+         */
+        @Nullable
+        public byte[] getSerializedPreconditions(ModelFileManager.ModelFile modelInUse) {
+            if (this == INVALID) {
+                return null;
+            }
+            if (modelInUse.getVersion() != mRequiredModelVersion) {
+                Log.w(TAG, String.format(
+                        "Not applying mSerializedPreconditions, required version=%d, actual=%d",
+                        mRequiredModelVersion, modelInUse.getVersion()));
+                return null;
+            }
+            if (!Objects.equals(modelInUse.getSupportedLocalesStr(), mRequiredModelLocales)) {
+                Log.w(TAG, String.format(
+                        "Not applying mSerializedPreconditions, required locales=%s, actual=%s",
+                        mRequiredModelLocales, modelInUse.getSupportedLocalesStr()));
+                return null;
+            }
+            return mSerializedPreconditions;
+        }
+    }
+
+    private static final class SettingsObserver extends ContentObserver {
+
+        private final WeakReference<Runnable> mOnChangedListener;
+
+        SettingsObserver(Context appContext, Runnable listener) {
+            super(null);
+            mOnChangedListener = new WeakReference<>(listener);
+            appContext.getContentResolver().registerContentObserver(
+                    Settings.Global.getUriFor(Settings.Global.TEXT_CLASSIFIER_ACTION_MODEL_PARAMS),
+                    false /* notifyForDescendants */,
+                    this);
+        }
+
+        public void onChange(boolean selfChange) {
+            if (mOnChangedListener.get() != null) {
+                mOnChangedListener.get().run();
+            }
+        }
+    }
+}
diff --git a/core/java/android/view/textclassifier/ModelFileManager.java b/core/java/android/view/textclassifier/ModelFileManager.java
index 8558a46..e04285d 100644
--- a/core/java/android/view/textclassifier/ModelFileManager.java
+++ b/core/java/android/view/textclassifier/ModelFileManager.java
@@ -167,6 +167,7 @@
                         file,
                         version,
                         supportedLocales,
+                        supportedLocalesStr,
                         ModelFile.LANGUAGE_INDEPENDENT.equals(supportedLocalesStr));
             } catch (FileNotFoundException e) {
                 Log.e(DEFAULT_LOG_TAG, "Failed to find " + file.getAbsolutePath(), e);
@@ -201,13 +202,16 @@
         private final File mFile;
         private final int mVersion;
         private final List<Locale> mSupportedLocales;
+        private final String mSupportedLocalesStr;
         private final boolean mLanguageIndependent;
 
         public ModelFile(File file, int version, List<Locale> supportedLocales,
+                String supportedLocalesStr,
                 boolean languageIndependent) {
             mFile = Preconditions.checkNotNull(file);
             mVersion = version;
             mSupportedLocales = Preconditions.checkNotNull(supportedLocales);
+            mSupportedLocalesStr = Preconditions.checkNotNull(supportedLocalesStr);
             mLanguageIndependent = languageIndependent;
         }
 
@@ -237,6 +241,11 @@
             return Collections.unmodifiableList(mSupportedLocales);
         }
 
+        /** Returns the original supported locals string read from the model file. */
+        public String getSupportedLocalesStr() {
+            return mSupportedLocalesStr;
+        }
+
         /**
          * Returns if this model file is preferred to the given one.
          */
diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java
index 8f40a3e..323bf59 100644
--- a/core/java/android/view/textclassifier/TextClassifierImpl.java
+++ b/core/java/android/view/textclassifier/TextClassifierImpl.java
@@ -29,6 +29,7 @@
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Pair;
+import android.view.textclassifier.ActionsModelParamsSupplier.ActionsModelParams;
 import android.view.textclassifier.intent.ClassificationIntentFactory;
 import android.view.textclassifier.intent.LabeledIntent;
 import android.view.textclassifier.intent.LegacyClassificationIntentFactory;
@@ -57,6 +58,7 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.function.Supplier;
 
 /**
  * Default implementation of the {@link TextClassifier} interface.
@@ -124,6 +126,7 @@
 
     private final ClassificationIntentFactory mClassificationIntentFactory;
     private final TemplateIntentFactory mTemplateIntentFactory;
+    private final Supplier<ActionsModelParams> mActionsModelParamsSupplier;
 
     public TextClassifierImpl(
             Context context, TextClassificationConstants settings, TextClassifier fallback) {
@@ -158,6 +161,15 @@
                 ? new TemplateClassificationIntentFactory(
                 mTemplateIntentFactory, new LegacyClassificationIntentFactory())
                 : new LegacyClassificationIntentFactory();
+        mActionsModelParamsSupplier = new ActionsModelParamsSupplier(mContext,
+                () -> {
+                    synchronized (mLock) {
+                        // Clear mActionsImpl here, so that we will create a new
+                        // ActionsSuggestionsModel object with the new flag in the next request.
+                        mActionsImpl = null;
+                        mActionModelInUse = null;
+                    }
+                });
     }
 
     public TextClassifierImpl(Context context, TextClassificationConstants settings) {
@@ -584,10 +596,14 @@
                 final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
                         new File(bestModel.getPath()), ParcelFileDescriptor.MODE_READ_ONLY);
                 try {
-                    if (pfd != null) {
-                        mActionsImpl = new ActionsSuggestionsModel(pfd.getFd());
-                        mActionModelInUse = bestModel;
+                    if (pfd == null) {
+                        Log.d(LOG_TAG, "Failed to read the model file: " + bestModel.getPath());
+                        return null;
                     }
+                    ActionsModelParams params = mActionsModelParamsSupplier.get();
+                    mActionsImpl = new ActionsSuggestionsModel(
+                            pfd.getFd(), params.getSerializedPreconditions(bestModel));
+                    mActionModelInUse = bestModel;
                 } finally {
                     maybeCloseAndLogError(pfd);
                 }
diff --git a/core/tests/coretests/src/android/view/textclassifier/ActionsModelParamsSupplierTest.java b/core/tests/coretests/src/android/view/textclassifier/ActionsModelParamsSupplierTest.java
new file mode 100644
index 0000000..8744997
--- /dev/null
+++ b/core/tests/coretests/src/android/view/textclassifier/ActionsModelParamsSupplierTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2019 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.view.textclassifier;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.Locale;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ActionsModelParamsSupplierTest {
+
+    @Test
+    public void getSerializedPreconditions_validActionsModelParams() {
+        ModelFileManager.ModelFile modelFile = new ModelFileManager.ModelFile(
+                new File("/model/file"),
+                200 /* version */,
+                Collections.singletonList(Locale.forLanguageTag("en")),
+                "en",
+                false);
+        byte[] serializedPreconditions = new byte[]{0x12, 0x24, 0x36};
+        ActionsModelParamsSupplier.ActionsModelParams params =
+                new ActionsModelParamsSupplier.ActionsModelParams(
+                        200 /* version */,
+                        "en",
+                        serializedPreconditions);
+
+        byte[] actual = params.getSerializedPreconditions(modelFile);
+
+        assertThat(actual).isEqualTo(serializedPreconditions);
+    }
+
+    @Test
+    public void getSerializedPreconditions_invalidVersion() {
+        ModelFileManager.ModelFile modelFile = new ModelFileManager.ModelFile(
+                new File("/model/file"),
+                201 /* version */,
+                Collections.singletonList(Locale.forLanguageTag("en")),
+                "en",
+                false);
+        byte[] serializedPreconditions = new byte[]{0x12, 0x24, 0x36};
+        ActionsModelParamsSupplier.ActionsModelParams params =
+                new ActionsModelParamsSupplier.ActionsModelParams(
+                        200 /* version */,
+                        "en",
+                        serializedPreconditions);
+
+        byte[] actual = params.getSerializedPreconditions(modelFile);
+
+        assertThat(actual).isNull();
+    }
+
+    @Test
+    public void getSerializedPreconditions_invalidLocales() {
+        final String LANGUAGE_TAG = "zh";
+        ModelFileManager.ModelFile modelFile = new ModelFileManager.ModelFile(
+                new File("/model/file"),
+                200 /* version */,
+                Collections.singletonList(Locale.forLanguageTag(LANGUAGE_TAG)),
+                LANGUAGE_TAG,
+                false);
+        byte[] serializedPreconditions = new byte[]{0x12, 0x24, 0x36};
+        ActionsModelParamsSupplier.ActionsModelParams params =
+                new ActionsModelParamsSupplier.ActionsModelParams(
+                        200 /* version */,
+                        "en",
+                        serializedPreconditions);
+
+        byte[] actual = params.getSerializedPreconditions(modelFile);
+
+        assertThat(actual).isNull();
+    }
+
+}
diff --git a/core/tests/coretests/src/android/view/textclassifier/ModelFileManagerTest.java b/core/tests/coretests/src/android/view/textclassifier/ModelFileManagerTest.java
index 74b8e3b..79e1406 100644
--- a/core/tests/coretests/src/android/view/textclassifier/ModelFileManagerTest.java
+++ b/core/tests/coretests/src/android/view/textclassifier/ModelFileManagerTest.java
@@ -86,7 +86,7 @@
     public void get() {
         ModelFileManager.ModelFile modelFile =
                 new ModelFileManager.ModelFile(
-                        new File("/path/a"), 1, Collections.emptyList(), true);
+                        new File("/path/a"), 1, Collections.emptyList(), "", true);
         when(mModelFileSupplier.get()).thenReturn(Collections.singletonList(modelFile));
 
         List<ModelFileManager.ModelFile> modelFiles = mModelFileManager.listModelFiles();
@@ -100,12 +100,12 @@
         ModelFileManager.ModelFile olderModelFile =
                 new ModelFileManager.ModelFile(
                         new File("/path/a"), 1,
-                        Collections.emptyList(), true);
+                        Collections.emptyList(), "", true);
 
         ModelFileManager.ModelFile newerModelFile =
                 new ModelFileManager.ModelFile(
                         new File("/path/b"), 2,
-                        Collections.emptyList(), true);
+                        Collections.emptyList(), "", true);
         when(mModelFileSupplier.get())
                 .thenReturn(Arrays.asList(olderModelFile, newerModelFile));
 
@@ -121,12 +121,12 @@
         ModelFileManager.ModelFile languageIndependentModelFile =
                 new ModelFileManager.ModelFile(
                         new File("/path/a"), 1,
-                        Collections.emptyList(), true);
+                        Collections.emptyList(), "", true);
 
         ModelFileManager.ModelFile languageDependentModelFile =
                 new ModelFileManager.ModelFile(
                         new File("/path/b"), 1,
-                        Collections.singletonList(locale), false);
+                        Collections.singletonList(locale), locale.toLanguageTag(), false);
         when(mModelFileSupplier.get())
                 .thenReturn(
                         Arrays.asList(languageIndependentModelFile, languageDependentModelFile));
@@ -143,12 +143,12 @@
         ModelFileManager.ModelFile languageIndependentModelFile =
                 new ModelFileManager.ModelFile(
                         new File("/path/a"), 1,
-                        Collections.emptyList(), true);
+                        Collections.emptyList(), "", true);
 
         ModelFileManager.ModelFile languageDependentModelFile =
                 new ModelFileManager.ModelFile(
                         new File("/path/b"), 1,
-                        Collections.singletonList(locale), false);
+                        Collections.singletonList(locale), locale.toLanguageTag(), false);
 
         when(mModelFileSupplier.get())
                 .thenReturn(
@@ -165,12 +165,13 @@
         ModelFileManager.ModelFile languageIndependentModelFile =
                 new ModelFileManager.ModelFile(
                         new File("/path/a"), 1,
-                        Collections.emptyList(), true);
+                        Collections.emptyList(), "", true);
 
         ModelFileManager.ModelFile languageDependentModelFile =
                 new ModelFileManager.ModelFile(
                         new File("/path/b"), 1,
-                        Collections.singletonList(DEFAULT_LOCALE), false);
+                        Collections.singletonList(
+                                DEFAULT_LOCALE), DEFAULT_LOCALE.toLanguageTag(), false);
 
         when(mModelFileSupplier.get())
                 .thenReturn(
@@ -187,12 +188,12 @@
         ModelFileManager.ModelFile matchButOlderModel =
                 new ModelFileManager.ModelFile(
                         new File("/path/a"), 1,
-                        Collections.singletonList(Locale.forLanguageTag("fr")), false);
+                        Collections.singletonList(Locale.forLanguageTag("fr")), "fr", false);
 
         ModelFileManager.ModelFile mismatchButNewerModel =
                 new ModelFileManager.ModelFile(
                         new File("/path/b"), 2,
-                        Collections.singletonList(Locale.forLanguageTag("ja")), false);
+                        Collections.singletonList(Locale.forLanguageTag("ja")), "ja", false);
 
         when(mModelFileSupplier.get())
                 .thenReturn(
@@ -209,12 +210,12 @@
         ModelFileManager.ModelFile matchLocaleModel =
                 new ModelFileManager.ModelFile(
                         new File("/path/b"), 1,
-                        Collections.singletonList(Locale.forLanguageTag("ja")), false);
+                        Collections.singletonList(Locale.forLanguageTag("ja")), "ja", false);
 
         ModelFileManager.ModelFile languageIndependentModel =
                 new ModelFileManager.ModelFile(
                         new File("/path/a"), 2,
-                        Collections.emptyList(), true);
+                        Collections.emptyList(), "", true);
         when(mModelFileSupplier.get())
                 .thenReturn(
                         Arrays.asList(matchLocaleModel, languageIndependentModel));
@@ -231,12 +232,12 @@
         ModelFileManager.ModelFile modelA =
                 new ModelFileManager.ModelFile(
                         new File("/path/a"), 1,
-                        Collections.singletonList(Locale.forLanguageTag("ja")), false);
+                        Collections.singletonList(Locale.forLanguageTag("ja")), "ja", false);
 
         ModelFileManager.ModelFile modelB =
                 new ModelFileManager.ModelFile(
                         new File("/path/a"), 1,
-                        Collections.singletonList(Locale.forLanguageTag("ja")), false);
+                        Collections.singletonList(Locale.forLanguageTag("ja")), "ja", false);
 
         assertThat(modelA).isEqualTo(modelB);
     }
@@ -246,12 +247,12 @@
         ModelFileManager.ModelFile modelA =
                 new ModelFileManager.ModelFile(
                         new File("/path/a"), 1,
-                        Collections.singletonList(Locale.forLanguageTag("ja")), false);
+                        Collections.singletonList(Locale.forLanguageTag("ja")), "ja", false);
 
         ModelFileManager.ModelFile modelB =
                 new ModelFileManager.ModelFile(
                         new File("/path/b"), 1,
-                        Collections.singletonList(Locale.forLanguageTag("ja")), false);
+                        Collections.singletonList(Locale.forLanguageTag("ja")), "ja", false);
 
         assertThat(modelA).isNotEqualTo(modelB);
     }
@@ -262,7 +263,7 @@
         ModelFileManager.ModelFile modelA =
                 new ModelFileManager.ModelFile(
                         new File("/path/a"), 1,
-                        Collections.singletonList(Locale.forLanguageTag("ja")), false);
+                        Collections.singletonList(Locale.forLanguageTag("ja")), "ja", false);
 
         assertThat(modelA.getPath()).isEqualTo("/path/a");
     }
@@ -272,7 +273,7 @@
         ModelFileManager.ModelFile modelA =
                 new ModelFileManager.ModelFile(
                         new File("/path/a"), 1,
-                        Collections.singletonList(Locale.forLanguageTag("ja")), false);
+                        Collections.singletonList(Locale.forLanguageTag("ja")), "ja", false);
 
         assertThat(modelA.getName()).isEqualTo("a");
     }
@@ -282,12 +283,12 @@
         ModelFileManager.ModelFile modelA =
                 new ModelFileManager.ModelFile(
                         new File("/path/a"), 1,
-                        Collections.singletonList(Locale.forLanguageTag("ja")), false);
+                        Collections.singletonList(Locale.forLanguageTag("ja")), "ja", false);
 
         ModelFileManager.ModelFile modelB =
                 new ModelFileManager.ModelFile(
                         new File("/path/b"), 2,
-                        Collections.emptyList(), true);
+                        Collections.emptyList(), "", true);
 
         assertThat(modelA.isPreferredTo(modelB)).isTrue();
     }
@@ -297,12 +298,12 @@
         ModelFileManager.ModelFile modelA =
                 new ModelFileManager.ModelFile(
                         new File("/path/a"), 2,
-                        Collections.singletonList(Locale.forLanguageTag("ja")), false);
+                        Collections.singletonList(Locale.forLanguageTag("ja")), "ja", false);
 
         ModelFileManager.ModelFile modelB =
                 new ModelFileManager.ModelFile(
                         new File("/path/b"), 1,
-                        Collections.emptyList(), false);
+                        Collections.emptyList(), "", false);
 
         assertThat(modelA.isPreferredTo(modelB)).isTrue();
     }