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 final CapturedState mCapturedStates[];
62     private WaveDataRingBuffer mWaveDataBuffer;
63 
64     //for creating AudioFileOutput objects
65     private final Context mContext;
66     private final int mSamplingRate;
67 
CaptureHolder(int captureCapacity, String fileNamePrefix, boolean captureWavs, boolean captureSystraces, boolean captureBugreports, Context context, int samplingRate)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 
setWaveDataBuffer(WaveDataRingBuffer waveDataBuffer)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      */
captureState(int rank)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      **/
stopLoopbackListenerScript()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 
stopLoopbackListenerScriptImpl()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      */
getIndexOfLeastInterestingCapture(CapturedState candidateCS)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 
isCapturing()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 
CapturedState(String fileNameBase, long timeFromStartOfTestMS, int rank)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
toString()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          **/
CaptureThread(CapturedState cs, int indexToPlace)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
run()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
updateRank(int rank)313         public synchronized void updateRank(int rank) {
314             mNewCapturedState.rank = Math.max(mNewCapturedState.rank, rank);
315         }
316 
stopLoopbackListenerScript()317         public synchronized boolean stopLoopbackListenerScript() {
318             if (mIsRunning) {
319                 mSignalScriptToQuit = true;
320                 return true;
321             } else {
322                 return false;
323             }
324         }
325     }
326 }
327