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 17 package com.android.inputmethod.research; 18 19 import android.content.Context; 20 import android.util.JsonWriter; 21 import android.util.Log; 22 23 import com.android.inputmethod.annotations.UsedForTesting; 24 import com.android.inputmethod.latin.define.ProductionFlag; 25 26 import java.io.BufferedWriter; 27 import java.io.File; 28 import java.io.FileNotFoundException; 29 import java.io.IOException; 30 import java.io.OutputStreamWriter; 31 import java.util.concurrent.Callable; 32 import java.util.concurrent.Executors; 33 import java.util.concurrent.RejectedExecutionException; 34 import java.util.concurrent.ScheduledExecutorService; 35 import java.util.concurrent.ScheduledFuture; 36 import java.util.concurrent.TimeUnit; 37 38 /** 39 * Logs the use of the LatinIME keyboard. 40 * 41 * This class logs operations on the IME keyboard, including what the user has typed. Data is 42 * written to a {@link JsonWriter}, which will write to a local file. 43 * 44 * The JsonWriter is created on-demand by calling {@link #getInitializedJsonWriterLocked}. 45 * 46 * This class uses an executor to perform file-writing operations on a separate thread. It also 47 * tries to avoid creating unnecessary files if there is nothing to write. It also handles 48 * flushing, making sure it happens, but not too frequently. 49 * 50 * This functionality is off by default. See 51 * {@link ProductionFlag#USES_DEVELOPMENT_ONLY_DIAGNOSTICS}. 52 */ 53 public class ResearchLog { 54 // TODO: Automatically initialize the JsonWriter rather than requiring the caller to manage it. 55 private static final String TAG = ResearchLog.class.getSimpleName(); 56 private static final boolean DEBUG = false 57 && ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG; 58 private static final long FLUSH_DELAY_IN_MS = TimeUnit.SECONDS.toMillis(5); 59 60 /* package */ final ScheduledExecutorService mExecutor; 61 /* package */ final File mFile; 62 private final Context mContext; 63 64 // Earlier implementations used a dummy JsonWriter that just swallowed what it was given, but 65 // this was tricky to do well, because JsonWriter throws an exception if it is passed more than 66 // one top-level object. 67 private JsonWriter mJsonWriter = null; 68 69 // true if at least one byte of data has been written out to the log file. This must be 70 // remembered because JsonWriter requires that calls matching calls to beginObject and 71 // endObject, as well as beginArray and endArray, and the file is opened lazily, only when 72 // it is certain that data will be written. Alternatively, the matching call exceptions 73 // could be caught, but this might suppress other errors. 74 private boolean mHasWrittenData = false; 75 76 public ResearchLog(final File outputFile, final Context context) { 77 mExecutor = Executors.newSingleThreadScheduledExecutor(); 78 mFile = outputFile; 79 mContext = context; 80 } 81 82 /** 83 * Returns true if this is a FeedbackLog. 84 * 85 * FeedbackLogs record only the data associated with a Feedback dialog. Instead of normal 86 * logging, they contain a LogStatement with the complete feedback string and optionally a 87 * recording of the user's supplied demo of the problem. 88 */ 89 public boolean isFeedbackLog() { 90 return false; 91 } 92 93 /** 94 * Waits for any publication requests to finish and closes the {@link JsonWriter} used for 95 * output. 96 * 97 * See class comment for details about {@code JsonWriter} construction. 98 * 99 * @param onClosed run after the close() operation has completed asynchronously 100 */ 101 private synchronized void close(final Runnable onClosed) { 102 mExecutor.submit(new Callable<Object>() { 103 @Override 104 public Object call() throws Exception { 105 try { 106 if (mJsonWriter == null) return null; 107 // TODO: This is necessary to avoid an exception. Better would be to not even 108 // open the JsonWriter if the file is not even opened unless there is valid data 109 // to write. 110 if (!mHasWrittenData) { 111 mJsonWriter.beginArray(); 112 } 113 mJsonWriter.endArray(); 114 mHasWrittenData = false; 115 mJsonWriter.flush(); 116 mJsonWriter.close(); 117 if (DEBUG) { 118 Log.d(TAG, "closed " + mFile); 119 } 120 } catch (final Exception e) { 121 Log.d(TAG, "error when closing ResearchLog:", e); 122 } finally { 123 // Marking the file as read-only signals that this log file is ready to be 124 // uploaded. 125 if (mFile != null && mFile.exists()) { 126 mFile.setWritable(false, false); 127 } 128 if (onClosed != null) { 129 onClosed.run(); 130 } 131 } 132 return null; 133 } 134 }); 135 removeAnyScheduledFlush(); 136 mExecutor.shutdown(); 137 } 138 139 /** 140 * Block until the research log has shut down and spooled out all output or {@code timeout} 141 * occurs. 142 * 143 * @param timeout time to wait for close in milliseconds 144 */ 145 public void blockingClose(final long timeout) { 146 close(null); 147 awaitTermination(timeout, TimeUnit.MILLISECONDS); 148 } 149 150 /** 151 * Waits for publication requests to finish, closes the JsonWriter, but then deletes the backing 152 * output file. 153 * 154 * @param onAbort run after the abort() operation has completed asynchronously 155 */ 156 private synchronized void abort(final Runnable onAbort) { 157 mExecutor.submit(new Callable<Object>() { 158 @Override 159 public Object call() throws Exception { 160 try { 161 if (mJsonWriter == null) return null; 162 if (mHasWrittenData) { 163 // TODO: This is necessary to avoid an exception. Better would be to not 164 // even open the JsonWriter if the file is not even opened unless there is 165 // valid data to write. 166 if (!mHasWrittenData) { 167 mJsonWriter.beginArray(); 168 } 169 mJsonWriter.endArray(); 170 mJsonWriter.close(); 171 mHasWrittenData = false; 172 } 173 } finally { 174 if (mFile != null) { 175 mFile.delete(); 176 } 177 if (onAbort != null) { 178 onAbort.run(); 179 } 180 } 181 return null; 182 } 183 }); 184 removeAnyScheduledFlush(); 185 mExecutor.shutdown(); 186 } 187 188 /** 189 * Block until the research log has aborted or {@code timeout} occurs. 190 * 191 * @param timeout time to wait for close in milliseconds 192 */ 193 public void blockingAbort(final long timeout) { 194 abort(null); 195 awaitTermination(timeout, TimeUnit.MILLISECONDS); 196 } 197 198 @UsedForTesting 199 public void awaitTermination(final long delay, final TimeUnit timeUnit) { 200 try { 201 if (!mExecutor.awaitTermination(delay, timeUnit)) { 202 Log.e(TAG, "ResearchLog executor timed out while awaiting terminaion"); 203 } 204 } catch (final InterruptedException e) { 205 Log.e(TAG, "ResearchLog executor interrupted while awaiting terminaion", e); 206 } 207 } 208 209 /* package */ synchronized void flush() { 210 removeAnyScheduledFlush(); 211 mExecutor.submit(mFlushCallable); 212 } 213 214 private final Callable<Object> mFlushCallable = new Callable<Object>() { 215 @Override 216 public Object call() throws Exception { 217 if (mJsonWriter != null) mJsonWriter.flush(); 218 return null; 219 } 220 }; 221 222 private ScheduledFuture<Object> mFlushFuture; 223 224 private void removeAnyScheduledFlush() { 225 if (mFlushFuture != null) { 226 mFlushFuture.cancel(false); 227 mFlushFuture = null; 228 } 229 } 230 231 private void scheduleFlush() { 232 removeAnyScheduledFlush(); 233 mFlushFuture = mExecutor.schedule(mFlushCallable, FLUSH_DELAY_IN_MS, TimeUnit.MILLISECONDS); 234 } 235 236 /** 237 * Queues up {@code logUnit} to be published in the background. 238 * 239 * @param logUnit the {@link LogUnit} to be published 240 * @param canIncludePrivateData whether private data in the LogUnit should be included 241 */ 242 public synchronized void publish(final LogUnit logUnit, final boolean canIncludePrivateData) { 243 try { 244 mExecutor.submit(new Callable<Object>() { 245 @Override 246 public Object call() throws Exception { 247 logUnit.publishTo(ResearchLog.this, canIncludePrivateData); 248 scheduleFlush(); 249 return null; 250 } 251 }); 252 } catch (final RejectedExecutionException e) { 253 // TODO: Add code to record loss of data, and report. 254 if (DEBUG) { 255 Log.d(TAG, "ResearchLog.publish() rejecting scheduled execution", e); 256 } 257 } 258 } 259 260 /** 261 * Return a JsonWriter for this ResearchLog. It is initialized the first time this method is 262 * called. The cached value is returned in future calls. 263 * 264 * @throws IOException if opening the JsonWriter is not possible 265 */ 266 public JsonWriter getInitializedJsonWriterLocked() throws IOException { 267 if (mJsonWriter != null) return mJsonWriter; 268 if (mFile == null) throw new FileNotFoundException(); 269 try { 270 final JsonWriter jsonWriter = createJsonWriter(mContext, mFile); 271 if (jsonWriter == null) throw new IOException("Could not create JsonWriter"); 272 273 jsonWriter.beginArray(); 274 mJsonWriter = jsonWriter; 275 mHasWrittenData = true; 276 return mJsonWriter; 277 } catch (final IOException e) { 278 if (DEBUG) { 279 Log.w(TAG, "Exception when creating JsonWriter", e); 280 Log.w(TAG, "Closing JsonWriter"); 281 } 282 if (mJsonWriter != null) mJsonWriter.close(); 283 mJsonWriter = null; 284 throw e; 285 } 286 } 287 288 /** 289 * Create the JsonWriter to write the ResearchLog to. 290 * 291 * This method may be overriden in testing to redirect the output. 292 */ 293 /* package for test */ JsonWriter createJsonWriter(final Context context, final File file) 294 throws IOException { 295 return new JsonWriter(new BufferedWriter(new OutputStreamWriter( 296 context.openFileOutput(file.getName(), Context.MODE_PRIVATE)))); 297 } 298 } 299