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.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