| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.drrickorang.loopback; |
| |
| import android.content.Context; |
| import android.net.Uri; |
| import android.util.Log; |
| |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.io.PrintWriter; |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * Captures systrace, bugreport, and wav snippets. Capable of relieving capture requests from |
| * multiple threads and maintains queue of most interesting records |
| */ |
| public class CaptureHolder { |
| |
| private static final String TAG = "CAPTURE"; |
| public static final String STORAGE = "/sdcard/"; |
| public static final String DIRECTORY = STORAGE + "Loopback"; |
| private static final String SIGNAL_FILE = DIRECTORY + "/loopback_signal"; |
| // These suffixes are used to tell the listener script what types of data to collect. |
| // They MUST match the definitions in the script file. |
| private static final String SYSTRACE_SUFFIX = ".trace"; |
| private static final String BUGREPORT_SUFFIX = "_bugreport.txt.gz"; |
| |
| private static final String WAV_SUFFIX = ".wav"; |
| private static final String TERMINATE_SIGNAL = "QUIT"; |
| |
| // Status codes returned by captureState |
| public static final int NEW_CAPTURE_IS_LEAST_INTERESTING = -1; |
| public static final int CAPTURE_ALREADY_IN_PROGRESS = 0; |
| public static final int STATE_CAPTURED = 1; |
| public static final int CAPTURING_DISABLED = 2; |
| |
| private final String mFileNamePrefix; |
| private final long mStartTimeMS; |
| private final boolean mIsCapturingWavs; |
| private final boolean mIsCapturingSystraces; |
| private final boolean mIsCapturingBugreports; |
| private final int mCaptureCapacity; |
| private CaptureThread mCaptureThread; |
| private final CapturedState mCapturedStates[]; |
| private WaveDataRingBuffer mWaveDataBuffer; |
| |
| //for creating AudioFileOutput objects |
| private final Context mContext; |
| private final int mSamplingRate; |
| |
| public CaptureHolder(int captureCapacity, String fileNamePrefix, boolean captureWavs, |
| boolean captureSystraces, boolean captureBugreports, Context context, |
| int samplingRate) { |
| mCaptureCapacity = captureCapacity; |
| mFileNamePrefix = fileNamePrefix; |
| mIsCapturingWavs = captureWavs; |
| mIsCapturingSystraces = captureSystraces; |
| mIsCapturingBugreports = captureBugreports; |
| mStartTimeMS = System.currentTimeMillis(); |
| mCapturedStates = new CapturedState[mCaptureCapacity]; |
| mContext = context; |
| mSamplingRate = samplingRate; |
| } |
| |
| public void setWaveDataBuffer(WaveDataRingBuffer waveDataBuffer) { |
| mWaveDataBuffer = waveDataBuffer; |
| } |
| |
| /** |
| * Launch thread to capture a systrace/bugreport and/or wav snippets and insert into collection |
| * If capturing is not enabled or capture state thread is already running returns immediately |
| * If newly requested capture is determined to be less interesting than all previous captures |
| * returns without running capture thread |
| * |
| * Can be called from both GlitchDetectionThread and Sles/Java buffer callbacks. |
| * Rank parameter and time of capture can be used by getIndexOfLeastInterestingCapture to |
| * determine which records to delete when at capacity. |
| * Therefore rank could represent glitchiness or callback behaviour and comparisons will need to |
| * be adjusted based on testing priorities |
| * |
| * Please note if calling from audio thread could cause glitches to occur because of blocking on |
| * this synchronized method. Additionally capturing a systrace and bugreport and writing to |
| * disk will likely have an affect on audio performance. |
| */ |
| public synchronized int captureState(int rank) { |
| |
| if (!isCapturing()) { |
| Log.d(TAG, "captureState: Capturing state not enabled"); |
| return CAPTURING_DISABLED; |
| } |
| |
| if (mCaptureThread != null && mCaptureThread.getState() != Thread.State.TERMINATED) { |
| // Capture already in progress |
| Log.d(TAG, "captureState: Capture thread already running"); |
| mCaptureThread.updateRank(rank); |
| return CAPTURE_ALREADY_IN_PROGRESS; |
| } |
| |
| long timeFromTestStartMS = System.currentTimeMillis() - mStartTimeMS; |
| long hours = TimeUnit.MILLISECONDS.toHours(timeFromTestStartMS); |
| long minutes = TimeUnit.MILLISECONDS.toMinutes(timeFromTestStartMS) - |
| TimeUnit.HOURS.toMinutes(TimeUnit.MILLISECONDS.toHours(timeFromTestStartMS)); |
| long seconds = TimeUnit.MILLISECONDS.toSeconds(timeFromTestStartMS) - |
| TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(timeFromTestStartMS)); |
| String timeString = String.format("%02dh%02dm%02ds", hours, minutes, seconds); |
| |
| String fileNameBase = STORAGE + mFileNamePrefix + '_' + timeString; |
| CapturedState cs = new CapturedState(fileNameBase, timeFromTestStartMS, rank); |
| |
| int indexOfLeastInteresting = getIndexOfLeastInterestingCapture(cs); |
| if (indexOfLeastInteresting == NEW_CAPTURE_IS_LEAST_INTERESTING) { |
| Log.d(TAG, "captureState: All Previously captured states were more interesting than" + |
| " requested capture"); |
| return NEW_CAPTURE_IS_LEAST_INTERESTING; |
| } |
| |
| mCaptureThread = new CaptureThread(cs, indexOfLeastInteresting); |
| mCaptureThread.start(); |
| |
| return STATE_CAPTURED; |
| } |
| |
| /** |
| * Send signal to listener script to terminate and stop atrace |
| **/ |
| public void stopLoopbackListenerScript() { |
| if (mCaptureThread == null || !mCaptureThread.stopLoopbackListenerScript()) { |
| // The capture thread is unable to execute this operation. |
| stopLoopbackListenerScriptImpl(); |
| } |
| } |
| |
| static void stopLoopbackListenerScriptImpl() { |
| try { |
| OutputStream outputStream = new FileOutputStream(SIGNAL_FILE); |
| outputStream.write(TERMINATE_SIGNAL.getBytes()); |
| outputStream.close(); |
| } catch (IOException e) { |
| e.printStackTrace(); |
| } |
| |
| Log.d(TAG, "stopLoopbackListenerScript: Signaled Listener Script to exit"); |
| } |
| |
| /** |
| * Currently returns recorded state with lowest Glitch count |
| * Alternate criteria can be established here and in captureState rank parameter |
| * |
| * returns -1 (NEW_CAPTURE_IS_LEAST_INTERESTING) if candidate is least interesting, otherwise |
| * returns index of record to replace |
| */ |
| private int getIndexOfLeastInterestingCapture(CapturedState candidateCS) { |
| CapturedState leastInteresting = candidateCS; |
| int index = NEW_CAPTURE_IS_LEAST_INTERESTING; |
| for (int i = 0; i < mCapturedStates.length; i++) { |
| if (mCapturedStates[i] == null) { |
| // Array is not yet at capacity, insert in next available position |
| return i; |
| } |
| if (mCapturedStates[i].rank < leastInteresting.rank) { |
| index = i; |
| leastInteresting = mCapturedStates[i]; |
| } |
| } |
| return index; |
| } |
| |
| public boolean isCapturing() { |
| return mIsCapturingWavs || mIsCapturingSystraces || mIsCapturingBugreports; |
| } |
| |
| /** |
| * Data struct for filenames of previously captured results. Rank and time captured can be used |
| * for determining position in rolling queue |
| */ |
| private class CapturedState { |
| public final String fileNameBase; |
| public final long timeFromStartOfTestMS; |
| public int rank; |
| |
| public CapturedState(String fileNameBase, long timeFromStartOfTestMS, int rank) { |
| this.fileNameBase = fileNameBase; |
| this.timeFromStartOfTestMS = timeFromStartOfTestMS; |
| this.rank = rank; |
| } |
| |
| @Override |
| public String toString() { |
| return "CapturedState { fileName:" + fileNameBase + ", Rank:" + rank + "}"; |
| } |
| } |
| |
| private class CaptureThread extends Thread { |
| |
| private CapturedState mNewCapturedState; |
| private int mIndexToPlace; |
| private boolean mIsRunning; |
| private boolean mSignalScriptToQuit; |
| |
| /** |
| * Create new thread with capture state struct for captured systrace, bugreport and wav |
| **/ |
| public CaptureThread(CapturedState cs, int indexToPlace) { |
| mNewCapturedState = cs; |
| mIndexToPlace = indexToPlace; |
| setName("CaptureThread"); |
| setPriority(Thread.MIN_PRIORITY); |
| } |
| |
| @Override |
| public void run() { |
| synchronized (this) { |
| mIsRunning = true; |
| } |
| |
| // Write names of desired captures to signal file, signalling |
| // the listener script to write systrace and/or bugreport to those files |
| if (mIsCapturingSystraces || mIsCapturingBugreports) { |
| Log.d(TAG, "CaptureThread: signaling listener to write to:" + |
| mNewCapturedState.fileNameBase + "*"); |
| try { |
| PrintWriter writer = new PrintWriter(SIGNAL_FILE); |
| // mNewCapturedState.fileNameBase is the path and basename of the state files. |
| // Each suffix is used to tell the listener script to record that type of data. |
| if (mIsCapturingSystraces) { |
| writer.println(mNewCapturedState.fileNameBase + SYSTRACE_SUFFIX); |
| } |
| if (mIsCapturingBugreports) { |
| writer.println(mNewCapturedState.fileNameBase + BUGREPORT_SUFFIX); |
| } |
| writer.close(); |
| } catch (IOException e) { |
| e.printStackTrace(); |
| } |
| } |
| |
| // Write wav if member mWaveDataBuffer has been set |
| if (mIsCapturingWavs && mWaveDataBuffer != null) { |
| Log.d(TAG, "CaptureThread: begin Writing wav data to file"); |
| WaveDataRingBuffer.ReadableWaveDeck deck = mWaveDataBuffer.getWaveDeck(); |
| if (deck != null) { |
| AudioFileOutput audioFile = new AudioFileOutput(mContext, |
| Uri.parse("file://mnt" + mNewCapturedState.fileNameBase |
| + WAV_SUFFIX), |
| mSamplingRate); |
| boolean success = deck.writeToFile(audioFile); |
| Log.d(TAG, "CaptureThread: wav data written successfully: " + success); |
| } |
| } |
| |
| // Check for sys and bug finished |
| // loopback listener script signals completion by deleting signal file |
| if (mIsCapturingSystraces || mIsCapturingBugreports) { |
| File signalFile = new File(SIGNAL_FILE); |
| while (signalFile.exists()) { |
| try { |
| sleep(100); |
| } catch (InterruptedException e) { |
| e.printStackTrace(); |
| } |
| } |
| } |
| |
| // Delete least interesting if necessary and insert new capture in list |
| String suffixes[] = {SYSTRACE_SUFFIX, BUGREPORT_SUFFIX, WAV_SUFFIX}; |
| if (mCapturedStates[mIndexToPlace] != null) { |
| Log.d(TAG, "Deleting capture: " + mCapturedStates[mIndexToPlace]); |
| for (String suffix : suffixes) { |
| File oldFile = new File(mCapturedStates[mIndexToPlace].fileNameBase + suffix); |
| boolean deleted = oldFile.delete(); |
| if (!deleted) { |
| Log.d(TAG, "Delete old capture: " + oldFile.toString() + |
| (oldFile.exists() ? " unable to delete" : " was not present")); |
| } |
| } |
| } |
| Log.d(TAG, "Adding capture to list: " + mNewCapturedState); |
| mCapturedStates[mIndexToPlace] = mNewCapturedState; |
| |
| // Log captured states |
| String log = "Captured states:"; |
| for (CapturedState cs:mCapturedStates) log += "\n...." + cs; |
| Log.d(TAG, log); |
| |
| synchronized (this) { |
| if (mSignalScriptToQuit) { |
| CaptureHolder.stopLoopbackListenerScriptImpl(); |
| mSignalScriptToQuit = false; |
| } |
| mIsRunning = false; |
| } |
| Log.d(TAG, "Completed capture thread terminating"); |
| } |
| |
| // Sets the rank of the current capture to rank if it is greater than the current value |
| public synchronized void updateRank(int rank) { |
| mNewCapturedState.rank = Math.max(mNewCapturedState.rank, rank); |
| } |
| |
| public synchronized boolean stopLoopbackListenerScript() { |
| if (mIsRunning) { |
| mSignalScriptToQuit = true; |
| return true; |
| } else { |
| return false; |
| } |
| } |
| } |
| } |