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