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