Read and use user dictionary shortcuts.

Bug: 4646172

Change-Id: I51002c73d5bad1a698110c5cda02253348be8eed
diff --git a/java/src/com/android/inputmethod/latin/ContactsDictionary.java b/java/src/com/android/inputmethod/latin/ContactsDictionary.java
index 8a7dfb8..83bc904 100644
--- a/java/src/com/android/inputmethod/latin/ContactsDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ContactsDictionary.java
@@ -149,7 +149,8 @@
                                 // capitalization of i.
                                 final int wordLen = word.length();
                                 if (wordLen < maxWordLength && wordLen > 1) {
-                                    super.addWord(word, FREQUENCY_FOR_CONTACTS);
+                                    super.addWord(word, null /* shortcut */,
+                                            FREQUENCY_FOR_CONTACTS);
                                     if (!TextUtils.isEmpty(prevWord)) {
                                         super.setBigram(prevWord, word,
                                                 FREQUENCY_FOR_CONTACTS_BIGRAM);
diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java
index a405aa4..1ec678f 100644
--- a/java/src/com/android/inputmethod/latin/Dictionary.java
+++ b/java/src/com/android/inputmethod/latin/Dictionary.java
@@ -24,11 +24,6 @@
  */
 public abstract class Dictionary {
     /**
-     * Whether or not to replicate the typed word in the suggested list, even if it's valid.
-     */
-    protected static final boolean INCLUDE_TYPED_WORD_IF_VALID = false;
-
-    /**
      * The weight to give to a word if it's length is the same as the number of typed characters.
      */
     protected static final int FULL_WORD_SCORE_MULTIPLIER = 2;
diff --git a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
index fe21ebe..7a740b3 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableDictionary.java
@@ -22,6 +22,7 @@
 import com.android.inputmethod.keyboard.Keyboard;
 import com.android.inputmethod.keyboard.ProximityInfo;
 
+import java.util.ArrayList;
 import java.util.LinkedList;
 
 /**
@@ -53,6 +54,8 @@
         boolean mTerminal;
         Node mParent;
         NodeArray mChildren;
+        ArrayList<char[]> mShortcutTargets;
+        boolean mShortcutOnly;
         LinkedList<NextWord> mNGrams; // Supports ngram
     }
 
@@ -150,15 +153,15 @@
         return BinaryDictionary.MAX_WORD_LENGTH;
     }
 
-    public void addWord(String word, int frequency) {
+    public void addWord(final String word, final String shortcutTarget, final int frequency) {
         if (word.length() >= BinaryDictionary.MAX_WORD_LENGTH) {
             return;
         }
-        addWordRec(mRoots, word, 0, frequency, null);
+        addWordRec(mRoots, word, 0, shortcutTarget, frequency, null);
     }
 
     private void addWordRec(NodeArray children, final String word, final int depth,
-            final int frequency, Node parentNode) {
+            final String shortcutTarget, final int frequency, Node parentNode) {
         final int wordLength = word.length();
         if (wordLength <= depth) return;
         final char c = word.charAt(depth);
@@ -172,15 +175,25 @@
                 break;
             }
         }
+        final boolean isShortcutOnly = (null != shortcutTarget);
         if (childNode == null) {
             childNode = new Node();
             childNode.mCode = c;
             childNode.mParent = parentNode;
+            childNode.mShortcutOnly = isShortcutOnly;
             children.add(childNode);
         }
         if (wordLength == depth + 1) {
             // Terminate this word
             childNode.mTerminal = true;
+            if (isShortcutOnly) {
+                if (null == childNode.mShortcutTargets) {
+                    childNode.mShortcutTargets = new ArrayList<char[]>();
+                }
+                childNode.mShortcutTargets.add(shortcutTarget.toCharArray());
+            } else {
+                childNode.mShortcutOnly = false;
+            }
             childNode.mFrequency = Math.max(frequency, childNode.mFrequency);
             if (childNode.mFrequency > 255) childNode.mFrequency = 255;
             return;
@@ -188,7 +201,7 @@
         if (childNode.mChildren == null) {
             childNode.mChildren = new NodeArray();
         }
-        addWordRec(childNode.mChildren, word, depth + 1, frequency, childNode);
+        addWordRec(childNode.mChildren, word, depth + 1, shortcutTarget, frequency, childNode);
     }
 
     @Override
@@ -239,7 +252,13 @@
             if (mRequiresReload) startDictionaryLoadingTaskLocked();
             if (mUpdatingDictionary) return false;
         }
-        return getWordFrequency(word) > -1;
+        final Node node = searchNode(mRoots, word, 0, word.length());
+        // If node is null, we didn't find the word, so it's not valid.
+        // If node.mShortcutOnly is true, then it exists as a shortcut but not as a word,
+        // so that means it's not a valid word.
+        // If node.mShortcutOnly is false, then it exists as a word (it may also exist as
+        // a shortcut, but this does not matter), so it's a valid word.
+        return (node == null) ? false : !node.mShortcutOnly;
     }
 
     /**
@@ -247,7 +266,7 @@
      */
     protected int getWordFrequency(CharSequence word) {
         // Case-sensitive search
-        Node node = searchNode(mRoots, word, 0, word.length());
+        final Node node = searchNode(mRoots, word, 0, word.length());
         return (node == null) ? -1 : node.mFrequency;
     }
 
@@ -262,6 +281,35 @@
     }
 
     /**
+     * Helper method to add a word and its shortcuts.
+     *
+     * @param node the terminal node
+     * @param word the word to insert, as an array of code points
+     * @param depth the depth of the node in the tree
+     * @param finalFreq the frequency for this word
+     * @return whether there is still space for more words. {@see Dictionary.WordCallback#addWord}.
+     */
+    private boolean addWordAndShortcutsFromNode(final Node node, final char[] word, final int depth,
+            final int finalFreq, final WordCallback callback) {
+        if (finalFreq > 0 && !node.mShortcutOnly) {
+            if (!callback.addWord(word, 0, depth + 1, finalFreq, mDicTypeId, Dictionary.UNIGRAM)) {
+                return false;
+            }
+        }
+        if (null != node.mShortcutTargets) {
+            final int length = node.mShortcutTargets.size();
+            for (int shortcutIndex = 0; shortcutIndex < length; ++shortcutIndex) {
+                final char[] shortcut = node.mShortcutTargets.get(shortcutIndex);
+                if (!callback.addWord(shortcut, 0, shortcut.length, finalFreq, mDicTypeId,
+                        Dictionary.UNIGRAM)) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
      * Recursively traverse the tree for words that match the input. Input consists of
      * a list of arrays. Each item in the list is one input character position. An input
      * character is actually an array of multiple possible candidates. This function is not
@@ -313,8 +361,8 @@
                     } else {
                         finalFreq = computeSkippedWordFinalFreq(freq, snr, mInputLength);
                     }
-                    if (!callback.addWord(word, 0, depth + 1, finalFreq, mDicTypeId,
-                            Dictionary.UNIGRAM)) {
+                    if (!addWordAndShortcutsFromNode(node, word, depth, finalFreq, callback)) {
+                        // No space left in the queue, bail out
                         return;
                     }
                 }
@@ -344,18 +392,18 @@
 
                         if (codeSize == inputIndex + 1) {
                             if (terminal) {
-                                if (INCLUDE_TYPED_WORD_IF_VALID
-                                        || !same(word, depth + 1, codes.getTypedWord())) {
-                                    final int finalFreq;
-                                    if (skipPos < 0) {
-                                        finalFreq = freq * snr * addedAttenuation
-                                                * FULL_WORD_SCORE_MULTIPLIER;
-                                    } else {
-                                        finalFreq = computeSkippedWordFinalFreq(freq,
-                                                snr * addedAttenuation, mInputLength);
-                                    }
-                                    callback.addWord(word, 0, depth + 1, finalFreq, mDicTypeId,
-                                            Dictionary.UNIGRAM);
+                                final int finalFreq;
+                                if (skipPos < 0) {
+                                    finalFreq = freq * snr * addedAttenuation
+                                            * FULL_WORD_SCORE_MULTIPLIER;
+                                } else {
+                                    finalFreq = computeSkippedWordFinalFreq(freq,
+                                            snr * addedAttenuation, mInputLength);
+                                }
+                                if (!addWordAndShortcutsFromNode(node, word, depth, finalFreq,
+                                        callback)) {
+                                    // No space left in the queue, bail out
+                                    return;
                                 }
                             }
                             if (children != null) {
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 011b512..165df5d 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -1120,7 +1120,7 @@
 
     @Override
     public boolean addWordToDictionary(String word) {
-        mUserDictionary.addWord(word, 128);
+        mUserDictionary.addWordToUserDictionary(word, 128);
         // Suggestion strip should be updated after the operation of adding word to the
         // user dictionary
         mHandler.postUpdateSuggestions();
diff --git a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserDictionary.java b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserDictionary.java
index 50e8b24..b78be89 100644
--- a/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserDictionary.java
+++ b/java/src/com/android/inputmethod/latin/SynchronouslyLoadedUserDictionary.java
@@ -42,6 +42,6 @@
     @Override
     public synchronized boolean isValidWord(CharSequence word) {
         blockingReloadDictionaryIfRequired();
-        return getWordFrequency(word) > -1;
+        return super.isValidWord(word);
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/UserDictionary.java b/java/src/com/android/inputmethod/latin/UserDictionary.java
index 6beeaac..218bac7 100644
--- a/java/src/com/android/inputmethod/latin/UserDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserDictionary.java
@@ -31,8 +31,11 @@
 
 public class UserDictionary extends ExpandableDictionary {
 
+    // TODO: use Words.SHORTCUT when it's public in the SDK
+    final static String SHORTCUT = "shortcut";
     private static final String[] PROJECTION_QUERY = {
         Words.WORD,
+        SHORTCUT,
         Words.FREQUENCY,
     };
 
@@ -149,15 +152,18 @@
     }
 
     /**
-     * Adds a word to the dictionary and makes it persistent.
+     * Adds a word to the user dictionary and makes it persistent.
+     *
+     * This will call upon the system interface to do the actual work through the intent
+     * readied by the system to this effect.
+     *
      * @param word the word to add. If the word is capitalized, then the dictionary will
      * recognize it as a capitalized word when searched.
      * @param frequency the frequency of occurrence of the word. A frequency of 255 is considered
      * the highest.
      * @TODO use a higher or float range for frequency
      */
-    @Override
-    public synchronized void addWord(final String word, final int frequency) {
+    public synchronized void addWordToUserDictionary(final String word, final int frequency) {
         // Force load the dictionary here synchronously
         if (getRequiresReload()) loadDictionaryAsync();
         // TODO: do something for the UI. With the following, any sufficiently long word will
@@ -191,14 +197,19 @@
         final int maxWordLength = getMaxWordLength();
         if (cursor.moveToFirst()) {
             final int indexWord = cursor.getColumnIndex(Words.WORD);
+            final int indexShortcut = cursor.getColumnIndex(SHORTCUT);
             final int indexFrequency = cursor.getColumnIndex(Words.FREQUENCY);
             while (!cursor.isAfterLast()) {
                 String word = cursor.getString(indexWord);
+                String shortcut = cursor.getString(indexShortcut);
                 int frequency = cursor.getInt(indexFrequency);
                 // Safeguard against adding really long words. Stack may overflow due
                 // to recursion
                 if (word.length() < maxWordLength) {
-                    super.addWord(word, frequency);
+                    super.addWord(word, null, frequency);
+                }
+                if (null != shortcut && shortcut.length() < maxWordLength) {
+                    super.addWord(shortcut, word, frequency);
                 }
                 cursor.moveToNext();
             }
diff --git a/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java b/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java
index 9191aa9..e13602e 100644
--- a/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java
@@ -176,7 +176,7 @@
      * The second word may not be null (a NullPointerException would be thrown).
      */
     public int addToUserHistory(final String word1, String word2) {
-        super.addWord(word2, FREQUENCY_FOR_TYPED);
+        super.addWord(word2, null /* shortcut */, FREQUENCY_FOR_TYPED);
         // Do not insert a word as a bigram of itself
         if (word2.equals(word1)) {
             return 0;
@@ -246,7 +246,7 @@
                     // Safeguard against adding really long words. Stack may overflow due
                     // to recursive lookup
                     if (null == word1) {
-                        super.addWord(word2, frequency);
+                        super.addWord(word2, null /* shortcut */, frequency);
                     } else if (word1.length() < BinaryDictionary.MAX_WORD_LENGTH
                             && word2.length() < BinaryDictionary.MAX_WORD_LENGTH) {
                         super.setBigram(word1, word2, frequency);
diff --git a/java/src/com/android/inputmethod/latin/WhitelistDictionary.java b/java/src/com/android/inputmethod/latin/WhitelistDictionary.java
index bb3ba86..a0de2f9 100644
--- a/java/src/com/android/inputmethod/latin/WhitelistDictionary.java
+++ b/java/src/com/android/inputmethod/latin/WhitelistDictionary.java
@@ -66,7 +66,7 @@
                 if (before != null && after != null) {
                     mWhitelistWords.put(
                             before.toLowerCase(), new Pair<Integer, String>(score, after));
-                    addWord(after, score);
+                    addWord(after, null /* shortcut */, score);
                 }
             }
         } catch (NumberFormatException e) {