录音自动上传
This commit is contained in:
1
.idea/misc.xml
generated
1
.idea/misc.xml
generated
@@ -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">
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.example.initiateaphonecallapp.data.model
|
||||
|
||||
data class CallRecordCheckRequest(
|
||||
val data: List<String>? = null
|
||||
)
|
||||
@@ -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>>>
|
||||
}
|
||||
@@ -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()}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user