From 532f23f106fdd6369316ed410a49863423b4ae80 Mon Sep 17 00:00:00 2001 From: xiaoe Date: Sun, 7 Dec 2025 15:30:22 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BD=95=E9=9F=B3=E8=87=AA=E5=8A=A8=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/misc.xml | 1 - .../data/model/ApiCallRecordCheck.kt | 5 + .../data/network/ApiCallLogService.kt | 6 +- .../data/repository/CallLogRepository.kt | 43 ++++++- .../manager/CallRecordManager.kt | 13 +- .../ui/calllog/CallLogScreen.kt | 69 ++++++++++- .../ui/calllog/CallLogViewModel.kt | 116 +++++++++++++++++- .../ui/login/LoginState.kt | 4 +- 8 files changed, 238 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/com/example/initiateaphonecallapp/data/model/ApiCallRecordCheck.kt diff --git a/.idea/misc.xml b/.idea/misc.xml index 0a29763..b914def 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/app/src/main/java/com/example/initiateaphonecallapp/data/model/ApiCallRecordCheck.kt b/app/src/main/java/com/example/initiateaphonecallapp/data/model/ApiCallRecordCheck.kt new file mode 100644 index 0000000..18f79c0 --- /dev/null +++ b/app/src/main/java/com/example/initiateaphonecallapp/data/model/ApiCallRecordCheck.kt @@ -0,0 +1,5 @@ +package com.example.initiateaphonecallapp.data.model + +data class CallRecordCheckRequest( + val data: List? = null +) diff --git a/app/src/main/java/com/example/initiateaphonecallapp/data/network/ApiCallLogService.kt b/app/src/main/java/com/example/initiateaphonecallapp/data/network/ApiCallLogService.kt index d6db876..744fe21 100644 --- a/app/src/main/java/com/example/initiateaphonecallapp/data/network/ApiCallLogService.kt +++ b/app/src/main/java/com/example/initiateaphonecallapp/data/network/ApiCallLogService.kt @@ -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>> + + @POST("app/call-log/check") + suspend fun callLogCheck(@Body callRecordRequest: CallRecordCheckRequest): Response>> } \ No newline at end of file diff --git a/app/src/main/java/com/example/initiateaphonecallapp/data/repository/CallLogRepository.kt b/app/src/main/java/com/example/initiateaphonecallapp/data/repository/CallLogRepository.kt index f1f3f38..ce7ed56 100644 --- a/app/src/main/java/com/example/initiateaphonecallapp/data/repository/CallLogRepository.kt +++ b/app/src/main/java/com/example/initiateaphonecallapp/data/repository/CallLogRepository.kt @@ -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> { + 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>>): Result> { + 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()}")) + } + } + } + } \ No newline at end of file diff --git a/app/src/main/java/com/example/initiateaphonecallapp/manager/CallRecordManager.kt b/app/src/main/java/com/example/initiateaphonecallapp/manager/CallRecordManager.kt index 1f3db66..bfa8a8e 100644 --- a/app/src/main/java/com/example/initiateaphonecallapp/manager/CallRecordManager.kt +++ b/app/src/main/java/com/example/initiateaphonecallapp/manager/CallRecordManager.kt @@ -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) -> Unit) { + scope.launch { + val files = queryAudioInTargetDirs(context) + withContext(Dispatchers.Main) { + onResult(files) + } + } + } + private suspend fun queryAudioInTargetDirs(context: Context): List = withContext(Dispatchers.IO) { val audioList = mutableListOf() val TAG = "CallRecordManager" diff --git a/app/src/main/java/com/example/initiateaphonecallapp/ui/calllog/CallLogScreen.kt b/app/src/main/java/com/example/initiateaphonecallapp/ui/calllog/CallLogScreen.kt index f944e62..f311a11 100644 --- a/app/src/main/java/com/example/initiateaphonecallapp/ui/calllog/CallLogScreen.kt +++ b/app/src/main/java/com/example/initiateaphonecallapp/ui/calllog/CallLogScreen.kt @@ -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) diff --git a/app/src/main/java/com/example/initiateaphonecallapp/ui/calllog/CallLogViewModel.kt b/app/src/main/java/com/example/initiateaphonecallapp/ui/calllog/CallLogViewModel.kt index effaf34..fc08ff4 100644 --- a/app/src/main/java/com/example/initiateaphonecallapp/ui/calllog/CallLogViewModel.kt +++ b/app/src/main/java/com/example/initiateaphonecallapp/ui/calllog/CallLogViewModel.kt @@ -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(null) val playbackError: StateFlow = _playbackError.asStateFlow() + + private val _audioFiles = mutableStateListOf() + val audioFiles: List 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() { - - } } \ No newline at end of file diff --git a/app/src/main/java/com/example/initiateaphonecallapp/ui/login/LoginState.kt b/app/src/main/java/com/example/initiateaphonecallapp/ui/login/LoginState.kt index 3dd4ee1..79df18d 100644 --- a/app/src/main/java/com/example/initiateaphonecallapp/ui/login/LoginState.kt +++ b/app/src/main/java/com/example/initiateaphonecallapp/ui/login/LoginState.kt @@ -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,