Home | History | Annotate | Download | only in research
      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 
     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      */
     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      */
    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      */
    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      */
    183     public void blockingAbort(final long timeout) {
    184         abort(null);
    185         awaitTermination(timeout, TimeUnit.MILLISECONDS);
    186     }
    187 
    188     @UsedForTesting
    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 
    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 
    214     private void removeAnyScheduledFlush() {
    215         if (mFlushFuture != null) {
    216             mFlushFuture.cancel(false);
    217             mFlushFuture = null;
    218         }
    219     }
    220 
    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      */
    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      */
    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      */
    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