Merge "Tweak ICS theme a bit"
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleInputMethodServiceProxy.java b/java/src/com/android/inputmethod/accessibility/AccessibleInputMethodServiceProxy.java
index 7199550..89adc15 100644
--- a/java/src/com/android/inputmethod/accessibility/AccessibleInputMethodServiceProxy.java
+++ b/java/src/com/android/inputmethod/accessibility/AccessibleInputMethodServiceProxy.java
@@ -16,11 +16,15 @@
 
 package com.android.inputmethod.accessibility;
 
+import android.content.Context;
 import android.content.SharedPreferences;
 import android.inputmethodservice.InputMethodService;
+import android.media.AudioManager;
 import android.os.Looper;
 import android.os.Message;
+import android.os.Vibrator;
 import android.text.TextUtils;
+import android.view.KeyEvent;
 import android.view.inputmethod.ExtractedText;
 import android.view.inputmethod.ExtractedTextRequest;
 
@@ -38,8 +42,14 @@
      */
     private static final long DELAY_NO_HOVER_SELECTION = 250;
 
-    private InputMethodService mInputMethod;
+    /**
+     * Duration of the key click vibration in milliseconds.
+     */
+    private static final long VIBRATE_KEY_CLICK = 50;
 
+    private InputMethodService mInputMethod;
+    private Vibrator mVibrator;
+    private AudioManager mAudioManager;
     private AccessibilityHandler mAccessibilityHandler;
 
     private static class AccessibilityHandler
@@ -84,6 +94,8 @@
 
     private void initInternal(InputMethodService inputMethod, SharedPreferences prefs) {
         mInputMethod = inputMethod;
+        mVibrator = (Vibrator) inputMethod.getSystemService(Context.VIBRATOR_SERVICE);
+        mAudioManager = (AudioManager) inputMethod.getSystemService(Context.AUDIO_SERVICE);
         mAccessibilityHandler = new AccessibilityHandler(this, inputMethod.getMainLooper());
     }
 
@@ -107,6 +119,35 @@
     }
 
     /**
+     * Handle flick gestures by mapping them to directional pad keys.
+     */
+    @Override
+    public void onFlickGesture(int direction) {
+        final int keyEventCode;
+
+        switch (direction) {
+        case FlickGestureDetector.FLICK_LEFT:
+            sendDownUpKeyEvents(KeyEvent.KEYCODE_DPAD_LEFT);
+            break;
+        case FlickGestureDetector.FLICK_RIGHT:
+            sendDownUpKeyEvents(KeyEvent.KEYCODE_DPAD_RIGHT);
+            break;
+        }
+    }
+
+    /**
+     * Provide haptic feedback and send the specified keyCode to the input
+     * connection as a pair of down/up events.
+     *
+     * @param keyCode
+     */
+    private void sendDownUpKeyEvents(int keyCode) {
+        mVibrator.vibrate(VIBRATE_KEY_CLICK);
+        mAudioManager.playSoundEffect(AudioManager.FX_KEY_CLICK);
+        mInputMethod.sendDownUpKeyEvents(keyCode);
+    }
+
+    /**
      * When Accessibility is turned on, notifies the user that they are not
      * currently hovering above a key. By default this will speak the currently
      * entered text.
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardActionListener.java b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardActionListener.java
index 12c59d0..c1e92be 100644
--- a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardActionListener.java
+++ b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardActionListener.java
@@ -34,4 +34,15 @@
      * @param primaryCode the code of the key that was hovered over
      */
     public void onHoverExit(int primaryCode);
+
+    /**
+     * @param direction the direction of the flick gesture, one of
+     *            <ul>
+     *              <li>{@link FlickGestureDetector#FLICK_UP}
+     *              <li>{@link FlickGestureDetector#FLICK_DOWN}
+     *              <li>{@link FlickGestureDetector#FLICK_LEFT}
+     *              <li>{@link FlickGestureDetector#FLICK_RIGHT}
+     *            </ul>
+     */
+    public void onFlickGesture(int direction);
 }
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
index 96f7fc9..a87ff98 100644
--- a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
+++ b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java
@@ -42,6 +42,7 @@
     private int mScaledEdgeSlop;
     private KeyboardView mView;
     private AccessibleKeyboardActionListener mListener;
+    private FlickGestureDetector mGestureDetector;
 
     private int mLastHoverKeyIndex = KeyDetector.NOT_A_KEY;
     private int mLastX = -1;
@@ -71,6 +72,7 @@
         paint.setAntiAlias(true);
         paint.setColor(Color.YELLOW);
 
+        mGestureDetector = new KeyboardFlickGestureDetector(context);
         mScaledEdgeSlop = ViewConfiguration.get(context).getScaledEdgeSlop();
     }
 
@@ -110,7 +112,10 @@
      * @return {@code true} if the event is handled
      */
     public boolean onHoverEvent(MotionEvent event, PointerTracker tracker) {
-        return onTouchExplorationEvent(event, tracker);
+        if (mGestureDetector.onHoverEvent(event, this, tracker))
+            return true;
+
+        return onHoverEventInternal(event, tracker);
     }
 
     public boolean dispatchTouchEvent(MotionEvent event) {
@@ -128,7 +133,7 @@
      * @param event The touch exploration hover event.
      * @return {@code true} if the event was handled
      */
-    private boolean onTouchExplorationEvent(MotionEvent event, PointerTracker tracker) {
+    /*package*/ boolean onHoverEventInternal(MotionEvent event, PointerTracker tracker) {
         final int x = (int) event.getX();
         final int y = (int) event.getY();
 
@@ -198,4 +203,18 @@
         tracker.onDownEvent(x, y, eventTime, null);
         tracker.onUpEvent(x, y, eventTime + DELAY_KEY_PRESS, null);
     }
+
+    private class KeyboardFlickGestureDetector extends FlickGestureDetector {
+        public KeyboardFlickGestureDetector(Context context) {
+            super(context);
+        }
+
+        @Override
+        public boolean onFlick(MotionEvent e1, MotionEvent e2, int direction) {
+            if (mListener != null) {
+                mListener.onFlickGesture(direction);
+            }
+            return true;
+        }
+    }
 }
diff --git a/java/src/com/android/inputmethod/accessibility/FlickGestureDetector.java b/java/src/com/android/inputmethod/accessibility/FlickGestureDetector.java
new file mode 100644
index 0000000..9d99e31
--- /dev/null
+++ b/java/src/com/android/inputmethod/accessibility/FlickGestureDetector.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2011 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.accessibility;
+
+import android.content.Context;
+import android.os.Message;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+import com.android.inputmethod.compat.MotionEventCompatUtils;
+import com.android.inputmethod.keyboard.PointerTracker;
+import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
+
+/**
+ * Detects flick gestures within a stream of hover events.
+ * <p>
+ * A flick gesture is defined as a stream of hover events with the following
+ * properties:
+ * <ul>
+ *   <li>Begins with a {@link MotionEventCompatUtils#ACTION_HOVER_ENTER} event
+ *   <li>Contains any number of {@link MotionEventCompatUtils#ACTION_HOVER_MOVE}
+ *       events
+ *   <li>Ends with a {@link MotionEventCompatUtils#ACTION_HOVER_EXIT} event
+ *   <li>Maximum duration of 250 milliseconds
+ *   <li>Minimum distance between enter and exit points must be at least equal to
+ *       scaled double tap slop (see
+ *       {@link ViewConfiguration#getScaledDoubleTapSlop()})
+ * </ul>
+ * <p>
+ * Initial enter events are intercepted and cached until the stream fails to
+ * satisfy the constraints defined above, at which point the cached enter event
+ * is sent to its source {@link AccessibleKeyboardViewProxy} and subsequent move
+ * and exit events are ignored.
+ */
+public abstract class FlickGestureDetector {
+    public static final int FLICK_UP = 0;
+    public static final int FLICK_RIGHT = 1;
+    public static final int FLICK_LEFT = 2;
+    public static final int FLICK_DOWN = 3;
+
+    private final FlickHandler mFlickHandler;
+    private final int mFlickRadiusSquare;
+
+    private AccessibleKeyboardViewProxy mCachedView;
+    private PointerTracker mCachedTracker;
+    private MotionEvent mCachedHoverEnter;
+
+    private static class FlickHandler extends StaticInnerHandlerWrapper<FlickGestureDetector> {
+        private static final int MSG_FLICK_TIMEOUT = 1;
+
+        /** The maximum duration of a flick gesture in milliseconds. */
+        private static final int DELAY_FLICK_TIMEOUT = 250;
+
+        public FlickHandler(FlickGestureDetector outerInstance) {
+            super(outerInstance);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            final FlickGestureDetector gestureDetector = getOuterInstance();
+
+            switch (msg.what) {
+            case MSG_FLICK_TIMEOUT:
+                gestureDetector.clearFlick(true);
+            }
+        }
+
+        public void startFlickTimeout() {
+            cancelFlickTimeout();
+            sendEmptyMessageDelayed(MSG_FLICK_TIMEOUT, DELAY_FLICK_TIMEOUT);
+        }
+
+        public void cancelFlickTimeout() {
+            removeMessages(MSG_FLICK_TIMEOUT);
+        }
+    }
+
+    /**
+     * Creates a new flick gesture detector.
+     *
+     * @param context The parent context.
+     */
+    public FlickGestureDetector(Context context) {
+        final int doubleTapSlop = ViewConfiguration.get(context).getScaledDoubleTapSlop();
+
+        mFlickHandler = new FlickHandler(this);
+        mFlickRadiusSquare = doubleTapSlop * doubleTapSlop;
+    }
+
+    /**
+     * Processes motion events to detect flick gestures.
+     *
+     * @param event The current event.
+     * @param view The source of the event.
+     * @param tracker A pointer tracker for the event.
+     * @return {@code true} if the event was handled.
+     */
+    public boolean onHoverEvent(MotionEvent event, AccessibleKeyboardViewProxy view,
+            PointerTracker tracker) {
+        // Always cache and consume the first hover event.
+        if (event.getAction() == MotionEventCompatUtils.ACTION_HOVER_ENTER) {
+            mCachedView = view;
+            mCachedTracker = tracker;
+            mCachedHoverEnter = MotionEvent.obtain(event);
+            mFlickHandler.startFlickTimeout();
+            return true;
+        }
+
+        // Stop if the event has already been canceled.
+        if (mCachedHoverEnter == null) {
+            return false;
+        }
+
+        final float distanceSquare = calculateDistanceSquare(mCachedHoverEnter, event);
+        final long timeout = event.getEventTime() - mCachedHoverEnter.getEventTime();
+
+        switch (event.getAction()) {
+        case MotionEventCompatUtils.ACTION_HOVER_MOVE:
+            // Consume all valid move events before timeout.
+            return true;
+        case MotionEventCompatUtils.ACTION_HOVER_EXIT:
+            // Ignore exit events outside the flick radius.
+            if (distanceSquare < mFlickRadiusSquare) {
+                clearFlick(true);
+                return false;
+            } else {
+                return dispatchFlick(mCachedHoverEnter, event);
+            }
+        default:
+            return false;
+        }
+    }
+
+    /**
+     * Clears the cached flick information and optionally forwards the event to
+     * the source view's internal hover event handler.
+     *
+     * @param sendCachedEvent Set to {@code true} to forward the hover event to
+     *            the source view.
+     */
+    private void clearFlick(boolean sendCachedEvent) {
+        mFlickHandler.cancelFlickTimeout();
+
+        if (mCachedHoverEnter != null) {
+            if (sendCachedEvent) {
+                mCachedView.onHoverEventInternal(mCachedHoverEnter, mCachedTracker);
+            }
+            mCachedHoverEnter.recycle();
+            mCachedHoverEnter = null;
+        }
+
+        mCachedTracker = null;
+        mCachedView = null;
+    }
+
+    /**
+     * Computes the direction of a flick gesture and forwards it to
+     * {@link #onFlick(MotionEvent, MotionEvent, int)} for handling.
+     *
+     * @param e1 The {@link MotionEventCompatUtils#ACTION_HOVER_ENTER} event
+     *            where the flick started.
+     * @param e2 The {@link MotionEventCompatUtils#ACTION_HOVER_EXIT} event
+     *            where the flick ended.
+     * @return {@code true} if the flick event was handled.
+     */
+    private boolean dispatchFlick(MotionEvent e1, MotionEvent e2) {
+        clearFlick(false);
+
+        final float dX = e2.getX() - e1.getX();
+        final float dY = e2.getY() - e1.getY();
+        final int direction;
+
+        if (dY > dX) {
+            if (dY > -dX) {
+                direction = FLICK_DOWN;
+            } else {
+                direction = FLICK_LEFT;
+            }
+        } else {
+            if (dY > -dX) {
+                direction = FLICK_RIGHT;
+            } else {
+                direction = FLICK_UP;
+            }
+        }
+
+        return onFlick(e1, e2, direction);
+    }
+
+    private float calculateDistanceSquare(MotionEvent e1, MotionEvent e2) {
+        final float dX = e2.getX() - e1.getX();
+        final float dY = e2.getY() - e1.getY();
+        return (dX * dX) + (dY * dY);
+    }
+
+    /**
+     * Handles a detected flick gesture.
+     *
+     * @param e1 The {@link MotionEventCompatUtils#ACTION_HOVER_ENTER} event
+     *            where the flick started.
+     * @param e2 The {@link MotionEventCompatUtils#ACTION_HOVER_EXIT} event
+     *            where the flick ended.
+     * @param direction The direction of the flick event, one of:
+     *            <ul>
+     *              <li>{@link #FLICK_UP}
+     *              <li>{@link #FLICK_DOWN}
+     *              <li>{@link #FLICK_LEFT}
+     *              <li>{@link #FLICK_RIGHT}
+     *            </ul>
+     * @return {@code true} if the flick event was handled.
+     */
+    public abstract boolean onFlick(MotionEvent e1, MotionEvent e2, int direction);
+}
diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/Settings.java
index 97ff3e4..d6b9890 100644
--- a/java/src/com/android/inputmethod/latin/Settings.java
+++ b/java/src/com/android/inputmethod/latin/Settings.java
@@ -18,14 +18,14 @@
 
 import com.android.inputmethod.compat.CompatUtils;
 import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
-import com.android.inputmethod.compat.InputMethodServiceCompatWrapper;
 import com.android.inputmethod.deprecated.VoiceProxy;
 import com.android.inputmethod.compat.VibratorCompatWrapper;
 
+import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.Dialog;
+import android.app.Fragment;
 import android.app.backup.BackupManager;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
@@ -40,7 +40,6 @@
 import android.preference.PreferenceGroup;
 import android.preference.PreferenceScreen;
 import android.speech.SpeechRecognizer;
-import android.text.AutoText;
 import android.text.TextUtils;
 import android.text.method.LinkMovementMethod;
 import android.util.Log;
@@ -321,10 +320,22 @@
         mBigramPrediction.setEnabled(!currentSetting.equals(autoCorrectionOff));
     }
 
+    public Activity getActivityInternal() {
+        Object thisObject = (Object) this;
+        if (thisObject instanceof Activity) {
+            return (Activity) thisObject;
+        } else if (thisObject instanceof Fragment) {
+            return ((Fragment) thisObject).getActivity();
+        } else {
+            return null;
+        }
+    }
+
     @Override
-    protected void onCreate(Bundle icicle) {
+    public void onCreate(Bundle icicle) {
         super.onCreate(icicle);
         final Resources res = getResources();
+        final Context context = getActivityInternal();
 
         addPreferencesFromResource(R.xml.prefs);
         mInputLanguageSelection = (PreferenceScreen) findPreference(PREF_SUBTYPES);
@@ -340,7 +351,7 @@
         mVoiceModeOff = getString(R.string.voice_mode_off);
         mVoiceOn = !(prefs.getString(PREF_VOICE_SETTINGS_KEY, mVoiceModeOff)
                 .equals(mVoiceModeOff));
-        mVoiceLogger = VoiceProxy.VoiceLoggerWrapper.getInstance(this);
+        mVoiceLogger = VoiceProxy.VoiceLoggerWrapper.getInstance(context);
 
         mAutoCorrectionThreshold = (ListPreference) findPreference(PREF_AUTO_CORRECTION_THRESHOLD);
         mBigramSuggestion = (CheckBoxPreference) findPreference(PREF_BIGRAM_SUGGESTIONS);
@@ -348,7 +359,8 @@
         mDebugSettingsPreference = findPreference(PREF_DEBUG_SETTINGS);
         if (mDebugSettingsPreference != null) {
             final Intent debugSettingsIntent = new Intent(Intent.ACTION_MAIN);
-            debugSettingsIntent.setClassName(getPackageName(), DebugSettings.class.getName());
+            debugSettingsIntent.setClassName(
+                    context.getPackageName(), DebugSettings.class.getName());
             mDebugSettingsPreference.setIntent(debugSettingsIntent);
         }
 
@@ -371,7 +383,7 @@
             generalSettings.removePreference(mVoicePreference);
         }
 
-        if (!VibratorCompatWrapper.getInstance(this).hasVibrator()) {
+        if (!VibratorCompatWrapper.getInstance(context).hasVibrator()) {
             generalSettings.removePreference(findPreference(PREF_VIBRATE_ON));
         }
 
@@ -427,22 +439,17 @@
                 (PreferenceScreen) findPreference(PREF_CONFIGURE_DICTIONARIES_KEY);
         final Intent intent = dictionaryLink.getIntent();
 
-        final int number = getPackageManager().queryIntentActivities(intent, 0).size();
+        final int number = context.getPackageManager().queryIntentActivities(intent, 0).size();
         if (0 >= number) {
             textCorrectionGroup.removePreference(dictionaryLink);
         }
     }
 
     @Override
-    protected void onResume() {
+    public void onResume() {
         super.onResume();
-        int autoTextSize = AutoText.getSize(getListView());
-        if (autoTextSize < 1) {
-            ((PreferenceGroup) findPreference(PREF_CORRECTION_SETTINGS_KEY))
-                    .removePreference(mQuickFixes);
-        }
         if (!VoiceProxy.VOICE_INSTALLED
-                || !SpeechRecognizer.isRecognitionAvailable(this)) {
+                || !SpeechRecognizer.isRecognitionAvailable(getActivityInternal())) {
             getPreferenceScreen().removePreference(mVoicePreference);
         } else {
             updateVoiceModeSummary();
@@ -453,7 +460,7 @@
     }
 
     @Override
-    protected void onDestroy() {
+    public void onDestroy() {
         getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(
                 this);
         super.onDestroy();
@@ -461,7 +468,7 @@
 
     @Override
     public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
-        (new BackupManager(this)).dataChanged();
+        (new BackupManager(getActivityInternal())).dataChanged();
         // If turning on voice input, show dialog
         if (key.equals(PREF_VOICE_SETTINGS_KEY) && !mVoiceOn) {
             if (!prefs.getString(PREF_VOICE_SETTINGS_KEY, mVoiceModeOff)
@@ -488,8 +495,9 @@
     public boolean onPreferenceClick(Preference pref) {
         if (pref == mInputLanguageSelection) {
             startActivity(CompatUtils.getInputLanguageSelectionIntent(
-                    Utils.getInputMethodId(InputMethodManagerCompatWrapper.getInstance(this),
-                            getApplicationInfo().packageName), 0));
+                    Utils.getInputMethodId(
+                            InputMethodManagerCompatWrapper.getInstance(getActivityInternal()),
+                            getActivityInternal().getApplicationInfo().packageName), 0));
             return true;
         }
         return false;
@@ -515,7 +523,7 @@
 
     private void showVoiceConfirmation() {
         mOkClicked = false;
-        showDialog(VOICE_INPUT_CONFIRM_DIALOG);
+        getActivityInternal().showDialog(VOICE_INPUT_CONFIRM_DIALOG);
         // Make URL in the dialog message clickable
         if (mDialog != null) {
             TextView textView = (TextView) mDialog.findViewById(android.R.id.message);
@@ -531,7 +539,6 @@
                 [mVoicePreference.findIndexOfValue(mVoicePreference.getValue())]);
     }
 
-    @Override
     protected Dialog onCreateDialog(int id) {
         switch (id) {
             case VOICE_INPUT_CONFIRM_DIALOG:
@@ -548,7 +555,7 @@
                         updateVoicePreference();
                     }
                 };
-                AlertDialog.Builder builder = new AlertDialog.Builder(this)
+                AlertDialog.Builder builder = new AlertDialog.Builder(getActivityInternal())
                         .setTitle(R.string.voice_warning_title)
                         .setPositiveButton(android.R.string.ok, listener)
                         .setNegativeButton(android.R.string.cancel, listener);