Home | History | Annotate | Download | only in latin
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.inputmethod.latin;
     18 
     19 import android.content.Context;
     20 import android.content.SharedPreferences;
     21 import android.content.res.Resources;
     22 import android.inputmethodservice.InputMethodService;
     23 import android.os.AsyncTask;
     24 import android.os.Build;
     25 import android.os.Handler;
     26 import android.os.HandlerThread;
     27 import android.os.Process;
     28 import android.text.InputType;
     29 import android.text.TextUtils;
     30 import android.text.format.DateUtils;
     31 import android.util.Log;
     32 import android.view.inputmethod.EditorInfo;
     33 
     34 import com.android.inputmethod.compat.InputMethodInfoCompatWrapper;
     35 import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
     36 import com.android.inputmethod.compat.InputMethodSubtypeCompatWrapper;
     37 import com.android.inputmethod.compat.InputTypeCompatUtils;
     38 import com.android.inputmethod.keyboard.Keyboard;
     39 import com.android.inputmethod.keyboard.KeyboardId;
     40 
     41 import java.io.BufferedReader;
     42 import java.io.File;
     43 import java.io.FileNotFoundException;
     44 import java.io.FileOutputStream;
     45 import java.io.FileReader;
     46 import java.io.IOException;
     47 import java.io.PrintWriter;
     48 import java.text.SimpleDateFormat;
     49 import java.util.ArrayList;
     50 import java.util.Date;
     51 import java.util.List;
     52 import java.util.Locale;
     53 
     54 public class Utils {
     55     private static final String TAG = Utils.class.getSimpleName();
     56     private static final int MINIMUM_SAFETY_NET_CHAR_LENGTH = 4;
     57     private static boolean DBG = LatinImeLogger.sDBG;
     58     private static boolean DBG_EDIT_DISTANCE = false;
     59 
     60     private Utils() {
     61         // Intentional empty constructor for utility class.
     62     }
     63 
     64     /**
     65      * Cancel an {@link AsyncTask}.
     66      *
     67      * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
     68      *        task should be interrupted; otherwise, in-progress tasks are allowed
     69      *        to complete.
     70      */
     71     public static void cancelTask(AsyncTask<?, ?, ?> task, boolean mayInterruptIfRunning) {
     72         if (task != null && task.getStatus() != AsyncTask.Status.FINISHED) {
     73             task.cancel(mayInterruptIfRunning);
     74         }
     75     }
     76 
     77     public static class GCUtils {
     78         private static final String GC_TAG = GCUtils.class.getSimpleName();
     79         public static final int GC_TRY_COUNT = 2;
     80         // GC_TRY_LOOP_MAX is used for the hard limit of GC wait,
     81         // GC_TRY_LOOP_MAX should be greater than GC_TRY_COUNT.
     82         public static final int GC_TRY_LOOP_MAX = 5;
     83         private static final long GC_INTERVAL = DateUtils.SECOND_IN_MILLIS;
     84         private static GCUtils sInstance = new GCUtils();
     85         private int mGCTryCount = 0;
     86 
     87         public static GCUtils getInstance() {
     88             return sInstance;
     89         }
     90 
     91         public void reset() {
     92             mGCTryCount = 0;
     93         }
     94 
     95         public boolean tryGCOrWait(String metaData, Throwable t) {
     96             if (mGCTryCount == 0) {
     97                 System.gc();
     98             }
     99             if (++mGCTryCount > GC_TRY_COUNT) {
    100                 LatinImeLogger.logOnException(metaData, t);
    101                 return false;
    102             } else {
    103                 try {
    104                     Thread.sleep(GC_INTERVAL);
    105                     return true;
    106                 } catch (InterruptedException e) {
    107                     Log.e(GC_TAG, "Sleep was interrupted.");
    108                     LatinImeLogger.logOnException(metaData, t);
    109                     return false;
    110                 }
    111             }
    112         }
    113     }
    114 
    115     public static boolean hasMultipleEnabledIMEsOrSubtypes(
    116             final InputMethodManagerCompatWrapper imm,
    117             final boolean shouldIncludeAuxiliarySubtypes) {
    118         final List<InputMethodInfoCompatWrapper> enabledImis = imm.getEnabledInputMethodList();
    119 
    120         // Number of the filtered IMEs
    121         int filteredImisCount = 0;
    122 
    123         for (InputMethodInfoCompatWrapper imi : enabledImis) {
    124             // We can return true immediately after we find two or more filtered IMEs.
    125             if (filteredImisCount > 1) return true;
    126             final List<InputMethodSubtypeCompatWrapper> subtypes =
    127                     imm.getEnabledInputMethodSubtypeList(imi, true);
    128             // IMEs that have no subtypes should be counted.
    129             if (subtypes.isEmpty()) {
    130                 ++filteredImisCount;
    131                 continue;
    132             }
    133 
    134             int auxCount = 0;
    135             for (InputMethodSubtypeCompatWrapper subtype : subtypes) {
    136                 if (subtype.isAuxiliary()) {
    137                     ++auxCount;
    138                 }
    139             }
    140             final int nonAuxCount = subtypes.size() - auxCount;
    141 
    142             // IMEs that have one or more non-auxiliary subtypes should be counted.
    143             // If shouldIncludeAuxiliarySubtypes is true, IMEs that have two or more auxiliary
    144             // subtypes should be counted as well.
    145             if (nonAuxCount > 0 || (shouldIncludeAuxiliarySubtypes && auxCount > 1)) {
    146                 ++filteredImisCount;
    147                 continue;
    148             }
    149         }
    150 
    151         return filteredImisCount > 1
    152         // imm.getEnabledInputMethodSubtypeList(null, false) will return the current IME's enabled
    153         // input method subtype (The current IME should be LatinIME.)
    154                 || imm.getEnabledInputMethodSubtypeList(null, false).size() > 1;
    155     }
    156 
    157     public static String getInputMethodId(InputMethodManagerCompatWrapper imm, String packageName) {
    158         return getInputMethodInfo(imm, packageName).getId();
    159     }
    160 
    161     public static InputMethodInfoCompatWrapper getInputMethodInfo(
    162             InputMethodManagerCompatWrapper imm, String packageName) {
    163         for (final InputMethodInfoCompatWrapper imi : imm.getEnabledInputMethodList()) {
    164             if (imi.getPackageName().equals(packageName))
    165                 return imi;
    166         }
    167         throw new RuntimeException("Can not find input method id for " + packageName);
    168     }
    169 
    170     // TODO: Resolve the inconsistencies between the native auto correction algorithms and
    171     // this safety net
    172     public static boolean shouldBlockAutoCorrectionBySafetyNet(SuggestedWords suggestions,
    173             Suggest suggest) {
    174         // Safety net for auto correction.
    175         // Actually if we hit this safety net, it's actually a bug.
    176         if (suggestions.size() <= 1 || suggestions.mTypedWordValid) return false;
    177         // If user selected aggressive auto correction mode, there is no need to use the safety
    178         // net.
    179         if (suggest.isAggressiveAutoCorrectionMode()) return false;
    180         final CharSequence typedWord = suggestions.getWord(0);
    181         // If the length of typed word is less than MINIMUM_SAFETY_NET_CHAR_LENGTH,
    182         // we should not use net because relatively edit distance can be big.
    183         if (typedWord.length() < MINIMUM_SAFETY_NET_CHAR_LENGTH) return false;
    184         final CharSequence suggestionWord = suggestions.getWord(1);
    185         final int typedWordLength = typedWord.length();
    186         final int maxEditDistanceOfNativeDictionary =
    187                 (typedWordLength < 5 ? 2 : typedWordLength / 2) + 1;
    188         final int distance = Utils.editDistance(typedWord, suggestionWord);
    189         if (DBG) {
    190             Log.d(TAG, "Autocorrected edit distance = " + distance
    191                     + ", " + maxEditDistanceOfNativeDictionary);
    192         }
    193         if (distance > maxEditDistanceOfNativeDictionary) {
    194             if (DBG) {
    195                 Log.e(TAG, "Safety net: before = " + typedWord + ", after = " + suggestionWord);
    196                 Log.e(TAG, "(Error) The edit distance of this correction exceeds limit. "
    197                         + "Turning off auto-correction.");
    198             }
    199             return true;
    200         } else {
    201             return false;
    202         }
    203     }
    204 
    205     public static boolean canBeFollowedByPeriod(final int codePoint) {
    206         // TODO: Check again whether there really ain't a better way to check this.
    207         // TODO: This should probably be language-dependant...
    208         return Character.isLetterOrDigit(codePoint)
    209                 || codePoint == Keyboard.CODE_SINGLE_QUOTE
    210                 || codePoint == Keyboard.CODE_DOUBLE_QUOTE
    211                 || codePoint == Keyboard.CODE_CLOSING_PARENTHESIS
    212                 || codePoint == Keyboard.CODE_CLOSING_SQUARE_BRACKET
    213                 || codePoint == Keyboard.CODE_CLOSING_CURLY_BRACKET
    214                 || codePoint == Keyboard.CODE_CLOSING_ANGLE_BRACKET;
    215     }
    216 
    217     /* package */ static class RingCharBuffer {
    218         private static RingCharBuffer sRingCharBuffer = new RingCharBuffer();
    219         private static final char PLACEHOLDER_DELIMITER_CHAR = '\uFFFC';
    220         private static final int INVALID_COORDINATE = -2;
    221         /* package */ static final int BUFSIZE = 20;
    222         private InputMethodService mContext;
    223         private boolean mEnabled = false;
    224         private boolean mUsabilityStudy = false;
    225         private int mEnd = 0;
    226         /* package */ int mLength = 0;
    227         private char[] mCharBuf = new char[BUFSIZE];
    228         private int[] mXBuf = new int[BUFSIZE];
    229         private int[] mYBuf = new int[BUFSIZE];
    230 
    231         private RingCharBuffer() {
    232             // Intentional empty constructor for singleton.
    233         }
    234         public static RingCharBuffer getInstance() {
    235             return sRingCharBuffer;
    236         }
    237         public static RingCharBuffer init(InputMethodService context, boolean enabled,
    238                 boolean usabilityStudy) {
    239             sRingCharBuffer.mContext = context;
    240             sRingCharBuffer.mEnabled = enabled || usabilityStudy;
    241             sRingCharBuffer.mUsabilityStudy = usabilityStudy;
    242             UsabilityStudyLogUtils.getInstance().init(context);
    243             return sRingCharBuffer;
    244         }
    245         private int normalize(int in) {
    246             int ret = in % BUFSIZE;
    247             return ret < 0 ? ret + BUFSIZE : ret;
    248         }
    249         public void push(char c, int x, int y) {
    250             if (!mEnabled) return;
    251             if (mUsabilityStudy) {
    252                 UsabilityStudyLogUtils.getInstance().writeChar(c, x, y);
    253             }
    254             mCharBuf[mEnd] = c;
    255             mXBuf[mEnd] = x;
    256             mYBuf[mEnd] = y;
    257             mEnd = normalize(mEnd + 1);
    258             if (mLength < BUFSIZE) {
    259                 ++mLength;
    260             }
    261         }
    262         public char pop() {
    263             if (mLength < 1) {
    264                 return PLACEHOLDER_DELIMITER_CHAR;
    265             } else {
    266                 mEnd = normalize(mEnd - 1);
    267                 --mLength;
    268                 return mCharBuf[mEnd];
    269             }
    270         }
    271         public char getBackwardNthChar(int n) {
    272             if (mLength <= n || n < 0) {
    273                 return PLACEHOLDER_DELIMITER_CHAR;
    274             } else {
    275                 return mCharBuf[normalize(mEnd - n - 1)];
    276             }
    277         }
    278         public int getPreviousX(char c, int back) {
    279             int index = normalize(mEnd - 2 - back);
    280             if (mLength <= back
    281                     || Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) {
    282                 return INVALID_COORDINATE;
    283             } else {
    284                 return mXBuf[index];
    285             }
    286         }
    287         public int getPreviousY(char c, int back) {
    288             int index = normalize(mEnd - 2 - back);
    289             if (mLength <= back
    290                     || Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) {
    291                 return INVALID_COORDINATE;
    292             } else {
    293                 return mYBuf[index];
    294             }
    295         }
    296         public String getLastWord(int ignoreCharCount) {
    297             StringBuilder sb = new StringBuilder();
    298             int i = ignoreCharCount;
    299             for (; i < mLength; ++i) {
    300                 char c = mCharBuf[normalize(mEnd - 1 - i)];
    301                 if (!((LatinIME)mContext).isWordSeparator(c)) {
    302                     break;
    303                 }
    304             }
    305             for (; i < mLength; ++i) {
    306                 char c = mCharBuf[normalize(mEnd - 1 - i)];
    307                 if (!((LatinIME)mContext).isWordSeparator(c)) {
    308                     sb.append(c);
    309                 } else {
    310                     break;
    311                 }
    312             }
    313             return sb.reverse().toString();
    314         }
    315         public void reset() {
    316             mLength = 0;
    317         }
    318     }
    319 
    320 
    321     /* Damerau-Levenshtein distance */
    322     public static int editDistance(CharSequence s, CharSequence t) {
    323         if (s == null || t == null) {
    324             throw new IllegalArgumentException("editDistance: Arguments should not be null.");
    325         }
    326         final int sl = s.length();
    327         final int tl = t.length();
    328         int[][] dp = new int [sl + 1][tl + 1];
    329         for (int i = 0; i <= sl; i++) {
    330             dp[i][0] = i;
    331         }
    332         for (int j = 0; j <= tl; j++) {
    333             dp[0][j] = j;
    334         }
    335         for (int i = 0; i < sl; ++i) {
    336             for (int j = 0; j < tl; ++j) {
    337                 final char sc = Character.toLowerCase(s.charAt(i));
    338                 final char tc = Character.toLowerCase(t.charAt(j));
    339                 final int cost = sc == tc ? 0 : 1;
    340                 dp[i + 1][j + 1] = Math.min(
    341                         dp[i][j + 1] + 1, Math.min(dp[i + 1][j] + 1, dp[i][j] + cost));
    342                 // Overwrite for transposition cases
    343                 if (i > 0 && j > 0
    344                         && sc == Character.toLowerCase(t.charAt(j - 1))
    345                         && tc == Character.toLowerCase(s.charAt(i - 1))) {
    346                     dp[i + 1][j + 1] = Math.min(dp[i + 1][j + 1], dp[i - 1][j - 1] + cost);
    347                 }
    348             }
    349         }
    350         if (DBG_EDIT_DISTANCE) {
    351             Log.d(TAG, "editDistance:" + s + "," + t);
    352             for (int i = 0; i < dp.length; ++i) {
    353                 StringBuffer sb = new StringBuffer();
    354                 for (int j = 0; j < dp[i].length; ++j) {
    355                     sb.append(dp[i][j]).append(',');
    356                 }
    357                 Log.d(TAG, i + ":" + sb.toString());
    358             }
    359         }
    360         return dp[sl][tl];
    361     }
    362 
    363     // Get the current stack trace
    364     public static String getStackTrace() {
    365         StringBuilder sb = new StringBuilder();
    366         try {
    367             throw new RuntimeException();
    368         } catch (RuntimeException e) {
    369             StackTraceElement[] frames = e.getStackTrace();
    370             // Start at 1 because the first frame is here and we don't care about it
    371             for (int j = 1; j < frames.length; ++j) sb.append(frames[j].toString() + "\n");
    372         }
    373         return sb.toString();
    374     }
    375 
    376     // In dictionary.cpp, getSuggestion() method,
    377     // suggestion scores are computed using the below formula.
    378     // original score
    379     //  := pow(mTypedLetterMultiplier (this is defined 2),
    380     //         (the number of matched characters between typed word and suggested word))
    381     //     * (individual word's score which defined in the unigram dictionary,
    382     //         and this score is defined in range [0, 255].)
    383     // Then, the following processing is applied.
    384     //     - If the dictionary word is matched up to the point of the user entry
    385     //       (full match up to min(before.length(), after.length())
    386     //       => Then multiply by FULL_MATCHED_WORDS_PROMOTION_RATE (this is defined 1.2)
    387     //     - If the word is a true full match except for differences in accents or
    388     //       capitalization, then treat it as if the score was 255.
    389     //     - If before.length() == after.length()
    390     //       => multiply by mFullWordMultiplier (this is defined 2))
    391     // So, maximum original score is pow(2, min(before.length(), after.length())) * 255 * 2 * 1.2
    392     // For historical reasons we ignore the 1.2 modifier (because the measure for a good
    393     // autocorrection threshold was done at a time when it didn't exist). This doesn't change
    394     // the result.
    395     // So, we can normalize original score by dividing pow(2, min(b.l(),a.l())) * 255 * 2.
    396     private static final int MAX_INITIAL_SCORE = 255;
    397     private static final int TYPED_LETTER_MULTIPLIER = 2;
    398     private static final int FULL_WORD_MULTIPLIER = 2;
    399     private static final int S_INT_MAX = 2147483647;
    400     public static double calcNormalizedScore(CharSequence before, CharSequence after, int score) {
    401         final int beforeLength = before.length();
    402         final int afterLength = after.length();
    403         if (beforeLength == 0 || afterLength == 0) return 0;
    404         final int distance = editDistance(before, after);
    405         // If afterLength < beforeLength, the algorithm is suggesting a word by excessive character
    406         // correction.
    407         int spaceCount = 0;
    408         for (int i = 0; i < afterLength; ++i) {
    409             if (after.charAt(i) == Keyboard.CODE_SPACE) {
    410                 ++spaceCount;
    411             }
    412         }
    413         if (spaceCount == afterLength) return 0;
    414         final double maximumScore = score == S_INT_MAX ? S_INT_MAX : MAX_INITIAL_SCORE
    415                 * Math.pow(
    416                         TYPED_LETTER_MULTIPLIER, Math.min(beforeLength, afterLength - spaceCount))
    417                 * FULL_WORD_MULTIPLIER;
    418         // add a weight based on edit distance.
    419         // distance <= max(afterLength, beforeLength) == afterLength,
    420         // so, 0 <= distance / afterLength <= 1
    421         final double weight = 1.0 - (double) distance / afterLength;
    422         return (score / maximumScore) * weight;
    423     }
    424 
    425     public static class UsabilityStudyLogUtils {
    426         private static final String USABILITY_TAG = UsabilityStudyLogUtils.class.getSimpleName();
    427         private static final String FILENAME = "log.txt";
    428         private static final UsabilityStudyLogUtils sInstance =
    429                 new UsabilityStudyLogUtils();
    430         private final Handler mLoggingHandler;
    431         private File mFile;
    432         private File mDirectory;
    433         private InputMethodService mIms;
    434         private PrintWriter mWriter;
    435         private final Date mDate;
    436         private final SimpleDateFormat mDateFormat;
    437 
    438         private UsabilityStudyLogUtils() {
    439             mDate = new Date();
    440             mDateFormat = new SimpleDateFormat("dd MMM HH:mm:ss.SSS");
    441 
    442             HandlerThread handlerThread = new HandlerThread("UsabilityStudyLogUtils logging task",
    443                     Process.THREAD_PRIORITY_BACKGROUND);
    444             handlerThread.start();
    445             mLoggingHandler = new Handler(handlerThread.getLooper());
    446         }
    447 
    448         public static UsabilityStudyLogUtils getInstance() {
    449             return sInstance;
    450         }
    451 
    452         public void init(InputMethodService ims) {
    453             mIms = ims;
    454             mDirectory = ims.getFilesDir();
    455         }
    456 
    457         private void createLogFileIfNotExist() {
    458             if ((mFile == null || !mFile.exists())
    459                     && (mDirectory != null && mDirectory.exists())) {
    460                 try {
    461                     mWriter = getPrintWriter(mDirectory, FILENAME, false);
    462                 } catch (IOException e) {
    463                     Log.e(USABILITY_TAG, "Can't create log file.");
    464                 }
    465             }
    466         }
    467 
    468         public void writeBackSpace() {
    469             UsabilityStudyLogUtils.getInstance().write("<backspace>\t0\t0");
    470         }
    471 
    472         public void writeChar(char c, int x, int y) {
    473             String inputChar = String.valueOf(c);
    474             switch (c) {
    475                 case '\n':
    476                     inputChar = "<enter>";
    477                     break;
    478                 case '\t':
    479                     inputChar = "<tab>";
    480                     break;
    481                 case ' ':
    482                     inputChar = "<space>";
    483                     break;
    484             }
    485             UsabilityStudyLogUtils.getInstance().write(inputChar + "\t" + x + "\t" + y);
    486             LatinImeLogger.onPrintAllUsabilityStudyLogs();
    487         }
    488 
    489         public void write(final String log) {
    490             mLoggingHandler.post(new Runnable() {
    491                 @Override
    492                 public void run() {
    493                     createLogFileIfNotExist();
    494                     final long currentTime = System.currentTimeMillis();
    495                     mDate.setTime(currentTime);
    496 
    497                     final String printString = String.format("%s\t%d\t%s\n",
    498                             mDateFormat.format(mDate), currentTime, log);
    499                     if (LatinImeLogger.sDBG) {
    500                         Log.d(USABILITY_TAG, "Write: " + log);
    501                     }
    502                     mWriter.print(printString);
    503                 }
    504             });
    505         }
    506 
    507         public void printAll() {
    508             mLoggingHandler.post(new Runnable() {
    509                 @Override
    510                 public void run() {
    511                     mWriter.flush();
    512                     StringBuilder sb = new StringBuilder();
    513                     BufferedReader br = getBufferedReader();
    514                     String line;
    515                     try {
    516                         while ((line = br.readLine()) != null) {
    517                             sb.append('\n');
    518                             sb.append(line);
    519                         }
    520                     } catch (IOException e) {
    521                         Log.e(USABILITY_TAG, "Can't read log file.");
    522                     } finally {
    523                         if (LatinImeLogger.sDBG) {
    524                             Log.d(USABILITY_TAG, "output all logs\n" + sb.toString());
    525                         }
    526                         mIms.getCurrentInputConnection().commitText(sb.toString(), 0);
    527                         try {
    528                             br.close();
    529                         } catch (IOException e) {
    530                             // ignore.
    531                         }
    532                     }
    533                 }
    534             });
    535         }
    536 
    537         public void clearAll() {
    538             mLoggingHandler.post(new Runnable() {
    539                 @Override
    540                 public void run() {
    541                     if (mFile != null && mFile.exists()) {
    542                         if (LatinImeLogger.sDBG) {
    543                             Log.d(USABILITY_TAG, "Delete log file.");
    544                         }
    545                         mFile.delete();
    546                         mWriter.close();
    547                     }
    548                 }
    549             });
    550         }
    551 
    552         private BufferedReader getBufferedReader() {
    553             createLogFileIfNotExist();
    554             try {
    555                 return new BufferedReader(new FileReader(mFile));
    556             } catch (FileNotFoundException e) {
    557                 return null;
    558             }
    559         }
    560 
    561         private PrintWriter getPrintWriter(
    562                 File dir, String filename, boolean renew) throws IOException {
    563             mFile = new File(dir, filename);
    564             if (mFile.exists()) {
    565                 if (renew) {
    566                     mFile.delete();
    567                 }
    568             }
    569             return new PrintWriter(new FileOutputStream(mFile), true /* autoFlush */);
    570         }
    571     }
    572 
    573     public static int getKeyboardMode(EditorInfo editorInfo) {
    574         if (editorInfo == null)
    575             return KeyboardId.MODE_TEXT;
    576 
    577         final int inputType = editorInfo.inputType;
    578         final int variation = inputType & InputType.TYPE_MASK_VARIATION;
    579 
    580         switch (inputType & InputType.TYPE_MASK_CLASS) {
    581         case InputType.TYPE_CLASS_NUMBER:
    582         case InputType.TYPE_CLASS_DATETIME:
    583             return KeyboardId.MODE_NUMBER;
    584         case InputType.TYPE_CLASS_PHONE:
    585             return KeyboardId.MODE_PHONE;
    586         case InputType.TYPE_CLASS_TEXT:
    587             if (InputTypeCompatUtils.isEmailVariation(variation)) {
    588                 return KeyboardId.MODE_EMAIL;
    589             } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) {
    590                 return KeyboardId.MODE_URL;
    591             } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) {
    592                 return KeyboardId.MODE_IM;
    593             } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
    594                 return KeyboardId.MODE_TEXT;
    595             } else {
    596                 return KeyboardId.MODE_TEXT;
    597             }
    598         default:
    599             return KeyboardId.MODE_TEXT;
    600         }
    601     }
    602 
    603     public static boolean containsInCsv(String key, String csv) {
    604         if (csv == null)
    605             return false;
    606         for (String option : csv.split(",")) {
    607             if (option.equals(key))
    608                 return true;
    609         }
    610         return false;
    611     }
    612 
    613     public static boolean inPrivateImeOptions(String packageName, String key,
    614             EditorInfo editorInfo) {
    615         if (editorInfo == null)
    616             return false;
    617         return containsInCsv(packageName != null ? packageName + "." + key : key,
    618                 editorInfo.privateImeOptions);
    619     }
    620 
    621     /**
    622      * Returns a main dictionary resource id
    623      * @return main dictionary resource id
    624      */
    625     public static int getMainDictionaryResourceId(Resources res) {
    626         final String MAIN_DIC_NAME = "main";
    627         String packageName = LatinIME.class.getPackage().getName();
    628         return res.getIdentifier(MAIN_DIC_NAME, "raw", packageName);
    629     }
    630 
    631     public static void loadNativeLibrary() {
    632         try {
    633             System.loadLibrary("jni_latinime");
    634         } catch (UnsatisfiedLinkError ule) {
    635             Log.e(TAG, "Could not load native library jni_latinime");
    636         }
    637     }
    638 
    639     /**
    640      * Returns true if a and b are equal ignoring the case of the character.
    641      * @param a first character to check
    642      * @param b second character to check
    643      * @return {@code true} if a and b are equal, {@code false} otherwise.
    644      */
    645     public static boolean equalsIgnoreCase(char a, char b) {
    646         // Some language, such as Turkish, need testing both cases.
    647         return a == b
    648                 || Character.toLowerCase(a) == Character.toLowerCase(b)
    649                 || Character.toUpperCase(a) == Character.toUpperCase(b);
    650     }
    651 
    652     /**
    653      * Returns true if a and b are equal ignoring the case of the characters, including if they are
    654      * both null.
    655      * @param a first CharSequence to check
    656      * @param b second CharSequence to check
    657      * @return {@code true} if a and b are equal, {@code false} otherwise.
    658      */
    659     public static boolean equalsIgnoreCase(CharSequence a, CharSequence b) {
    660         if (a == b)
    661             return true;  // including both a and b are null.
    662         if (a == null || b == null)
    663             return false;
    664         final int length = a.length();
    665         if (length != b.length())
    666             return false;
    667         for (int i = 0; i < length; i++) {
    668             if (!equalsIgnoreCase(a.charAt(i), b.charAt(i)))
    669                 return false;
    670         }
    671         return true;
    672     }
    673 
    674     /**
    675      * Returns true if a and b are equal ignoring the case of the characters, including if a is null
    676      * and b is zero length.
    677      * @param a CharSequence to check
    678      * @param b character array to check
    679      * @param offset start offset of array b
    680      * @param length length of characters in array b
    681      * @return {@code true} if a and b are equal, {@code false} otherwise.
    682      * @throws IndexOutOfBoundsException
    683      *   if {@code offset < 0 || length < 0 || offset + length > data.length}.
    684      * @throws NullPointerException if {@code b == null}.
    685      */
    686     public static boolean equalsIgnoreCase(CharSequence a, char[] b, int offset, int length) {
    687         if (offset < 0 || length < 0 || length > b.length - offset)
    688             throw new IndexOutOfBoundsException("array.length=" + b.length + " offset=" + offset
    689                     + " length=" + length);
    690         if (a == null)
    691             return length == 0;  // including a is null and b is zero length.
    692         if (a.length() != length)
    693             return false;
    694         for (int i = 0; i < length; i++) {
    695             if (!equalsIgnoreCase(a.charAt(i), b[offset + i]))
    696                 return false;
    697         }
    698         return true;
    699     }
    700 
    701     public static float getDipScale(Context context) {
    702         final float scale = context.getResources().getDisplayMetrics().density;
    703         return scale;
    704     }
    705 
    706     /** Convert pixel to DIP */
    707     public static int dipToPixel(float scale, int dip) {
    708         return (int) (dip * scale + 0.5);
    709     }
    710 
    711     /**
    712      * Remove duplicates from an array of strings.
    713      *
    714      * This method will always keep the first occurence of all strings at their position
    715      * in the array, removing the subsequent ones.
    716      */
    717     public static void removeDupes(final ArrayList<CharSequence> suggestions) {
    718         if (suggestions.size() < 2) return;
    719         int i = 1;
    720         // Don't cache suggestions.size(), since we may be removing items
    721         while (i < suggestions.size()) {
    722             final CharSequence cur = suggestions.get(i);
    723             // Compare each suggestion with each previous suggestion
    724             for (int j = 0; j < i; j++) {
    725                 CharSequence previous = suggestions.get(j);
    726                 if (TextUtils.equals(cur, previous)) {
    727                     removeFromSuggestions(suggestions, i);
    728                     i--;
    729                     break;
    730                 }
    731             }
    732             i++;
    733         }
    734     }
    735 
    736     private static void removeFromSuggestions(final ArrayList<CharSequence> suggestions,
    737             final int index) {
    738         final CharSequence garbage = suggestions.remove(index);
    739         if (garbage instanceof StringBuilder) {
    740             StringBuilderPool.recycle((StringBuilder)garbage);
    741         }
    742     }
    743 
    744     public static String getFullDisplayName(Locale locale, boolean returnsNameInThisLocale) {
    745         if (returnsNameInThisLocale) {
    746             return toTitleCase(SubtypeLocale.getFullDisplayName(locale), locale);
    747         } else {
    748             return toTitleCase(locale.getDisplayName(), locale);
    749         }
    750     }
    751 
    752     public static String getDisplayLanguage(Locale locale) {
    753         return toTitleCase(SubtypeLocale.getFullDisplayName(locale), locale);
    754     }
    755 
    756     public static String getMiddleDisplayLanguage(Locale locale) {
    757         return toTitleCase((LocaleUtils.constructLocaleFromString(
    758                 locale.getLanguage()).getDisplayLanguage(locale)), locale);
    759     }
    760 
    761     public static String getShortDisplayLanguage(Locale locale) {
    762         return toTitleCase(locale.getLanguage(), locale);
    763     }
    764 
    765     public static String toTitleCase(String s, Locale locale) {
    766         if (s.length() <= 1) {
    767             // TODO: is this really correct? Shouldn't this be s.toUpperCase()?
    768             return s;
    769         }
    770         // TODO: fix the bugs below
    771         // - This does not work for Greek, because it returns upper case instead of title case.
    772         // - It does not work for Serbian, because it fails to account for the "lj" character,
    773         // which should be "Lj" in title case and "LJ" in upper case.
    774         // - It does not work for Dutch, because it fails to account for the "ij" digraph, which
    775         // are two different characters but both should be capitalized as "IJ" as if they were
    776         // a single letter.
    777         // - It also does not work with unicode surrogate code points.
    778         return s.toUpperCase(locale).charAt(0) + s.substring(1);
    779     }
    780 
    781     public static int getCurrentVibrationDuration(SharedPreferences sp, Resources res) {
    782         final int ms = sp.getInt(Settings.PREF_VIBRATION_DURATION_SETTINGS, -1);
    783         if (ms >= 0) {
    784             return ms;
    785         }
    786         final String[] durationPerHardwareList = res.getStringArray(
    787                 R.array.keypress_vibration_durations);
    788         final String hardwarePrefix = Build.HARDWARE + ",";
    789         for (final String element : durationPerHardwareList) {
    790             if (element.startsWith(hardwarePrefix)) {
    791                 return (int)Long.parseLong(element.substring(element.lastIndexOf(',') + 1));
    792             }
    793         }
    794         return -1;
    795     }
    796 
    797     public static boolean willAutoCorrect(SuggestedWords suggestions) {
    798         return !suggestions.mTypedWordValid && suggestions.mHasAutoCorrectionCandidate
    799                 && !suggestions.shouldBlockAutoCorrection();
    800     }
    801 }
    802