Merge "Move policy and session to AOSP"
diff --git a/java/AndroidManifest.xml b/java/AndroidManifest.xml
index 80cd085..17d11c0 100644
--- a/java/AndroidManifest.xml
+++ b/java/AndroidManifest.xml
@@ -95,6 +95,12 @@
             </intent-filter>
         </receiver>
 
+        <receiver android:name=".DictionaryPackInstallBroadcastReceiver">
+            <intent-filter>
+                <action android:name="com.android.inputmethod.dictionarypack.UNKNOWN_CLIENT" />
+            </intent-filter>
+        </receiver>
+
         <provider android:name="com.android.inputmethod.dictionarypack.DictionaryProvider"
                   android:grantUriPermissions="true"
                   android:exported="false"
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryPackConstants.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryPackConstants.java
index 0c8b466..6961588 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionaryPackConstants.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryPackConstants.java
@@ -25,16 +25,34 @@
  */
 public class DictionaryPackConstants {
     /**
+     * The root domain for the dictionary pack, upon which authorities and actions will append
+     * their own distinctive strings.
+     */
+    private static final String DICTIONARY_DOMAIN = "com.android.inputmethod.dictionarypack";
+
+    /**
      * Authority for the ContentProvider protocol.
      */
     // TODO: find some way to factorize this string with the one in the resources
-    public static final String AUTHORITY = "com.android.inputmethod.dictionarypack.aosp";
+    public static final String AUTHORITY = DICTIONARY_DOMAIN + ".aosp";
 
     /**
      * The action of the intent for publishing that new dictionary data is available.
      */
     // TODO: make this different across different packages. A suggested course of action is
     // to use the package name inside this string.
-    public static final String NEW_DICTIONARY_INTENT_ACTION =
-            "com.android.inputmethod.dictionarypack.newdict";
+    // NOTE: The appended string should be uppercase like all other actions, but it's not for
+    // historical reasons.
+    public static final String NEW_DICTIONARY_INTENT_ACTION = DICTIONARY_DOMAIN + ".newdict";
+
+    /**
+     * The action of the intent sent by the dictionary pack to ask for a client to make
+     * itself known. This is used when the settings activity is brought up for a client the
+     * dictionary pack does not know about.
+     */
+    public static final String UNKNOWN_DICTIONARY_PROVIDER_CLIENT = DICTIONARY_DOMAIN
+            + ".UNKNOWN_CLIENT";
+    // In the above intents, the name of the string extra that contains the name of the client
+    // we want information about.
+    public static final String DICTIONARY_PROVIDER_CLIENT_EXTRA = "client";
 }
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java
index 77b3b8e..f8d1c4f 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java
@@ -509,6 +509,11 @@
                 } catch (final BadFormatException e) {
                     Log.w(TAG, "Not enough information to insert this dictionary " + values, e);
                 }
+                // We just received new information about the list of dictionary for this client.
+                // For all intents and purposes, this is new metadata, so we should publish it
+                // so that any listeners (like the Settings interface for example) can update
+                // themselves.
+                UpdateHandler.publishUpdateMetadataCompleted(getContext(), true);
                 break;
             case DICTIONARY_V1_WHOLE_LIST:
             case DICTIONARY_V1_DICT_INFO:
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java
index e85bb0d..9e27c1f 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java
@@ -110,6 +110,15 @@
         super.onResume();
         mChangedSettings = false;
         UpdateHandler.registerUpdateEventListener(this);
+        final Activity activity = getActivity();
+        if (!MetadataDbHelper.isClientKnown(activity, mClientId)) {
+            Log.i(TAG, "Unknown dictionary pack client: " + mClientId + ". Requesting info.");
+            final Intent unknownClientBroadcast =
+                    new Intent(DictionaryPackConstants.UNKNOWN_DICTIONARY_PROVIDER_CLIENT);
+            unknownClientBroadcast.putExtra(
+                    DictionaryPackConstants.DICTIONARY_PROVIDER_CLIENT_EXTRA, mClientId);
+            activity.sendBroadcast(unknownClientBroadcast);
+        }
         final IntentFilter filter = new IntentFilter();
         filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
         getActivity().registerReceiver(mConnectivityChangedReceiver, filter);
@@ -363,7 +372,12 @@
                             getActivity(), android.R.anim.fade_out));
                     preferenceView.startAnimation(AnimationUtils.loadAnimation(
                             getActivity(), android.R.anim.fade_in));
-                    mUpdateNowMenu.setTitle(R.string.check_for_updates_now);
+                    // The menu is created by the framework asynchronously after the activity,
+                    // which means it's possible to have the activity running but the menu not
+                    // created yet - hence the necessity for a null check here.
+                    if (null != mUpdateNowMenu) {
+                        mUpdateNowMenu.setTitle(R.string.check_for_updates_now);
+                    }
                 }
             });
     }
diff --git a/java/src/com/android/inputmethod/dictionarypack/EventHandler.java b/java/src/com/android/inputmethod/dictionarypack/EventHandler.java
index 96c4a83..d8aa33b 100644
--- a/java/src/com/android/inputmethod/dictionarypack/EventHandler.java
+++ b/java/src/com/android/inputmethod/dictionarypack/EventHandler.java
@@ -16,13 +16,9 @@
 
 package com.android.inputmethod.dictionarypack;
 
-import com.android.inputmethod.latin.LatinIME;
-import com.android.inputmethod.latin.R;
-
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
-import android.util.Log;
 
 public final class EventHandler extends BroadcastReceiver {
     private static final String TAG = EventHandler.class.getName();
diff --git a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
index b472750..e05a79b 100644
--- a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
+++ b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
@@ -444,7 +444,19 @@
         manager.remove(fileId);
     }
 
-    private static void publishUpdateMetadataCompleted(final Context context,
+    /**
+     * Sends a broadcast informing listeners that the dictionaries were updated.
+     *
+     * This will call all local listeners through the UpdateEventListener#downloadedMetadata
+     * callback (for example, the dictionary provider interface uses this to stop the Loading
+     * animation) and send a broadcast about the metadata having been updated. For a client of
+     * the dictionary pack like Latin IME, this means it should re-query the dictionary pack
+     * for any relevant new data.
+     *
+     * @param context the context, to send the broadcast.
+     * @param downloadSuccessful whether the download of the metadata was successful or not.
+     */
+    public static void publishUpdateMetadataCompleted(final Context context,
             final boolean downloadSuccessful) {
         // We need to warn all listeners of what happened. But some listeners may want to
         // remove themselves or re-register something in response. Hence we should take a
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
index 4bec99c..562e1d0 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
@@ -450,4 +450,25 @@
                     info.toContentValues());
         }
     }
+
+    /**
+     * Initialize a client record with the dictionary content provider.
+     *
+     * This merely acquires the content provider and calls
+     * #reinitializeClientRecordInDictionaryContentProvider.
+     *
+     * @param context the context for resources and providers.
+     * @param clientId the client ID to use.
+     */
+    public static void initializeClientRecordHelper(final Context context,
+            final String clientId) {
+        try {
+            final ContentProviderClient client = context.getContentResolver().
+                    acquireContentProviderClient(getProviderUriBuilder("").build());
+            if (null == client) return;
+            reinitializeClientRecordInDictionaryContentProvider(context, client, clientId);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Cannot contact the dictionary content provider", e);
+        }
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java b/java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java
index 35f3119..41fcb83 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryPackInstallBroadcastReceiver.java
@@ -25,14 +25,35 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ProviderInfo;
 import android.net.Uri;
+import android.util.Log;
 
 /**
- * Takes action to reload the necessary data when a dictionary pack was added/removed.
+ * Receives broadcasts pertaining to dictionary management and takes the appropriate action.
+ *
+ * This object receives three types of broadcasts.
+ * - Package installed/added. When a dictionary provider application is added or removed, we
+ * need to query the dictionaries.
+ * - New dictionary broadcast. The dictionary provider broadcasts new dictionary availability. When
+ * this happens, we need to re-query the dictionaries.
+ * - Unknown client. If the dictionary provider is in urgent need of data about some client that
+ * it does not know, it sends this broadcast. When we receive this, we need to tell the dictionary
+ * provider about ourselves. This happens when the settings for the dictionary pack are accessed,
+ * but Latin IME never got a chance to register itself.
  */
 public final class DictionaryPackInstallBroadcastReceiver extends BroadcastReceiver {
+    private static final String TAG = DictionaryPackInstallBroadcastReceiver.class.getSimpleName();
 
     final LatinIME mService;
 
+    public DictionaryPackInstallBroadcastReceiver() {
+        // This empty constructor is necessary for the system to instantiate this receiver.
+        // This happens when the dictionary pack says it can't find a record for our client,
+        // which happens when the dictionary pack settings are called before the keyboard
+        // was ever started once.
+        Log.i(TAG, "Latin IME dictionary broadcast receiver instantiated from the framework.");
+        mService = null;
+    }
+
     public DictionaryPackInstallBroadcastReceiver(final LatinIME service) {
         mService = service;
     }
@@ -44,6 +65,11 @@
 
         // We need to reread the dictionary if a new dictionary package is installed.
         if (action.equals(Intent.ACTION_PACKAGE_ADDED)) {
+            if (null == mService) {
+                Log.e(TAG, "Called with intent " + action + " but we don't know the service: this "
+                        + "should never happen");
+                return;
+            }
             final Uri packageUri = intent.getData();
             if (null == packageUri) return; // No package name : we can't do anything
             final String packageName = packageUri.getSchemeSpecificPart();
@@ -71,6 +97,11 @@
             return;
         } else if (action.equals(Intent.ACTION_PACKAGE_REMOVED)
                 && !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
+            if (null == mService) {
+                Log.e(TAG, "Called with intent " + action + " but we don't know the service: this "
+                        + "should never happen");
+                return;
+            }
             // When the dictionary package is removed, we need to reread dictionary (to use the
             // next-priority one, or stop using a dictionary at all if this was the only one,
             // since this is the user request).
@@ -82,7 +113,28 @@
             // read dictionary from?
             mService.resetSuggestMainDict();
         } else if (action.equals(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION)) {
+            if (null == mService) {
+                Log.e(TAG, "Called with intent " + action + " but we don't know the service: this "
+                        + "should never happen");
+                return;
+            }
             mService.resetSuggestMainDict();
+        } else if (action.equals(DictionaryPackConstants.UNKNOWN_DICTIONARY_PROVIDER_CLIENT)) {
+            if (null != mService) {
+                // Careful! This is returning if the service is NOT null. This is because we
+                // should come here instantiated by the framework in reaction to a broadcast of
+                // the above action, so we should gave gone through the no-args constructor.
+                Log.e(TAG, "Called with intent " + action + " but we have a reference to the "
+                        + "service: this should never happen");
+                return;
+            }
+            // The dictionary provider does not know about some client. We check that it's really
+            // us that it needs to know about, and if it's the case, we register with the provider.
+            final String wantedClientId =
+                    intent.getStringExtra(DictionaryPackConstants.DICTIONARY_PROVIDER_CLIENT_EXTRA);
+            final String myClientId = context.getString(R.string.dictionary_pack_client_id);
+            if (!wantedClientId.equals(myClientId)) return; // Not for us
+            BinaryDictionaryFileDumper.initializeClientRecordHelper(context, myClientId);
         }
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
index 5c80559..e7c7e2b 100644
--- a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
+++ b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
@@ -620,34 +620,34 @@
      * Helper method to find a word in a given branch.
      */
     @SuppressWarnings("unused")
-    public static CharGroup findWordInTree(Node node, final String s) {
+    public static CharGroup findWordInTree(Node node, final String string) {
         int index = 0;
         final StringBuilder checker = DBG ? new StringBuilder() : null;
+        final int[] codePoints = getCodePoints(string);
 
         CharGroup currentGroup;
-        final int codePointCountInS = s.codePointCount(0, s.length());
         do {
-            int indexOfGroup = findIndexOfChar(node, s.codePointAt(index));
+            int indexOfGroup = findIndexOfChar(node, codePoints[index]);
             if (CHARACTER_NOT_FOUND == indexOfGroup) return null;
             currentGroup = node.mData.get(indexOfGroup);
 
-            if (s.length() - index < currentGroup.mChars.length) return null;
+            if (codePoints.length - index < currentGroup.mChars.length) return null;
             int newIndex = index;
-            while (newIndex < s.length() && newIndex - index < currentGroup.mChars.length) {
-                if (currentGroup.mChars[newIndex - index] != s.codePointAt(newIndex)) return null;
+            while (newIndex < codePoints.length && newIndex - index < currentGroup.mChars.length) {
+                if (currentGroup.mChars[newIndex - index] != codePoints[newIndex]) return null;
                 newIndex++;
             }
             index = newIndex;
 
             if (DBG) checker.append(new String(currentGroup.mChars, 0, currentGroup.mChars.length));
-            if (index < codePointCountInS) {
+            if (index < codePoints.length) {
                 node = currentGroup.mChildren;
             }
-        } while (null != node && index < codePointCountInS);
+        } while (null != node && index < codePoints.length);
 
-        if (index < codePointCountInS) return null;
+        if (index < codePoints.length) return null;
         if (!currentGroup.isTerminal()) return null;
-        if (DBG && !s.equals(checker.toString())) return null;
+        if (DBG && !codePoints.equals(checker.toString())) return null;
         return currentGroup;
     }
 
@@ -847,12 +847,12 @@
         @Override
         public Word next() {
             Position currentPos = mPositions.getLast();
-            mCurrentString.setLength(mCurrentString.length() - currentPos.length);
+            mCurrentString.setLength(currentPos.length);
 
             do {
                 if (currentPos.pos.hasNext()) {
                     final CharGroup currentGroup = currentPos.pos.next();
-                    currentPos.length = currentGroup.mChars.length;
+                    currentPos.length = mCurrentString.length();
                     for (int i : currentGroup.mChars)
                         mCurrentString.append(Character.toChars(i));
                     if (null != currentGroup.mChildren) {
@@ -866,7 +866,7 @@
                 } else {
                     mPositions.removeLast();
                     currentPos = mPositions.getLast();
-                    mCurrentString.setLength(mCurrentString.length() - mPositions.getLast().length);
+                    mCurrentString.setLength(mPositions.getLast().length);
                 }
             } while (true);
         }
diff --git a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOTests.java b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOTests.java
index ade0109..bd87292 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOTests.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictIOTests.java
@@ -72,15 +72,12 @@
     private static final FormatSpec.FormatOptions VERSION3_WITH_DYNAMIC_UPDATE =
             new FormatSpec.FormatOptions(3, true /* supportsDynamicUpdate */);
 
-    private static final String[] CHARACTERS = {
-        "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
-        "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"
-    };
-
     public BinaryDictIOTests() {
         super();
 
-        final Random random = new Random(123456);
+        final long time = System.currentTimeMillis();
+        Log.e(TAG, "Testing dictionary: seed is " + time);
+        final Random random = new Random(time);
         sWords.clear();
         generateWords(MAX_UNIGRAMS, random);
 
@@ -132,13 +129,16 @@
     /**
      * Generates a random word.
      */
-    private String generateWord(final int value) {
-        final int lengthOfChars = CHARACTERS.length;
+    private String generateWord(final Random random) {
         StringBuilder builder = new StringBuilder("a");
-        long lvalue = Math.abs((long)value);
-        while (lvalue > 0) {
-            builder.append(CHARACTERS[(int)(lvalue % lengthOfChars)]);
-            lvalue /= lengthOfChars;
+        int count = random.nextInt() % 30; // Arbitrarily 30 chars max
+        while (count > 0) {
+            final long r = Math.abs(random.nextInt());
+            if (r < 0) continue;
+            // Don't insert 0~20, but insert any other code point.
+            // Code points are in the range 0~0x10FFFF.
+            builder.appendCodePoint((int)(20 + r % (0x10FFFF - 20)));
+            --count;
         }
         return builder.toString();
     }
@@ -146,7 +146,7 @@
     private void generateWords(final int number, final Random random) {
         final Set<String> wordSet = CollectionUtils.newHashSet();
         while (wordSet.size() < number) {
-            wordSet.add(generateWord(random.nextInt()));
+            wordSet.add(generateWord(random));
         }
         sWords.addAll(wordSet);
     }
@@ -555,7 +555,7 @@
         // Test a word that isn't contained within the dictionary.
         final Random random = new Random((int)System.currentTimeMillis());
         for (int i = 0; i < 1000; ++i) {
-            final String word = generateWord(random.nextInt());
+            final String word = generateWord(random);
             if (sWords.indexOf(word) != -1) continue;
             runGetTerminalPosition(buffer, word, i, false);
         }
diff --git a/tools/dicttool/tests/com/android/inputmethod/latin/makedict/FusionDictionaryTest.java b/tools/dicttool/tests/com/android/inputmethod/latin/makedict/FusionDictionaryTest.java
new file mode 100644
index 0000000..fe3781d
--- /dev/null
+++ b/tools/dicttool/tests/com/android/inputmethod/latin/makedict/FusionDictionaryTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2013 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.inputmethod.latin.makedict;
+
+import com.android.inputmethod.latin.makedict.FusionDictionary;
+import com.android.inputmethod.latin.makedict.FusionDictionary.CharGroup;
+import com.android.inputmethod.latin.makedict.FusionDictionary.DictionaryOptions;
+import com.android.inputmethod.latin.makedict.FusionDictionary.Node;
+import com.android.inputmethod.latin.makedict.Word;
+
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Random;
+
+/**
+ * Unit tests for BinaryDictInputOutput.
+ */
+public class FusionDictionaryTest extends TestCase {
+    private static final ArrayList<String> sWords = new ArrayList<String>();
+    private static final int MAX_UNIGRAMS = 1000;
+
+    private void prepare(final long seed) {
+        System.out.println("Seed is " + seed);
+        final Random random = new Random(seed);
+        sWords.clear();
+        generateWords(MAX_UNIGRAMS, random);
+    }
+
+    /**
+     * Generates a random word.
+     */
+    private String generateWord(final Random random) {
+        StringBuilder builder = new StringBuilder("a");
+        int count = random.nextInt() % 30;
+        while (count > 0) {
+            final long r = Math.abs(random.nextInt());
+            if (r < 0) continue;
+            // Don't insert 0~20, but insert any other code point.
+            // Code points are in the range 0~0x10FFFF.
+            if (builder.length() < 7)
+                builder.appendCodePoint((int)(20 +r % (0x10FFFF - 20)));
+            --count;
+        }
+        if (builder.length() == 1) return generateWord(random);
+        return builder.toString();
+    }
+
+    private void generateWords(final int number, final Random random) {
+        while (sWords.size() < number) {
+            sWords.add(generateWord(random));
+        }
+    }
+
+    private void checkDictionary(final FusionDictionary dict, final ArrayList<String> words,
+            int limit) {
+        assertNotNull(dict);
+        for (final String word : words) {
+            if (--limit < 0) return;
+            final CharGroup cg = FusionDictionary.findWordInTree(dict.mRoot, word);
+            if (null == cg) {
+                System.out.println("word " + dumpWord(word));
+                dumpDict(dict);
+            }
+            assertNotNull(cg);
+        }
+    }
+
+    private String dumpWord(final String word) {
+        final StringBuilder sb = new StringBuilder("");
+        for (int i = 0; i < word.length(); i = word.offsetByCodePoints(i, 1)) {
+            sb.append(word.codePointAt(i));
+            sb.append(" ");
+        }
+        return sb.toString();
+    }
+
+    private void dumpDict(final FusionDictionary dict) {
+        for (Word w : dict) {
+            System.out.println("Word " + dumpWord(w.mWord));
+        }
+    }
+
+    // Test the flattened array contains the expected number of nodes, and
+    // that it does not contain any duplicates.
+    public void testFusion() {
+        final FusionDictionary dict = new FusionDictionary(new Node(),
+                new DictionaryOptions(new HashMap<String, String>(),
+                        false /* germanUmlautProcessing */, false /* frenchLigatureProcessing */));
+        final long time = System.currentTimeMillis();
+        prepare(time);
+        for (int i = 0; i < sWords.size(); ++i) {
+            System.out.println("Adding in pos " + i + " : " + dumpWord(sWords.get(i)));
+            dict.add(sWords.get(i), 180, null, false);
+            dumpDict(dict);
+            checkDictionary(dict, sWords, i);
+        }
+    }
+}