• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
Utils()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      */
cancelTask(AsyncTask<?, ?, ?> task, boolean mayInterruptIfRunning)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 
getInstance()87         public static GCUtils getInstance() {
88             return sInstance;
89         }
90 
reset()91         public void reset() {
92             mGCTryCount = 0;
93         }
94 
tryGCOrWait(String metaData, Throwable t)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 
hasMultipleEnabledIMEsOrSubtypes( final InputMethodManagerCompatWrapper imm, final boolean shouldIncludeAuxiliarySubtypes)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 
getInputMethodId(InputMethodManagerCompatWrapper imm, String packageName)157     public static String getInputMethodId(InputMethodManagerCompatWrapper imm, String packageName) {
158         return getInputMethodInfo(imm, packageName).getId();
159     }
160 
getInputMethodInfo( InputMethodManagerCompatWrapper imm, String packageName)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
shouldBlockAutoCorrectionBySafetyNet(SuggestedWords suggestions, Suggest suggest)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 
canBeFollowedByPeriod(final int codePoint)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 
RingCharBuffer()231         private RingCharBuffer() {
232             // Intentional empty constructor for singleton.
233         }
getInstance()234         public static RingCharBuffer getInstance() {
235             return sRingCharBuffer;
236         }
init(InputMethodService context, boolean enabled, boolean usabilityStudy)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         }
normalize(int in)245         private int normalize(int in) {
246             int ret = in % BUFSIZE;
247             return ret < 0 ? ret + BUFSIZE : ret;
248         }
push(char c, int x, int y)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         }
pop()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         }
getBackwardNthChar(int n)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         }
getPreviousX(char c, int back)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         }
getPreviousY(char c, int back)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         }
getLastWord(int ignoreCharCount)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         }
reset()315         public void reset() {
316             mLength = 0;
317         }
318     }
319 
320 
321     /* Damerau-Levenshtein distance */
editDistance(CharSequence s, CharSequence t)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
getStackTrace()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;
calcNormalizedScore(CharSequence before, CharSequence after, int score)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 
UsabilityStudyLogUtils()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 
getInstance()448         public static UsabilityStudyLogUtils getInstance() {
449             return sInstance;
450         }
451 
init(InputMethodService ims)452         public void init(InputMethodService ims) {
453             mIms = ims;
454             mDirectory = ims.getFilesDir();
455         }
456 
createLogFileIfNotExist()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 
writeBackSpace()468         public void writeBackSpace() {
469             UsabilityStudyLogUtils.getInstance().write("<backspace>\t0\t0");
470         }
471 
writeChar(char c, int x, int y)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 
write(final String log)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 
printAll()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 
clearAll()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 
getBufferedReader()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 
getPrintWriter( File dir, String filename, boolean renew)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 
getKeyboardMode(EditorInfo editorInfo)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 
containsInCsv(String key, String csv)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 
inPrivateImeOptions(String packageName, String key, EditorInfo editorInfo)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      */
getMainDictionaryResourceId(Resources res)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 
loadNativeLibrary()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      */
equalsIgnoreCase(char a, char b)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      */
equalsIgnoreCase(CharSequence a, CharSequence b)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      */
equalsIgnoreCase(CharSequence a, char[] b, int offset, int length)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 
getDipScale(Context context)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 */
dipToPixel(float scale, int 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      */
removeDupes(final ArrayList<CharSequence> suggestions)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 
removeFromSuggestions(final ArrayList<CharSequence> suggestions, final int index)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 
getFullDisplayName(Locale locale, boolean returnsNameInThisLocale)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 
getDisplayLanguage(Locale locale)752     public static String getDisplayLanguage(Locale locale) {
753         return toTitleCase(SubtypeLocale.getFullDisplayName(locale), locale);
754     }
755 
getMiddleDisplayLanguage(Locale locale)756     public static String getMiddleDisplayLanguage(Locale locale) {
757         return toTitleCase((LocaleUtils.constructLocaleFromString(
758                 locale.getLanguage()).getDisplayLanguage(locale)), locale);
759     }
760 
getShortDisplayLanguage(Locale locale)761     public static String getShortDisplayLanguage(Locale locale) {
762         return toTitleCase(locale.getLanguage(), locale);
763     }
764 
toTitleCase(String s, Locale locale)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 
getCurrentVibrationDuration(SharedPreferences sp, Resources res)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 
willAutoCorrect(SuggestedWords suggestions)797     public static boolean willAutoCorrect(SuggestedWords suggestions) {
798         return !suggestions.mTypedWordValid && suggestions.mHasAutoCorrectionCandidate
799                 && !suggestions.shouldBlockAutoCorrection();
800     }
801 }
802