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_KEYPRESS_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 float getCurrentKeypressSoundVolume(SharedPreferences sp, Resources res) { 798 final float volume = sp.getFloat(Settings.PREF_KEYPRESS_SOUND_VOLUME, -1.0f); 799 if (volume >= 0) { 800 return volume; 801 } 802 803 final String[] volumePerHardwareList = res.getStringArray(R.array.keypress_volumes); 804 final String hardwarePrefix = Build.HARDWARE + ","; 805 for (final String element : volumePerHardwareList) { 806 if (element.startsWith(hardwarePrefix)) { 807 return Float.parseFloat(element.substring(element.lastIndexOf(',') + 1)); 808 } 809 } 810 return -1.0f; 811 } 812 813 public static boolean willAutoCorrect(SuggestedWords suggestions) { 814 return !suggestions.mTypedWordValid && suggestions.mHasAutoCorrectionCandidate 815 && !suggestions.shouldBlockAutoCorrection(); 816 } 817 } 818