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