Skip to content

Commit acf0117

Browse files
authored
Merge pull request #65 from chenxiaolong/debug
Add hidden debug mode for release builds
2 parents 27922b8 + 087ac13 commit acf0117

File tree

5 files changed

+115
-53
lines changed

5 files changed

+115
-53
lines changed

app/src/main/java/com/chiller3/bcr/Preferences.kt

+13
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,23 @@ object Preferences {
1414
const val PREF_VERSION = "version"
1515

1616
// Not associated with a UI preference
17+
private const val PREF_DEBUG_MODE = "debug_mode"
1718
private const val PREF_FORMAT_NAME = "codec_name"
1819
private const val PREF_FORMAT_PARAM_PREFIX = "codec_param_"
1920
const val PREF_SAMPLE_RATE = "sample_rate"
2021

22+
fun isDebugMode(context: Context): Boolean {
23+
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
24+
return prefs.getBoolean(PREF_DEBUG_MODE, false)
25+
}
26+
27+
fun setDebugMode(context: Context, enabled: Boolean) {
28+
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
29+
val editor = prefs.edit()
30+
editor.putBoolean(PREF_DEBUG_MODE, enabled)
31+
editor.apply()
32+
}
33+
2134
/**
2235
* Get the default output directory. The directory should always be writable and is suitable for
2336
* use as a fallback.

app/src/main/java/com/chiller3/bcr/RecorderInCallService.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -183,14 +183,16 @@ class RecorderInCallService : InCallService(), RecorderThread.OnRecordingComplet
183183
}
184184

185185
override fun onRecordingCompleted(thread: RecorderThread, uri: Uri) {
186-
Log.i(TAG, "Recording completed: ${thread.id}: ${thread.redact(uri.toString())}")
186+
val decoded = Uri.decode(uri.toString())
187+
Log.i(TAG, "Recording completed: ${thread.id}: ${thread.redact(decoded)}")
187188
handler.post {
188189
onThreadExited()
189190
}
190191
}
191192

192193
override fun onRecordingFailed(thread: RecorderThread, errorMsg: String?, uri: Uri?) {
193-
Log.w(TAG, "Recording failed: ${thread.id}: ${thread.redact(uri.toString())}")
194+
val decoded = Uri.decode(uri.toString())
195+
Log.w(TAG, "Recording failed: ${thread.id}: ${thread.redact(decoded)}")
194196
handler.post {
195197
onThreadExited()
196198

app/src/main/java/com/chiller3/bcr/RecorderThread.kt

+79-48
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import android.media.MediaRecorder
88
import android.net.Uri
99
import android.os.Build
1010
import android.os.ParcelFileDescriptor
11+
import android.system.Os
12+
import android.system.OsConstants
1113
import android.telecom.Call
1214
import android.telecom.PhoneAccount
1315
import android.util.Log
@@ -43,7 +45,10 @@ class RecorderThread(
4345
private val context: Context,
4446
private val listener: OnRecordingCompletedListener,
4547
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+
4752
// Thread state
4853
@Volatile private var isCancelled = false
4954
private var captureFailed = false
@@ -59,7 +64,7 @@ class RecorderThread(
5964
private val sampleRate = SampleRates.fromPreferences(context)
6065

6166
init {
62-
logI("Created thread for call: $call")
67+
Log.i(tag, "Created thread for call: $call")
6368

6469
onCallDetailsChanged(call.details)
6570

@@ -68,34 +73,12 @@ class RecorderThread(
6873
formatParam = savedFormat.second
6974
}
7075

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-
9176
fun redact(msg: String): String {
9277
synchronized(filenameLock) {
9378
var result = msg
9479

9580
for ((source, target) in redactions) {
96-
result = result
97-
.replace(Uri.encode(source), target)
98-
.replace(source, target)
81+
result = result.replace(source, target)
9982
}
10083

10184
return result
@@ -153,7 +136,7 @@ class RecorderThread(
153136
// directory traversal attacks.
154137
.replace('/', '_').trim()
155138

156-
logI("Updated filename due to call details change: ${redact(filename)}")
139+
Log.i(tag, "Updated filename due to call details change: ${redact(filename)}")
157140
}
158141
}
159142

@@ -163,14 +146,14 @@ class RecorderThread(
163146
var resultUri: Uri? = null
164147

165148
try {
166-
logI("Recording thread started")
149+
Log.i(tag, "Recording thread started")
167150

168151
if (isCancelled) {
169-
logI("Recording cancelled before it began")
152+
Log.i(tag, "Recording cancelled before it began")
170153
} else {
171154
val initialFilename = synchronized(filenameLock) { filename }
172155

173-
val (file, pfd) = openOutputFile(initialFilename)
156+
val (file, pfd) = openOutputFile(initialFilename, format.mimeTypeContainer)
174157
resultUri = file.uri
175158

176159
pfd.use {
@@ -179,22 +162,31 @@ class RecorderThread(
179162

180163
val finalFilename = synchronized(filenameLock) { filename }
181164
if (finalFilename != initialFilename) {
182-
logI("Renaming ${redact(initialFilename)} to ${redact(finalFilename)}")
165+
Log.i(tag, "Renaming ${redact(initialFilename)} to ${redact(finalFilename)}")
183166

184167
if (file.renameTo(finalFilename)) {
185168
resultUri = file.uri
186169
} else {
187-
logW("Failed to rename to final filename: ${redact(finalFilename)}")
170+
Log.w(tag, "Failed to rename to final filename: ${redact(finalFilename)}")
188171
}
189172
}
190173

191174
success = !captureFailed
192175
}
193176
} catch (e: Exception) {
194-
logE("Error during recording", e)
177+
Log.e(tag, "Error during recording", e)
195178
errorMsg = e.localizedMessage
196179
} 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+
}
198190

199191
if (success) {
200192
listener.onRecordingCompleted(this, resultUri!!)
@@ -217,40 +209,60 @@ class RecorderThread(
217209
isCancelled = true
218210
}
219211

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+
220226
data class OutputFile(val file: DocumentFile, val pfd: ParcelFileDescriptor)
221227

222228
/**
223229
* 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].
225232
*
226233
* @throws IOException if the file could not be created in either directory
227234
*/
228-
private fun openOutputFile(name: String): OutputFile {
235+
private fun openOutputFile(name: String, mimeType: String): OutputFile {
229236
val userUri = Preferences.getSavedOutputDir(context)
230237
if (userUri != null) {
231238
try {
232239
// Only returns null on API <21
233240
val userDir = DocumentFile.fromTreeUri(context, userUri)
234-
return openOutputFileInDir(userDir!!, name)
241+
return openOutputFileInDir(userDir!!, name, mimeType)
235242
} 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)
237244
}
238245
}
239246

240247
val fallbackDir = DocumentFile.fromFile(Preferences.getDefaultOutputDir(context))
241-
logD("Using fallback directory: ${fallbackDir.uri}")
248+
Log.d(tag, "Using fallback directory: ${fallbackDir.uri}")
242249

243-
return openOutputFileInDir(fallbackDir, name)
250+
return openOutputFileInDir(fallbackDir, name, mimeType)
244251
}
245252

246253
/**
247254
* 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].
249257
*
250258
* @throws IOException if file creation or opening fails
251259
*/
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)
254266
?: throw IOException("Failed to create file in ${directory.uri}")
255267
val pfd = context.contentResolver.openFileDescriptor(file.uri, "rw")
256268
?: throw IOException("Failed to open file at ${file.uri}")
@@ -339,40 +351,59 @@ class RecorderThread(
339351
if (bufSize < 0) {
340352
throw Exception("Failure when querying minimum buffer size: $bufSize")
341353
}
342-
logD("AudioRecord buffer size: $bufSize")
354+
Log.d(tag, "AudioRecord buffer size: $bufSize")
343355

344356
// Use a slightly larger buffer to reduce the chance of problems under load
345357
val buffer = ByteBuffer.allocateDirect(bufSize * 2)
358+
val bufferFrames = buffer.capacity().toLong() / frameSize
359+
val bufferNs = bufferFrames * 1_000_000_000L / audioRecord.sampleRate
346360

347361
while (!isCancelled) {
362+
val begin = System.nanoTime()
348363
val n = audioRecord.read(buffer, buffer.remaining())
364+
val recordElapsed = System.nanoTime() - begin
365+
var encodeElapsed = 0L
366+
349367
if (n < 0) {
350-
logE("Error when reading samples from $audioRecord: $n")
368+
Log.e(tag, "Error when reading samples from $audioRecord: $n")
351369
isCancelled = true
352370
captureFailed = true
353371
} else if (n == 0) {
354-
logE( "Unexpected EOF from AudioRecord")
372+
Log.e(tag, "Unexpected EOF from AudioRecord")
355373
isCancelled = true
356374
} else {
357375
buffer.limit(n)
376+
377+
val encodeBegin = System.nanoTime()
358378
encoder.encode(buffer, false)
379+
encodeElapsed = System.nanoTime() - encodeBegin
380+
359381
buffer.clear()
360382

361383
numFrames += n / frameSize
362384
}
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+
}
363395
}
364396

365397
// Signal EOF with empty buffer
366-
logD("Sending EOF to encoder")
398+
Log.d(tag, "Sending EOF to encoder")
367399
buffer.limit(buffer.position())
368400
encoder.encode(buffer, true)
369401

370402
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")
372404
}
373405

374406
companion object {
375-
private val TAG = RecorderThread::class.java.simpleName
376407
private const val CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO
377408
private const val ENCODING = AudioFormat.ENCODING_PCM_16BIT
378409

app/src/main/java/com/chiller3/bcr/SettingsActivity.kt

+18-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class SettingsActivity : AppCompatActivity() {
3737
private lateinit var prefOutputDir: LongClickablePreference
3838
private lateinit var prefOutputFormat: Preference
3939
private lateinit var prefInhibitBatteryOpt: SwitchPreferenceCompat
40-
private lateinit var prefVersion: Preference
40+
private lateinit var prefVersion: LongClickablePreference
4141

4242
private val requestPermissionRequired =
4343
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { granted ->
@@ -85,7 +85,8 @@ class SettingsActivity : AppCompatActivity() {
8585

8686
prefVersion = findPreference(Preferences.PREF_VERSION)!!
8787
prefVersion.onPreferenceClickListener = this
88-
prefVersion.summary = "${BuildConfig.VERSION_NAME} (${BuildConfig.BUILD_TYPE})"
88+
prefVersion.onPreferenceLongClickListener = this
89+
refreshVersion()
8990
}
9091

9192
override fun onResume() {
@@ -123,6 +124,15 @@ class SettingsActivity : AppCompatActivity() {
123124
prefOutputFormat.summary = "${summary}\n\n${format.name} (${prefix}${sampleRate})"
124125
}
125126

127+
private fun refreshVersion() {
128+
val suffix = if (!BuildConfig.DEBUG && Preferences.isDebugMode(requireContext())) {
129+
"+debugmode"
130+
} else {
131+
""
132+
}
133+
prefVersion.summary = "${BuildConfig.VERSION_NAME} (${BuildConfig.BUILD_TYPE}${suffix})"
134+
}
135+
126136
private fun refreshInhibitBatteryOptState() {
127137
val inhibiting = Permissions.isInhibitingBatteryOpt(requireContext())
128138
prefInhibitBatteryOpt.isChecked = inhibiting
@@ -179,6 +189,12 @@ class SettingsActivity : AppCompatActivity() {
179189
refreshOutputDir()
180190
return true
181191
}
192+
prefVersion -> {
193+
val context = requireContext()
194+
Preferences.setDebugMode(context, !Preferences.isDebugMode(context))
195+
refreshVersion()
196+
return true
197+
}
182198
}
183199

184200
return false

app/src/main/res/xml/root_preferences.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
app:title="@string/pref_header_about"
3737
app:iconSpaceReserved="false">
3838

39-
<Preference
39+
<com.chiller3.bcr.LongClickablePreference
4040
app:key="version"
4141
app:persistent="false"
4242
app:title="@string/pref_version_name"

0 commit comments

Comments
 (0)