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