Home | History | Annotate | Download | only in loopback
      1 /*
      2  * Copyright (C) 2016 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 org.drrickorang.loopback;
     18 
     19 import android.content.Context;
     20 import android.net.Uri;
     21 import android.util.Log;
     22 
     23 import java.io.File;
     24 import java.io.FileOutputStream;
     25 import java.io.IOException;
     26 import java.io.OutputStream;
     27 import java.io.PrintWriter;
     28 import java.util.concurrent.TimeUnit;
     29 
     30 /**
     31  * Captures systrace, bugreport, and wav snippets. Capable of relieving capture requests from
     32  * multiple threads and maintains queue of most interesting records
     33  */
     34 public class CaptureHolder {
     35 
     36     private static final String TAG = "CAPTURE";
     37     public static final String STORAGE = "/sdcard/";
     38     public static final String DIRECTORY = STORAGE + "Loopback";
     39     private static final String SIGNAL_FILE = DIRECTORY + "/loopback_signal";
     40     // These suffixes are used to tell the listener script what types of data to collect.
     41     // They MUST match the definitions in the script file.
     42     private static final String SYSTRACE_SUFFIX = ".trace";
     43     private static final String BUGREPORT_SUFFIX = "_bugreport.txt.gz";
     44 
     45     private static final String WAV_SUFFIX = ".wav";
     46     private static final String TERMINATE_SIGNAL = "QUIT";
     47 
     48     // Status codes returned by captureState
     49     public static final int NEW_CAPTURE_IS_LEAST_INTERESTING = -1;
     50     public static final int CAPTURE_ALREADY_IN_PROGRESS = 0;
     51     public static final int STATE_CAPTURED = 1;
     52     public static final int CAPTURING_DISABLED = 2;
     53 
     54     private final String mFileNamePrefix;
     55     private final long mStartTimeMS;
     56     private final boolean mIsCapturingWavs;
     57     private final boolean mIsCapturingSystraces;
     58     private final boolean mIsCapturingBugreports;
     59     private final int mCaptureCapacity;
     60     private CaptureThread mCaptureThread;
     61     private volatile CapturedState mCapturedStates[];
     62     private WaveDataRingBuffer mWaveDataBuffer;
     63 
     64     //for creating AudioFileOutput objects
     65     private final Context mContext;
     66     private final int mSamplingRate;
     67 
     68     public CaptureHolder(int captureCapacity, String fileNamePrefix, boolean captureWavs,
     69                          boolean captureSystraces, boolean captureBugreports, Context context,
     70                          int samplingRate) {
     71         mCaptureCapacity = captureCapacity;
     72         mFileNamePrefix = fileNamePrefix;
     73         mIsCapturingWavs = captureWavs;
     74         mIsCapturingSystraces = captureSystraces;
     75         mIsCapturingBugreports = captureBugreports;
     76         mStartTimeMS = System.currentTimeMillis();
     77         mCapturedStates = new CapturedState[mCaptureCapacity];
     78         mContext = context;
     79         mSamplingRate = samplingRate;
     80     }
     81 
     82     public void setWaveDataBuffer(WaveDataRingBuffer waveDataBuffer) {
     83         mWaveDataBuffer = waveDataBuffer;
     84     }
     85 
     86     /**
     87      * Launch thread to capture a systrace/bugreport and/or wav snippets and insert into collection
     88      * If capturing is not enabled or capture state thread is already running returns immediately
     89      * If newly requested capture is determined to be less interesting than all previous captures
     90      * returns without running capture thread
     91      *
     92      * Can be called from both GlitchDetectionThread and Sles/Java buffer callbacks.
     93      * Rank parameter and time of capture can be used by getIndexOfLeastInterestingCapture to
     94      * determine which records to delete when at capacity.
     95      * Therefore rank could represent glitchiness or callback behaviour and comparisons will need to
     96      * be adjusted based on testing priorities
     97      *
     98      * Please note if calling from audio thread could cause glitches to occur because of blocking on
     99      * this synchronized method.  Additionally capturing a systrace and bugreport and writing to
    100      * disk will likely have an affect on audio performance.
    101      */
    102     public synchronized int captureState(int rank) {
    103 
    104         if (!isCapturing()) {
    105             Log.d(TAG, "captureState: Capturing state not enabled");
    106             return CAPTURING_DISABLED;
    107         }
    108 
    109         if (mCaptureThread != null && mCaptureThread.getState() != Thread.State.TERMINATED) {
    110             // Capture already in progress
    111             Log.d(TAG, "captureState: Capture thread already running");
    112             mCaptureThread.updateRank(rank);
    113             return CAPTURE_ALREADY_IN_PROGRESS;
    114         }
    115 
    116         long timeFromTestStartMS = System.currentTimeMillis() - mStartTimeMS;
    117         long hours = TimeUnit.MILLISECONDS.toHours(timeFromTestStartMS);
    118         long minutes = TimeUnit.MILLISECONDS.toMinutes(timeFromTestStartMS) -
    119                 TimeUnit.HOURS.toMinutes(TimeUnit.MILLISECONDS.toHours(timeFromTestStartMS));
    120         long seconds = TimeUnit.MILLISECONDS.toSeconds(timeFromTestStartMS) -
    121                 TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(timeFromTestStartMS));
    122         String timeString = String.format("%02dh%02dm%02ds", hours, minutes, seconds);
    123 
    124         String fileNameBase = STORAGE + mFileNamePrefix + '_' + timeString;
    125         CapturedState cs = new CapturedState(fileNameBase, timeFromTestStartMS, rank);
    126 
    127         int indexOfLeastInteresting = getIndexOfLeastInterestingCapture(cs);
    128         if (indexOfLeastInteresting == NEW_CAPTURE_IS_LEAST_INTERESTING) {
    129             Log.d(TAG, "captureState: All Previously captured states were more interesting than" +
    130                     " requested capture");
    131             return NEW_CAPTURE_IS_LEAST_INTERESTING;
    132         }
    133 
    134         mCaptureThread = new CaptureThread(cs, indexOfLeastInteresting);
    135         mCaptureThread.start();
    136 
    137         return STATE_CAPTURED;
    138     }
    139 
    140     /**
    141      * Send signal to listener script to terminate and stop atrace
    142      **/
    143     public void stopLoopbackListenerScript() {
    144         if (mCaptureThread == null || !mCaptureThread.stopLoopbackListenerScript()) {
    145             // The capture thread is unable to execute this operation.
    146             stopLoopbackListenerScriptImpl();
    147         }
    148     }
    149 
    150     static void stopLoopbackListenerScriptImpl() {
    151         try {
    152             OutputStream outputStream = new FileOutputStream(SIGNAL_FILE);
    153             outputStream.write(TERMINATE_SIGNAL.getBytes());
    154             outputStream.close();
    155         } catch (IOException e) {
    156             e.printStackTrace();
    157         }
    158 
    159         Log.d(TAG, "stopLoopbackListenerScript: Signaled Listener Script to exit");
    160     }
    161 
    162     /**
    163      * Currently returns recorded state with lowest Glitch count
    164      * Alternate criteria can be established here and in captureState rank parameter
    165      *
    166      * returns -1 (NEW_CAPTURE_IS_LEAST_INTERESTING) if candidate is least interesting, otherwise
    167      * returns index of record to replace
    168      */
    169     private int getIndexOfLeastInterestingCapture(CapturedState candidateCS) {
    170         CapturedState leastInteresting = candidateCS;
    171         int index = NEW_CAPTURE_IS_LEAST_INTERESTING;
    172         for (int i = 0; i < mCapturedStates.length; i++) {
    173             if (mCapturedStates[i] == null) {
    174                 // Array is not yet at capacity, insert in next available position
    175                 return i;
    176             }
    177             if (mCapturedStates[i].rank < leastInteresting.rank) {
    178                 index = i;
    179                 leastInteresting = mCapturedStates[i];
    180             }
    181         }
    182         return index;
    183     }
    184 
    185     public boolean isCapturing() {
    186         return mIsCapturingWavs || mIsCapturingSystraces || mIsCapturingBugreports;
    187     }
    188 
    189     /**
    190      * Data struct for filenames of previously captured results. Rank and time captured can be used
    191      * for determining position in rolling queue
    192      */
    193     private class CapturedState {
    194         public final String fileNameBase;
    195         public final long timeFromStartOfTestMS;
    196         public int rank;
    197 
    198         public CapturedState(String fileNameBase, long timeFromStartOfTestMS, int rank) {
    199             this.fileNameBase = fileNameBase;
    200             this.timeFromStartOfTestMS = timeFromStartOfTestMS;
    201             this.rank = rank;
    202         }
    203 
    204         @Override
    205         public String toString() {
    206             return "CapturedState { fileName:" + fileNameBase + ", Rank:" + rank + "}";
    207         }
    208     }
    209 
    210     private class CaptureThread extends Thread {
    211 
    212         private CapturedState mNewCapturedState;
    213         private int mIndexToPlace;
    214         private boolean mIsRunning;
    215         private boolean mSignalScriptToQuit;
    216 
    217         /**
    218          * Create new thread with capture state struct for captured systrace, bugreport and wav
    219          **/
    220         public CaptureThread(CapturedState cs, int indexToPlace) {
    221             mNewCapturedState = cs;
    222             mIndexToPlace = indexToPlace;
    223             setName("CaptureThread");
    224             setPriority(Thread.MIN_PRIORITY);
    225         }
    226 
    227         @Override
    228         public void run() {
    229             synchronized (this) {
    230                 mIsRunning = true;
    231             }
    232 
    233             // Write names of desired captures to signal file, signalling
    234             // the listener script to write systrace and/or bugreport to those files
    235             if (mIsCapturingSystraces || mIsCapturingBugreports) {
    236                 Log.d(TAG, "CaptureThread: signaling listener to write to:" +
    237                         mNewCapturedState.fileNameBase + "*");
    238                 try {
    239                     PrintWriter writer = new PrintWriter(SIGNAL_FILE);
    240                     // mNewCapturedState.fileNameBase is the path and basename of the state files.
    241                     // Each suffix is used to tell the listener script to record that type of data.
    242                     if (mIsCapturingSystraces) {
    243                         writer.println(mNewCapturedState.fileNameBase + SYSTRACE_SUFFIX);
    244                     }
    245                     if (mIsCapturingBugreports) {
    246                         writer.println(mNewCapturedState.fileNameBase + BUGREPORT_SUFFIX);
    247                     }
    248                     writer.close();
    249                 } catch (IOException e) {
    250                     e.printStackTrace();
    251                 }
    252             }
    253 
    254             // Write wav if member mWaveDataBuffer has been set
    255             if (mIsCapturingWavs && mWaveDataBuffer != null) {
    256                 Log.d(TAG, "CaptureThread: begin Writing wav data to file");
    257                 WaveDataRingBuffer.ReadableWaveDeck deck = mWaveDataBuffer.getWaveDeck();
    258                 if (deck != null) {
    259                     AudioFileOutput audioFile = new AudioFileOutput(mContext,
    260                             Uri.parse("file://mnt" + mNewCapturedState.fileNameBase
    261                                     + WAV_SUFFIX),
    262                             mSamplingRate);
    263                     boolean success = deck.writeToFile(audioFile);
    264                     Log.d(TAG, "CaptureThread: wav data written successfully: " + success);
    265                 }
    266             }
    267 
    268             // Check for sys and bug finished
    269             // loopback listener script signals completion by deleting signal file
    270             if (mIsCapturingSystraces || mIsCapturingBugreports) {
    271                 File signalFile = new File(SIGNAL_FILE);
    272                 while (signalFile.exists()) {
    273                     try {
    274                         sleep(100);
    275                     } catch (InterruptedException e) {
    276                         e.printStackTrace();
    277                     }
    278                 }
    279             }
    280 
    281             // Delete least interesting if necessary and insert new capture in list
    282             String suffixes[] = {SYSTRACE_SUFFIX, BUGREPORT_SUFFIX, WAV_SUFFIX};
    283             if (mCapturedStates[mIndexToPlace] != null) {
    284                 Log.d(TAG, "Deleting capture: " + mCapturedStates[mIndexToPlace]);
    285                 for (String suffix : suffixes) {
    286                     File oldFile = new File(mCapturedStates[mIndexToPlace].fileNameBase + suffix);
    287                     boolean deleted = oldFile.delete();
    288                     if (!deleted) {
    289                         Log.d(TAG, "Delete old capture: " + oldFile.toString() +
    290                                 (oldFile.exists() ? " unable to delete" : " was not present"));
    291                     }
    292                 }
    293             }
    294             Log.d(TAG, "Adding capture to list: " + mNewCapturedState);
    295             mCapturedStates[mIndexToPlace] = mNewCapturedState;
    296 
    297             // Log captured states
    298             String log = "Captured states:";
    299             for (CapturedState cs:mCapturedStates) log += "\n...." + cs;
    300             Log.d(TAG, log);
    301 
    302             synchronized (this) {
    303                 if (mSignalScriptToQuit) {
    304                     CaptureHolder.stopLoopbackListenerScriptImpl();
    305                     mSignalScriptToQuit = false;
    306                 }
    307                 mIsRunning = false;
    308             }
    309             Log.d(TAG, "Completed capture thread terminating");
    310         }
    311 
    312         // Sets the rank of the current capture to rank if it is greater than the current value
    313         public synchronized void updateRank(int rank) {
    314             mNewCapturedState.rank = Math.max(mNewCapturedState.rank, rank);
    315         }
    316 
    317         public synchronized boolean stopLoopbackListenerScript() {
    318             if (mIsRunning) {
    319                 mSignalScriptToQuit = true;
    320                 return true;
    321             } else {
    322                 return false;
    323             }
    324         }
    325     }
    326 }
    327