ResearchLogger.java revision 40417aed94a32dcd4d6a4c469320e2d6ddf83865
1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.android.inputmethod.research;
18
19import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
20
21import android.app.AlarmManager;
22import android.app.AlertDialog;
23import android.app.Dialog;
24import android.app.PendingIntent;
25import android.content.Context;
26import android.content.DialogInterface;
27import android.content.DialogInterface.OnCancelListener;
28import android.content.Intent;
29import android.content.SharedPreferences;
30import android.content.SharedPreferences.Editor;
31import android.content.pm.PackageInfo;
32import android.content.pm.PackageManager.NameNotFoundException;
33import android.graphics.Canvas;
34import android.graphics.Color;
35import android.graphics.Paint;
36import android.graphics.Paint.Style;
37import android.inputmethodservice.InputMethodService;
38import android.net.Uri;
39import android.os.Build;
40import android.os.IBinder;
41import android.os.SystemClock;
42import android.text.TextUtils;
43import android.text.format.DateUtils;
44import android.util.Log;
45import android.view.KeyEvent;
46import android.view.MotionEvent;
47import android.view.Window;
48import android.view.WindowManager;
49import android.view.inputmethod.CompletionInfo;
50import android.view.inputmethod.EditorInfo;
51import android.view.inputmethod.InputConnection;
52import android.widget.Toast;
53
54import com.android.inputmethod.keyboard.Key;
55import com.android.inputmethod.keyboard.Keyboard;
56import com.android.inputmethod.keyboard.KeyboardId;
57import com.android.inputmethod.keyboard.KeyboardView;
58import com.android.inputmethod.keyboard.MainKeyboardView;
59import com.android.inputmethod.latin.Constants;
60import com.android.inputmethod.latin.Dictionary;
61import com.android.inputmethod.latin.InputTypeUtils;
62import com.android.inputmethod.latin.LatinIME;
63import com.android.inputmethod.latin.R;
64import com.android.inputmethod.latin.RichInputConnection;
65import com.android.inputmethod.latin.RichInputConnection.Range;
66import com.android.inputmethod.latin.Suggest;
67import com.android.inputmethod.latin.SuggestedWords;
68import com.android.inputmethod.latin.define.ProductionFlag;
69
70import java.io.File;
71import java.text.SimpleDateFormat;
72import java.util.Date;
73import java.util.Locale;
74import java.util.UUID;
75
76/**
77 * Logs the use of the LatinIME keyboard.
78 *
79 * This class logs operations on the IME keyboard, including what the user has typed.
80 * Data is stored locally in a file in app-specific storage.
81 *
82 * This functionality is off by default. See {@link ProductionFlag#IS_EXPERIMENTAL}.
83 */
84public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener {
85    private static final String TAG = ResearchLogger.class.getSimpleName();
86    private static final boolean DEBUG = false;
87    private static final boolean LOG_EVERYTHING = false;  // true will disclose private info
88    public static final boolean DEFAULT_USABILITY_STUDY_MODE = false;
89    /* package */ static boolean sIsLogging = false;
90    private static final int OUTPUT_FORMAT_VERSION = 5;
91    private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
92    private static final String PREF_RESEARCH_HAS_SEEN_SPLASH = "pref_research_has_seen_splash";
93    /* package */ static final String FILENAME_PREFIX = "researchLog";
94    private static final String FILENAME_SUFFIX = ".txt";
95    private static final SimpleDateFormat TIMESTAMP_DATEFORMAT =
96            new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US);
97    private static final boolean IS_SHOWING_INDICATOR = true;
98    private static final boolean IS_SHOWING_INDICATOR_CLEARLY = false;
99    public static final int FEEDBACK_WORD_BUFFER_SIZE = 5;
100
101    // constants related to specific log points
102    private static final String WHITESPACE_SEPARATORS = " \t\n\r";
103    private static final int MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1
104    private static final String PREF_RESEARCH_LOGGER_UUID_STRING = "pref_research_logger_uuid";
105
106    private static final ResearchLogger sInstance = new ResearchLogger();
107    // to write to a different filename, e.g., for testing, set mFile before calling start()
108    /* package */ File mFilesDir;
109    /* package */ String mUUIDString;
110    /* package */ ResearchLog mMainResearchLog;
111    // mFeedbackLog records all events for the session, private or not (excepting
112    // passwords).  It is written to permanent storage only if the user explicitly commands
113    // the system to do so.
114    // LogUnits are queued in the LogBuffers and published to the ResearchLogs when words are
115    // complete.
116    /* package */ ResearchLog mFeedbackLog;
117    /* package */ MainLogBuffer mMainLogBuffer;
118    /* package */ LogBuffer mFeedbackLogBuffer;
119
120    private boolean mIsPasswordView = false;
121    private boolean mIsLoggingSuspended = false;
122    private SharedPreferences mPrefs;
123
124    // digits entered by the user are replaced with this codepoint.
125    /* package for test */ static final int DIGIT_REPLACEMENT_CODEPOINT =
126            Character.codePointAt("\uE000", 0);  // U+E000 is in the "private-use area"
127    // U+E001 is in the "private-use area"
128    /* package for test */ static final String WORD_REPLACEMENT_STRING = "\uE001";
129    private static final String PREF_LAST_CLEANUP_TIME = "pref_last_cleanup_time";
130    private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = DateUtils.DAY_IN_MILLIS;
131    private static final long MAX_LOGFILE_AGE_IN_MS = DateUtils.DAY_IN_MILLIS;
132    protected static final int SUSPEND_DURATION_IN_MINUTES = 1;
133    // set when LatinIME should ignore an onUpdateSelection() callback that
134    // arises from operations in this class
135    private static boolean sLatinIMEExpectingUpdateSelection = false;
136
137    // used to check whether words are not unique
138    private Suggest mSuggest;
139    private MainKeyboardView mMainKeyboardView;
140    private InputMethodService mInputMethodService;
141    private final Statistics mStatistics;
142
143    private Intent mUploadIntent;
144
145    private LogUnit mCurrentLogUnit = new LogUnit();
146
147    private ResearchLogger() {
148        mStatistics = Statistics.getInstance();
149    }
150
151    public static ResearchLogger getInstance() {
152        return sInstance;
153    }
154
155    public void init(final InputMethodService ims, final SharedPreferences prefs) {
156        assert ims != null;
157        if (ims == null) {
158            Log.w(TAG, "IMS is null; logging is off");
159        } else {
160            mFilesDir = ims.getFilesDir();
161            if (mFilesDir == null || !mFilesDir.exists()) {
162                Log.w(TAG, "IME storage directory does not exist.");
163            }
164        }
165        if (prefs != null) {
166            mUUIDString = getUUID(prefs);
167            if (!prefs.contains(PREF_USABILITY_STUDY_MODE)) {
168                Editor e = prefs.edit();
169                e.putBoolean(PREF_USABILITY_STUDY_MODE, DEFAULT_USABILITY_STUDY_MODE);
170                e.apply();
171            }
172            sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
173            prefs.registerOnSharedPreferenceChangeListener(this);
174
175            final long lastCleanupTime = prefs.getLong(PREF_LAST_CLEANUP_TIME, 0L);
176            final long now = System.currentTimeMillis();
177            if (lastCleanupTime + DURATION_BETWEEN_DIR_CLEANUP_IN_MS < now) {
178                final long timeHorizon = now - MAX_LOGFILE_AGE_IN_MS;
179                cleanupLoggingDir(mFilesDir, timeHorizon);
180                Editor e = prefs.edit();
181                e.putLong(PREF_LAST_CLEANUP_TIME, now);
182                e.apply();
183            }
184        }
185        mInputMethodService = ims;
186        mPrefs = prefs;
187        mUploadIntent = new Intent(mInputMethodService, UploaderService.class);
188
189        if (ProductionFlag.IS_EXPERIMENTAL) {
190            scheduleUploadingService(mInputMethodService);
191        }
192    }
193
194    /**
195     * Arrange for the UploaderService to be run on a regular basis.
196     *
197     * Any existing scheduled invocation of UploaderService is removed and rescheduled.  This may
198     * cause problems if this method is called often and frequent updates are required, but since
199     * the user will likely be sleeping at some point, if the interval is less that the expected
200     * sleep duration and this method is not called during that time, the service should be invoked
201     * at some point.
202     */
203    public static void scheduleUploadingService(Context context) {
204        final Intent intent = new Intent(context, UploaderService.class);
205        final PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0);
206        final AlarmManager manager =
207                (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
208        manager.cancel(pendingIntent);
209        manager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
210                UploaderService.RUN_INTERVAL, UploaderService.RUN_INTERVAL, pendingIntent);
211    }
212
213    private void cleanupLoggingDir(final File dir, final long time) {
214        for (File file : dir.listFiles()) {
215            if (file.getName().startsWith(ResearchLogger.FILENAME_PREFIX) &&
216                    file.lastModified() < time) {
217                file.delete();
218            }
219        }
220    }
221
222    public void mainKeyboardView_onAttachedToWindow(final MainKeyboardView mainKeyboardView) {
223        mMainKeyboardView = mainKeyboardView;
224        maybeShowSplashScreen();
225    }
226
227    public void mainKeyboardView_onDetachedFromWindow() {
228        mMainKeyboardView = null;
229    }
230
231    private boolean hasSeenSplash() {
232        return mPrefs.getBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, false);
233    }
234
235    private Dialog mSplashDialog = null;
236
237    private void maybeShowSplashScreen() {
238        if (hasSeenSplash()) {
239            return;
240        }
241        if (mSplashDialog != null && mSplashDialog.isShowing()) {
242            return;
243        }
244        final IBinder windowToken = mMainKeyboardView != null
245                ? mMainKeyboardView.getWindowToken() : null;
246        if (windowToken == null) {
247            return;
248        }
249        final AlertDialog.Builder builder = new AlertDialog.Builder(mInputMethodService)
250                .setTitle(R.string.research_splash_title)
251                .setMessage(R.string.research_splash_content)
252                .setPositiveButton(android.R.string.yes,
253                        new DialogInterface.OnClickListener() {
254                            @Override
255                            public void onClick(DialogInterface dialog, int which) {
256                                onUserLoggingConsent();
257                                mSplashDialog.dismiss();
258                            }
259                })
260                .setNegativeButton(android.R.string.no,
261                        new DialogInterface.OnClickListener() {
262                            @Override
263                            public void onClick(DialogInterface dialog, int which) {
264                                final String packageName = mInputMethodService.getPackageName();
265                                final Uri packageUri = Uri.parse("package:" + packageName);
266                                final Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE,
267                                        packageUri);
268                                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
269                                mInputMethodService.startActivity(intent);
270                            }
271                })
272                .setCancelable(true)
273                .setOnCancelListener(
274                        new OnCancelListener() {
275                            @Override
276                            public void onCancel(DialogInterface dialog) {
277                                mInputMethodService.requestHideSelf(0);
278                            }
279                });
280        mSplashDialog = builder.create();
281        final Window w = mSplashDialog.getWindow();
282        final WindowManager.LayoutParams lp = w.getAttributes();
283        lp.token = windowToken;
284        lp.type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
285        w.setAttributes(lp);
286        w.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
287        mSplashDialog.show();
288    }
289
290    public void onUserLoggingConsent() {
291        setLoggingAllowed(true);
292        if (mPrefs == null) {
293            return;
294        }
295        final Editor e = mPrefs.edit();
296        e.putBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, true);
297        e.apply();
298        restart();
299    }
300
301    private void setLoggingAllowed(boolean enableLogging) {
302        if (mPrefs == null) {
303            return;
304        }
305        Editor e = mPrefs.edit();
306        e.putBoolean(PREF_USABILITY_STUDY_MODE, enableLogging);
307        e.apply();
308        sIsLogging = enableLogging;
309    }
310
311    private File createLogFile(File filesDir) {
312        final StringBuilder sb = new StringBuilder();
313        sb.append(FILENAME_PREFIX).append('-');
314        sb.append(mUUIDString).append('-');
315        sb.append(TIMESTAMP_DATEFORMAT.format(new Date()));
316        sb.append(FILENAME_SUFFIX);
317        return new File(filesDir, sb.toString());
318    }
319
320    private void checkForEmptyEditor() {
321        if (mInputMethodService == null) {
322            return;
323        }
324        final InputConnection ic = mInputMethodService.getCurrentInputConnection();
325        if (ic == null) {
326            return;
327        }
328        final CharSequence textBefore = ic.getTextBeforeCursor(1, 0);
329        if (!TextUtils.isEmpty(textBefore)) {
330            mStatistics.setIsEmptyUponStarting(false);
331            return;
332        }
333        final CharSequence textAfter = ic.getTextAfterCursor(1, 0);
334        if (!TextUtils.isEmpty(textAfter)) {
335            mStatistics.setIsEmptyUponStarting(false);
336            return;
337        }
338        if (textBefore != null && textAfter != null) {
339            mStatistics.setIsEmptyUponStarting(true);
340        }
341    }
342
343    private void start() {
344        if (DEBUG) {
345            Log.d(TAG, "start called");
346        }
347        maybeShowSplashScreen();
348        updateSuspendedState();
349        requestIndicatorRedraw();
350        mStatistics.reset();
351        checkForEmptyEditor();
352        if (!isAllowedToLog()) {
353            // Log.w(TAG, "not in usability mode; not logging");
354            return;
355        }
356        if (mFilesDir == null || !mFilesDir.exists()) {
357            Log.w(TAG, "IME storage directory does not exist.  Cannot start logging.");
358            return;
359        }
360        if (mMainLogBuffer == null) {
361            mMainResearchLog = new ResearchLog(createLogFile(mFilesDir));
362            mMainLogBuffer = new MainLogBuffer(mMainResearchLog);
363            mMainLogBuffer.setSuggest(mSuggest);
364        }
365        if (mFeedbackLogBuffer == null) {
366            mFeedbackLog = new ResearchLog(createLogFile(mFilesDir));
367            // LogBuffer is one more than FEEDBACK_WORD_BUFFER_SIZE, because it must also hold
368            // the feedback LogUnit itself.
369            mFeedbackLogBuffer = new LogBuffer(FEEDBACK_WORD_BUFFER_SIZE + 1);
370        }
371    }
372
373    /* package */ void stop() {
374        if (DEBUG) {
375            Log.d(TAG, "stop called");
376        }
377        logStatistics();
378        commitCurrentLogUnit();
379
380        if (mMainLogBuffer != null) {
381            publishLogBuffer(mMainLogBuffer, mMainResearchLog,
382                    LOG_EVERYTHING /* isIncludingPrivateData */);
383            mMainResearchLog.close(null /* callback */);
384            mMainLogBuffer = null;
385        }
386        if (mFeedbackLogBuffer != null) {
387            mFeedbackLog.close(null /* callback */);
388            mFeedbackLogBuffer = null;
389        }
390    }
391
392    public boolean abort() {
393        if (DEBUG) {
394            Log.d(TAG, "abort called");
395        }
396        boolean didAbortMainLog = false;
397        if (mMainLogBuffer != null) {
398            mMainLogBuffer.clear();
399            try {
400                didAbortMainLog = mMainResearchLog.blockingAbort();
401            } catch (InterruptedException e) {
402                // Don't know whether this succeeded or not.  We assume not; this is reported
403                // to the caller.
404            }
405            mMainLogBuffer = null;
406        }
407        boolean didAbortFeedbackLog = false;
408        if (mFeedbackLogBuffer != null) {
409            mFeedbackLogBuffer.clear();
410            try {
411                didAbortFeedbackLog = mFeedbackLog.blockingAbort();
412            } catch (InterruptedException e) {
413                // Don't know whether this succeeded or not.  We assume not; this is reported
414                // to the caller.
415            }
416            mFeedbackLogBuffer = null;
417        }
418        return didAbortMainLog && didAbortFeedbackLog;
419    }
420
421    private void restart() {
422        stop();
423        start();
424    }
425
426    private long mResumeTime = 0L;
427    private void updateSuspendedState() {
428        final long time = System.currentTimeMillis();
429        if (time > mResumeTime) {
430            mIsLoggingSuspended = false;
431        }
432    }
433
434    @Override
435    public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
436        if (key == null || prefs == null) {
437            return;
438        }
439        sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
440        if (sIsLogging == false) {
441            abort();
442        }
443        requestIndicatorRedraw();
444        mPrefs = prefs;
445        prefsChanged(prefs);
446    }
447
448    public void onResearchKeySelected(final LatinIME latinIME) {
449        if (mInFeedbackDialog) {
450            Toast.makeText(latinIME, R.string.research_please_exit_feedback_form,
451                    Toast.LENGTH_LONG).show();
452            return;
453        }
454        presentFeedbackDialog(latinIME);
455    }
456
457    // TODO: currently unreachable.  Remove after being sure no menu is needed.
458    /*
459    public void presentResearchDialog(final LatinIME latinIME) {
460        final CharSequence title = latinIME.getString(R.string.english_ime_research_log);
461        final boolean showEnable = mIsLoggingSuspended || !sIsLogging;
462        final CharSequence[] items = new CharSequence[] {
463                latinIME.getString(R.string.research_feedback_menu_option),
464                showEnable ? latinIME.getString(R.string.research_enable_session_logging) :
465                        latinIME.getString(R.string.research_do_not_log_this_session)
466        };
467        final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
468            @Override
469            public void onClick(DialogInterface di, int position) {
470                di.dismiss();
471                switch (position) {
472                    case 0:
473                        presentFeedbackDialog(latinIME);
474                        break;
475                    case 1:
476                        enableOrDisable(showEnable, latinIME);
477                        break;
478                }
479            }
480
481        };
482        final AlertDialog.Builder builder = new AlertDialog.Builder(latinIME)
483                .setItems(items, listener)
484                .setTitle(title);
485        latinIME.showOptionDialog(builder.create());
486    }
487    */
488
489    private boolean mInFeedbackDialog = false;
490    public void presentFeedbackDialog(LatinIME latinIME) {
491        mInFeedbackDialog = true;
492        latinIME.launchKeyboardedDialogActivity(FeedbackActivity.class);
493    }
494
495    // TODO: currently unreachable.  Remove after being sure enable/disable is
496    // not needed.
497    /*
498    public void enableOrDisable(final boolean showEnable, final LatinIME latinIME) {
499        if (showEnable) {
500            if (!sIsLogging) {
501                setLoggingAllowed(true);
502            }
503            resumeLogging();
504            Toast.makeText(latinIME,
505                    R.string.research_notify_session_logging_enabled,
506                    Toast.LENGTH_LONG).show();
507        } else {
508            Toast toast = Toast.makeText(latinIME,
509                    R.string.research_notify_session_log_deleting,
510                    Toast.LENGTH_LONG);
511            toast.show();
512            boolean isLogDeleted = abort();
513            final long currentTime = System.currentTimeMillis();
514            final long resumeTime = currentTime + 1000 * 60 *
515                    SUSPEND_DURATION_IN_MINUTES;
516            suspendLoggingUntil(resumeTime);
517            toast.cancel();
518            Toast.makeText(latinIME, R.string.research_notify_logging_suspended,
519                    Toast.LENGTH_LONG).show();
520        }
521    }
522    */
523
524    static class LogStatement {
525        final String mName;
526
527        // mIsPotentiallyPrivate indicates that event contains potentially private information.  If
528        // the word that this event is a part of is determined to be privacy-sensitive, then this
529        // event should not be included in the output log.  The system waits to output until the
530        // containing word is known.
531        final boolean mIsPotentiallyPrivate;
532
533        // mIsPotentiallyRevealing indicates that this statement may disclose details about other
534        // words typed in other LogUnits.  This can happen if the user is not inserting spaces, and
535        // data from Suggestions and/or Composing text reveals the entire "megaword".  For example,
536        // say the user is typing "for the win", and the system wants to record the bigram "the
537        // win".  If the user types "forthe", omitting the space, the system will give "for the" as
538        // a suggestion.  If the user accepts the autocorrection, the suggestion for "for the" is
539        // included in the log for the word "the", disclosing that the previous word had been "for".
540        // For now, we simply do not include this data when logging part of a "megaword".
541        final boolean mIsPotentiallyRevealing;
542
543        // mKeys stores the names that are the attributes in the output json objects
544        final String[] mKeys;
545        private static final String[] NULL_KEYS = new String[0];
546
547        LogStatement(final String name, final boolean isPotentiallyPrivate,
548                final boolean isPotentiallyRevealing, final String... keys) {
549            mName = name;
550            mIsPotentiallyPrivate = isPotentiallyPrivate;
551            mIsPotentiallyRevealing = isPotentiallyRevealing;
552            mKeys = (keys == null) ? NULL_KEYS : keys;
553        }
554    }
555
556    private static final LogStatement LOGSTATEMENT_FEEDBACK =
557            new LogStatement("UserTimestamp", false, false, "contents");
558    public void sendFeedback(final String feedbackContents, final boolean includeHistory) {
559        if (mFeedbackLogBuffer == null) {
560            return;
561        }
562        if (includeHistory) {
563            commitCurrentLogUnit();
564        } else {
565            mFeedbackLogBuffer.clear();
566        }
567        final LogUnit feedbackLogUnit = new LogUnit();
568        feedbackLogUnit.addLogStatement(LOGSTATEMENT_FEEDBACK, SystemClock.uptimeMillis(),
569                feedbackContents);
570        mFeedbackLogBuffer.shiftIn(feedbackLogUnit);
571        publishLogBuffer(mFeedbackLogBuffer, mFeedbackLog, true /* isIncludingPrivateData */);
572        mFeedbackLog.close(new Runnable() {
573            @Override
574            public void run() {
575                uploadNow();
576            }
577        });
578        mFeedbackLog = new ResearchLog(createLogFile(mFilesDir));
579    }
580
581    public void uploadNow() {
582        if (DEBUG) {
583            Log.d(TAG, "calling uploadNow()");
584        }
585        mInputMethodService.startService(mUploadIntent);
586    }
587
588    public void onLeavingSendFeedbackDialog() {
589        mInFeedbackDialog = false;
590    }
591
592    public void initSuggest(Suggest suggest) {
593        mSuggest = suggest;
594        if (mMainLogBuffer != null) {
595            mMainLogBuffer.setSuggest(mSuggest);
596        }
597    }
598
599    private Dictionary getDictionary() {
600        if (mSuggest == null) {
601            return null;
602        }
603        return mSuggest.getMainDictionary();
604    }
605
606    private void setIsPasswordView(boolean isPasswordView) {
607        mIsPasswordView = isPasswordView;
608    }
609
610    private boolean isAllowedToLog() {
611        if (DEBUG) {
612            Log.d(TAG, "iatl: " +
613                "mipw=" + mIsPasswordView +
614                ", mils=" + mIsLoggingSuspended +
615                ", sil=" + sIsLogging +
616                ", mInFeedbackDialog=" + mInFeedbackDialog);
617        }
618        return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog;
619    }
620
621    public void requestIndicatorRedraw() {
622        if (!IS_SHOWING_INDICATOR) {
623            return;
624        }
625        if (mMainKeyboardView == null) {
626            return;
627        }
628        mMainKeyboardView.invalidateAllKeys();
629    }
630
631
632    public void paintIndicator(KeyboardView view, Paint paint, Canvas canvas, int width,
633            int height) {
634        // TODO: Reimplement using a keyboard background image specific to the ResearchLogger
635        // and remove this method.
636        // The check for MainKeyboardView ensures that a red border is only placed around
637        // the main keyboard, not every keyboard.
638        if (IS_SHOWING_INDICATOR && isAllowedToLog() && view instanceof MainKeyboardView) {
639            final int savedColor = paint.getColor();
640            paint.setColor(Color.RED);
641            final Style savedStyle = paint.getStyle();
642            paint.setStyle(Style.STROKE);
643            final float savedStrokeWidth = paint.getStrokeWidth();
644            if (IS_SHOWING_INDICATOR_CLEARLY) {
645                paint.setStrokeWidth(5);
646                canvas.drawRect(0, 0, width, height, paint);
647            } else {
648                // Put a tiny red dot on the screen so a knowledgeable user can check whether
649                // it is enabled.  The dot is actually a zero-width, zero-height rectangle,
650                // placed at the lower-right corner of the canvas, painted with a non-zero border
651                // width.
652                paint.setStrokeWidth(3);
653                canvas.drawRect(width, height, width, height, paint);
654            }
655            paint.setColor(savedColor);
656            paint.setStyle(savedStyle);
657            paint.setStrokeWidth(savedStrokeWidth);
658        }
659    }
660
661    /**
662     * Buffer a research log event, flagging it as privacy-sensitive.
663     */
664    private synchronized void enqueueEvent(LogStatement logStatement, Object... values) {
665        assert values.length == logStatement.mKeys.length;
666        if (isAllowedToLog()) {
667            final long time = SystemClock.uptimeMillis();
668            mCurrentLogUnit.addLogStatement(logStatement, time, values);
669        }
670    }
671
672    private void setCurrentLogUnitContainsDigitFlag() {
673        mCurrentLogUnit.setMayContainDigit();
674    }
675
676    /* package for test */ void commitCurrentLogUnit() {
677        if (DEBUG) {
678            Log.d(TAG, "commitCurrentLogUnit" + (mCurrentLogUnit.hasWord() ?
679                    ": " + mCurrentLogUnit.getWord() : ""));
680        }
681        if (!mCurrentLogUnit.isEmpty()) {
682            if (mMainLogBuffer != null) {
683                mMainLogBuffer.shiftIn(mCurrentLogUnit);
684                if (mMainLogBuffer.isSafeToLog() && mMainResearchLog != null) {
685                    publishLogBuffer(mMainLogBuffer, mMainResearchLog,
686                            true /* isIncludingPrivateData */);
687                    mMainLogBuffer.resetWordCounter();
688                }
689            }
690            if (mFeedbackLogBuffer != null) {
691                mFeedbackLogBuffer.shiftIn(mCurrentLogUnit);
692            }
693            mCurrentLogUnit = new LogUnit();
694        }
695    }
696
697    private static final LogStatement LOGSTATEMENT_LOG_SEGMENT_OPENING =
698            new LogStatement("logSegmentStart", false, false, "isIncludingPrivateData");
699    private static final LogStatement LOGSTATEMENT_LOG_SEGMENT_CLOSING =
700            new LogStatement("logSegmentEnd", false, false);
701    /* package for test */ void publishLogBuffer(final LogBuffer logBuffer,
702            final ResearchLog researchLog, final boolean isIncludingPrivateData) {
703        final LogUnit openingLogUnit = new LogUnit();
704        openingLogUnit.addLogStatement(LOGSTATEMENT_LOG_SEGMENT_OPENING, SystemClock.uptimeMillis(),
705                isIncludingPrivateData);
706        researchLog.publish(openingLogUnit, true /* isIncludingPrivateData */);
707        LogUnit logUnit;
708        while ((logUnit = logBuffer.shiftOut()) != null) {
709            if (DEBUG) {
710                Log.d(TAG, "publishLogBuffer: " + (logUnit.hasWord() ? logUnit.getWord()
711                        : "<wordless>"));
712            }
713            researchLog.publish(logUnit, isIncludingPrivateData);
714        }
715        final LogUnit closingLogUnit = new LogUnit();
716        closingLogUnit.addLogStatement(LOGSTATEMENT_LOG_SEGMENT_CLOSING,
717                SystemClock.uptimeMillis());
718        researchLog.publish(closingLogUnit, true /* isIncludingPrivateData */);
719    }
720
721    public static boolean hasLetters(final String word) {
722        final int length = word.length();
723        for (int i = 0; i < length; i = word.offsetByCodePoints(i, 1)) {
724            final int codePoint = word.codePointAt(i);
725            if (Character.isLetter(codePoint)) {
726                return true;
727            }
728        }
729        return false;
730    }
731
732    private static final LogStatement LOGSTATEMENT_COMMIT_RECORD_SPLIT_WORDS =
733            new LogStatement("recordSplitWords", true, false);
734    private void onWordComplete(final String word, final long maxTime, final boolean isSplitWords) {
735        final Dictionary dictionary = getDictionary();
736        if (word != null && word.length() > 0 && hasLetters(word)) {
737            mCurrentLogUnit.setWord(word);
738            final boolean isDictionaryWord = dictionary != null
739                    && dictionary.isValidWord(word);
740            mStatistics.recordWordEntered(isDictionaryWord);
741        }
742        final LogUnit newLogUnit = mCurrentLogUnit.splitByTime(maxTime);
743        enqueueCommitText(word);
744        if (isSplitWords) {
745            enqueueEvent(LOGSTATEMENT_COMMIT_RECORD_SPLIT_WORDS);
746            enqueueCommitText(" ");
747            mStatistics.recordSplitWords();
748        }
749        commitCurrentLogUnit();
750        mCurrentLogUnit = newLogUnit;
751    }
752
753    private static int scrubDigitFromCodePoint(int codePoint) {
754        return Character.isDigit(codePoint) ? DIGIT_REPLACEMENT_CODEPOINT : codePoint;
755    }
756
757    /* package for test */ static String scrubDigitsFromString(String s) {
758        StringBuilder sb = null;
759        final int length = s.length();
760        for (int i = 0; i < length; i = s.offsetByCodePoints(i, 1)) {
761            final int codePoint = Character.codePointAt(s, i);
762            if (Character.isDigit(codePoint)) {
763                if (sb == null) {
764                    sb = new StringBuilder(length);
765                    sb.append(s.substring(0, i));
766                }
767                sb.appendCodePoint(DIGIT_REPLACEMENT_CODEPOINT);
768            } else {
769                if (sb != null) {
770                    sb.appendCodePoint(codePoint);
771                }
772            }
773        }
774        if (sb == null) {
775            return s;
776        } else {
777            return sb.toString();
778        }
779    }
780
781    private static String getUUID(final SharedPreferences prefs) {
782        String uuidString = prefs.getString(PREF_RESEARCH_LOGGER_UUID_STRING, null);
783        if (null == uuidString) {
784            UUID uuid = UUID.randomUUID();
785            uuidString = uuid.toString();
786            Editor editor = prefs.edit();
787            editor.putString(PREF_RESEARCH_LOGGER_UUID_STRING, uuidString);
788            editor.apply();
789        }
790        return uuidString;
791    }
792
793    private String scrubWord(String word) {
794        final Dictionary dictionary = getDictionary();
795        if (dictionary == null) {
796            return WORD_REPLACEMENT_STRING;
797        }
798        if (dictionary.isValidWord(word)) {
799            return word;
800        }
801        return WORD_REPLACEMENT_STRING;
802    }
803
804    private static final LogStatement LOGSTATEMENT_LATIN_IME_ON_START_INPUT_VIEW_INTERNAL =
805            new LogStatement("LatinImeOnStartInputViewInternal", false, false, "uuid",
806                    "packageName", "inputType", "imeOptions", "fieldId", "display", "model",
807                    "prefs", "versionCode", "versionName", "outputFormatVersion", "logEverything");
808    public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo,
809            final SharedPreferences prefs) {
810        final ResearchLogger researchLogger = getInstance();
811        if (editorInfo != null) {
812            final boolean isPassword = InputTypeUtils.isPasswordInputType(editorInfo.inputType)
813                    || InputTypeUtils.isVisiblePasswordInputType(editorInfo.inputType);
814            getInstance().setIsPasswordView(isPassword);
815            researchLogger.start();
816            final Context context = researchLogger.mInputMethodService;
817            try {
818                final PackageInfo packageInfo;
819                packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(),
820                        0);
821                final Integer versionCode = packageInfo.versionCode;
822                final String versionName = packageInfo.versionName;
823                researchLogger.enqueueEvent(LOGSTATEMENT_LATIN_IME_ON_START_INPUT_VIEW_INTERNAL,
824                        researchLogger.mUUIDString, editorInfo.packageName,
825                        Integer.toHexString(editorInfo.inputType),
826                        Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId,
827                        Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName,
828                        OUTPUT_FORMAT_VERSION, LOG_EVERYTHING);
829            } catch (NameNotFoundException e) {
830                e.printStackTrace();
831            }
832        }
833    }
834
835    public void latinIME_onFinishInputViewInternal() {
836        stop();
837    }
838
839    private static final LogStatement LOGSTATEMENT_PREFS_CHANGED =
840            new LogStatement("PrefsChanged", false, false, "prefs");
841    public static void prefsChanged(final SharedPreferences prefs) {
842        final ResearchLogger researchLogger = getInstance();
843        researchLogger.enqueueEvent(LOGSTATEMENT_PREFS_CHANGED, prefs);
844    }
845
846    // Regular logging methods
847
848    private static final LogStatement LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT =
849            new LogStatement("MainKeyboardViewProcessMotionEvent", true, false, "action",
850                    "eventTime", "id", "x", "y", "size", "pressure");
851    public static void mainKeyboardView_processMotionEvent(final MotionEvent me, final int action,
852            final long eventTime, final int index, final int id, final int x, final int y) {
853        if (me != null) {
854            final String actionString;
855            switch (action) {
856                case MotionEvent.ACTION_CANCEL: actionString = "CANCEL"; break;
857                case MotionEvent.ACTION_UP: actionString = "UP"; break;
858                case MotionEvent.ACTION_DOWN: actionString = "DOWN"; break;
859                case MotionEvent.ACTION_POINTER_UP: actionString = "POINTER_UP"; break;
860                case MotionEvent.ACTION_POINTER_DOWN: actionString = "POINTER_DOWN"; break;
861                case MotionEvent.ACTION_MOVE: actionString = "MOVE"; break;
862                case MotionEvent.ACTION_OUTSIDE: actionString = "OUTSIDE"; break;
863                default: actionString = "ACTION_" + action; break;
864            }
865            final float size = me.getSize(index);
866            final float pressure = me.getPressure(index);
867            getInstance().enqueueEvent(LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT,
868                    actionString, eventTime, id, x, y, size, pressure);
869        }
870    }
871
872    private static final LogStatement LOGSTATEMENT_LATIN_IME_ON_CODE_INPUT =
873            new LogStatement("LatinImeOnCodeInput", true, false, "code", "x", "y");
874    public static void latinIME_onCodeInput(final int code, final int x, final int y) {
875        final long time = SystemClock.uptimeMillis();
876        final ResearchLogger researchLogger = getInstance();
877        researchLogger.enqueueEvent(LOGSTATEMENT_LATIN_IME_ON_CODE_INPUT,
878                Constants.printableCode(scrubDigitFromCodePoint(code)), x, y);
879        if (Character.isDigit(code)) {
880            researchLogger.setCurrentLogUnitContainsDigitFlag();
881        }
882        researchLogger.mStatistics.recordChar(code, time);
883    }
884    private static final LogStatement LOGSTATEMENT_LATINIME_ONDISPLAYCOMPLETIONS =
885            new LogStatement("LatinIMEOnDisplayCompletions", true, true,
886                    "applicationSpecifiedCompletions");
887    public static void latinIME_onDisplayCompletions(
888            final CompletionInfo[] applicationSpecifiedCompletions) {
889        // Note; passing an array as a single element in a vararg list.  Must create a new
890        // dummy array around it or it will get expanded.
891        getInstance().enqueueEvent(LOGSTATEMENT_LATINIME_ONDISPLAYCOMPLETIONS,
892                new Object[] { applicationSpecifiedCompletions });
893    }
894
895    public static boolean getAndClearLatinIMEExpectingUpdateSelection() {
896        boolean returnValue = sLatinIMEExpectingUpdateSelection;
897        sLatinIMEExpectingUpdateSelection = false;
898        return returnValue;
899    }
900
901    private static final LogStatement LOGSTATEMENT_LATINIME_ONWINDOWHIDDEN =
902            new LogStatement("LatinIMEOnWindowHidden", false, false, "isTextTruncated", "text");
903    public static void latinIME_onWindowHidden(final int savedSelectionStart,
904            final int savedSelectionEnd, final InputConnection ic) {
905        if (ic != null) {
906            final boolean isTextTruncated;
907            final String text;
908            if (LOG_EVERYTHING) {
909                // Capture the TextView contents.  This will trigger onUpdateSelection(), so we
910                // set sLatinIMEExpectingUpdateSelection so that when onUpdateSelection() is called,
911                // it can tell that it was generated by the logging code, and not by the user, and
912                // therefore keep user-visible state as is.
913                ic.beginBatchEdit();
914                ic.performContextMenuAction(android.R.id.selectAll);
915                CharSequence charSequence = ic.getSelectedText(0);
916                if (savedSelectionStart != -1 && savedSelectionEnd != -1) {
917                    ic.setSelection(savedSelectionStart, savedSelectionEnd);
918                }
919                ic.endBatchEdit();
920                sLatinIMEExpectingUpdateSelection = true;
921                if (TextUtils.isEmpty(charSequence)) {
922                    isTextTruncated = false;
923                    text = "";
924                } else {
925                    if (charSequence.length() > MAX_INPUTVIEW_LENGTH_TO_CAPTURE) {
926                        int length = MAX_INPUTVIEW_LENGTH_TO_CAPTURE;
927                        // do not cut in the middle of a supplementary character
928                        final char c = charSequence.charAt(length - 1);
929                        if (Character.isHighSurrogate(c)) {
930                            length--;
931                        }
932                        final CharSequence truncatedCharSequence = charSequence.subSequence(0,
933                                length);
934                        isTextTruncated = true;
935                        text = truncatedCharSequence.toString();
936                    } else {
937                        isTextTruncated = false;
938                        text = charSequence.toString();
939                    }
940                }
941            } else {
942                isTextTruncated = true;
943                text = "";
944            }
945            final ResearchLogger researchLogger = getInstance();
946            // Assume that OUTPUT_ENTIRE_BUFFER is only true when we don't care about privacy (e.g.
947            // during a live user test), so the normal isPotentiallyPrivate and
948            // isPotentiallyRevealing flags do not apply
949            researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONWINDOWHIDDEN, isTextTruncated,
950                    text);
951            researchLogger.commitCurrentLogUnit();
952            getInstance().stop();
953        }
954    }
955
956    private static final LogStatement LOGSTATEMENT_LATINIME_ONUPDATESELECTION =
957            new LogStatement("LatinIMEOnUpdateSelection", true, false, "lastSelectionStart",
958                    "lastSelectionEnd", "oldSelStart", "oldSelEnd", "newSelStart", "newSelEnd",
959                    "composingSpanStart", "composingSpanEnd", "expectingUpdateSelection",
960                    "expectingUpdateSelectionFromLogger", "context");
961    public static void latinIME_onUpdateSelection(final int lastSelectionStart,
962            final int lastSelectionEnd, final int oldSelStart, final int oldSelEnd,
963            final int newSelStart, final int newSelEnd, final int composingSpanStart,
964            final int composingSpanEnd, final boolean expectingUpdateSelection,
965            final boolean expectingUpdateSelectionFromLogger,
966            final RichInputConnection connection) {
967        String word = "";
968        if (connection != null) {
969            Range range = connection.getWordRangeAtCursor(WHITESPACE_SEPARATORS, 1);
970            if (range != null) {
971                word = range.mWord;
972            }
973        }
974        final ResearchLogger researchLogger = getInstance();
975        final String scrubbedWord = researchLogger.scrubWord(word);
976        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONUPDATESELECTION, lastSelectionStart,
977                lastSelectionEnd, oldSelStart, oldSelEnd, newSelStart, newSelEnd,
978                composingSpanStart, composingSpanEnd, expectingUpdateSelection,
979                expectingUpdateSelectionFromLogger, scrubbedWord);
980    }
981
982    private static final LogStatement LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY =
983            new LogStatement("LatinIMEPickSuggestionManually", true, false, "replacedWord", "index",
984                    "suggestion", "x", "y");
985    public static void latinIME_pickSuggestionManually(final String replacedWord,
986            final int index, CharSequence suggestion) {
987        final ResearchLogger researchLogger = getInstance();
988        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_PICKSUGGESTIONMANUALLY,
989                scrubDigitsFromString(replacedWord), index,
990                suggestion == null ? null : scrubDigitsFromString(suggestion.toString()),
991                Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE);
992    }
993
994    private static final LogStatement LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION =
995            new LogStatement("LatinIMEPunctuationSuggestion", false, false, "index", "suggestion",
996                    "x", "y");
997    public static void latinIME_punctuationSuggestion(final int index,
998            final CharSequence suggestion) {
999        getInstance().enqueueEvent(LOGSTATEMENT_LATINIME_PUNCTUATIONSUGGESTION, index, suggestion,
1000                Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE);
1001    }
1002
1003    private static final LogStatement LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT =
1004            new LogStatement("LatinIMESendKeyCodePoint", true, false, "code");
1005    public static void latinIME_sendKeyCodePoint(final int code) {
1006        final ResearchLogger researchLogger = getInstance();
1007        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_SENDKEYCODEPOINT,
1008                Constants.printableCode(scrubDigitFromCodePoint(code)));
1009        if (Character.isDigit(code)) {
1010            researchLogger.setCurrentLogUnitContainsDigitFlag();
1011        }
1012    }
1013
1014    private static final LogStatement LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE =
1015            new LogStatement("LatinIMESwapSwapperAndSpace", false, false);
1016    public static void latinIME_swapSwapperAndSpace() {
1017        getInstance().enqueueEvent(LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE);
1018    }
1019
1020    private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_ONLONGPRESS =
1021            new LogStatement("MainKeyboardViewOnLongPress", false, false);
1022    public static void mainKeyboardView_onLongPress() {
1023        getInstance().enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_ONLONGPRESS);
1024    }
1025
1026    private static final LogStatement LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD =
1027            new LogStatement("MainKeyboardViewSetKeyboard", false, false, "elementId", "locale",
1028                    "orientation", "width", "modeName", "action", "navigateNext",
1029                    "navigatePrevious", "clobberSettingsKey", "passwordInput", "shortcutKeyEnabled",
1030                    "hasShortcutKey", "languageSwitchKeyEnabled", "isMultiLine", "tw", "th",
1031                    "keys");
1032    public static void mainKeyboardView_setKeyboard(final Keyboard keyboard) {
1033        final KeyboardId kid = keyboard.mId;
1034        final boolean isPasswordView = kid.passwordInput();
1035        getInstance().setIsPasswordView(isPasswordView);
1036        getInstance().enqueueEvent(LOGSTATEMENT_MAINKEYBOARDVIEW_SETKEYBOARD,
1037                KeyboardId.elementIdToName(kid.mElementId),
1038                kid.mLocale + ":" + kid.mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET),
1039                kid.mOrientation, kid.mWidth, KeyboardId.modeName(kid.mMode), kid.imeAction(),
1040                kid.navigateNext(), kid.navigatePrevious(), kid.mClobberSettingsKey,
1041                isPasswordView, kid.mShortcutKeyEnabled, kid.mHasShortcutKey,
1042                kid.mLanguageSwitchKeyEnabled, kid.isMultiLine(), keyboard.mOccupiedWidth,
1043                keyboard.mOccupiedHeight, keyboard.mKeys);
1044    }
1045
1046    private static final LogStatement LOGSTATEMENT_LATINIME_REVERTCOMMIT =
1047            new LogStatement("LatinIMERevertCommit", true, false, "originallyTypedWord");
1048    public static void latinIME_revertCommit(final String originallyTypedWord) {
1049        getInstance().enqueueEvent(LOGSTATEMENT_LATINIME_REVERTCOMMIT, originallyTypedWord);
1050    }
1051
1052    private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCANCELINPUT =
1053            new LogStatement("PointerTrackerCallListenerOnCancelInput", false, false);
1054    public static void pointerTracker_callListenerOnCancelInput() {
1055        getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCANCELINPUT);
1056    }
1057
1058    private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT =
1059            new LogStatement("PointerTrackerCallListenerOnCodeInput", true, false, "code",
1060                    "outputText", "x", "y", "ignoreModifierKey", "altersCode", "isEnabled");
1061    public static void pointerTracker_callListenerOnCodeInput(final Key key, final int x,
1062            final int y, final boolean ignoreModifierKey, final boolean altersCode,
1063            final int code) {
1064        if (key != null) {
1065            String outputText = key.getOutputText();
1066            getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT,
1067                    Constants.printableCode(scrubDigitFromCodePoint(code)),
1068                    outputText == null ? null : scrubDigitsFromString(outputText.toString()),
1069                    x, y, ignoreModifierKey, altersCode, key.isEnabled());
1070        }
1071    }
1072
1073    private static final LogStatement LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONRELEASE =
1074            new LogStatement("PointerTrackerCallListenerOnRelease", true, false, "code",
1075                    "withSliding", "ignoreModifierKey", "isEnabled");
1076    public static void pointerTracker_callListenerOnRelease(final Key key, final int primaryCode,
1077            final boolean withSliding, final boolean ignoreModifierKey) {
1078        if (key != null) {
1079            getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONRELEASE,
1080                    Constants.printableCode(scrubDigitFromCodePoint(primaryCode)), withSliding,
1081                    ignoreModifierKey, key.isEnabled());
1082        }
1083    }
1084
1085    private static final LogStatement LOGSTATEMENT_POINTERTRACKER_ONDOWNEVENT =
1086            new LogStatement("PointerTrackerOnDownEvent", true, false, "deltaT", "distanceSquared");
1087    public static void pointerTracker_onDownEvent(long deltaT, int distanceSquared) {
1088        getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_ONDOWNEVENT, deltaT,
1089                distanceSquared);
1090    }
1091
1092    private static final LogStatement LOGSTATEMENT_POINTERTRACKER_ONMOVEEVENT =
1093            new LogStatement("PointerTrackerOnMoveEvent", true, false, "x", "y", "lastX", "lastY");
1094    public static void pointerTracker_onMoveEvent(final int x, final int y, final int lastX,
1095            final int lastY) {
1096        getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_ONMOVEEVENT, x, y, lastX, lastY);
1097    }
1098
1099    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_COMMITCOMPLETION =
1100            new LogStatement("RichInputConnectionCommitCompletion", true, false, "completionInfo");
1101    public static void richInputConnection_commitCompletion(final CompletionInfo completionInfo) {
1102        final ResearchLogger researchLogger = getInstance();
1103        researchLogger.enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_COMMITCOMPLETION,
1104                completionInfo);
1105    }
1106
1107    private boolean isExpectingCommitText = false;
1108    public static void latinIME_commitPartialText(final CharSequence committedWord,
1109            final long lastTimestampOfWordData) {
1110        final ResearchLogger researchLogger = getInstance();
1111        final String scrubbedWord = scrubDigitsFromString(committedWord.toString());
1112        researchLogger.onWordComplete(scrubbedWord, lastTimestampOfWordData, true /* isPartial */);
1113        researchLogger.isExpectingCommitText = true;
1114    }
1115
1116    private static final LogStatement LOGSTATEMENT_COMMITTEXT_UPDATECURSOR =
1117            new LogStatement("CommitTextUpdateCursor", true, false, "newCursorPosition");
1118    public static void richInputConnection_commitText(final CharSequence committedWord,
1119            final int newCursorPosition) {
1120        final ResearchLogger researchLogger = getInstance();
1121        final String scrubbedWord = scrubDigitsFromString(committedWord.toString());
1122        if (!researchLogger.isExpectingCommitText) {
1123            researchLogger.onWordComplete(scrubbedWord, Long.MAX_VALUE, false /* isPartial */);
1124            researchLogger.enqueueEvent(LOGSTATEMENT_COMMITTEXT_UPDATECURSOR, newCursorPosition);
1125        }
1126        researchLogger.isExpectingCommitText = false;
1127    }
1128
1129    private static final LogStatement LOGSTATEMENT_COMMITTEXT =
1130            new LogStatement("CommitText", true, false, "committedText");
1131    private void enqueueCommitText(final CharSequence word) {
1132        enqueueEvent(LOGSTATEMENT_COMMITTEXT, word);
1133    }
1134
1135    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT =
1136            new LogStatement("RichInputConnectionDeleteSurroundingText", true, false,
1137                    "beforeLength", "afterLength");
1138    public static void richInputConnection_deleteSurroundingText(final int beforeLength,
1139            final int afterLength) {
1140        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_DELETESURROUNDINGTEXT,
1141                beforeLength, afterLength);
1142    }
1143
1144    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT =
1145            new LogStatement("RichInputConnectionFinishComposingText", false, false);
1146    public static void richInputConnection_finishComposingText() {
1147        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_FINISHCOMPOSINGTEXT);
1148    }
1149
1150    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_PERFORMEDITORACTION =
1151            new LogStatement("RichInputConnectionPerformEditorAction", false, false,
1152                    "imeActionNext");
1153    public static void richInputConnection_performEditorAction(final int imeActionNext) {
1154        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_PERFORMEDITORACTION,
1155                imeActionNext);
1156    }
1157
1158    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SENDKEYEVENT =
1159            new LogStatement("RichInputConnectionSendKeyEvent", true, false, "eventTime", "action",
1160                    "code");
1161    public static void richInputConnection_sendKeyEvent(final KeyEvent keyEvent) {
1162        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SENDKEYEVENT,
1163                keyEvent.getEventTime(), keyEvent.getAction(), keyEvent.getKeyCode());
1164    }
1165
1166    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SETCOMPOSINGTEXT =
1167            new LogStatement("RichInputConnectionSetComposingText", true, true, "text",
1168                    "newCursorPosition");
1169    public static void richInputConnection_setComposingText(final CharSequence text,
1170            final int newCursorPosition) {
1171        if (text == null) {
1172            throw new RuntimeException("setComposingText is null");
1173        }
1174        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SETCOMPOSINGTEXT, text,
1175                newCursorPosition);
1176    }
1177
1178    private static final LogStatement LOGSTATEMENT_RICHINPUTCONNECTION_SETSELECTION =
1179            new LogStatement("RichInputConnectionSetSelection", true, false, "from", "to");
1180    public static void richInputConnection_setSelection(final int from, final int to) {
1181        getInstance().enqueueEvent(LOGSTATEMENT_RICHINPUTCONNECTION_SETSELECTION, from, to);
1182    }
1183
1184    private static final LogStatement LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT =
1185            new LogStatement("SuddenJumpingTouchEventHandlerOnTouchEvent", true, false,
1186                    "motionEvent");
1187    public static void suddenJumpingTouchEventHandler_onTouchEvent(final MotionEvent me) {
1188        if (me != null) {
1189            getInstance().enqueueEvent(LOGSTATEMENT_SUDDENJUMPINGTOUCHEVENTHANDLER_ONTOUCHEVENT,
1190                    me.toString());
1191        }
1192    }
1193
1194    private static final LogStatement LOGSTATEMENT_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS =
1195            new LogStatement("SuggestionStripViewSetSuggestions", true, true, "suggestedWords");
1196    public static void suggestionStripView_setSuggestions(final SuggestedWords suggestedWords) {
1197        if (suggestedWords != null) {
1198            getInstance().enqueueEvent(LOGSTATEMENT_SUGGESTIONSTRIPVIEW_SETSUGGESTIONS,
1199                    suggestedWords);
1200        }
1201    }
1202
1203    private static final LogStatement LOGSTATEMENT_USER_TIMESTAMP =
1204            new LogStatement("UserTimestamp", false, false);
1205    public void userTimestamp() {
1206        getInstance().enqueueEvent(LOGSTATEMENT_USER_TIMESTAMP);
1207    }
1208
1209    private static final LogStatement LOGSTATEMENT_LATINIME_ONENDBATCHINPUT =
1210            new LogStatement("LatinIMEOnEndBatchInput", true, false, "enteredText",
1211                    "enteredWordPos");
1212    public static void latinIME_onEndBatchInput(final CharSequence enteredText,
1213            final int enteredWordPos) {
1214        final ResearchLogger researchLogger = getInstance();
1215        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_ONENDBATCHINPUT, enteredText,
1216                enteredWordPos);
1217        researchLogger.mStatistics.recordGestureInput();
1218    }
1219
1220    private static final LogStatement LOGSTATEMENT_STATISTICS =
1221            new LogStatement("Statistics", false, false, "charCount", "letterCount", "numberCount",
1222                    "spaceCount", "deleteOpsCount", "wordCount", "isEmptyUponStarting",
1223                    "isEmptinessStateKnown", "averageTimeBetweenKeys", "averageTimeBeforeDelete",
1224                    "averageTimeDuringRepeatedDelete", "averageTimeAfterDelete",
1225                    "dictionaryWordCount", "splitWordsCount", "gestureInputCount");
1226    private static void logStatistics() {
1227        final ResearchLogger researchLogger = getInstance();
1228        final Statistics statistics = researchLogger.mStatistics;
1229        researchLogger.enqueueEvent(LOGSTATEMENT_STATISTICS, statistics.mCharCount,
1230                statistics.mLetterCount, statistics.mNumberCount, statistics.mSpaceCount,
1231                statistics.mDeleteKeyCount, statistics.mWordCount, statistics.mIsEmptyUponStarting,
1232                statistics.mIsEmptinessStateKnown, statistics.mKeyCounter.getAverageTime(),
1233                statistics.mBeforeDeleteKeyCounter.getAverageTime(),
1234                statistics.mDuringRepeatedDeleteKeysCounter.getAverageTime(),
1235                statistics.mAfterDeleteKeyCounter.getAverageTime(),
1236                statistics.mDictionaryWordCount, statistics.mSplitWordsCount,
1237                statistics.mGestureInputCount);
1238    }
1239}
1240