LogUnit.java revision 0c16a5c6eef645fd536671994e0b4f05864ac338
1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.inputmethod.research;
18
19import android.content.SharedPreferences;
20import android.text.TextUtils;
21import android.util.JsonWriter;
22import android.util.Log;
23import android.view.MotionEvent;
24import android.view.inputmethod.CompletionInfo;
25
26import com.android.inputmethod.keyboard.Key;
27import com.android.inputmethod.latin.SuggestedWords;
28import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
29import com.android.inputmethod.latin.Utils;
30import com.android.inputmethod.latin.define.ProductionFlag;
31import com.android.inputmethod.research.ResearchLogger.LogStatement;
32
33import java.io.IOException;
34import java.io.StringWriter;
35import java.util.ArrayList;
36import java.util.List;
37import java.util.Map;
38
39/**
40 * A group of log statements related to each other.
41 *
42 * A LogUnit is collection of LogStatements, each of which is generated by at a particular point
43 * in the code.  (There is no LogStatement class; the data is stored across the instance variables
44 * here.)  A single LogUnit's statements can correspond to all the calls made while in the same
45 * composing region, or all the calls between committing the last composing region, and the first
46 * character of the next composing region.
47 *
48 * Individual statements in a log may be marked as potentially private.  If so, then they are only
49 * published to a ResearchLog if the ResearchLogger determines that publishing the entire LogUnit
50 * will not violate the user's privacy.  Checks for this may include whether other LogUnits have
51 * been published recently, or whether the LogUnit contains numbers, etc.
52 */
53/* package */ class LogUnit {
54    private static final String TAG = LogUnit.class.getSimpleName();
55    private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
56    private final ArrayList<LogStatement> mLogStatementList;
57    private final ArrayList<Object[]> mValuesList;
58    // Assume that mTimeList is sorted in increasing order.  Do not insert null values into
59    // mTimeList.
60    private final ArrayList<Long> mTimeList;
61    // Word that this LogUnit generates.  Should be null if the LogUnit does not generate a genuine
62    // word (i.e. separators alone do not count as a word).  Should never be empty.
63    private String mWord;
64    private boolean mMayContainDigit;
65    private boolean mIsPartOfMegaword;
66    private boolean mContainsCorrection;
67
68    // mCorrectionType indicates whether the word was corrected at all, and if so, whether it was
69    // to a different word or just a "typo" correction.  It is considered a "typo" if the final
70    // word was listed in the suggestions available the first time the word was gestured or
71    // tapped.
72    private int mCorrectionType;
73    public static final int CORRECTIONTYPE_NO_CORRECTION = 0;
74    public static final int CORRECTIONTYPE_CORRECTION = 1;
75    public static final int CORRECTIONTYPE_DIFFERENT_WORD = 2;
76    public static final int CORRECTIONTYPE_TYPO = 3;
77
78    private SuggestedWords mSuggestedWords;
79
80    public LogUnit() {
81        mLogStatementList = new ArrayList<LogStatement>();
82        mValuesList = new ArrayList<Object[]>();
83        mTimeList = new ArrayList<Long>();
84        mIsPartOfMegaword = false;
85        mCorrectionType = CORRECTIONTYPE_NO_CORRECTION;
86        mSuggestedWords = null;
87    }
88
89    private LogUnit(final ArrayList<LogStatement> logStatementList,
90            final ArrayList<Object[]> valuesList,
91            final ArrayList<Long> timeList,
92            final boolean isPartOfMegaword) {
93        mLogStatementList = logStatementList;
94        mValuesList = valuesList;
95        mTimeList = timeList;
96        mIsPartOfMegaword = isPartOfMegaword;
97        mCorrectionType = CORRECTIONTYPE_NO_CORRECTION;
98        mSuggestedWords = null;
99    }
100
101    private static final Object[] NULL_VALUES = new Object[0];
102    /**
103     * Adds a new log statement.  The time parameter in successive calls to this method must be
104     * monotonically increasing, or splitByTime() will not work.
105     */
106    public void addLogStatement(final LogStatement logStatement, final long time,
107            Object... values) {
108        if (values == null) {
109            values = NULL_VALUES;
110        }
111        mLogStatementList.add(logStatement);
112        mValuesList.add(values);
113        mTimeList.add(time);
114    }
115
116    /**
117     * Publish the contents of this LogUnit to researchLog.
118     */
119    public synchronized void publishTo(final ResearchLog researchLog,
120            final boolean canIncludePrivateData) {
121        // Prepare debugging output if necessary
122        final StringWriter debugStringWriter;
123        final JsonWriter debugJsonWriter;
124        if (DEBUG) {
125            debugStringWriter = new StringWriter();
126            debugJsonWriter = new JsonWriter(debugStringWriter);
127            debugJsonWriter.setIndent("  ");
128            try {
129                debugJsonWriter.beginArray();
130            } catch (IOException e) {
131                Log.e(TAG, "Could not open array in JsonWriter", e);
132            }
133        } else {
134            debugStringWriter = null;
135            debugJsonWriter = null;
136        }
137        // Write out any logStatement that passes the privacy filter.
138        final int size = mLogStatementList.size();
139        if (size != 0) {
140            // Note that jsonWriter is only set to a non-null value if the logUnit start text is
141            // output and at least one logStatement is output.
142            JsonWriter jsonWriter = null;
143            for (int i = 0; i < size; i++) {
144                final LogStatement logStatement = mLogStatementList.get(i);
145                if (!canIncludePrivateData && logStatement.mIsPotentiallyPrivate) {
146                    continue;
147                }
148                if (mIsPartOfMegaword && logStatement.mIsPotentiallyRevealing) {
149                    continue;
150                }
151                // Only retrieve the jsonWriter if we need to.  If we don't get this far, then
152                // researchLog.getValidJsonWriterLocked() will not ever be called, and the file
153                // will not have been opened for writing.
154                if (jsonWriter == null) {
155                    jsonWriter = researchLog.getValidJsonWriterLocked();
156                    outputLogUnitStart(jsonWriter, canIncludePrivateData);
157                }
158                outputLogStatementToLocked(jsonWriter, mLogStatementList.get(i), mValuesList.get(i),
159                        mTimeList.get(i));
160                if (DEBUG) {
161                    outputLogStatementToLocked(debugJsonWriter, mLogStatementList.get(i),
162                            mValuesList.get(i), mTimeList.get(i));
163                }
164            }
165            if (jsonWriter != null) {
166                // We must have called logUnitStart earlier, so emit a logUnitStop.
167                outputLogUnitStop(jsonWriter);
168            }
169        }
170        if (DEBUG) {
171            try {
172                debugJsonWriter.endArray();
173                debugJsonWriter.flush();
174            } catch (IOException e) {
175                Log.e(TAG, "Could not close array in JsonWriter", e);
176            }
177            final String bigString = debugStringWriter.getBuffer().toString();
178            final String[] lines = bigString.split("\n");
179            for (String line : lines) {
180                Log.d(TAG, line);
181            }
182        }
183    }
184
185    private static final String CURRENT_TIME_KEY = "_ct";
186    private static final String UPTIME_KEY = "_ut";
187    private static final String EVENT_TYPE_KEY = "_ty";
188    private static final String WORD_KEY = "_wo";
189    private static final String CORRECTION_TYPE_KEY = "_corType";
190    private static final String LOG_UNIT_BEGIN_KEY = "logUnitStart";
191    private static final String LOG_UNIT_END_KEY = "logUnitEnd";
192
193    private void outputLogUnitStart(final JsonWriter jsonWriter,
194            final boolean canIncludePrivateData) {
195        try {
196            jsonWriter.beginObject();
197            jsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis());
198            if (canIncludePrivateData) {
199                jsonWriter.name(WORD_KEY).value(getWord());
200                jsonWriter.name(CORRECTION_TYPE_KEY).value(getCorrectionType());
201            }
202            jsonWriter.name(EVENT_TYPE_KEY).value(LOG_UNIT_BEGIN_KEY);
203            jsonWriter.endObject();
204        } catch (IOException e) {
205            e.printStackTrace();
206            Log.w(TAG, "Error in JsonWriter; cannot write LogUnitStart");
207        }
208    }
209
210    private void outputLogUnitStop(final JsonWriter jsonWriter) {
211        try {
212            jsonWriter.beginObject();
213            jsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis());
214            jsonWriter.name(EVENT_TYPE_KEY).value(LOG_UNIT_END_KEY);
215            jsonWriter.endObject();
216        } catch (IOException e) {
217            e.printStackTrace();
218            Log.w(TAG, "Error in JsonWriter; cannot write LogUnitStop");
219        }
220    }
221
222    /**
223     * Write the logStatement and its contents out through jsonWriter.
224     *
225     * Note that this method is not thread safe for the same jsonWriter.  Callers must ensure
226     * thread safety.
227     */
228    private boolean outputLogStatementToLocked(final JsonWriter jsonWriter,
229            final LogStatement logStatement, final Object[] values, final Long time) {
230        if (DEBUG) {
231            if (logStatement.mKeys.length != values.length) {
232                Log.d(TAG, "Key and Value list sizes do not match. " + logStatement.mName);
233            }
234        }
235        try {
236            jsonWriter.beginObject();
237            jsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis());
238            jsonWriter.name(UPTIME_KEY).value(time);
239            jsonWriter.name(EVENT_TYPE_KEY).value(logStatement.mName);
240            final String[] keys = logStatement.mKeys;
241            final int length = values.length;
242            for (int i = 0; i < length; i++) {
243                jsonWriter.name(keys[i]);
244                final Object value = values[i];
245                if (value instanceof CharSequence) {
246                    jsonWriter.value(value.toString());
247                } else if (value instanceof Number) {
248                    jsonWriter.value((Number) value);
249                } else if (value instanceof Boolean) {
250                    jsonWriter.value((Boolean) value);
251                } else if (value instanceof CompletionInfo[]) {
252                    JsonUtils.writeJson((CompletionInfo[]) value, jsonWriter);
253                } else if (value instanceof SharedPreferences) {
254                    JsonUtils.writeJson((SharedPreferences) value, jsonWriter);
255                } else if (value instanceof Key[]) {
256                    JsonUtils.writeJson((Key[]) value, jsonWriter);
257                } else if (value instanceof SuggestedWords) {
258                    JsonUtils.writeJson((SuggestedWords) value, jsonWriter);
259                } else if (value instanceof MotionEvent) {
260                    JsonUtils.writeJson((MotionEvent) value, jsonWriter);
261                } else if (value == null) {
262                    jsonWriter.nullValue();
263                } else {
264                    Log.w(TAG, "Unrecognized type to be logged: " +
265                            (value == null ? "<null>" : value.getClass().getName()));
266                    jsonWriter.nullValue();
267                }
268            }
269            jsonWriter.endObject();
270        } catch (IOException e) {
271            e.printStackTrace();
272            Log.w(TAG, "Error in JsonWriter; skipping LogStatement");
273            return false;
274        }
275        return true;
276    }
277
278    /**
279     * Mark the current logUnit as containing data to generate {@code word}.
280     *
281     * If {@code setWord()} was previously called for this LogUnit, then the method will try to
282     * determine what kind of correction it is, and update its internal state of the correctionType
283     * accordingly.
284     *
285     * @param word The word this LogUnit generates.  Caller should not pass null or the empty
286     * string.
287     */
288    public void setWord(final String word) {
289        if (mWord != null) {
290            // The word was already set once, and it is now being changed.  See if the new word
291            // is close to the old word.  If so, then the change is probably a typo correction.
292            // If not, the user may have decided to enter a different word, so flag it.
293            if (mSuggestedWords != null) {
294                if (isInSuggestedWords(word, mSuggestedWords)) {
295                    mCorrectionType = CORRECTIONTYPE_TYPO;
296                } else {
297                    mCorrectionType = CORRECTIONTYPE_DIFFERENT_WORD;
298                }
299            } else {
300                // No suggested words, so it's not clear whether it's a typo or different word.
301                // Mark it as a generic correction.
302                mCorrectionType = CORRECTIONTYPE_CORRECTION;
303            }
304        }
305        mWord = word;
306    }
307
308    public String getWord() {
309        return mWord;
310    }
311
312    public boolean hasWord() {
313        return mWord != null;
314    }
315
316    public void setMayContainDigit() {
317        mMayContainDigit = true;
318    }
319
320    public boolean mayContainDigit() {
321        return mMayContainDigit;
322    }
323
324    public void setContainsCorrection() {
325        mContainsCorrection = true;
326    }
327
328    public boolean containsCorrection() {
329        return mContainsCorrection;
330    }
331
332    public void setCorrectionType(final int correctionType) {
333        mCorrectionType = correctionType;
334    }
335
336    public int getCorrectionType() {
337        return mCorrectionType;
338    }
339
340    public boolean isEmpty() {
341        return mLogStatementList.isEmpty();
342    }
343
344    /**
345     * Split this logUnit, with all events before maxTime staying in the current logUnit, and all
346     * events after maxTime going into a new LogUnit that is returned.
347     */
348    public LogUnit splitByTime(final long maxTime) {
349        // Assume that mTimeList is in sorted order.
350        final int length = mTimeList.size();
351        // TODO: find time by binary search, e.g. using Collections#binarySearch()
352        for (int index = 0; index < length; index++) {
353            if (mTimeList.get(index) > maxTime) {
354                final List<LogStatement> laterLogStatements =
355                        mLogStatementList.subList(index, length);
356                final List<Object[]> laterValues = mValuesList.subList(index, length);
357                final List<Long> laterTimes = mTimeList.subList(index, length);
358
359                // Create the LogUnit containing the later logStatements and associated data.
360                final LogUnit newLogUnit = new LogUnit(
361                        new ArrayList<LogStatement>(laterLogStatements),
362                        new ArrayList<Object[]>(laterValues),
363                        new ArrayList<Long>(laterTimes),
364                        true /* isPartOfMegaword */);
365                newLogUnit.mWord = null;
366                newLogUnit.mMayContainDigit = mMayContainDigit;
367                newLogUnit.mContainsCorrection = mContainsCorrection;
368
369                // Purge the logStatements and associated data from this LogUnit.
370                laterLogStatements.clear();
371                laterValues.clear();
372                laterTimes.clear();
373                mIsPartOfMegaword = true;
374
375                return newLogUnit;
376            }
377        }
378        return new LogUnit();
379    }
380
381    public void append(final LogUnit logUnit) {
382        mLogStatementList.addAll(logUnit.mLogStatementList);
383        mValuesList.addAll(logUnit.mValuesList);
384        mTimeList.addAll(logUnit.mTimeList);
385        mWord = null;
386        if (logUnit.mWord != null) {
387            setWord(logUnit.mWord);
388        }
389        mMayContainDigit = mMayContainDigit || logUnit.mMayContainDigit;
390        mContainsCorrection = mContainsCorrection || logUnit.mContainsCorrection;
391        mIsPartOfMegaword = false;
392    }
393
394    public SuggestedWords getSuggestions() {
395        return mSuggestedWords;
396    }
397
398    /**
399     * Initialize the suggestions.
400     *
401     * Once set to a non-null value, the suggestions may not be changed again.  This is to keep
402     * track of the list of words that are close to the user's initial effort to type the word.
403     * Only words that are close to the initial effort are considered typo corrections.
404     */
405    public void initializeSuggestions(final SuggestedWords suggestedWords) {
406        if (mSuggestedWords == null) {
407            mSuggestedWords = suggestedWords;
408        }
409    }
410
411    private static boolean isInSuggestedWords(final String queryWord,
412            final SuggestedWords suggestedWords) {
413        if (TextUtils.isEmpty(queryWord)) {
414            return false;
415        }
416        final int size = suggestedWords.size();
417        for (int i = 0; i < size; i++) {
418            final SuggestedWordInfo wordInfo = suggestedWords.getInfo(i);
419            if (queryWord.equals(wordInfo.mWord)) {
420                return true;
421            }
422        }
423        return false;
424    }
425}
426