Home | History | Annotate | Download | only in research
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.inputmethod.research;
     18 
     19 import android.content.SharedPreferences;
     20 import android.util.JsonWriter;
     21 import android.util.Log;
     22 import android.view.MotionEvent;
     23 import android.view.inputmethod.CompletionInfo;
     24 
     25 import com.android.inputmethod.keyboard.Key;
     26 import com.android.inputmethod.latin.SuggestedWords;
     27 import com.android.inputmethod.latin.define.ProductionFlag;
     28 
     29 import java.io.IOException;
     30 
     31 /**
     32  * A template for typed information stored in the logs.
     33  *
     34  * A LogStatement contains a name, keys, and flags about whether the {@code Object[] values}
     35  * associated with the {@code String[] keys} are likely to reveal information about the user.  The
     36  * actual values are stored separately.
     37  */
     38 public class LogStatement {
     39     private static final String TAG = LogStatement.class.getSimpleName();
     40     private static final boolean DEBUG = false
     41             && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG;
     42 
     43     // Constants for particular statements
     44     public static final String TYPE_POINTER_TRACKER_CALL_LISTENER_ON_CODE_INPUT =
     45             "PointerTrackerCallListenerOnCodeInput";
     46     public static final String KEY_CODE = "code";
     47     public static final String VALUE_RESEARCH = "research";
     48     public static final String TYPE_MAIN_KEYBOARD_VIEW_ON_LONG_PRESS =
     49             "MainKeyboardViewOnLongPress";
     50     public static final String ACTION = "action";
     51     public static final String VALUE_DOWN = "DOWN";
     52     public static final String TYPE_MOTION_EVENT = "MotionEvent";
     53     public static final String KEY_IS_LOGGING_RELATED = "isLoggingRelated";
     54 
     55     // Keys for internal key/value pairs
     56     private static final String CURRENT_TIME_KEY = "_ct";
     57     private static final String UPTIME_KEY = "_ut";
     58     private static final String EVENT_TYPE_KEY = "_ty";
     59 
     60     // Name specifying the LogStatement type.
     61     private final String mType;
     62 
     63     // mIsPotentiallyPrivate indicates that event contains potentially private information.  If
     64     // the word that this event is a part of is determined to be privacy-sensitive, then this
     65     // event should not be included in the output log.  The system waits to output until the
     66     // containing word is known.
     67     private final boolean mIsPotentiallyPrivate;
     68 
     69     // mIsPotentiallyRevealing indicates that this statement may disclose details about other
     70     // words typed in other LogUnits.  This can happen if the user is not inserting spaces, and
     71     // data from Suggestions and/or Composing text reveals the entire "megaword".  For example,
     72     // say the user is typing "for the win", and the system wants to record the bigram "the
     73     // win".  If the user types "forthe", omitting the space, the system will give "for the" as
     74     // a suggestion.  If the user accepts the autocorrection, the suggestion for "for the" is
     75     // included in the log for the word "the", disclosing that the previous word had been "for".
     76     // For now, we simply do not include this data when logging part of a "megaword".
     77     private final boolean mIsPotentiallyRevealing;
     78 
     79     // mKeys stores the names that are the attributes in the output json objects
     80     private final String[] mKeys;
     81     private static final String[] NULL_KEYS = new String[0];
     82 
     83     LogStatement(final String name, final boolean isPotentiallyPrivate,
     84             final boolean isPotentiallyRevealing, final String... keys) {
     85         mType = name;
     86         mIsPotentiallyPrivate = isPotentiallyPrivate;
     87         mIsPotentiallyRevealing = isPotentiallyRevealing;
     88         mKeys = (keys == null) ? NULL_KEYS : keys;
     89     }
     90 
     91     public String getType() {
     92         return mType;
     93     }
     94 
     95     public boolean isPotentiallyPrivate() {
     96         return mIsPotentiallyPrivate;
     97     }
     98 
     99     public boolean isPotentiallyRevealing() {
    100         return mIsPotentiallyRevealing;
    101     }
    102 
    103     public String[] getKeys() {
    104         return mKeys;
    105     }
    106 
    107     /**
    108      * Utility function to test whether a key-value pair exists in a LogStatement.
    109      *
    110      * A LogStatement is really just a template -- it does not contain the values, only the
    111      * keys.  So the values must be passed in as an argument.
    112      *
    113      * @param queryKey the String that is tested by {@code String.equals()} to the keys in the
    114      * LogStatement
    115      * @param queryValue an Object that must be {@code Object.equals()} to the key's corresponding
    116      * value in the {@code values} array
    117      * @param values the values corresponding to mKeys
    118      *
    119      * @returns {@true} if {@code queryKey} exists in the keys for this LogStatement, and {@code
    120      * queryValue} matches the corresponding value in {@code values}
    121      *
    122      * @throws IllegalArgumentException if {@code values.length} is not equal to keys().length()
    123      */
    124     public boolean containsKeyValuePair(final String queryKey, final Object queryValue,
    125             final Object[] values) {
    126         if (mKeys.length != values.length) {
    127             throw new IllegalArgumentException("Mismatched number of keys and values.");
    128         }
    129         final int length = mKeys.length;
    130         for (int i = 0; i < length; i++) {
    131             if (mKeys[i].equals(queryKey) && values[i].equals(queryValue)) {
    132                 return true;
    133             }
    134         }
    135         return false;
    136     }
    137 
    138     /**
    139      * Utility function to set a value in a LogStatement.
    140      *
    141      * A LogStatement is really just a template -- it does not contain the values, only the
    142      * keys.  So the values must be passed in as an argument.
    143      *
    144      * @param queryKey the String that is tested by {@code String.equals()} to the keys in the
    145      * LogStatement
    146      * @param values the array of values corresponding to mKeys
    147      * @param newValue the replacement value to go into the {@code values} array
    148      *
    149      * @returns {@true} if the key exists and the value was successfully set, {@false} otherwise
    150      *
    151      * @throws IllegalArgumentException if {@code values.length} is not equal to keys().length()
    152      */
    153     public boolean setValue(final String queryKey, final Object[] values, final Object newValue) {
    154         if (mKeys.length != values.length) {
    155             throw new IllegalArgumentException("Mismatched number of keys and values.");
    156         }
    157         final int length = mKeys.length;
    158         for (int i = 0; i < length; i++) {
    159             if (mKeys[i].equals(queryKey)) {
    160                 values[i] = newValue;
    161                 return true;
    162             }
    163         }
    164         return false;
    165     }
    166 
    167     /**
    168      * Write the contents out through jsonWriter.
    169      *
    170      * The JsonWriter class must have already had {@code JsonWriter.beginArray} called on it.
    171      *
    172      * Note that this method is not thread safe for the same jsonWriter.  Callers must ensure
    173      * thread safety.
    174      */
    175     public boolean outputToLocked(final JsonWriter jsonWriter, final Long time,
    176             final Object... values) {
    177         if (DEBUG) {
    178             if (mKeys.length != values.length) {
    179                 Log.d(TAG, "Key and Value list sizes do not match. " + mType);
    180             }
    181         }
    182         try {
    183             jsonWriter.beginObject();
    184             jsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis());
    185             jsonWriter.name(UPTIME_KEY).value(time);
    186             jsonWriter.name(EVENT_TYPE_KEY).value(mType);
    187             final int length = values.length;
    188             for (int i = 0; i < length; i++) {
    189                 jsonWriter.name(mKeys[i]);
    190                 final Object value = values[i];
    191                 if (value instanceof CharSequence) {
    192                     jsonWriter.value(value.toString());
    193                 } else if (value instanceof Number) {
    194                     jsonWriter.value((Number) value);
    195                 } else if (value instanceof Boolean) {
    196                     jsonWriter.value((Boolean) value);
    197                 } else if (value instanceof CompletionInfo[]) {
    198                     JsonUtils.writeJson((CompletionInfo[]) value, jsonWriter);
    199                 } else if (value instanceof SharedPreferences) {
    200                     JsonUtils.writeJson((SharedPreferences) value, jsonWriter);
    201                 } else if (value instanceof Key[]) {
    202                     JsonUtils.writeJson((Key[]) value, jsonWriter);
    203                 } else if (value instanceof SuggestedWords) {
    204                     JsonUtils.writeJson((SuggestedWords) value, jsonWriter);
    205                 } else if (value instanceof MotionEvent) {
    206                     JsonUtils.writeJson((MotionEvent) value, jsonWriter);
    207                 } else if (value == null) {
    208                     jsonWriter.nullValue();
    209                 } else {
    210                     if (DEBUG) {
    211                         Log.w(TAG, "Unrecognized type to be logged: "
    212                                 + (value == null ? "<null>" : value.getClass().getName()));
    213                     }
    214                     jsonWriter.nullValue();
    215                 }
    216             }
    217             jsonWriter.endObject();
    218         } catch (IOException e) {
    219             e.printStackTrace();
    220             Log.w(TAG, "Error in JsonWriter; skipping LogStatement");
    221             return false;
    222         }
    223         return true;
    224     }
    225 }
    226