Loading java/res/values/strings.xml +3 −4 Original line number Diff line number Diff line Loading @@ -223,15 +223,15 @@ <string name="notify_recorded_timestamp">Recorded timestamp</string> <!-- Title for dialog option to let users cancel logging and delete log for this session [CHAR LIMIT=35] --> <string name="do_not_log_this_session">Do not log this session</string> <string name="do_not_log_this_session">Suspend logging</string> <!-- Title for dialog option to let users reenable logging [CHAR LIMIT=35] --> <string name="enable_session_logging">Enable session logging</string> <string name="enable_session_logging">Enable logging</string> <!-- Title for dialog option to let users log all events in this session [CHAR LIMIT=35] --> <string name="log_whole_session_history">Log whole session history</string> <!-- Toast notification that the system is processing the request to delete the log for this session [CHAR LIMIT=35] --> <string name="notify_session_log_deleting">Deleting session log</string> <!-- Toast notification that the system has successfully deleted the log for this session [CHAR LIMIT=35] --> <string name="notify_session_log_deleted">Session log deleted</string> <string name="notify_logging_suspended">Logging temporarily suspended. To disable permanently, go to Android Keyboard Settings</string> <!-- Toast notification that the system has failed to delete the log for this session [CHAR LIMIT=35] --> <string name="notify_session_log_not_deleted">Session log NOT deleted</string> <!-- Toast notification that the system has recorded the whole session history [CHAR LIMIT=35] --> Loading @@ -240,7 +240,6 @@ <string name="notify_session_history_not_logged">Error: Session history NOT logged</string> <!-- Toast notification that the system is enabling logging [CHAR LIMIT=35] --> <string name="notify_session_logging_enabled">Session logging enabled</string> <!-- Preference for input language selection --> <string name="select_language">Input languages</string> Loading java/src/com/android/inputmethod/latin/LatinIME.java +1 −2 Original line number Diff line number Diff line Loading @@ -618,7 +618,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0)); } if (ProductionFlag.IS_EXPERIMENTAL) { ResearchLogger.getInstance().start(); ResearchLogger.latinIME_onStartInputViewInternal(editorInfo, mPrefs); } if (InputAttributes.inPrivateImeOptions(null, NO_MICROPHONE_COMPAT, editorInfo)) { Loading Loading @@ -711,7 +710,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen LatinImeLogger.commit(); if (ProductionFlag.IS_EXPERIMENTAL) { ResearchLogger.getInstance().stop(); ResearchLogger.getInstance().latinIME_onFinishInputInternal(); } KeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); Loading java/src/com/android/inputmethod/latin/ResearchLog.java +31 −17 Original line number Diff line number Diff line Loading @@ -55,13 +55,14 @@ public class ResearchLog { final ScheduledExecutorService mExecutor; /* package */ final File mFile; private JsonWriter mJsonWriter = NULL_JSON_WRITER; // should never be null private JsonWriter mJsonWriter = NULL_JSON_WRITER; private int mLoggingState; private static final int LOGGING_STATE_UNSTARTED = 0; private static final int LOGGING_STATE_RUNNING = 1; private static final int LOGGING_STATE_STOPPING = 2; private static final int LOGGING_STATE_STOPPED = 3; private static final int LOGGING_STATE_READY = 1; // don't create file until necessary private static final int LOGGING_STATE_RUNNING = 2; private static final int LOGGING_STATE_STOPPING = 3; private static final int LOGGING_STATE_STOPPED = 4; private static final long FLUSH_DELAY_IN_MS = 1000 * 5; private static class NullOutputStream extends OutputStream { Loading Loading @@ -94,11 +95,9 @@ public class ResearchLog { public synchronized void start() throws IOException { switch (mLoggingState) { case LOGGING_STATE_UNSTARTED: mJsonWriter = new JsonWriter(new BufferedWriter(new FileWriter(mFile))); mJsonWriter.setLenient(true); mJsonWriter.beginArray(); mLoggingState = LOGGING_STATE_RUNNING; mLoggingState = LOGGING_STATE_READY; break; case LOGGING_STATE_READY: case LOGGING_STATE_RUNNING: case LOGGING_STATE_STOPPING: case LOGGING_STATE_STOPPED: Loading @@ -111,6 +110,7 @@ public class ResearchLog { case LOGGING_STATE_UNSTARTED: mLoggingState = LOGGING_STATE_STOPPED; break; case LOGGING_STATE_READY: case LOGGING_STATE_RUNNING: mExecutor.submit(new Callable<Object>() { @Override Loading @@ -120,14 +120,13 @@ public class ResearchLog { mJsonWriter.flush(); mJsonWriter.close(); } finally { // the contentprovider only exports data if the writable // bit is cleared. boolean success = mFile.setWritable(false, false); mLoggingState = LOGGING_STATE_STOPPED; } return null; } }); removeAnyScheduledFlush(); mExecutor.shutdown(); mLoggingState = LOGGING_STATE_STOPPING; break; Loading @@ -139,27 +138,26 @@ public class ResearchLog { public boolean isAlive() { switch (mLoggingState) { case LOGGING_STATE_UNSTARTED: case LOGGING_STATE_READY: case LOGGING_STATE_RUNNING: return true; } return false; } public void waitUntilStopped(int timeoutInMs) throws InterruptedException { public void waitUntilStopped(final int timeoutInMs) throws InterruptedException { removeAnyScheduledFlush(); mExecutor.shutdown(); mExecutor.awaitTermination(timeoutInMs, TimeUnit.MILLISECONDS); } private boolean isAbortSuccessful; public boolean isAbortSuccessful() { return isAbortSuccessful; } public synchronized void abort() { switch (mLoggingState) { case LOGGING_STATE_UNSTARTED: mLoggingState = LOGGING_STATE_STOPPED; isAbortSuccessful = true; break; case LOGGING_STATE_READY: case LOGGING_STATE_RUNNING: mExecutor.submit(new Callable<Object>() { @Override Loading @@ -173,6 +171,7 @@ public class ResearchLog { return null; } }); removeAnyScheduledFlush(); mExecutor.shutdown(); mLoggingState = LOGGING_STATE_STOPPING; break; Loading @@ -181,10 +180,16 @@ public class ResearchLog { } } private boolean isAbortSuccessful; public boolean isAbortSuccessful() { return isAbortSuccessful; } /* package */ synchronized void flush() { switch (mLoggingState) { case LOGGING_STATE_UNSTARTED: break; case LOGGING_STATE_READY: case LOGGING_STATE_RUNNING: removeAnyScheduledFlush(); mExecutor.submit(mFlushCallable); Loading @@ -197,7 +202,9 @@ public class ResearchLog { private Callable<Object> mFlushCallable = new Callable<Object>() { @Override public Object call() throws Exception { if (mLoggingState == LOGGING_STATE_RUNNING) { mJsonWriter.flush(); } return null; } }; Loading @@ -220,6 +227,7 @@ public class ResearchLog { switch (mLoggingState) { case LOGGING_STATE_UNSTARTED: break; case LOGGING_STATE_READY: case LOGGING_STATE_RUNNING: mExecutor.submit(new Callable<Object>() { @Override Loading @@ -239,6 +247,7 @@ public class ResearchLog { switch (mLoggingState) { case LOGGING_STATE_UNSTARTED: break; case LOGGING_STATE_READY: case LOGGING_STATE_RUNNING: mExecutor.submit(new Callable<Object>() { @Override Loading @@ -260,6 +269,11 @@ public class ResearchLog { void outputEvent(final String[] keys, final Object[] values) { // not thread safe. try { if (mJsonWriter == NULL_JSON_WRITER) { mJsonWriter = new JsonWriter(new BufferedWriter(new FileWriter(mFile))); mJsonWriter.setLenient(true); mJsonWriter.beginArray(); } mJsonWriter.beginObject(); mJsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis()); mJsonWriter.name(UPTIME_KEY).value(SystemClock.uptimeMillis()); Loading java/src/com/android/inputmethod/latin/ResearchLogger.java +160 −83 Original line number Diff line number Diff line Loading @@ -44,7 +44,6 @@ import com.android.inputmethod.latin.RichInputConnection.Range; import com.android.inputmethod.latin.define.ProductionFlag; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; Loading @@ -64,13 +63,15 @@ import java.util.UUID; public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = ResearchLogger.class.getSimpleName(); private static final boolean OUTPUT_ENTIRE_BUFFER = false; // true may disclose private info /* package */ static final boolean DEFAULT_USABILITY_STUDY_MODE = false; /* package */ static boolean sIsLogging = false; private static final int OUTPUT_FORMAT_VERSION = 1; private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode"; private static final String FILENAME_PREFIX = "researchLog"; /* package */ static final String FILENAME_PREFIX = "researchLog"; private static final String FILENAME_SUFFIX = ".txt"; private static final SimpleDateFormat TIMESTAMP_DATEFORMAT = new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US); private static final boolean IS_SHOWING_INDICATOR = false; // constants related to specific log points private static final String WHITESPACE_SEPARATORS = " \t\n\r"; Loading @@ -92,6 +93,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private boolean mIsPasswordView = false; private boolean mIsLoggingSuspended = false; private SharedPreferences mPrefs; // digits entered by the user are replaced with this codepoint. /* package for test */ static final int DIGIT_REPLACEMENT_CODEPOINT = Loading @@ -101,6 +103,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private static final String PREF_LAST_CLEANUP_TIME = "pref_last_cleanup_time"; private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = DateUtils.DAY_IN_MILLIS; private static final long MAX_LOGFILE_AGE_IN_MS = DateUtils.DAY_IN_MILLIS; protected static final int SUSPEND_DURATION_IN_MINUTES = 1; // set when LatinIME should ignore an onUpdateSelection() callback that // arises from operations in this class private static boolean sLatinIMEExpectingUpdateSelection = false; Loading @@ -124,7 +127,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang if (ims == null) { Log.w(TAG, "IMS is null; logging is off"); } else { mContext = ims; mFilesDir = ims.getFilesDir(); if (mFilesDir == null || !mFilesDir.exists()) { Log.w(TAG, "IME storage directory does not exist."); Loading @@ -132,6 +134,11 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } if (prefs != null) { mUUIDString = getUUID(prefs); if (!prefs.contains(PREF_USABILITY_STUDY_MODE)) { Editor e = prefs.edit(); e.putBoolean(PREF_USABILITY_STUDY_MODE, DEFAULT_USABILITY_STUDY_MODE); e.apply(); } sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false); prefs.registerOnSharedPreferenceChangeListener(this); Loading @@ -146,6 +153,11 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } } mKeyboardSwitcher = keyboardSwitcher; mContext = ims; mPrefs = prefs; // TODO: force user to decide at splash screen instead of defaulting to on. setLoggingAllowed(true); } private void cleanupLoggingDir(final File dir, final long time) { Loading @@ -166,8 +178,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang return new File(filesDir, sb.toString()); } public void start() { if (!sIsLogging) { private void start() { updateSuspendedState(); requestIndicatorRedraw(); if (!isAllowedToLog()) { // Log.w(TAG, "not in usability mode; not logging"); return; } Loading @@ -175,10 +189,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang Log.w(TAG, "IME storage directory does not exist. Cannot start logging."); return; } try { if (mMainResearchLog == null || !mMainResearchLog.isAlive()) { mMainResearchLog = new ResearchLog(createLogFile(mFilesDir)); } try { mMainResearchLog.start(); if (mIntentionalResearchLog == null || !mIntentionalResearchLog.isAlive()) { mIntentionalResearchLog = new ResearchLog(createLogFile(mFilesDir)); Loading @@ -189,15 +203,26 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } } public void stop() { /* package */ void stop() { if (mMainResearchLog != null) { mMainResearchLog.stop(); } if (mIntentionalResearchLog != null) { mIntentionalResearchLog.stop(); } } private void setLoggingAllowed(boolean enableLogging) { if (mPrefs == null) { return; } Editor e = mPrefs.edit(); e.putBoolean(PREF_USABILITY_STUDY_MODE, enableLogging); e.apply(); sIsLogging = enableLogging; } public boolean abort() { mIsLoggingSuspended = true; requestIndicatorRedraw(); boolean didAbortMainLog = false; if (mMainResearchLog != null) { mMainResearchLog.abort(); Loading @@ -209,6 +234,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang if (mMainResearchLog.isAbortSuccessful()) { didAbortMainLog = true; } mMainResearchLog = null; } boolean didAbortIntentionalLog = false; if (mIntentionalResearchLog != null) { Loading @@ -221,6 +247,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang if (mIntentionalResearchLog.isAbortSuccessful()) { didAbortIntentionalLog = true; } mIntentionalResearchLog = null; } return didAbortMainLog && didAbortIntentionalLog; } Loading @@ -247,6 +274,34 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } } private void restart() { stop(); start(); } private long mResumeTime = 0L; private void suspendLoggingUntil(long time) { mIsLoggingSuspended = true; mResumeTime = time; requestIndicatorRedraw(); } private void resumeLogging() { mResumeTime = 0L; updateSuspendedState(); requestIndicatorRedraw(); if (isAllowedToLog()) { restart(); } } private void updateSuspendedState() { final long time = System.currentTimeMillis(); if (time > mResumeTime) { mIsLoggingSuspended = false; } } @Override public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { if (key == null || prefs == null) { Loading @@ -256,13 +311,15 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang if (sIsLogging == false) { abort(); } requestIndicatorRedraw(); } /* package */ void presentResearchDialog(final LatinIME latinIME) { final CharSequence title = latinIME.getString(R.string.english_ime_research_log); final boolean showEnable = mIsLoggingSuspended || !sIsLogging; final CharSequence[] items = new CharSequence[] { latinIME.getString(R.string.note_timestamp_for_researchlog), mIsLoggingSuspended ? latinIME.getString(R.string.enable_session_logging) : showEnable ? latinIME.getString(R.string.enable_session_logging) : latinIME.getString(R.string.do_not_log_this_session), latinIME.getString(R.string.log_whole_session_history), }; Loading @@ -277,25 +334,25 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang Toast.LENGTH_LONG).show(); break; case 1: if (mIsLoggingSuspended) { mIsLoggingSuspended = false; requestIndicatorRedraw(); Toast toast = Toast.makeText(latinIME, R.string.notify_session_logging_enabled, Toast.LENGTH_LONG); if (showEnable) { if (!sIsLogging) { setLoggingAllowed(true); } resumeLogging(); Toast.makeText(latinIME, R.string.notify_session_logging_enabled, Toast.LENGTH_LONG).show(); } else { Toast toast = Toast.makeText(latinIME, R.string.notify_session_log_deleting, Toast.LENGTH_LONG); toast.show(); boolean isLogDeleted = abort(); final long currentTime = System.currentTimeMillis(); final long resumeTime = currentTime + 1000 * 60 * SUSPEND_DURATION_IN_MINUTES; suspendLoggingUntil(resumeTime); toast.cancel(); if (isLogDeleted) { Toast.makeText(latinIME, R.string.notify_session_log_deleted, Toast.makeText(latinIME, R.string.notify_logging_suspended, Toast.LENGTH_LONG).show(); } else { Toast.makeText(latinIME, R.string.notify_session_log_not_deleted, Toast.LENGTH_LONG) .show(); } } break; case 2: Loading Loading @@ -328,15 +385,17 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } private boolean isAllowedToLog() { return !mIsPasswordView && !mIsLoggingSuspended; return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging; } public void requestIndicatorRedraw() { // invalidate any existing graphics if (IS_SHOWING_INDICATOR) { if (mKeyboardSwitcher != null) { mKeyboardSwitcher.getKeyboardView().invalidateAllKeys(); } } } private static final String CURRENT_TIME_KEY = "_ct"; private static final String UPTIME_KEY = "_ut"; Loading Loading @@ -467,6 +526,12 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } private void publishLogUnit(LogUnit logUnit, boolean isPrivacySensitive) { if (!isAllowedToLog()) { return; } if (mMainResearchLog == null) { return; } if (isPrivacySensitive) { mMainResearchLog.publishPublicEvents(logUnit); } else { Loading Loading @@ -536,6 +601,18 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } } private static String getUUID(final SharedPreferences prefs) { String uuidString = prefs.getString(PREF_RESEARCH_LOGGER_UUID_STRING, null); if (null == uuidString) { UUID uuid = UUID.randomUUID(); uuidString = uuid.toString(); Editor editor = prefs.edit(); editor.putString(PREF_RESEARCH_LOGGER_UUID_STRING, uuidString); editor.apply(); } return uuidString; } private String scrubWord(String word) { if (mDictionary == null) { return WORD_REPLACEMENT_STRING; Loading @@ -546,9 +623,62 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang return WORD_REPLACEMENT_STRING; } // Special methods related to startup, shutdown, logging itself private static final String[] EVENTKEYS_INTENTIONAL_LOG = { "IntentionalLog" }; private static final String[] EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL = { "LatinIMEOnStartInputViewInternal", "uuid", "packageName", "inputType", "imeOptions", "fieldId", "display", "model", "prefs", "versionCode", "versionName", "outputFormatVersion" }; public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo, final SharedPreferences prefs) { final ResearchLogger researchLogger = getInstance(); researchLogger.start(); if (editorInfo != null) { final Context context = researchLogger.mContext; try { final PackageInfo packageInfo; packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); final Integer versionCode = packageInfo.versionCode; final String versionName = packageInfo.versionName; final Object[] values = { researchLogger.mUUIDString, editorInfo.packageName, Integer.toHexString(editorInfo.inputType), Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId, Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName, OUTPUT_FORMAT_VERSION }; researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL, values); } catch (NameNotFoundException e) { e.printStackTrace(); } } } public void latinIME_onFinishInputInternal() { stop(); } private static final String[] EVENTKEYS_LATINIME_COMMITTEXT = { "LatinIMECommitText", "typedWord" }; public static void latinIME_commitText(final CharSequence typedWord) { final String scrubbedWord = scrubDigitsFromString(typedWord.toString()); final Object[] values = { scrubbedWord }; final ResearchLogger researchLogger = getInstance(); researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_COMMITTEXT, values); researchLogger.onWordComplete(scrubbedWord); } // Regular logging methods private static final String[] EVENTKEYS_LATINKEYBOARDVIEW_PROCESSMOTIONEVENT = { "LatinKeyboardViewProcessMotionEvent", "action", "eventTime", "id", "x", "y", "size", "pressure" Loading Loading @@ -611,19 +741,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang EVENTKEYS_LATINIME_COMMITCURRENTAUTOCORRECTION, values); } private static final String[] EVENTKEYS_LATINIME_COMMITTEXT = { "LatinIMECommitText", "typedWord" }; public static void latinIME_commitText(final CharSequence typedWord) { final String scrubbedWord = scrubDigitsFromString(typedWord.toString()); final Object[] values = { scrubbedWord }; final ResearchLogger researchLogger = getInstance(); researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_COMMITTEXT, values); researchLogger.onWordComplete(scrubbedWord); } private static final String[] EVENTKEYS_LATINIME_DELETESURROUNDINGTEXT = { "LatinIMEDeleteSurroundingText", "length" }; Loading Loading @@ -702,51 +819,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang // Play it safe. Remove privacy-sensitive events. researchLogger.publishLogUnit(researchLogger.mCurrentLogUnit, true); researchLogger.mCurrentLogUnit = new LogUnit(); getInstance().restart(); } } private static final String[] EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL = { "LatinIMEOnStartInputViewInternal", "uuid", "packageName", "inputType", "imeOptions", "fieldId", "display", "model", "prefs", "versionCode", "versionName", "outputFormatVersion" }; public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo, final SharedPreferences prefs) { final ResearchLogger researchLogger = getInstance(); researchLogger.start(); if (editorInfo != null) { final Context context = researchLogger.mContext; try { final PackageInfo packageInfo; packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); final Integer versionCode = packageInfo.versionCode; final String versionName = packageInfo.versionName; final Object[] values = { researchLogger.mUUIDString, editorInfo.packageName, Integer.toHexString(editorInfo.inputType), Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId, Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName, OUTPUT_FORMAT_VERSION }; researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL, values); } catch (NameNotFoundException e) { e.printStackTrace(); } } } private static String getUUID(final SharedPreferences prefs) { String uuidString = prefs.getString(PREF_RESEARCH_LOGGER_UUID_STRING, null); if (null == uuidString) { UUID uuid = UUID.randomUUID(); uuidString = uuid.toString(); Editor editor = prefs.edit(); editor.putString(PREF_RESEARCH_LOGGER_UUID_STRING, uuidString); editor.apply(); } return uuidString; } private static final String[] EVENTKEYS_LATINIME_ONUPDATESELECTION = { "LatinIMEOnUpdateSelection", "lastSelectionStart", "lastSelectionEnd", "oldSelStart", "oldSelEnd", "newSelStart", "newSelEnd", "composingSpanStart", "composingSpanEnd", Loading Loading @@ -873,6 +949,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang if (keyboard != null) { final KeyboardId kid = keyboard.mId; final boolean isPasswordView = kid.passwordInput(); getInstance().setIsPasswordView(isPasswordView); final Object[] values = { KeyboardId.elementIdToName(kid.mElementId), kid.mLocale + ":" + kid.mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET), Loading java/src/com/android/inputmethod/latin/Settings.java +2 −1 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
java/res/values/strings.xml +3 −4 Original line number Diff line number Diff line Loading @@ -223,15 +223,15 @@ <string name="notify_recorded_timestamp">Recorded timestamp</string> <!-- Title for dialog option to let users cancel logging and delete log for this session [CHAR LIMIT=35] --> <string name="do_not_log_this_session">Do not log this session</string> <string name="do_not_log_this_session">Suspend logging</string> <!-- Title for dialog option to let users reenable logging [CHAR LIMIT=35] --> <string name="enable_session_logging">Enable session logging</string> <string name="enable_session_logging">Enable logging</string> <!-- Title for dialog option to let users log all events in this session [CHAR LIMIT=35] --> <string name="log_whole_session_history">Log whole session history</string> <!-- Toast notification that the system is processing the request to delete the log for this session [CHAR LIMIT=35] --> <string name="notify_session_log_deleting">Deleting session log</string> <!-- Toast notification that the system has successfully deleted the log for this session [CHAR LIMIT=35] --> <string name="notify_session_log_deleted">Session log deleted</string> <string name="notify_logging_suspended">Logging temporarily suspended. To disable permanently, go to Android Keyboard Settings</string> <!-- Toast notification that the system has failed to delete the log for this session [CHAR LIMIT=35] --> <string name="notify_session_log_not_deleted">Session log NOT deleted</string> <!-- Toast notification that the system has recorded the whole session history [CHAR LIMIT=35] --> Loading @@ -240,7 +240,6 @@ <string name="notify_session_history_not_logged">Error: Session history NOT logged</string> <!-- Toast notification that the system is enabling logging [CHAR LIMIT=35] --> <string name="notify_session_logging_enabled">Session logging enabled</string> <!-- Preference for input language selection --> <string name="select_language">Input languages</string> Loading
java/src/com/android/inputmethod/latin/LatinIME.java +1 −2 Original line number Diff line number Diff line Loading @@ -618,7 +618,6 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen + ((editorInfo.inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0)); } if (ProductionFlag.IS_EXPERIMENTAL) { ResearchLogger.getInstance().start(); ResearchLogger.latinIME_onStartInputViewInternal(editorInfo, mPrefs); } if (InputAttributes.inPrivateImeOptions(null, NO_MICROPHONE_COMPAT, editorInfo)) { Loading Loading @@ -711,7 +710,7 @@ public class LatinIME extends InputMethodService implements KeyboardActionListen LatinImeLogger.commit(); if (ProductionFlag.IS_EXPERIMENTAL) { ResearchLogger.getInstance().stop(); ResearchLogger.getInstance().latinIME_onFinishInputInternal(); } KeyboardView inputView = mKeyboardSwitcher.getKeyboardView(); Loading
java/src/com/android/inputmethod/latin/ResearchLog.java +31 −17 Original line number Diff line number Diff line Loading @@ -55,13 +55,14 @@ public class ResearchLog { final ScheduledExecutorService mExecutor; /* package */ final File mFile; private JsonWriter mJsonWriter = NULL_JSON_WRITER; // should never be null private JsonWriter mJsonWriter = NULL_JSON_WRITER; private int mLoggingState; private static final int LOGGING_STATE_UNSTARTED = 0; private static final int LOGGING_STATE_RUNNING = 1; private static final int LOGGING_STATE_STOPPING = 2; private static final int LOGGING_STATE_STOPPED = 3; private static final int LOGGING_STATE_READY = 1; // don't create file until necessary private static final int LOGGING_STATE_RUNNING = 2; private static final int LOGGING_STATE_STOPPING = 3; private static final int LOGGING_STATE_STOPPED = 4; private static final long FLUSH_DELAY_IN_MS = 1000 * 5; private static class NullOutputStream extends OutputStream { Loading Loading @@ -94,11 +95,9 @@ public class ResearchLog { public synchronized void start() throws IOException { switch (mLoggingState) { case LOGGING_STATE_UNSTARTED: mJsonWriter = new JsonWriter(new BufferedWriter(new FileWriter(mFile))); mJsonWriter.setLenient(true); mJsonWriter.beginArray(); mLoggingState = LOGGING_STATE_RUNNING; mLoggingState = LOGGING_STATE_READY; break; case LOGGING_STATE_READY: case LOGGING_STATE_RUNNING: case LOGGING_STATE_STOPPING: case LOGGING_STATE_STOPPED: Loading @@ -111,6 +110,7 @@ public class ResearchLog { case LOGGING_STATE_UNSTARTED: mLoggingState = LOGGING_STATE_STOPPED; break; case LOGGING_STATE_READY: case LOGGING_STATE_RUNNING: mExecutor.submit(new Callable<Object>() { @Override Loading @@ -120,14 +120,13 @@ public class ResearchLog { mJsonWriter.flush(); mJsonWriter.close(); } finally { // the contentprovider only exports data if the writable // bit is cleared. boolean success = mFile.setWritable(false, false); mLoggingState = LOGGING_STATE_STOPPED; } return null; } }); removeAnyScheduledFlush(); mExecutor.shutdown(); mLoggingState = LOGGING_STATE_STOPPING; break; Loading @@ -139,27 +138,26 @@ public class ResearchLog { public boolean isAlive() { switch (mLoggingState) { case LOGGING_STATE_UNSTARTED: case LOGGING_STATE_READY: case LOGGING_STATE_RUNNING: return true; } return false; } public void waitUntilStopped(int timeoutInMs) throws InterruptedException { public void waitUntilStopped(final int timeoutInMs) throws InterruptedException { removeAnyScheduledFlush(); mExecutor.shutdown(); mExecutor.awaitTermination(timeoutInMs, TimeUnit.MILLISECONDS); } private boolean isAbortSuccessful; public boolean isAbortSuccessful() { return isAbortSuccessful; } public synchronized void abort() { switch (mLoggingState) { case LOGGING_STATE_UNSTARTED: mLoggingState = LOGGING_STATE_STOPPED; isAbortSuccessful = true; break; case LOGGING_STATE_READY: case LOGGING_STATE_RUNNING: mExecutor.submit(new Callable<Object>() { @Override Loading @@ -173,6 +171,7 @@ public class ResearchLog { return null; } }); removeAnyScheduledFlush(); mExecutor.shutdown(); mLoggingState = LOGGING_STATE_STOPPING; break; Loading @@ -181,10 +180,16 @@ public class ResearchLog { } } private boolean isAbortSuccessful; public boolean isAbortSuccessful() { return isAbortSuccessful; } /* package */ synchronized void flush() { switch (mLoggingState) { case LOGGING_STATE_UNSTARTED: break; case LOGGING_STATE_READY: case LOGGING_STATE_RUNNING: removeAnyScheduledFlush(); mExecutor.submit(mFlushCallable); Loading @@ -197,7 +202,9 @@ public class ResearchLog { private Callable<Object> mFlushCallable = new Callable<Object>() { @Override public Object call() throws Exception { if (mLoggingState == LOGGING_STATE_RUNNING) { mJsonWriter.flush(); } return null; } }; Loading @@ -220,6 +227,7 @@ public class ResearchLog { switch (mLoggingState) { case LOGGING_STATE_UNSTARTED: break; case LOGGING_STATE_READY: case LOGGING_STATE_RUNNING: mExecutor.submit(new Callable<Object>() { @Override Loading @@ -239,6 +247,7 @@ public class ResearchLog { switch (mLoggingState) { case LOGGING_STATE_UNSTARTED: break; case LOGGING_STATE_READY: case LOGGING_STATE_RUNNING: mExecutor.submit(new Callable<Object>() { @Override Loading @@ -260,6 +269,11 @@ public class ResearchLog { void outputEvent(final String[] keys, final Object[] values) { // not thread safe. try { if (mJsonWriter == NULL_JSON_WRITER) { mJsonWriter = new JsonWriter(new BufferedWriter(new FileWriter(mFile))); mJsonWriter.setLenient(true); mJsonWriter.beginArray(); } mJsonWriter.beginObject(); mJsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis()); mJsonWriter.name(UPTIME_KEY).value(SystemClock.uptimeMillis()); Loading
java/src/com/android/inputmethod/latin/ResearchLogger.java +160 −83 Original line number Diff line number Diff line Loading @@ -44,7 +44,6 @@ import com.android.inputmethod.latin.RichInputConnection.Range; import com.android.inputmethod.latin.define.ProductionFlag; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; Loading @@ -64,13 +63,15 @@ import java.util.UUID; public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String TAG = ResearchLogger.class.getSimpleName(); private static final boolean OUTPUT_ENTIRE_BUFFER = false; // true may disclose private info /* package */ static final boolean DEFAULT_USABILITY_STUDY_MODE = false; /* package */ static boolean sIsLogging = false; private static final int OUTPUT_FORMAT_VERSION = 1; private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode"; private static final String FILENAME_PREFIX = "researchLog"; /* package */ static final String FILENAME_PREFIX = "researchLog"; private static final String FILENAME_SUFFIX = ".txt"; private static final SimpleDateFormat TIMESTAMP_DATEFORMAT = new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US); private static final boolean IS_SHOWING_INDICATOR = false; // constants related to specific log points private static final String WHITESPACE_SEPARATORS = " \t\n\r"; Loading @@ -92,6 +93,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private boolean mIsPasswordView = false; private boolean mIsLoggingSuspended = false; private SharedPreferences mPrefs; // digits entered by the user are replaced with this codepoint. /* package for test */ static final int DIGIT_REPLACEMENT_CODEPOINT = Loading @@ -101,6 +103,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private static final String PREF_LAST_CLEANUP_TIME = "pref_last_cleanup_time"; private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = DateUtils.DAY_IN_MILLIS; private static final long MAX_LOGFILE_AGE_IN_MS = DateUtils.DAY_IN_MILLIS; protected static final int SUSPEND_DURATION_IN_MINUTES = 1; // set when LatinIME should ignore an onUpdateSelection() callback that // arises from operations in this class private static boolean sLatinIMEExpectingUpdateSelection = false; Loading @@ -124,7 +127,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang if (ims == null) { Log.w(TAG, "IMS is null; logging is off"); } else { mContext = ims; mFilesDir = ims.getFilesDir(); if (mFilesDir == null || !mFilesDir.exists()) { Log.w(TAG, "IME storage directory does not exist."); Loading @@ -132,6 +134,11 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } if (prefs != null) { mUUIDString = getUUID(prefs); if (!prefs.contains(PREF_USABILITY_STUDY_MODE)) { Editor e = prefs.edit(); e.putBoolean(PREF_USABILITY_STUDY_MODE, DEFAULT_USABILITY_STUDY_MODE); e.apply(); } sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false); prefs.registerOnSharedPreferenceChangeListener(this); Loading @@ -146,6 +153,11 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } } mKeyboardSwitcher = keyboardSwitcher; mContext = ims; mPrefs = prefs; // TODO: force user to decide at splash screen instead of defaulting to on. setLoggingAllowed(true); } private void cleanupLoggingDir(final File dir, final long time) { Loading @@ -166,8 +178,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang return new File(filesDir, sb.toString()); } public void start() { if (!sIsLogging) { private void start() { updateSuspendedState(); requestIndicatorRedraw(); if (!isAllowedToLog()) { // Log.w(TAG, "not in usability mode; not logging"); return; } Loading @@ -175,10 +189,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang Log.w(TAG, "IME storage directory does not exist. Cannot start logging."); return; } try { if (mMainResearchLog == null || !mMainResearchLog.isAlive()) { mMainResearchLog = new ResearchLog(createLogFile(mFilesDir)); } try { mMainResearchLog.start(); if (mIntentionalResearchLog == null || !mIntentionalResearchLog.isAlive()) { mIntentionalResearchLog = new ResearchLog(createLogFile(mFilesDir)); Loading @@ -189,15 +203,26 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } } public void stop() { /* package */ void stop() { if (mMainResearchLog != null) { mMainResearchLog.stop(); } if (mIntentionalResearchLog != null) { mIntentionalResearchLog.stop(); } } private void setLoggingAllowed(boolean enableLogging) { if (mPrefs == null) { return; } Editor e = mPrefs.edit(); e.putBoolean(PREF_USABILITY_STUDY_MODE, enableLogging); e.apply(); sIsLogging = enableLogging; } public boolean abort() { mIsLoggingSuspended = true; requestIndicatorRedraw(); boolean didAbortMainLog = false; if (mMainResearchLog != null) { mMainResearchLog.abort(); Loading @@ -209,6 +234,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang if (mMainResearchLog.isAbortSuccessful()) { didAbortMainLog = true; } mMainResearchLog = null; } boolean didAbortIntentionalLog = false; if (mIntentionalResearchLog != null) { Loading @@ -221,6 +247,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang if (mIntentionalResearchLog.isAbortSuccessful()) { didAbortIntentionalLog = true; } mIntentionalResearchLog = null; } return didAbortMainLog && didAbortIntentionalLog; } Loading @@ -247,6 +274,34 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } } private void restart() { stop(); start(); } private long mResumeTime = 0L; private void suspendLoggingUntil(long time) { mIsLoggingSuspended = true; mResumeTime = time; requestIndicatorRedraw(); } private void resumeLogging() { mResumeTime = 0L; updateSuspendedState(); requestIndicatorRedraw(); if (isAllowedToLog()) { restart(); } } private void updateSuspendedState() { final long time = System.currentTimeMillis(); if (time > mResumeTime) { mIsLoggingSuspended = false; } } @Override public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { if (key == null || prefs == null) { Loading @@ -256,13 +311,15 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang if (sIsLogging == false) { abort(); } requestIndicatorRedraw(); } /* package */ void presentResearchDialog(final LatinIME latinIME) { final CharSequence title = latinIME.getString(R.string.english_ime_research_log); final boolean showEnable = mIsLoggingSuspended || !sIsLogging; final CharSequence[] items = new CharSequence[] { latinIME.getString(R.string.note_timestamp_for_researchlog), mIsLoggingSuspended ? latinIME.getString(R.string.enable_session_logging) : showEnable ? latinIME.getString(R.string.enable_session_logging) : latinIME.getString(R.string.do_not_log_this_session), latinIME.getString(R.string.log_whole_session_history), }; Loading @@ -277,25 +334,25 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang Toast.LENGTH_LONG).show(); break; case 1: if (mIsLoggingSuspended) { mIsLoggingSuspended = false; requestIndicatorRedraw(); Toast toast = Toast.makeText(latinIME, R.string.notify_session_logging_enabled, Toast.LENGTH_LONG); if (showEnable) { if (!sIsLogging) { setLoggingAllowed(true); } resumeLogging(); Toast.makeText(latinIME, R.string.notify_session_logging_enabled, Toast.LENGTH_LONG).show(); } else { Toast toast = Toast.makeText(latinIME, R.string.notify_session_log_deleting, Toast.LENGTH_LONG); toast.show(); boolean isLogDeleted = abort(); final long currentTime = System.currentTimeMillis(); final long resumeTime = currentTime + 1000 * 60 * SUSPEND_DURATION_IN_MINUTES; suspendLoggingUntil(resumeTime); toast.cancel(); if (isLogDeleted) { Toast.makeText(latinIME, R.string.notify_session_log_deleted, Toast.makeText(latinIME, R.string.notify_logging_suspended, Toast.LENGTH_LONG).show(); } else { Toast.makeText(latinIME, R.string.notify_session_log_not_deleted, Toast.LENGTH_LONG) .show(); } } break; case 2: Loading Loading @@ -328,15 +385,17 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } private boolean isAllowedToLog() { return !mIsPasswordView && !mIsLoggingSuspended; return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging; } public void requestIndicatorRedraw() { // invalidate any existing graphics if (IS_SHOWING_INDICATOR) { if (mKeyboardSwitcher != null) { mKeyboardSwitcher.getKeyboardView().invalidateAllKeys(); } } } private static final String CURRENT_TIME_KEY = "_ct"; private static final String UPTIME_KEY = "_ut"; Loading Loading @@ -467,6 +526,12 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } private void publishLogUnit(LogUnit logUnit, boolean isPrivacySensitive) { if (!isAllowedToLog()) { return; } if (mMainResearchLog == null) { return; } if (isPrivacySensitive) { mMainResearchLog.publishPublicEvents(logUnit); } else { Loading Loading @@ -536,6 +601,18 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } } private static String getUUID(final SharedPreferences prefs) { String uuidString = prefs.getString(PREF_RESEARCH_LOGGER_UUID_STRING, null); if (null == uuidString) { UUID uuid = UUID.randomUUID(); uuidString = uuid.toString(); Editor editor = prefs.edit(); editor.putString(PREF_RESEARCH_LOGGER_UUID_STRING, uuidString); editor.apply(); } return uuidString; } private String scrubWord(String word) { if (mDictionary == null) { return WORD_REPLACEMENT_STRING; Loading @@ -546,9 +623,62 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang return WORD_REPLACEMENT_STRING; } // Special methods related to startup, shutdown, logging itself private static final String[] EVENTKEYS_INTENTIONAL_LOG = { "IntentionalLog" }; private static final String[] EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL = { "LatinIMEOnStartInputViewInternal", "uuid", "packageName", "inputType", "imeOptions", "fieldId", "display", "model", "prefs", "versionCode", "versionName", "outputFormatVersion" }; public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo, final SharedPreferences prefs) { final ResearchLogger researchLogger = getInstance(); researchLogger.start(); if (editorInfo != null) { final Context context = researchLogger.mContext; try { final PackageInfo packageInfo; packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); final Integer versionCode = packageInfo.versionCode; final String versionName = packageInfo.versionName; final Object[] values = { researchLogger.mUUIDString, editorInfo.packageName, Integer.toHexString(editorInfo.inputType), Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId, Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName, OUTPUT_FORMAT_VERSION }; researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL, values); } catch (NameNotFoundException e) { e.printStackTrace(); } } } public void latinIME_onFinishInputInternal() { stop(); } private static final String[] EVENTKEYS_LATINIME_COMMITTEXT = { "LatinIMECommitText", "typedWord" }; public static void latinIME_commitText(final CharSequence typedWord) { final String scrubbedWord = scrubDigitsFromString(typedWord.toString()); final Object[] values = { scrubbedWord }; final ResearchLogger researchLogger = getInstance(); researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_COMMITTEXT, values); researchLogger.onWordComplete(scrubbedWord); } // Regular logging methods private static final String[] EVENTKEYS_LATINKEYBOARDVIEW_PROCESSMOTIONEVENT = { "LatinKeyboardViewProcessMotionEvent", "action", "eventTime", "id", "x", "y", "size", "pressure" Loading Loading @@ -611,19 +741,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang EVENTKEYS_LATINIME_COMMITCURRENTAUTOCORRECTION, values); } private static final String[] EVENTKEYS_LATINIME_COMMITTEXT = { "LatinIMECommitText", "typedWord" }; public static void latinIME_commitText(final CharSequence typedWord) { final String scrubbedWord = scrubDigitsFromString(typedWord.toString()); final Object[] values = { scrubbedWord }; final ResearchLogger researchLogger = getInstance(); researchLogger.enqueuePotentiallyPrivateEvent(EVENTKEYS_LATINIME_COMMITTEXT, values); researchLogger.onWordComplete(scrubbedWord); } private static final String[] EVENTKEYS_LATINIME_DELETESURROUNDINGTEXT = { "LatinIMEDeleteSurroundingText", "length" }; Loading Loading @@ -702,51 +819,10 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang // Play it safe. Remove privacy-sensitive events. researchLogger.publishLogUnit(researchLogger.mCurrentLogUnit, true); researchLogger.mCurrentLogUnit = new LogUnit(); getInstance().restart(); } } private static final String[] EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL = { "LatinIMEOnStartInputViewInternal", "uuid", "packageName", "inputType", "imeOptions", "fieldId", "display", "model", "prefs", "versionCode", "versionName", "outputFormatVersion" }; public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo, final SharedPreferences prefs) { final ResearchLogger researchLogger = getInstance(); researchLogger.start(); if (editorInfo != null) { final Context context = researchLogger.mContext; try { final PackageInfo packageInfo; packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); final Integer versionCode = packageInfo.versionCode; final String versionName = packageInfo.versionName; final Object[] values = { researchLogger.mUUIDString, editorInfo.packageName, Integer.toHexString(editorInfo.inputType), Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId, Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName, OUTPUT_FORMAT_VERSION }; researchLogger.enqueueEvent(EVENTKEYS_LATINIME_ONSTARTINPUTVIEWINTERNAL, values); } catch (NameNotFoundException e) { e.printStackTrace(); } } } private static String getUUID(final SharedPreferences prefs) { String uuidString = prefs.getString(PREF_RESEARCH_LOGGER_UUID_STRING, null); if (null == uuidString) { UUID uuid = UUID.randomUUID(); uuidString = uuid.toString(); Editor editor = prefs.edit(); editor.putString(PREF_RESEARCH_LOGGER_UUID_STRING, uuidString); editor.apply(); } return uuidString; } private static final String[] EVENTKEYS_LATINIME_ONUPDATESELECTION = { "LatinIMEOnUpdateSelection", "lastSelectionStart", "lastSelectionEnd", "oldSelStart", "oldSelEnd", "newSelStart", "newSelEnd", "composingSpanStart", "composingSpanEnd", Loading Loading @@ -873,6 +949,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang if (keyboard != null) { final KeyboardId kid = keyboard.mId; final boolean isPasswordView = kid.passwordInput(); getInstance().setIsPasswordView(isPasswordView); final Object[] values = { KeyboardId.elementIdToName(kid.mElementId), kid.mLocale + ":" + kid.mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET), Loading
java/src/com/android/inputmethod/latin/Settings.java +2 −1 File changed.Preview size limit exceeded, changes collapsed. Show changes