summaryrefslogtreecommitdiff
path: root/LoopbackApp/app/src/main/java/org/drrickorang/loopback/CaptureHolder.java
blob: 99143f298b8a04f669e790485b7b558d9fd5135f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
/*
 * 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 volatile 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;
            }
        }
    }
}