Merge "TextClassifier: Switch model based on locale" into oc-dev
diff --git a/core/java/android/view/textclassifier/TextClassificationManager.java b/core/java/android/view/textclassifier/TextClassificationManager.java
index 35c9a29..5487965 100644
--- a/core/java/android/view/textclassifier/TextClassificationManager.java
+++ b/core/java/android/view/textclassifier/TextClassificationManager.java
@@ -44,8 +44,6 @@
     private final Object mLangIdLock = new Object();
 
     private final Context mContext;
-    // TODO: Implement a way to close the file descriptors.
-    private ParcelFileDescriptor mSmartSelectionFd;
     private ParcelFileDescriptor mLangIdFd;
     private TextClassifier mDefault;
     private LangId mLangId;
@@ -61,15 +59,7 @@
     public TextClassifier getDefaultTextClassifier() {
         synchronized (mTextClassifierLock) {
             if (mDefault == null) {
-                try {
-                    mSmartSelectionFd = ParcelFileDescriptor.open(
-                            new File("/etc/textclassifier/textclassifier.smartselection.en.model"),
-                            ParcelFileDescriptor.MODE_READ_ONLY);
-                    mDefault = new TextClassifierImpl(mContext, mSmartSelectionFd);
-                } catch (FileNotFoundException e) {
-                    Log.e(LOG_TAG, "Error accessing 'text classifier selection' model file.", e);
-                    mDefault = TextClassifier.NO_OP;
-                }
+                mDefault = new TextClassifierImpl(mContext);
             }
             return mDefault;
         }
diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java
index 66a62c3..f634a1b 100644
--- a/core/java/android/view/textclassifier/TextClassifierImpl.java
+++ b/core/java/android/view/textclassifier/TextClassifierImpl.java
@@ -38,17 +38,24 @@
 import android.util.Patterns;
 import android.view.View;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.Preconditions;
 
+import java.io.File;
 import java.io.FileNotFoundException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Objects;
+import java.util.StringJoiner;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * Default implementation of the {@link TextClassifier} interface.
@@ -62,16 +69,21 @@
 final class TextClassifierImpl implements TextClassifier {
 
     private static final String LOG_TAG = "TextClassifierImpl";
-
-    private final Object mSmartSelectionLock = new Object();
+    private static final String MODEL_DIR = "/etc/textclassifier/";
+    private static final String MODEL_FILE_REGEX = "textclassifier\\.smartselection\\.(.*)\\.model";
 
     private final Context mContext;
-    private final ParcelFileDescriptor mFd;
+
+    private final Object mSmartSelectionLock = new Object();
+    @GuardedBy("mSmartSelectionLock") // Do not access outside this lock.
+    private Map<Locale, String> mModelFilePaths;
+    @GuardedBy("mSmartSelectionLock") // Do not access outside this lock.
+    private Locale mLocale;
+    @GuardedBy("mSmartSelectionLock") // Do not access outside this lock.
     private SmartSelection mSmartSelection;
 
-    TextClassifierImpl(Context context, ParcelFileDescriptor fd) {
+    TextClassifierImpl(Context context) {
         mContext = Preconditions.checkNotNull(context);
-        mFd = Preconditions.checkNotNull(fd);
     }
 
     @Override
@@ -81,15 +93,16 @@
         validateInput(text, selectionStartIndex, selectionEndIndex);
         try {
             if (text.length() > 0) {
+                final SmartSelection smartSelection = getSmartSelection(defaultLocales);
                 final String string = text.toString();
-                final int[] startEnd = getSmartSelection()
-                        .suggest(string, selectionStartIndex, selectionEndIndex);
+                final int[] startEnd = smartSelection.suggest(
+                        string, selectionStartIndex, selectionEndIndex);
                 final int start = startEnd[0];
                 final int end = startEnd[1];
                 if (start >= 0 && end <= string.length() && start <= end) {
                     final TextSelection.Builder tsBuilder = new TextSelection.Builder(start, end);
                     final SmartSelection.ClassificationResult[] results =
-                            getSmartSelection().classifyText(
+                            smartSelection.classifyText(
                                     string, start, end,
                                     getHintFlags(string, start, end));
                     final int size = results.length;
@@ -120,7 +133,7 @@
         try {
             if (text.length() > 0) {
                 final String string = text.toString();
-                SmartSelection.ClassificationResult[] results = getSmartSelection()
+                SmartSelection.ClassificationResult[] results = getSmartSelection(defaultLocales)
                         .classifyText(string, startIndex, endIndex,
                                 getHintFlags(string, startIndex, endIndex));
                 if (results.length > 0) {
@@ -147,7 +160,7 @@
         Preconditions.checkArgument(text != null);
         try {
             return LinksInfoFactory.create(
-                    mContext, getSmartSelection(), text.toString(), linkMask);
+                    mContext, getSmartSelection(defaultLocales), text.toString(), linkMask);
         } catch (Throwable t) {
             // Avoid throwing from this method. Log the error.
             Log.e(LOG_TAG, "Error getting links info.", t);
@@ -156,15 +169,69 @@
         return TextClassifier.NO_OP.getLinks(text, linkMask, defaultLocales);
     }
 
-    private SmartSelection getSmartSelection() throws FileNotFoundException {
+    private SmartSelection getSmartSelection(LocaleList localeList) throws FileNotFoundException {
         synchronized (mSmartSelectionLock) {
-            if (mSmartSelection == null) {
-                mSmartSelection = new SmartSelection(mFd.getFd());
+            localeList = localeList == null ? LocaleList.getEmptyLocaleList() : localeList;
+            final Locale locale = findBestSupportedLocaleLocked(localeList);
+            if (mSmartSelection == null || !Objects.equals(mLocale, locale)) {
+                destroySmartSelectionIfExistsLocked();
+                mSmartSelection = new SmartSelection(
+                        ParcelFileDescriptor.open(
+                                // findBestSupportedLocaleLocked should have initialized
+                                // mModelFilePaths
+                                new File(mModelFilePaths.get(locale)),
+                                ParcelFileDescriptor.MODE_READ_ONLY)
+                                .getFd());
+                mLocale = locale;
             }
             return mSmartSelection;
         }
     }
 
+    @GuardedBy("mSmartSelectionLock") // Do not call outside this lock.
+    private void destroySmartSelectionIfExistsLocked() {
+        if (mSmartSelection != null) {
+            mSmartSelection.close();
+            mSmartSelection = null;
+        }
+    }
+
+    @GuardedBy("mSmartSelectionLock") // Do not call outside this lock.
+    @Nullable
+    private Locale findBestSupportedLocaleLocked(LocaleList localeList) {
+        final List<Locale.LanguageRange> languageRangeList = Locale.LanguageRange.parse(
+                new StringJoiner(",")
+                        // Specified localeList takes priority over the system default
+                        .add(localeList.toLanguageTags())
+                        .add(LocaleList.getDefault().toLanguageTags())
+                        .toString());
+        return Locale.lookup(languageRangeList, loadModelFilePathsLocked().keySet());
+    }
+
+    @GuardedBy("mSmartSelectionLock") // Do not call outside this lock.
+    private Map<Locale, String> loadModelFilePathsLocked() {
+        if (mModelFilePaths == null) {
+            final Map<Locale, String> modelFilePaths = new HashMap<>();
+            final File modelsDir = new File(MODEL_DIR);
+            if (modelsDir.exists() && modelsDir.isDirectory()) {
+                final File[] models = modelsDir.listFiles();
+                final Pattern modelFilenamePattern = Pattern.compile(MODEL_FILE_REGEX);
+                final int size = models.length;
+                for (int i = 0; i < size; i++) {
+                    final File modelFile = models[i];
+                    final Matcher matcher = modelFilenamePattern.matcher(modelFile.getName());
+                    if (matcher.matches() && modelFile.isFile()) {
+                        final String language = matcher.group(1);
+                        final Locale locale = Locale.forLanguageTag(language);
+                        modelFilePaths.put(locale, modelFile.getAbsolutePath());
+                    }
+                }
+            }
+            mModelFilePaths = modelFilePaths;
+        }
+        return mModelFilePaths;
+    }
+
     private TextClassificationResult createClassificationResult(
             SmartSelection.ClassificationResult[] classifications, CharSequence text) {
         final TextClassificationResult.Builder builder = new TextClassificationResult.Builder()