From db0fe8fb769cb25b65ada929edfbf3b3177d2b41 Mon Sep 17 00:00:00 2001 From: Andrew Gunnerson Date: Tue, 31 Dec 2024 23:22:53 -0500 Subject: [PATCH] Use kotlinx-serialization to produce metadata JSON file Signed-off-by: Andrew Gunnerson --- .../java/com/chiller3/bcr/RecorderThread.kt | 81 +++++----- .../com/chiller3/bcr/output/CallMetadata.kt | 141 ++++++++++++++---- 2 files changed, 157 insertions(+), 65 deletions(-) diff --git a/app/src/main/java/com/chiller3/bcr/RecorderThread.kt b/app/src/main/java/com/chiller3/bcr/RecorderThread.kt index 084dd7502..799c2b111 100644 --- a/app/src/main/java/com/chiller3/bcr/RecorderThread.kt +++ b/app/src/main/java/com/chiller3/bcr/RecorderThread.kt @@ -23,21 +23,24 @@ import com.chiller3.bcr.extension.phoneNumber import com.chiller3.bcr.extension.toDocumentFile import com.chiller3.bcr.format.Encoder import com.chiller3.bcr.format.Format -import com.chiller3.bcr.format.NoParamInfo -import com.chiller3.bcr.format.RangedParamInfo -import com.chiller3.bcr.format.RangedParamType import com.chiller3.bcr.output.CallMetadata import com.chiller3.bcr.output.CallMetadataCollector +import com.chiller3.bcr.output.CallMetadataJson import com.chiller3.bcr.output.DaysRetention +import com.chiller3.bcr.output.FormatJson import com.chiller3.bcr.output.NoRetention import com.chiller3.bcr.output.OutputDirUtils import com.chiller3.bcr.output.OutputFile import com.chiller3.bcr.output.OutputFilenameGenerator +import com.chiller3.bcr.output.OutputJson import com.chiller3.bcr.output.OutputPath +import com.chiller3.bcr.output.ParameterType import com.chiller3.bcr.output.PhoneNumber +import com.chiller3.bcr.output.RecordingJson import com.chiller3.bcr.output.Retention import com.chiller3.bcr.rule.RecordRule -import org.json.JSONObject +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import java.lang.Process import java.nio.ByteBuffer import java.time.Duration @@ -423,43 +426,37 @@ class RecorderThread( Log.i(tag, "Writing metadata file") try { - val formatJson = JSONObject().apply { - put("type", format.name) - put("mime_type_container", format.mimeTypeContainer) - put("mime_type_audio", format.mimeTypeAudio) - put("parameter_type", when (val info = format.paramInfo) { - NoParamInfo -> "none" - is RangedParamInfo -> when (info.type) { - RangedParamType.CompressionLevel -> "compression_level" - RangedParamType.Bitrate -> "bitrate" - } - }) - put("parameter", (formatParam ?: format.paramInfo.default).toInt()) - } - val recordingJson = if (recordingInfo != null) { - JSONObject().apply { - put("frames_total", recordingInfo.framesTotal) - put("frames_encoded", recordingInfo.framesEncoded) - put("sample_rate", recordingInfo.sampleRate) - put("channel_count", recordingInfo.channelCount) - put("duration_secs_total", recordingInfo.durationSecsTotal) - put("duration_secs_encoded", recordingInfo.durationSecsEncoded) - put("buffer_frames", recordingInfo.bufferFrames) - put("buffer_overruns", recordingInfo.bufferOverruns) - put("was_ever_paused", recordingInfo.wasEverPaused) - put("was_ever_holding", recordingInfo.wasEverHolding) - } - } else { - JSONObject.NULL - } - val outputJson = JSONObject().apply { - put("format", formatJson) - put("recording", recordingJson) - } - val metadataJson = callMetadataCollector.callMetadata.toJson(context).apply { - put("output", outputJson) + val formatJson = FormatJson( + type = format.name, + mimeTypeContainer = format.mimeTypeContainer, + mimeTypeAudio = format.mimeTypeAudio, + parameterType = ParameterType.fromParamInfo(format.paramInfo), + parameter = formatParam ?: format.paramInfo.default, + ) + val recordingJson = recordingInfo?.let { + RecordingJson( + framesTotal = it.framesTotal, + framesEncoded = it.framesEncoded, + sampleRate = it.sampleRate, + channelCount = it.channelCount, + durationSecsTotal = it.durationSecsTotal, + durationSecsEncoded = it.durationSecsEncoded, + bufferFrames = it.bufferFrames, + bufferOverruns = it.bufferOverruns, + wasEverPaused = it.wasEverPaused, + wasEverHolding = it.wasEverHolding, + ) } - val metadataBytes = metadataJson.toString(4).toByteArray() + val outputJson = OutputJson( + format = formatJson, + recording = recordingJson, + ) + val metadataJson = CallMetadataJson( + context, + callMetadataCollector.callMetadata, + outputJson, + ) + val metadataBytes = JSON_FORMAT.encodeToString(metadataJson).toByteArray() // Always create in the default directory and then move to ensure that we don't race // with the direct boot file migration process. @@ -741,6 +738,10 @@ class RecorderThread( const val MIME_LOGCAT = "text/plain" const val MIME_METADATA = "application/json" + + private val JSON_FORMAT = Json { + prettyPrint = true + } } private data class RecordingInfo( diff --git a/app/src/main/java/com/chiller3/bcr/output/CallMetadata.kt b/app/src/main/java/com/chiller3/bcr/output/CallMetadata.kt index 6032ce303..00593a911 100644 --- a/app/src/main/java/com/chiller3/bcr/output/CallMetadata.kt +++ b/app/src/main/java/com/chiller3/bcr/output/CallMetadata.kt @@ -1,41 +1,54 @@ /* - * SPDX-FileCopyrightText: 2023 Andrew Gunnerson + * SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson * SPDX-License-Identifier: GPL-3.0-only */ package com.chiller3.bcr.output import android.content.Context -import org.json.JSONArray -import org.json.JSONObject +import com.chiller3.bcr.format.FormatParamInfo +import com.chiller3.bcr.format.NoParamInfo +import com.chiller3.bcr.format.RangedParamInfo +import com.chiller3.bcr.format.RangedParamType +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import java.time.ZonedDateTime import java.time.format.DateTimeFormatter +@Serializable enum class CallDirection { + @SerialName("in") IN, + @SerialName("out") OUT, + @SerialName("conference") CONFERENCE, - ; - - override fun toString(): String = when (this) { - IN -> "in" - OUT -> "out" - CONFERENCE -> "conference" - } } data class CallPartyDetails( val phoneNumber: PhoneNumber?, val callerName: String?, val contactName: String?, +) + +@Serializable +data class CallPartyDetailsJson( + @SerialName("phone_number") + val phoneNumber: String?, + @SerialName("phone_number_formatted") + val phoneNumberFormatted: String?, + @SerialName("caller_name") + val callerName: String?, + @SerialName("contact_name") + val contactName: String?, ) { - fun toJson(context: Context) = JSONObject().apply { - put("phone_number", phoneNumber?.toString() ?: JSONObject.NULL) - put("phone_number_formatted", - phoneNumber?.format(context, PhoneNumber.Format.COUNTRY_SPECIFIC) ?: JSONObject.NULL) - put("caller_name", callerName ?: JSONObject.NULL) - put("contact_name", contactName ?: JSONObject.NULL) - } + constructor(context: Context, details: CallPartyDetails) : this( + phoneNumber = details.phoneNumber?.toString(), + phoneNumberFormatted = details.phoneNumber + ?.format(context, PhoneNumber.Format.COUNTRY_SPECIFIC), + callerName = details.callerName, + contactName = details.contactName, + ) } data class CallMetadata( @@ -45,13 +58,91 @@ data class CallMetadata( val simSlot: Int?, val callLogName: String?, val calls: List, +) + +@Serializable +data class CallMetadataJson( + @SerialName("timestamp_unix_ms") + val timestampUnixMs: Long, + val timestamp: String, + val direction: CallDirection?, + @SerialName("sim_slot") + val simSlot: Int?, + @SerialName("call_log_name") + val callLogName: String?, + val calls: List, + val output: OutputJson, ) { - fun toJson(context: Context) = JSONObject().apply { - put("timestamp_unix_ms", timestamp.toInstant().toEpochMilli()) - put("timestamp", DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(timestamp)) - put("direction", direction?.toString() ?: JSONObject.NULL) - put("sim_slot", simSlot ?: JSONObject.NULL) - put("call_log_name", callLogName ?: JSONObject.NULL) - put("calls", JSONArray(calls.map { it.toJson(context) })) + constructor(context: Context, metadata: CallMetadata, output: OutputJson) : this( + timestampUnixMs = metadata.timestamp.toInstant().toEpochMilli(), + timestamp = DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(metadata.timestamp), + direction = metadata.direction, + simSlot = metadata.simSlot, + callLogName = metadata.callLogName, + calls = metadata.calls.map { CallPartyDetailsJson(context, it) }, + output = output, + ) +} + +@Serializable +enum class ParameterType { + @SerialName("none") + NONE, + @SerialName("compression_level") + COMPRESSION_LEVEL, + @SerialName("bitrate") + BITRATE, + ; + + companion object { + fun fromParamInfo(info: FormatParamInfo): ParameterType = when (info) { + NoParamInfo -> NONE + is RangedParamInfo -> when (info.type) { + RangedParamType.CompressionLevel -> COMPRESSION_LEVEL + RangedParamType.Bitrate -> BITRATE + } + } } -} \ No newline at end of file +} + +@Serializable +data class FormatJson( + val type: String, + @SerialName("mime_type_container") + val mimeTypeContainer: String, + @SerialName("mime_type_audio") + val mimeTypeAudio: String, + @SerialName("parameter_type") + val parameterType: ParameterType, + val parameter: UInt, +) + +@Serializable +data class RecordingJson( + @SerialName("frames_total") + val framesTotal: Long, + @SerialName("frames_encoded") + val framesEncoded: Long, + @SerialName("sample_rate") + val sampleRate: Int, + @SerialName("channel_count") + val channelCount: Int, + @SerialName("duration_secs_total") + val durationSecsTotal: Double, + @SerialName("duration_secs_encoded") + val durationSecsEncoded: Double, + @SerialName("buffer_frames") + val bufferFrames: Long, + @SerialName("buffer_overruns") + val bufferOverruns: Int, + @SerialName("was_ever_paused") + val wasEverPaused: Boolean, + @SerialName("was_ever_holding") + val wasEverHolding: Boolean, +) + +@Serializable +data class OutputJson( + val format: FormatJson, + val recording: RecordingJson?, +)