录音自动上传

This commit is contained in:
2025-12-07 15:30:22 +08:00
parent b9225e720b
commit 532f23f106
8 changed files with 238 additions and 19 deletions

1
.idea/misc.xml generated
View File

@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="ms-21" project-jdk-type="JavaSDK">

View File

@@ -0,0 +1,5 @@
package com.example.initiateaphonecallapp.data.model
data class CallRecordCheckRequest(
val data: List<String>? = null
)

View File

@@ -2,9 +2,8 @@ package com.example.initiateaphonecallapp.data.network
import com.example.initiateaphonecallapp.data.model.ApiCallRecord
import com.example.initiateaphonecallapp.data.model.ApiResponse
import com.example.initiateaphonecallapp.data.model.CallRecordCheckRequest
import com.example.initiateaphonecallapp.data.model.CallRecordRequest
import com.example.initiateaphonecallapp.data.model.LoginRequest
import com.example.initiateaphonecallapp.data.model.LoginResponse
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.POST
@@ -12,4 +11,7 @@ import retrofit2.http.POST
interface ApiCallLogService {
@POST("app/call-log/my")
suspend fun callLog(@Body callRecordRequest: CallRecordRequest): Response<ApiResponse<List<ApiCallRecord>>>
@POST("app/call-log/check")
suspend fun callLogCheck(@Body callRecordRequest: CallRecordCheckRequest): Response<ApiResponse<List<String>>>
}

View File

@@ -4,9 +4,9 @@ package com.example.initiateaphonecallapp.data.repository
import com.example.initiateaphonecallapp.data.model.ApiCallRecord
import com.example.initiateaphonecallapp.data.model.ApiResponse
import com.example.initiateaphonecallapp.data.model.CallRecord
import com.example.initiateaphonecallapp.data.model.CallRecordCheckRequest
import com.example.initiateaphonecallapp.data.model.CallRecordRequest
import com.example.initiateaphonecallapp.data.model.toCallRecords
import com.example.initiateaphonecallapp.data.network.ApiCallLogService
import com.example.initiateaphonecallapp.data.network.RetrofitClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -16,6 +16,7 @@ class CallLogRepository(
) {
private val apiCallLogService = RetrofitClient.apiCallLogService
/**
* 获取通话记录列表
* @param request 时间范围请求参数
@@ -54,4 +55,44 @@ class CallLogRepository(
}
}
}
/**
* 验证录音文件
* @param data 请求参数
* @return 结果
*/
suspend fun checkCallLogs(data: CallRecordCheckRequest): Result<List<String>> {
return withContext(Dispatchers.IO) {
try {
val response = apiCallLogService.callLogCheck(data)
handleCheckResponse(response)
} catch (e: Exception) {
Result.failure(Exception("网络请求失败: ${e.message}"))
}
}
}
/**
* 处理 API 响应
*/
private fun handleCheckResponse(response: Response<ApiResponse<List<String>>>): Result<List<String>> {
return when {
response.isSuccessful -> {
val apiResponse = response.body()
when (apiResponse?.code) {
200 -> {
val callRecords = apiResponse.data
Result.success(callRecords)
}
else -> {
Result.failure(Exception(apiResponse?.message ?: "未知错误"))
}
}
}
else -> {
Result.failure(Exception("网络错误: ${response.code()} ${response.message()}"))
}
}
}
}

View File

@@ -288,7 +288,7 @@ object CallRecordManager {
}
// 上传通话记录
private fun uploadCallRecord(callRecord: CallRecord) {
fun uploadCallRecord(callRecord: CallRecord) {
println("📤 CallRecordManager: 开始上传通话记录...")
println(" 📞 电话号码: ${callRecord.phoneNumber}")
println(" ⏱️ 通话时长: ${callRecord.formattedDuration}")
@@ -373,7 +373,16 @@ object CallRecordManager {
}
}
// 以下辅助方法保持不变...
// 暴露接口
fun scanAudioFiles(context: Context, onResult: (List<AudioFile>) -> Unit) {
scope.launch {
val files = queryAudioInTargetDirs(context)
withContext(Dispatchers.Main) {
onResult(files)
}
}
}
private suspend fun queryAudioInTargetDirs(context: Context): List<AudioFile> = withContext(Dispatchers.IO) {
val audioList = mutableListOf<AudioFile>()
val TAG = "CallRecordManager"

View File

@@ -7,35 +7,32 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.Call
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Phone
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.initiateaphonecallapp.data.model.CallRecord
import com.example.initiateaphonecallapp.data.model.toCallRecord
import com.example.initiateaphonecallapp.enums.CallStatus
import com.example.initiateaphonecallapp.enums.CallStatus.*
import com.example.initiateaphonecallapp.manager.CallRecordManager
import com.example.initiateaphonecallapp.ui.contact.ContactsViewModel
import com.example.initiateaphonecallapp.utils.TimeRangeUtils
import java.text.SimpleDateFormat
@@ -65,6 +62,9 @@ fun CallLogScreen(
var selectedPhoneNumber by remember { mutableStateOf("") }
var showSuccessDialog by remember { mutableStateOf(false) } // 新增:成功提示框
var successMessage by remember { mutableStateOf("") } // 新增:成功消息
var showScanDialog by remember { mutableStateOf(false) }
val context = LocalContext.current
@@ -123,7 +123,7 @@ fun CallLogScreen(
viewModel.loadCallRecords(range, request)
},
onScanAudio = {
viewModel.scanAllAudio()
showScanDialog = true
}
)
},
@@ -195,6 +195,18 @@ fun CallLogScreen(
onDismiss = { showSuccessDialog = false }
)
}
// 扫描录音弹窗
if (showScanDialog) {
LaunchedEffect(Unit) {
viewModel.scanAndProcessAudio(context)
}
ScanAudioDialog(
viewModel = viewModel,
onDismiss = { showScanDialog = false }
)
}
}
}
@@ -531,6 +543,51 @@ fun EmptyCallLog(
}
}
@Composable
fun ScanAudioDialog(
viewModel: CallLogViewModel,
onDismiss: () -> Unit
) {
val audioFiles by remember { derivedStateOf { viewModel.audioFiles } }
val progress by remember { derivedStateOf { viewModel.progress } }
val isScanning by remember { derivedStateOf { viewModel.isScanning } }
AlertDialog(
onDismissRequest = { /* 禁止点击空白关闭 */ },
title = {
Text(
when {
isScanning -> "正在扫描录音…"
progress < audioFiles.size -> "处理中… $progress / ${audioFiles.size}"
else -> "处理完成!"
}
)
},
text = {
Column {
if (isScanning) {
CircularProgressIndicator()
Spacer(Modifier.height(12.dp))
Text("正在扫描录音文件,请稍候…")
} else {
LinearProgressIndicator(
progress = if (audioFiles.isNotEmpty()) progress.toFloat() / audioFiles.size else 0f,
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(8.dp))
Text("$progress / ${audioFiles.size}")
}
}
},
confirmButton = {
if (!isScanning && progress >= audioFiles.size) {
Button(onClick = onDismiss) { Text("关闭") }
}
},
dismissButton = {}
)
}
// 时间格式化辅助函数
fun formatTimeOnly(date: Date): String {
return SimpleDateFormat("HH:mm", Locale.getDefault()).format(date)

View File

@@ -1,17 +1,30 @@
package com.example.initiateaphonecallapp.ui.calllog
import android.content.Context
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.initiateaphonecallapp.data.model.AudioFile
import com.example.initiateaphonecallapp.data.model.CallRecord
import com.example.initiateaphonecallapp.data.model.CallRecordCheckRequest
import com.example.initiateaphonecallapp.data.model.CallRecordRequest
import com.example.initiateaphonecallapp.data.repository.CallLogRepository
import com.example.initiateaphonecallapp.enums.CallStatus
import com.example.initiateaphonecallapp.manager.AudioPlayManager
import com.example.initiateaphonecallapp.manager.CallRecordManager
import com.example.initiateaphonecallapp.utils.TimeRangeUtils
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.time.LocalDateTime
import kotlinx.coroutines.suspendCancellableCoroutine
import java.util.Date
class CallLogViewModel(
private val callLogRepository: CallLogRepository = CallLogRepository()
@@ -36,6 +49,16 @@ class CallLogViewModel(
private val _playbackError = MutableStateFlow<String?>(null)
val playbackError: StateFlow<String?> = _playbackError.asStateFlow()
private val _audioFiles = mutableStateListOf<AudioFile>()
val audioFiles: List<AudioFile> get() = _audioFiles
var progress by mutableStateOf(0)
private set
var isScanning by mutableStateOf(false)
private set
/**
* 播放音频
*/
@@ -127,6 +150,93 @@ class CallLogViewModel(
}
}
// 扫描并处理音频文件
@OptIn(ExperimentalCoroutinesApi::class)
fun scanAndProcessAudio(context: Context) {
viewModelScope.launch {
isScanning = true
_audioFiles.clear()
progress = 0
try {
// 1⃣ 扫描
val files = suspendCancellableCoroutine { cont ->
CallRecordManager.scanAudioFiles(context) { scanned ->
cont.resume(scanned) {}
}
}
Log.d("ScanAudio", "扫描到 ${files.size} 个文件")
// 2⃣ 验证录音文件
val fileNames = files.map { it.displayName }
val request = CallRecordCheckRequest(data = fileNames)
val checkResult = callLogRepository.checkCallLogs(request)
val needUploadNames = checkResult.getOrNull() ?: emptyList()
Log.d("ScanAudio", "需要上传的文件名列表: $needUploadNames")
// 3⃣ 过滤扫描结果,只保留需要上传且能提取手机号的文件
val uploadFiles = files.filter { file ->
val needUpload = file.displayName in needUploadNames
val phoneNumber = extractPhoneNumber(file.displayName)
val hasPhone = phoneNumber != null
if (!needUpload) {
Log.d("ScanAudio", "文件 ${file.displayName} 不在上传列表,跳过")
} else if (!hasPhone) {
Log.d("ScanAudio", "文件 ${file.displayName} 没有手机号,跳过")
} else {
Log.d("ScanAudio", "文件 ${file.displayName} 准备上传, 手机号=$phoneNumber")
}
needUpload && hasPhone
}
// 4⃣ 加入 _audioFiles
_audioFiles.addAll(uploadFiles)
Log.d("ScanAudio", "最终准备上传的文件数量: ${_audioFiles.size}")
// 5⃣ 上传
_audioFiles.forEachIndexed { index, file ->
pushFile(file)
progress = index + 1
Log.d("ScanAudio", "上传进度: $progress / ${_audioFiles.size}")
}
} catch (e: Exception) {
Log.e("ScanAudio", "扫描或验证异常: ${e.message}", e)
} finally {
isScanning = false
}
}
}
// 上传单个文件
fun pushFile(file: AudioFile) {
val phoneNumber = extractPhoneNumber(file.displayName) ?: run {
Log.d("ScanAudio", "文件 ${file.displayName} 没有手机号,跳过上传")
return
}
val callRecord = CallRecord(
phoneNumber = phoneNumber,
startTime = Date(file.dateAdded - file.duration),
endTime = Date(file.dateAdded),
duration = file.duration / 1000,
status = CallStatus.COMPLETED,
audioFileName = file.displayName,
audioFileUri = file.filePath
)
CallRecordManager.uploadCallRecord(callRecord)
Log.d("ScanAudio", "上传文件 ${file.displayName} 成功")
}
// 正则提取手机号
fun extractPhoneNumber(fileName: String): String? {
val regex = Regex("""1[3-9]\d{9}""")
return regex.find(fileName)?.value
}
/**
* 刷新数据(使用相同的请求参数)
*/
@@ -140,8 +250,4 @@ class CallLogViewModel(
fun clearError() {
_errorMessage.value = null
}
fun scanAllAudio() {
}
}

View File

@@ -1,8 +1,8 @@
package com.example.initiateaphonecallapp.ui.login
data class LoginState(
val username: String = "",
val password: String = "",
val username: String = "sadmin",
val password: String = "admin123",
val isLoading: Boolean = false,
val errorMessage: String = "",
val isLoginSuccess: Boolean = false,