@@ -8,6 +8,8 @@ import android.media.MediaRecorder
8
8
import android.net.Uri
9
9
import android.os.Build
10
10
import android.os.ParcelFileDescriptor
11
+ import android.system.Os
12
+ import android.system.OsConstants
11
13
import android.telecom.Call
12
14
import android.telecom.PhoneAccount
13
15
import android.util.Log
@@ -43,7 +45,10 @@ class RecorderThread(
43
45
private val context : Context ,
44
46
private val listener : OnRecordingCompletedListener ,
45
47
call : Call ,
46
- ): Thread() {
48
+ ) : Thread(RecorderThread : :class.java.simpleName) {
49
+ private val tag = " ${RecorderThread ::class .java.simpleName} /${id} "
50
+ private val isDebug = BuildConfig .DEBUG || Preferences .isDebugMode(context)
51
+
47
52
// Thread state
48
53
@Volatile private var isCancelled = false
49
54
private var captureFailed = false
@@ -59,7 +64,7 @@ class RecorderThread(
59
64
private val sampleRate = SampleRates .fromPreferences(context)
60
65
61
66
init {
62
- logI( " Created thread for call: $call " )
67
+ Log .i(tag, " Created thread for call: $call " )
63
68
64
69
onCallDetailsChanged(call.details)
65
70
@@ -68,34 +73,12 @@ class RecorderThread(
68
73
formatParam = savedFormat.second
69
74
}
70
75
71
- private fun logD (msg : String ) {
72
- Log .d(TAG , " [${id} ] $msg " )
73
- }
74
-
75
- private fun logE (msg : String , throwable : Throwable ) {
76
- Log .e(TAG , " [${id} ] $msg " , throwable)
77
- }
78
-
79
- private fun logE (msg : String ) {
80
- Log .e(TAG , " [${id} ] $msg " )
81
- }
82
-
83
- private fun logI (msg : String ) {
84
- Log .i(TAG , " [${id} ] $msg " )
85
- }
86
-
87
- private fun logW (msg : String ) {
88
- Log .w(TAG , " [${id} ] $msg " )
89
- }
90
-
91
76
fun redact (msg : String ): String {
92
77
synchronized(filenameLock) {
93
78
var result = msg
94
79
95
80
for ((source, target) in redactions) {
96
- result = result
97
- .replace(Uri .encode(source), target)
98
- .replace(source, target)
81
+ result = result.replace(source, target)
99
82
}
100
83
101
84
return result
@@ -153,7 +136,7 @@ class RecorderThread(
153
136
// directory traversal attacks.
154
137
.replace(' /' , ' _' ).trim()
155
138
156
- logI( " Updated filename due to call details change: ${redact(filename)} " )
139
+ Log .i(tag, " Updated filename due to call details change: ${redact(filename)} " )
157
140
}
158
141
}
159
142
@@ -163,14 +146,14 @@ class RecorderThread(
163
146
var resultUri: Uri ? = null
164
147
165
148
try {
166
- logI( " Recording thread started" )
149
+ Log .i(tag, " Recording thread started" )
167
150
168
151
if (isCancelled) {
169
- logI( " Recording cancelled before it began" )
152
+ Log .i(tag, " Recording cancelled before it began" )
170
153
} else {
171
154
val initialFilename = synchronized(filenameLock) { filename }
172
155
173
- val (file, pfd) = openOutputFile(initialFilename)
156
+ val (file, pfd) = openOutputFile(initialFilename, format.mimeTypeContainer )
174
157
resultUri = file.uri
175
158
176
159
pfd.use {
@@ -179,22 +162,31 @@ class RecorderThread(
179
162
180
163
val finalFilename = synchronized(filenameLock) { filename }
181
164
if (finalFilename != initialFilename) {
182
- logI( " Renaming ${redact(initialFilename)} to ${redact(finalFilename)} " )
165
+ Log .i(tag, " Renaming ${redact(initialFilename)} to ${redact(finalFilename)} " )
183
166
184
167
if (file.renameTo(finalFilename)) {
185
168
resultUri = file.uri
186
169
} else {
187
- logW( " Failed to rename to final filename: ${redact(finalFilename)} " )
170
+ Log .w(tag, " Failed to rename to final filename: ${redact(finalFilename)} " )
188
171
}
189
172
}
190
173
191
174
success = ! captureFailed
192
175
}
193
176
} catch (e: Exception ) {
194
- logE( " Error during recording" , e)
177
+ Log .e(tag, " Error during recording" , e)
195
178
errorMsg = e.localizedMessage
196
179
} finally {
197
- logI(" Recording thread completed" )
180
+ Log .i(tag, " Recording thread completed" )
181
+
182
+ try {
183
+ if (isDebug) {
184
+ Log .d(tag, " Dumping logcat due to debug mode" )
185
+ dumpLogcat()
186
+ }
187
+ } catch (e: Exception ) {
188
+ Log .w(tag, " Failed to dump logcat" , e)
189
+ }
198
190
199
191
if (success) {
200
192
listener.onRecordingCompleted(this , resultUri!! )
@@ -217,40 +209,60 @@ class RecorderThread(
217
209
isCancelled = true
218
210
}
219
211
212
+ private fun dumpLogcat () {
213
+ openOutputFile(" ${filename} .log" , " text/plain" ).pfd.use {
214
+ Os .lseek(it.fileDescriptor, 0 , OsConstants .SEEK_END )
215
+
216
+ val process = ProcessBuilder (" logcat" , " -d" ).start()
217
+ try {
218
+ val data = process.inputStream.use { stream -> stream.readBytes() }
219
+ Os .write(it.fileDescriptor, data, 0 , data.size)
220
+ } finally {
221
+ process.waitFor()
222
+ }
223
+ }
224
+ }
225
+
220
226
data class OutputFile (val file : DocumentFile , val pfd : ParcelFileDescriptor )
221
227
222
228
/* *
223
229
* Try to create and open a new output file in the user-chosen directory if possible and fall
224
- * back to the default output directory if not. [name] should not contain a file extension.
230
+ * back to the default output directory if not. [name] should not contain a file extension. The
231
+ * file extension is automatically determined from [mimeType].
225
232
*
226
233
* @throws IOException if the file could not be created in either directory
227
234
*/
228
- private fun openOutputFile (name : String ): OutputFile {
235
+ private fun openOutputFile (name : String , mimeType : String ): OutputFile {
229
236
val userUri = Preferences .getSavedOutputDir(context)
230
237
if (userUri != null ) {
231
238
try {
232
239
// Only returns null on API <21
233
240
val userDir = DocumentFile .fromTreeUri(context, userUri)
234
- return openOutputFileInDir(userDir!! , name)
241
+ return openOutputFileInDir(userDir!! , name, mimeType )
235
242
} catch (e: Exception ) {
236
- logE( " Failed to open file in user-specified directory: $userUri " , e)
243
+ Log .e(tag, " Failed to open file in user-specified directory: $userUri " , e)
237
244
}
238
245
}
239
246
240
247
val fallbackDir = DocumentFile .fromFile(Preferences .getDefaultOutputDir(context))
241
- logD( " Using fallback directory: ${fallbackDir.uri} " )
248
+ Log .d(tag, " Using fallback directory: ${fallbackDir.uri} " )
242
249
243
- return openOutputFileInDir(fallbackDir, name)
250
+ return openOutputFileInDir(fallbackDir, name, mimeType )
244
251
}
245
252
246
253
/* *
247
254
* Create and open a new output file with name [name] inside [directory]. [name] should not
248
- * contain a file extension. The file extension is automatically determined from [format].
255
+ * contain a file extension. The extension is determined [mimeType]. The file extension is
256
+ * automatically determined from [format].
249
257
*
250
258
* @throws IOException if file creation or opening fails
251
259
*/
252
- private fun openOutputFileInDir (directory : DocumentFile , name : String ): OutputFile {
253
- val file = directory.createFile(format.mimeTypeContainer, name)
260
+ private fun openOutputFileInDir (
261
+ directory : DocumentFile ,
262
+ name : String ,
263
+ mimeType : String ,
264
+ ): OutputFile {
265
+ val file = directory.createFile(mimeType, name)
254
266
? : throw IOException (" Failed to create file in ${directory.uri} " )
255
267
val pfd = context.contentResolver.openFileDescriptor(file.uri, " rw" )
256
268
? : throw IOException (" Failed to open file at ${file.uri} " )
@@ -339,40 +351,59 @@ class RecorderThread(
339
351
if (bufSize < 0 ) {
340
352
throw Exception (" Failure when querying minimum buffer size: $bufSize " )
341
353
}
342
- logD( " AudioRecord buffer size: $bufSize " )
354
+ Log .d(tag, " AudioRecord buffer size: $bufSize " )
343
355
344
356
// Use a slightly larger buffer to reduce the chance of problems under load
345
357
val buffer = ByteBuffer .allocateDirect(bufSize * 2 )
358
+ val bufferFrames = buffer.capacity().toLong() / frameSize
359
+ val bufferNs = bufferFrames * 1_000_000_000L / audioRecord.sampleRate
346
360
347
361
while (! isCancelled) {
362
+ val begin = System .nanoTime()
348
363
val n = audioRecord.read(buffer, buffer.remaining())
364
+ val recordElapsed = System .nanoTime() - begin
365
+ var encodeElapsed = 0L
366
+
349
367
if (n < 0 ) {
350
- logE( " Error when reading samples from $audioRecord : $n " )
368
+ Log .e(tag, " Error when reading samples from $audioRecord : $n " )
351
369
isCancelled = true
352
370
captureFailed = true
353
371
} else if (n == 0 ) {
354
- logE( " Unexpected EOF from AudioRecord" )
372
+ Log .e(tag, " Unexpected EOF from AudioRecord" )
355
373
isCancelled = true
356
374
} else {
357
375
buffer.limit(n)
376
+
377
+ val encodeBegin = System .nanoTime()
358
378
encoder.encode(buffer, false )
379
+ encodeElapsed = System .nanoTime() - encodeBegin
380
+
359
381
buffer.clear()
360
382
361
383
numFrames + = n / frameSize
362
384
}
385
+
386
+ val totalElapsed = System .nanoTime() - begin
387
+ if (encodeElapsed > bufferNs) {
388
+ Log .w(tag, " Encoding took too long: " +
389
+ " timestamp=${numFrames.toDouble() / audioRecord.sampleRate} s, " +
390
+ " buffer=${bufferNs / 1_000_000.0 } ms, " +
391
+ " total=${totalElapsed / 1_000_000.0 } ms, " +
392
+ " record=${recordElapsed / 1_000_000.0 } ms, " +
393
+ " encode=${encodeElapsed / 1_000_000.0 } ms" )
394
+ }
363
395
}
364
396
365
397
// Signal EOF with empty buffer
366
- logD( " Sending EOF to encoder" )
398
+ Log .d(tag, " Sending EOF to encoder" )
367
399
buffer.limit(buffer.position())
368
400
encoder.encode(buffer, true )
369
401
370
402
val durationSecs = numFrames.toDouble() / audioRecord.sampleRate
371
- logD( " Input complete after ${" %.1f" .format(durationSecs)} s" )
403
+ Log .d(tag, " Input complete after ${" %.1f" .format(durationSecs)} s" )
372
404
}
373
405
374
406
companion object {
375
- private val TAG = RecorderThread ::class .java.simpleName
376
407
private const val CHANNEL_CONFIG = AudioFormat .CHANNEL_IN_MONO
377
408
private const val ENCODING = AudioFormat .ENCODING_PCM_16BIT
378
409
0 commit comments