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.Intent; 21 import android.content.pm.PackageManager; 22 import android.content.res.Resources; 23 import android.inputmethodservice.InputMethodService; 24 import android.net.Uri; 25 import android.os.AsyncTask; 26 import android.os.Build; 27 import android.os.Environment; 28 import android.os.Handler; 29 import android.os.HandlerThread; 30 import android.os.Process; 31 import android.text.TextUtils; 32 import android.text.format.DateUtils; 33 import android.util.Log; 34 35 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; 36 37 import java.io.BufferedReader; 38 import java.io.File; 39 import java.io.FileInputStream; 40 import java.io.FileNotFoundException; 41 import java.io.FileOutputStream; 42 import java.io.FileReader; 43 import java.io.IOException; 44 import java.io.PrintWriter; 45 import java.nio.channels.FileChannel; 46 import java.text.SimpleDateFormat; 47 import java.util.Collections; 48 import java.util.Date; 49 import java.util.HashMap; 50 import java.util.Map; 51 52 public class Utils { Utils()53 private Utils() { 54 // This utility class is not publicly instantiable. 55 } 56 57 /** 58 * Cancel an {@link AsyncTask}. 59 * 60 * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this 61 * task should be interrupted; otherwise, in-progress tasks are allowed 62 * to complete. 63 */ cancelTask(AsyncTask<?, ?, ?> task, boolean mayInterruptIfRunning)64 public static void cancelTask(AsyncTask<?, ?, ?> task, boolean mayInterruptIfRunning) { 65 if (task != null && task.getStatus() != AsyncTask.Status.FINISHED) { 66 task.cancel(mayInterruptIfRunning); 67 } 68 } 69 70 public static class GCUtils { 71 private static final String GC_TAG = GCUtils.class.getSimpleName(); 72 public static final int GC_TRY_COUNT = 2; 73 // GC_TRY_LOOP_MAX is used for the hard limit of GC wait, 74 // GC_TRY_LOOP_MAX should be greater than GC_TRY_COUNT. 75 public static final int GC_TRY_LOOP_MAX = 5; 76 private static final long GC_INTERVAL = DateUtils.SECOND_IN_MILLIS; 77 private static GCUtils sInstance = new GCUtils(); 78 private int mGCTryCount = 0; 79 getInstance()80 public static GCUtils getInstance() { 81 return sInstance; 82 } 83 reset()84 public void reset() { 85 mGCTryCount = 0; 86 } 87 tryGCOrWait(String metaData, Throwable t)88 public boolean tryGCOrWait(String metaData, Throwable t) { 89 if (mGCTryCount == 0) { 90 System.gc(); 91 } 92 if (++mGCTryCount > GC_TRY_COUNT) { 93 LatinImeLogger.logOnException(metaData, t); 94 return false; 95 } else { 96 try { 97 Thread.sleep(GC_INTERVAL); 98 return true; 99 } catch (InterruptedException e) { 100 Log.e(GC_TAG, "Sleep was interrupted."); 101 LatinImeLogger.logOnException(metaData, t); 102 return false; 103 } 104 } 105 } 106 } 107 108 /* package */ static class RingCharBuffer { 109 private static RingCharBuffer sRingCharBuffer = new RingCharBuffer(); 110 private static final char PLACEHOLDER_DELIMITER_CHAR = '\uFFFC'; 111 private static final int INVALID_COORDINATE = -2; 112 /* package */ static final int BUFSIZE = 20; 113 private InputMethodService mContext; 114 private boolean mEnabled = false; 115 private int mEnd = 0; 116 /* package */ int mLength = 0; 117 private char[] mCharBuf = new char[BUFSIZE]; 118 private int[] mXBuf = new int[BUFSIZE]; 119 private int[] mYBuf = new int[BUFSIZE]; 120 RingCharBuffer()121 private RingCharBuffer() { 122 // Intentional empty constructor for singleton. 123 } getInstance()124 public static RingCharBuffer getInstance() { 125 return sRingCharBuffer; 126 } init(InputMethodService context, boolean enabled, boolean usabilityStudy)127 public static RingCharBuffer init(InputMethodService context, boolean enabled, 128 boolean usabilityStudy) { 129 if (!(enabled || usabilityStudy)) return null; 130 sRingCharBuffer.mContext = context; 131 sRingCharBuffer.mEnabled = true; 132 UsabilityStudyLogUtils.getInstance().init(context); 133 return sRingCharBuffer; 134 } normalize(int in)135 private static int normalize(int in) { 136 int ret = in % BUFSIZE; 137 return ret < 0 ? ret + BUFSIZE : ret; 138 } 139 // TODO: accept code points push(char c, int x, int y)140 public void push(char c, int x, int y) { 141 if (!mEnabled) return; 142 mCharBuf[mEnd] = c; 143 mXBuf[mEnd] = x; 144 mYBuf[mEnd] = y; 145 mEnd = normalize(mEnd + 1); 146 if (mLength < BUFSIZE) { 147 ++mLength; 148 } 149 } pop()150 public char pop() { 151 if (mLength < 1) { 152 return PLACEHOLDER_DELIMITER_CHAR; 153 } else { 154 mEnd = normalize(mEnd - 1); 155 --mLength; 156 return mCharBuf[mEnd]; 157 } 158 } getBackwardNthChar(int n)159 public char getBackwardNthChar(int n) { 160 if (mLength <= n || n < 0) { 161 return PLACEHOLDER_DELIMITER_CHAR; 162 } else { 163 return mCharBuf[normalize(mEnd - n - 1)]; 164 } 165 } getPreviousX(char c, int back)166 public int getPreviousX(char c, int back) { 167 int index = normalize(mEnd - 2 - back); 168 if (mLength <= back 169 || Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) { 170 return INVALID_COORDINATE; 171 } else { 172 return mXBuf[index]; 173 } 174 } getPreviousY(char c, int back)175 public int getPreviousY(char c, int back) { 176 int index = normalize(mEnd - 2 - back); 177 if (mLength <= back 178 || Character.toLowerCase(c) != Character.toLowerCase(mCharBuf[index])) { 179 return INVALID_COORDINATE; 180 } else { 181 return mYBuf[index]; 182 } 183 } getLastWord(int ignoreCharCount)184 public String getLastWord(int ignoreCharCount) { 185 StringBuilder sb = new StringBuilder(); 186 int i = ignoreCharCount; 187 for (; i < mLength; ++i) { 188 char c = mCharBuf[normalize(mEnd - 1 - i)]; 189 if (!((LatinIME)mContext).isWordSeparator(c)) { 190 break; 191 } 192 } 193 for (; i < mLength; ++i) { 194 char c = mCharBuf[normalize(mEnd - 1 - i)]; 195 if (!((LatinIME)mContext).isWordSeparator(c)) { 196 sb.append(c); 197 } else { 198 break; 199 } 200 } 201 return sb.reverse().toString(); 202 } reset()203 public void reset() { 204 mLength = 0; 205 } 206 } 207 208 // Get the current stack trace getStackTrace()209 public static String getStackTrace() { 210 StringBuilder sb = new StringBuilder(); 211 try { 212 throw new RuntimeException(); 213 } catch (RuntimeException e) { 214 StackTraceElement[] frames = e.getStackTrace(); 215 // Start at 1 because the first frame is here and we don't care about it 216 for (int j = 1; j < frames.length; ++j) sb.append(frames[j].toString() + "\n"); 217 } 218 return sb.toString(); 219 } 220 221 public static class UsabilityStudyLogUtils { 222 // TODO: remove code duplication with ResearchLog class 223 private static final String USABILITY_TAG = UsabilityStudyLogUtils.class.getSimpleName(); 224 private static final String FILENAME = "log.txt"; 225 private final Handler mLoggingHandler; 226 private File mFile; 227 private File mDirectory; 228 private InputMethodService mIms; 229 private PrintWriter mWriter; 230 private final Date mDate; 231 private final SimpleDateFormat mDateFormat; 232 UsabilityStudyLogUtils()233 private UsabilityStudyLogUtils() { 234 mDate = new Date(); 235 mDateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss.SSSZ"); 236 237 HandlerThread handlerThread = new HandlerThread("UsabilityStudyLogUtils logging task", 238 Process.THREAD_PRIORITY_BACKGROUND); 239 handlerThread.start(); 240 mLoggingHandler = new Handler(handlerThread.getLooper()); 241 } 242 243 // Initialization-on-demand holder 244 private static class OnDemandInitializationHolder { 245 public static final UsabilityStudyLogUtils sInstance = new UsabilityStudyLogUtils(); 246 } 247 getInstance()248 public static UsabilityStudyLogUtils getInstance() { 249 return OnDemandInitializationHolder.sInstance; 250 } 251 init(InputMethodService ims)252 public void init(InputMethodService ims) { 253 mIms = ims; 254 mDirectory = ims.getFilesDir(); 255 } 256 createLogFileIfNotExist()257 private void createLogFileIfNotExist() { 258 if ((mFile == null || !mFile.exists()) 259 && (mDirectory != null && mDirectory.exists())) { 260 try { 261 mWriter = getPrintWriter(mDirectory, FILENAME, false); 262 } catch (IOException e) { 263 Log.e(USABILITY_TAG, "Can't create log file."); 264 } 265 } 266 } 267 writeBackSpace(int x, int y)268 public static void writeBackSpace(int x, int y) { 269 UsabilityStudyLogUtils.getInstance().write("<backspace>\t" + x + "\t" + y); 270 } 271 writeChar(char c, int x, int y)272 public void writeChar(char c, int x, int y) { 273 String inputChar = String.valueOf(c); 274 switch (c) { 275 case '\n': 276 inputChar = "<enter>"; 277 break; 278 case '\t': 279 inputChar = "<tab>"; 280 break; 281 case ' ': 282 inputChar = "<space>"; 283 break; 284 } 285 UsabilityStudyLogUtils.getInstance().write(inputChar + "\t" + x + "\t" + y); 286 LatinImeLogger.onPrintAllUsabilityStudyLogs(); 287 } 288 write(final String log)289 public void write(final String log) { 290 mLoggingHandler.post(new Runnable() { 291 @Override 292 public void run() { 293 createLogFileIfNotExist(); 294 final long currentTime = System.currentTimeMillis(); 295 mDate.setTime(currentTime); 296 297 final String printString = String.format("%s\t%d\t%s\n", 298 mDateFormat.format(mDate), currentTime, log); 299 if (LatinImeLogger.sDBG) { 300 Log.d(USABILITY_TAG, "Write: " + log); 301 } 302 mWriter.print(printString); 303 } 304 }); 305 } 306 getBufferedLogs()307 private synchronized String getBufferedLogs() { 308 mWriter.flush(); 309 StringBuilder sb = new StringBuilder(); 310 BufferedReader br = getBufferedReader(); 311 String line; 312 try { 313 while ((line = br.readLine()) != null) { 314 sb.append('\n'); 315 sb.append(line); 316 } 317 } catch (IOException e) { 318 Log.e(USABILITY_TAG, "Can't read log file."); 319 } finally { 320 if (LatinImeLogger.sDBG) { 321 Log.d(USABILITY_TAG, "Got all buffered logs\n" + sb.toString()); 322 } 323 try { 324 br.close(); 325 } catch (IOException e) { 326 // ignore. 327 } 328 } 329 return sb.toString(); 330 } 331 emailResearcherLogsAll()332 public void emailResearcherLogsAll() { 333 mLoggingHandler.post(new Runnable() { 334 @Override 335 public void run() { 336 final Date date = new Date(); 337 date.setTime(System.currentTimeMillis()); 338 final String currentDateTimeString = 339 new SimpleDateFormat("yyyyMMdd-HHmmssZ").format(date); 340 if (mFile == null) { 341 Log.w(USABILITY_TAG, "No internal log file found."); 342 return; 343 } 344 if (mIms.checkCallingOrSelfPermission( 345 android.Manifest.permission.WRITE_EXTERNAL_STORAGE) 346 != PackageManager.PERMISSION_GRANTED) { 347 Log.w(USABILITY_TAG, "Doesn't have the permission WRITE_EXTERNAL_STORAGE"); 348 return; 349 } 350 mWriter.flush(); 351 final String destPath = Environment.getExternalStorageDirectory() 352 + "/research-" + currentDateTimeString + ".log"; 353 final File destFile = new File(destPath); 354 try { 355 final FileChannel src = (new FileInputStream(mFile)).getChannel(); 356 final FileChannel dest = (new FileOutputStream(destFile)).getChannel(); 357 src.transferTo(0, src.size(), dest); 358 src.close(); 359 dest.close(); 360 } catch (FileNotFoundException e1) { 361 Log.w(USABILITY_TAG, e1); 362 return; 363 } catch (IOException e2) { 364 Log.w(USABILITY_TAG, e2); 365 return; 366 } 367 if (destFile == null || !destFile.exists()) { 368 Log.w(USABILITY_TAG, "Dest file doesn't exist."); 369 return; 370 } 371 final Intent intent = new Intent(Intent.ACTION_SEND); 372 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 373 if (LatinImeLogger.sDBG) { 374 Log.d(USABILITY_TAG, "Destination file URI is " + destFile.toURI()); 375 } 376 intent.setType("text/plain"); 377 intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + destPath)); 378 intent.putExtra(Intent.EXTRA_SUBJECT, 379 "[Research Logs] " + currentDateTimeString); 380 mIms.startActivity(intent); 381 } 382 }); 383 } 384 printAll()385 public void printAll() { 386 mLoggingHandler.post(new Runnable() { 387 @Override 388 public void run() { 389 mIms.getCurrentInputConnection().commitText(getBufferedLogs(), 0); 390 } 391 }); 392 } 393 clearAll()394 public void clearAll() { 395 mLoggingHandler.post(new Runnable() { 396 @Override 397 public void run() { 398 if (mFile != null && mFile.exists()) { 399 if (LatinImeLogger.sDBG) { 400 Log.d(USABILITY_TAG, "Delete log file."); 401 } 402 mFile.delete(); 403 mWriter.close(); 404 } 405 } 406 }); 407 } 408 getBufferedReader()409 private BufferedReader getBufferedReader() { 410 createLogFileIfNotExist(); 411 try { 412 return new BufferedReader(new FileReader(mFile)); 413 } catch (FileNotFoundException e) { 414 return null; 415 } 416 } 417 getPrintWriter( File dir, String filename, boolean renew)418 private PrintWriter getPrintWriter( 419 File dir, String filename, boolean renew) throws IOException { 420 mFile = new File(dir, filename); 421 if (mFile.exists()) { 422 if (renew) { 423 mFile.delete(); 424 } 425 } 426 return new PrintWriter(new FileOutputStream(mFile), true /* autoFlush */); 427 } 428 } 429 getDipScale(Context context)430 public static float getDipScale(Context context) { 431 final float scale = context.getResources().getDisplayMetrics().density; 432 return scale; 433 } 434 435 /** Convert pixel to DIP */ dipToPixel(float scale, int dip)436 public static int dipToPixel(float scale, int dip) { 437 return (int) (dip * scale + 0.5); 438 } 439 440 public static class Stats { onNonSeparator(final char code, final int x, final int y)441 public static void onNonSeparator(final char code, final int x, 442 final int y) { 443 RingCharBuffer.getInstance().push(code, x, y); 444 LatinImeLogger.logOnInputChar(); 445 } 446 onSeparator(final int code, final int x, final int y)447 public static void onSeparator(final int code, final int x, 448 final int y) { 449 // TODO: accept code points 450 RingCharBuffer.getInstance().push((char)code, x, y); 451 LatinImeLogger.logOnInputSeparator(); 452 } 453 onAutoCorrection(final String typedWord, final String correctedWord, final int separatorCode)454 public static void onAutoCorrection(final String typedWord, final String correctedWord, 455 final int separatorCode) { 456 if (TextUtils.isEmpty(typedWord)) return; 457 LatinImeLogger.logOnAutoCorrection(typedWord, correctedWord, separatorCode); 458 } 459 onAutoCorrectionCancellation()460 public static void onAutoCorrectionCancellation() { 461 LatinImeLogger.logOnAutoCorrectionCancelled(); 462 } 463 } 464 getDebugInfo(final SuggestedWords suggestions, final int pos)465 public static String getDebugInfo(final SuggestedWords suggestions, final int pos) { 466 if (!LatinImeLogger.sDBG) return null; 467 final SuggestedWordInfo wordInfo = suggestions.getInfo(pos); 468 if (wordInfo == null) return null; 469 final String info = wordInfo.getDebugString(); 470 if (TextUtils.isEmpty(info)) return null; 471 return info; 472 } 473 474 private static final String HARDWARE_PREFIX = Build.HARDWARE + ","; 475 private static final HashMap<String, String> sDeviceOverrideValueMap = 476 new HashMap<String, String>(); 477 getDeviceOverrideValue(Resources res, int overrideResId, String defValue)478 public static String getDeviceOverrideValue(Resources res, int overrideResId, String defValue) { 479 final int orientation = res.getConfiguration().orientation; 480 final String key = overrideResId + "-" + orientation; 481 if (!sDeviceOverrideValueMap.containsKey(key)) { 482 String overrideValue = defValue; 483 for (final String element : res.getStringArray(overrideResId)) { 484 if (element.startsWith(HARDWARE_PREFIX)) { 485 overrideValue = element.substring(HARDWARE_PREFIX.length()); 486 break; 487 } 488 } 489 sDeviceOverrideValueMap.put(key, overrideValue); 490 } 491 return sDeviceOverrideValueMap.get(key); 492 } 493 494 private static final HashMap<String, Long> EMPTY_LT_HASH_MAP = new HashMap<String, Long>(); 495 private static final String LOCALE_AND_TIME_STR_SEPARATER = ","; localeAndTimeStrToHashMap(String str)496 public static HashMap<String, Long> localeAndTimeStrToHashMap(String str) { 497 if (TextUtils.isEmpty(str)) { 498 return EMPTY_LT_HASH_MAP; 499 } 500 final String[] ss = str.split(LOCALE_AND_TIME_STR_SEPARATER); 501 final int N = ss.length; 502 if (N < 2 || N % 2 != 0) { 503 return EMPTY_LT_HASH_MAP; 504 } 505 final HashMap<String, Long> retval = new HashMap<String, Long>(); 506 for (int i = 0; i < N / 2; ++i) { 507 final String localeStr = ss[i * 2]; 508 final long time = Long.valueOf(ss[i * 2 + 1]); 509 retval.put(localeStr, time); 510 } 511 return retval; 512 } 513 localeAndTimeHashMapToStr(HashMap<String, Long> map)514 public static String localeAndTimeHashMapToStr(HashMap<String, Long> map) { 515 if (map == null || map.isEmpty()) { 516 return ""; 517 } 518 final StringBuilder builder = new StringBuilder(); 519 for (String localeStr : map.keySet()) { 520 if (builder.length() > 0) { 521 builder.append(LOCALE_AND_TIME_STR_SEPARATER); 522 } 523 final Long time = map.get(localeStr); 524 builder.append(localeStr).append(LOCALE_AND_TIME_STR_SEPARATER); 525 builder.append(String.valueOf(time)); 526 } 527 return builder.toString(); 528 } 529 } 530