Merge "Don't resume suggestion on links."
diff --git a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
index d5e638e..706bdea 100644
--- a/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
+++ b/java/src/com/android/inputmethod/dictionarypack/ActionBatch.java
@@ -117,16 +117,11 @@
             final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db,
                     mWordList.mId, mWordList.mVersion);
             final int status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN);
-            final DownloadManager manager =
-                    (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
+            final DownloadManagerWrapper manager = new DownloadManagerWrapper(context);
             if (MetadataDbHelper.STATUS_DOWNLOADING == status) {
                 // The word list is still downloading. Cancel the download and revert the
                 // word list status to "available".
-                if (null != manager) {
-                    // DownloadManager is disabled (or not installed?). We can't cancel - there
-                    // is nothing we can do. We still need to mark the entry as available.
-                    manager.remove(values.getAsLong(MetadataDbHelper.PENDINGID_COLUMN));
-                }
+                 manager.remove(values.getAsLong(MetadataDbHelper.PENDINGID_COLUMN));
                 MetadataDbHelper.markEntryAsAvailable(db, mWordList.mId, mWordList.mVersion);
             } else if (MetadataDbHelper.STATUS_AVAILABLE != status) {
                 // Should never happen
@@ -136,9 +131,6 @@
             // Download it.
             DebugLogUtils.l("Upgrade word list, downloading", mWordList.mRemoteFilename);
 
-            // TODO: if DownloadManager is disabled or not installed, download by ourselves
-            if (null == manager) return;
-
             // This is an upgraded word list: we should download it.
             // Adding a disambiguator to circumvent a bug in older versions of DownloadManager.
             // DownloadManager also stupidly cuts the extension to replace with its own that it
@@ -293,13 +285,8 @@
                 }
                 // The word list is still downloading. Cancel the download and revert the
                 // word list status to "available".
-                final DownloadManager manager =
-                        (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
-                if (null != manager) {
-                    // If we can't cancel the download because DownloadManager is not available,
-                    // we still need to mark the entry as available.
-                    manager.remove(values.getAsLong(MetadataDbHelper.PENDINGID_COLUMN));
-                }
+                final DownloadManagerWrapper manager = new DownloadManagerWrapper(context);
+                manager.remove(values.getAsLong(MetadataDbHelper.PENDINGID_COLUMN));
                 MetadataDbHelper.markEntryAsAvailable(db, mWordList.mId, mWordList.mVersion);
             }
         }
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java
index 384ee3e..2623eff 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java
@@ -107,26 +107,22 @@
 
     private class UpdaterThread extends Thread {
         private final static int REPORT_PERIOD = 150; // how often to report progress, in ms
-        final DownloadManager mDownloadManager;
+        final DownloadManagerWrapper mDownloadManagerWrapper;
         final int mId;
         public UpdaterThread(final Context context, final int id) {
             super();
-            mDownloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
+            mDownloadManagerWrapper = new DownloadManagerWrapper(context);
             mId = id;
         }
         @Override
         public void run() {
             try {
-                // It's almost impossible that mDownloadManager is null (it would mean it has been
-                // disabled between pressing the 'install' button and displaying the progress
-                // bar), but just in case.
-                if (null == mDownloadManager) return;
                 final UpdateHelper updateHelper = new UpdateHelper();
                 final Query query = new Query().setFilterById(mId);
                 int lastProgress = 0;
                 setIndeterminate(true);
                 while (!isInterrupted()) {
-                    final Cursor cursor = mDownloadManager.query(query);
+                    final Cursor cursor = mDownloadManagerWrapper.query(query);
                     if (null == cursor) {
                         // Can't contact DownloadManager: this should never happen.
                         return;
diff --git a/java/src/com/android/inputmethod/dictionarypack/DownloadManagerWrapper.java b/java/src/com/android/inputmethod/dictionarypack/DownloadManagerWrapper.java
new file mode 100644
index 0000000..e95ca17
--- /dev/null
+++ b/java/src/com/android/inputmethod/dictionarypack/DownloadManagerWrapper.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2014 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.dictionarypack;
+
+import android.app.DownloadManager;
+import android.app.DownloadManager.Query;
+import android.app.DownloadManager.Request;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import java.io.FileNotFoundException;
+
+/**
+ * A class to help with calling DownloadManager methods.
+ *
+ * Mostly, the problem here is that most methods from DownloadManager may throw SQL exceptions if
+ * they can't open the database on disk. We want to avoid crashing in these cases but can't do
+ * much more, so this class insulates the callers from these. SQLiteException also inherit from
+ * RuntimeException so they are unchecked :(
+ * While we're at it, we also insulate callers from the cases where DownloadManager is disabled,
+ * and getSystemService returns null.
+ */
+public class DownloadManagerWrapper {
+    private final static String TAG = DownloadManagerWrapper.class.getSimpleName();
+    private final DownloadManager mDownloadManager;
+
+    public DownloadManagerWrapper(final Context context) {
+        this((DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE));
+    }
+
+    private DownloadManagerWrapper(final DownloadManager downloadManager) {
+        mDownloadManager = downloadManager;
+    }
+
+    public void remove(final long... ids) {
+        try {
+            if (null != mDownloadManager) {
+                mDownloadManager.remove(ids);
+            }
+        } catch (SQLiteException e) {
+            // We couldn't remove the file from DownloadManager. Apparently, the database can't
+            // be opened. It may be a problem with file system corruption. In any case, there is
+            // not much we can do apart from avoiding crashing.
+            Log.e(TAG, "Can't remove files with ID " + ids + " from download manager", e);
+        }
+    }
+
+    public ParcelFileDescriptor openDownloadedFile(final long fileId) throws FileNotFoundException {
+        try {
+            if (null != mDownloadManager) {
+                return mDownloadManager.openDownloadedFile(fileId);
+            }
+        } catch (SQLiteException e) {
+            Log.e(TAG, "Can't open downloaded file with ID " + fileId, e);
+        }
+        // We come here if mDownloadManager is null or if an exception was thrown.
+        throw new FileNotFoundException();
+    }
+
+    public Cursor query(final Query query) {
+        try {
+            if (null != mDownloadManager) {
+                return mDownloadManager.query(query);
+            }
+        } catch (SQLiteException e) {
+            Log.e(TAG, "Can't query the download manager", e);
+        }
+        // We come here if mDownloadManager is null or if an exception was thrown.
+        return null;
+    }
+
+    public long enqueue(final Request request) {
+        try {
+            if (null != mDownloadManager) {
+                return mDownloadManager.enqueue(request);
+            }
+        } catch (SQLiteException e) {
+            Log.e(TAG, "Can't enqueue a request with the download manager", e);
+        }
+        return 0;
+    }
+}
diff --git a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
index 0e7c3bb..dcff490 100644
--- a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
+++ b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
@@ -249,13 +249,7 @@
         metadataRequest.setVisibleInDownloadsUi(
                 res.getBoolean(R.bool.metadata_downloads_visible_in_download_UI));
 
-        final DownloadManager manager =
-                (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
-        if (null == manager) {
-            // Download manager is not installed or disabled.
-            // TODO: fall back to self-managed download?
-            return;
-        }
+        final DownloadManagerWrapper manager = new DownloadManagerWrapper(context);
         cancelUpdateWithDownloadManager(context, metadataUri, manager);
         final long downloadId;
         synchronized (sSharedIdProtector) {
@@ -278,10 +272,10 @@
      *
      * @param context the context to open the database on
      * @param metadataUri the URI to cancel
-     * @param manager an instance of DownloadManager
+     * @param manager an wrapped instance of DownloadManager
      */
     private static void cancelUpdateWithDownloadManager(final Context context,
-            final String metadataUri, final DownloadManager manager) {
+            final String metadataUri, final DownloadManagerWrapper manager) {
         synchronized (sSharedIdProtector) {
             final long metadataDownloadId =
                     MetadataDbHelper.getMetadataDownloadIdForURI(context, metadataUri);
@@ -306,10 +300,9 @@
      * @param clientId the ID of the client we want to cancel the update of
      */
     public static void cancelUpdate(final Context context, final String clientId) {
-        final DownloadManager manager =
-                    (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
+        final DownloadManagerWrapper manager = new DownloadManagerWrapper(context);
         final String metadataUri = MetadataDbHelper.getMetadataUriAsString(context, clientId);
-        if (null != manager) cancelUpdateWithDownloadManager(context, metadataUri, manager);
+        cancelUpdateWithDownloadManager(context, metadataUri, manager);
     }
 
     /**
@@ -323,15 +316,15 @@
      * download request id, which is not known before submitting the request to the download
      * manager. Hence, it only updates the relevant line.
      *
-     * @param manager the download manager service to register the request with.
+     * @param manager a wrapped download manager service to register the request with.
      * @param request the request to register.
      * @param db the metadata database.
      * @param id the id of the word list.
      * @param version the version of the word list.
      * @return the download id returned by the download manager.
      */
-    public static long registerDownloadRequest(final DownloadManager manager, final Request request,
-            final SQLiteDatabase db, final String id, final int version) {
+    public static long registerDownloadRequest(final DownloadManagerWrapper manager,
+            final Request request, final SQLiteDatabase db, final String id, final int version) {
         DebugLogUtils.l("RegisterDownloadRequest for word list id : ", id, ", version ", version);
         final long downloadId;
         synchronized (sSharedIdProtector) {
@@ -345,8 +338,8 @@
     /**
      * Retrieve information about a specific download from DownloadManager.
      */
-    private static CompletedDownloadInfo getCompletedDownloadInfo(final DownloadManager manager,
-            final long downloadId) {
+    private static CompletedDownloadInfo getCompletedDownloadInfo(
+            final DownloadManagerWrapper manager, final long downloadId) {
         final Query query = new Query().setFilterById(downloadId);
         final Cursor cursor = manager.query(query);
 
@@ -425,8 +418,7 @@
         DebugLogUtils.l("DownloadFinished with id", fileId);
         if (NOT_AN_ID == fileId) return; // Spurious wake-up: ignore
 
-        final DownloadManager manager =
-                (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
+        final DownloadManagerWrapper manager = new DownloadManagerWrapper(context);
         final CompletedDownloadInfo downloadInfo = getCompletedDownloadInfo(manager, fileId);
 
         final ArrayList<DownloadRecord> recordList =
@@ -517,7 +509,7 @@
     }
 
     private static boolean handleDownloadedFile(final Context context,
-            final DownloadRecord downloadRecord, final DownloadManager manager,
+            final DownloadRecord downloadRecord, final DownloadManagerWrapper manager,
             final long fileId) {
         try {
             // {@link handleWordList(Context,InputStream,ContentValues)}.
diff --git a/java/src/com/android/inputmethod/latin/InputView.java b/java/src/com/android/inputmethod/latin/InputView.java
index 76b0912..ea7859e 100644
--- a/java/src/com/android/inputmethod/latin/InputView.java
+++ b/java/src/com/android/inputmethod/latin/InputView.java
@@ -31,6 +31,7 @@
     private final Rect mInputViewRect = new Rect();
     private KeyboardTopPaddingForwarder mKeyboardTopPaddingForwarder;
     private MoreSuggestionsViewCanceler mMoreSuggestionsViewCanceler;
+    private MotionEventForwarder<?, ?> mActiveForwarder;
 
     public InputView(final Context context, final AttributeSet attrs) {
         super(context, attrs, 0);
@@ -53,42 +54,62 @@
     }
 
     @Override
-    public boolean dispatchTouchEvent(final MotionEvent me) {
+    public boolean onInterceptTouchEvent(final MotionEvent me) {
         final Rect rect = mInputViewRect;
         getGlobalVisibleRect(rect);
-        final int x = (int)me.getX() + rect.left;
-        final int y = (int)me.getY() + rect.top;
+        final int index = me.getActionIndex();
+        final int x = (int)me.getX(index) + rect.left;
+        final int y = (int)me.getY(index) + rect.top;
 
-        // The touch events that hit the top padding of keyboard should be
-        // forwarded to {@link SuggestionStripView}.
-        if (mKeyboardTopPaddingForwarder.dispatchTouchEvent(x, y, me)) {
+        // The touch events that hit the top padding of keyboard should be forwarded to
+        // {@link SuggestionStripView}.
+        if (mKeyboardTopPaddingForwarder.onInterceptTouchEvent(x, y, me)) {
+            mActiveForwarder = mKeyboardTopPaddingForwarder;
             return true;
         }
+
         // To cancel {@link MoreSuggestionsView}, we should intercept a touch event to
         // {@link MainKeyboardView} and dismiss the {@link MoreSuggestionsView}.
-        if (mMoreSuggestionsViewCanceler.dispatchTouchEvent(x, y, me)) {
+        if (mMoreSuggestionsViewCanceler.onInterceptTouchEvent(x, y, me)) {
+            mActiveForwarder = mMoreSuggestionsViewCanceler;
             return true;
         }
-        return super.dispatchTouchEvent(me);
+
+        mActiveForwarder = null;
+        return false;
+    }
+
+    @Override
+    public boolean onTouchEvent(final MotionEvent me) {
+        if (mActiveForwarder == null) {
+            return super.onTouchEvent(me);
+        }
+
+        final Rect rect = mInputViewRect;
+        getGlobalVisibleRect(rect);
+        final int index = me.getActionIndex();
+        final int x = (int)me.getX(index) + rect.left;
+        final int y = (int)me.getY(index) + rect.top;
+        return mActiveForwarder.onTouchEvent(x, y, me);
     }
 
     /**
-     * This class forwards series of {@link MotionEvent}s from <code>Forwarder</code> view to
-     * <code>Receiver</code> view.
+     * This class forwards series of {@link MotionEvent}s from <code>SenderView</code> to
+     * <code>ReceiverView</code>.
      *
-     * @param <Sender> a {@link View} that may send a {@link MotionEvent} to <Receiver>.
-     * @param <Receiver> a {@link View} that receives forwarded {@link MotionEvent} from
-     *     <Forwarder>.
+     * @param <SenderView> a {@link View} that may send a {@link MotionEvent} to <ReceiverView>.
+     * @param <ReceiverView> a {@link View} that receives forwarded {@link MotionEvent} from
+     *     <SenderView>.
      */
-    private static abstract class MotionEventForwarder<Sender extends View, Receiver extends View> {
-        protected final Sender mSenderView;
-        protected final Receiver mReceiverView;
+    private static abstract class
+            MotionEventForwarder<SenderView extends View, ReceiverView extends View> {
+        protected final SenderView mSenderView;
+        protected final ReceiverView mReceiverView;
 
-        private boolean mIsForwardingEvent;
         protected final Rect mEventSendingRect = new Rect();
         protected final Rect mEventReceivingRect = new Rect();
 
-        public MotionEventForwarder(final Sender senderView, final Receiver receiverView) {
+        public MotionEventForwarder(final SenderView senderView, final ReceiverView receiverView) {
             mSenderView = senderView;
             mReceiverView = receiverView;
         }
@@ -96,12 +117,12 @@
         // Return true if a touch event of global coordinate x, y needs to be forwarded.
         protected abstract boolean needsToForward(final int x, final int y);
 
-        // Translate global x-coordinate to <code>Receiver</code> local coordinate.
+        // Translate global x-coordinate to <code>ReceiverView</code> local coordinate.
         protected int translateX(final int x) {
             return x - mEventReceivingRect.left;
         }
 
-        // Translate global y-coordinate to <code>Receiver</code> local coordinate.
+        // Translate global y-coordinate to <code>ReceiverView</code> local coordinate.
         protected int translateY(final int y) {
             return y - mEventReceivingRect.top;
         }
@@ -109,49 +130,36 @@
         // Callback when a {@link MotionEvent} is forwarded.
         protected void onForwardingEvent(final MotionEvent me) {}
 
-        // Dispatches a {@link MotioneEvent} to <code>Receiver</code> if needed and returns true.
-        // Otherwise returns false.
-        public boolean dispatchTouchEvent(final int x, final int y, final MotionEvent me) {
-            // Forwards a {link MotionEvent} only if both <code>Sender</code> and
-            // <code>Receiver</code> are visible.
+        // Returns true if a {@link MotionEvent} is needed to be forwarded to
+        // <code>ReceiverView</code>. Otherwise returns false.
+        public boolean onInterceptTouchEvent(final int x, final int y, final MotionEvent me) {
+            // Forwards a {link MotionEvent} only if both <code>SenderView</code> and
+            // <code>ReceiverView</code> are visible.
             if (mSenderView.getVisibility() != View.VISIBLE ||
                     mReceiverView.getVisibility() != View.VISIBLE) {
                 return false;
             }
-            final Rect sendingRect = mEventSendingRect;
-            mSenderView.getGlobalVisibleRect(sendingRect);
-            if (!mIsForwardingEvent && !sendingRect.contains(x, y)) {
+            mSenderView.getGlobalVisibleRect(mEventSendingRect);
+            if (!mEventSendingRect.contains(x, y)) {
                 return false;
             }
 
-            boolean shouldForwardToReceiver = false;
-
-            switch (me.getActionMasked()) {
-            case MotionEvent.ACTION_DOWN:
-                // If the down event happens in the forwarding area, successive {@link MotionEvent}s
-                // should be forwarded.
+            if (me.getActionMasked() == MotionEvent.ACTION_DOWN) {
+                // If the down event happens in the forwarding area, successive
+                // {@link MotionEvent}s should be forwarded to <code>ReceiverView</code>.
                 if (needsToForward(x, y)) {
-                    mIsForwardingEvent = true;
-                    shouldForwardToReceiver = true;
+                    return true;
                 }
-                break;
-            case MotionEvent.ACTION_MOVE:
-                shouldForwardToReceiver = mIsForwardingEvent;
-                break;
-            case MotionEvent.ACTION_UP:
-            case MotionEvent.ACTION_CANCEL:
-                shouldForwardToReceiver = mIsForwardingEvent;
-                mIsForwardingEvent = false;
-                break;
             }
 
-            if (!shouldForwardToReceiver) {
-                return false;
-            }
+            return false;
+        }
 
-            final Rect receivingRect = mEventReceivingRect;
-            mReceiverView.getGlobalVisibleRect(receivingRect);
-            // Translate global coordinates to <code>Receiver</code> local coordinates.
+        // Returns true if a {@link MotionEvent} is forwarded to <code>ReceiverView</code>.
+        // Otherwise returns false.
+        public boolean onTouchEvent(final int x, final int y, final MotionEvent me) {
+            mReceiverView.getGlobalVisibleRect(mEventReceivingRect);
+            // Translate global coordinates to <code>ReceiverView</code> local coordinates.
             me.setLocation(translateX(x), translateY(y));
             mReceiverView.dispatchTouchEvent(me);
             onForwardingEvent(me);
@@ -189,8 +197,7 @@
         protected int translateY(final int y) {
             final int translatedY = super.translateY(y);
             if (isInKeyboardTopPadding(y)) {
-                // The forwarded event should have coordinates that are inside of
-                // the target.
+                // The forwarded event should have coordinates that are inside of the target.
                 return Math.min(translatedY, mEventReceivingRect.height() - 1);
             }
             return translatedY;
@@ -200,8 +207,8 @@
     /**
      * This class forwards {@link MotionEvent}s happened in the {@link MainKeyboardView} to
      * {@link SuggestionStripView} when the {@link MoreSuggestionsView} is showing.
-     * {@link SuggestionStripView} dismisses {@link MoreSuggestionsView} when it receives those
-     * events.
+     * {@link SuggestionStripView} dismisses {@link MoreSuggestionsView} when it receives any event
+     * outside of it.
      */
     private static class MoreSuggestionsViewCanceler
             extends MotionEventForwarder<MainKeyboardView, SuggestionStripView> {