feat(servicing): 添加客户语音通知功能

- 新增 CustomerSpeechManager 对象,用于处理文本转语音功能
- 添加 AppForegroundListener 接口和 BaseActivityLifecycleCallbacks 类,用于监听应用前后台切换- 更新 BaseActivity,使其支持推送消息
- 新增 ServicePeopleConfirmActivity 活动
- 优化订单处理逻辑,过滤掉已接受的订单
- 更新版本号至 1.0.1.9.9.12
This commit is contained in:
songzhiling
2025-04-27 17:49:05 +08:00
parent b0c2f7352d
commit 863329d107
32 changed files with 1505 additions and 223 deletions

View File

@ -117,6 +117,7 @@ object GlobalData : GlobalLocalData() {
currentLocation = null
driverInfoBean = null
loginTime = null
isLoginRecognition = null
}
fun clearAllOrderCache() {

View File

@ -5,6 +5,7 @@ import com.tencent.bugly.Bugly
import com.tencent.mmkv.MMKV
import com.za.base.AppConfig
import com.za.common.log.LogUtil
import com.za.common.speech.SpeechManager
import com.za.room.RoomHelper
import com.za.service.location.ZdLocationManager
@ -28,5 +29,6 @@ object ZDManager {
LogUtil.init(application)
RoomHelper.init(application)
ZdLocationManager.init(application)
SpeechManager.init(application)
}
}

View File

@ -0,0 +1,122 @@
package com.za.common.speech
import android.media.MediaPlayer
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.za.common.log.LogUtil
import okhttp3.OkHttpClient
import okhttp3.ResponseBody
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.Body
import retrofit2.http.POST
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
interface SpeechApiService {
@POST("v1/audio/speech")
fun textToSpeech(@Body request: JsonObject): Call<ResponseBody>
}
object CustomerSpeechManager {
private const val BASE_URL = "http://192.168.3.129:8880/"
private val gson: Gson by lazy {
Gson().newBuilder()
.setLenient()
.create()
}
private val retrofit: Retrofit by lazy {
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
val client = OkHttpClient.Builder()
.addInterceptor(loggingInterceptor)
.build()
Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.build()
}
private val speechApiService: SpeechApiService by lazy {
retrofit.create(SpeechApiService::class.java)
}
fun textToSpeech(input: String, destinationFile: File): Boolean {
val requestBody = JsonObject().apply {
addProperty("input", input)
addProperty("voice", "zf_xiaoxiao")
addProperty("response_format", "mp3")
addProperty("stream", true)
addProperty("speed", 1)
addProperty("return_download_link", false) // Set to false to get the stream directly
addProperty("lang_code", "z")
}
val call = speechApiService.textToSpeech(requestBody)
try {
val response = call.execute()
LogUtil.print("CustomerSpeechManager", "response: $response")
if (response.isSuccessful) {
val responseBody = response.body()
if (responseBody != null) {
saveToFile(responseBody, destinationFile)
playAudio(destinationFile)
return true
}
} else {
LogUtil.print("CustomerSpeechManager", "Request failed: ${response.code()}")
}
} catch (e: IOException) {
e.printStackTrace()
}
return false
}
private fun saveToFile(body: ResponseBody, destinationFile: File) {
body.byteStream().use {
FileOutputStream(destinationFile).buffered().use { outputStream ->
val buffer = ByteArray(4096)
var bytesRead: Int
while (it.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
}
}
}
}
private fun playAudio(file: File) {
val mediaPlayer = MediaPlayer().apply {
try {
setDataSource(file.absolutePath)
prepare()
start()
} catch (e: IOException) {
e.printStackTrace()
release()
}
}
mediaPlayer.setOnCompletionListener {
it.release()
}
mediaPlayer.setOnErrorListener { mp, what, extra ->
mp.release()
false
}
}
}

View File

@ -0,0 +1,221 @@
package com.za.common.speech
import android.app.Application
import android.media.AudioManager
import android.media.MediaPlayer
import androidx.core.content.ContextCompat
import com.blankj.utilcode.util.ThreadUtils
import com.za.base.AppConfig
import com.za.bean.request.AppNewOrderVoiceRequest
import com.za.common.GlobalData
import com.za.common.log.LogUtil
import com.za.net.BaseObserver
import com.za.net.RetrofitHelper
import com.za.room.RoomHelper
import com.za.room.db.user.LocalResourceBean
import com.za.servicing.R
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
object SpeechManager {
private var mContext : Application? = null
fun init(context : Application?) {
mContext = context //语音合成初始化|
TTSManager.init(context, object : OnTTSListener {
override fun onTTSInitialized() { // TTS初始化成功可以开始播放语音
LogUtil.print("TTS", "TTS initialized successfully")
}
override fun onTTSSuccess() {
LogUtil.print("TTS", "TTS speech completed successfully")
resetAudioVolume()
}
override fun onTTSFailed(errorMessage : String?) {
resetAudioVolume() // TTS初始化失败或语音播放失败使用科大讯飞进行播放
LogUtil.print("TTS", "TTS failed: $errorMessage")
if (lastFailedText != null && ! lastFailedText.isNullOrBlank()) {
speechKDXF(lastFailedText)
lastFailedText = null
}
}
})
}
private var lastFailedText : String? = null
fun speech(msg : String?) {
setMaxAudioVolume()
lastFailedText = msg
TTSManager.speak(msg)
}
private fun speechKDXF(msg : String?) {
}
private var originVolume = 0
private fun setMaxAudioVolume() {
try {
if (! AppConfig.isRelease) {
return
}
val audioManager =
ContextCompat.getSystemService(GlobalData.application, AudioManager::class.java)
val maxVolume = audioManager?.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
audioManager?.setStreamVolume(AudioManager.STREAM_MUSIC,
maxVolume ?: 1,
AudioManager.FLAG_PLAY_SOUND)
} catch (e : Exception) {
LogUtil.print("setMaxAudioVolume", e)
}
}
private fun resetAudioVolume() {
try {
if (! AppConfig.isRelease) {
return
}
val audioManager =
ContextCompat.getSystemService(GlobalData.application, AudioManager::class.java)
audioManager?.setStreamVolume(AudioManager.STREAM_MUSIC,
originVolume,
AudioManager.FLAG_PLAY_SOUND)
} catch (e : Exception) {
LogUtil.print("resetAudioVolume", e)
}
}
fun stopSpeech() {
TTSManager.stop()
lastFailedText = null
}
private var mediaPlayer : MediaPlayer? = null
// 当前订单被取消
fun playCurrentOrderCanceled() {
mediaPlayer = MediaPlayer.create(mContext, R.raw.current_order_cancel)
mediaPlayer?.start()
}
// 订单被取消
fun playOrderCanceled() {
mediaPlayer = MediaPlayer.create(mContext, R.raw.cancel_order)
mediaPlayer?.start()
}
// 面部居中
fun playFaceCenter() {
mediaPlayer = MediaPlayer.create(mContext, R.raw.face_center)
mediaPlayer?.start()
}
// 面部居中
fun playFaceLeft() {
mediaPlayer = MediaPlayer.create(mContext, R.raw.face_left)
mediaPlayer?.start()
}
// 面部居中
fun playFaceRight() {
mediaPlayer = MediaPlayer.create(mContext, R.raw.face_right)
mediaPlayer?.start()
}
private fun playNewOrder() {
stopPlayMedia()
setMaxAudioVolume()
mediaPlayer = MediaPlayer.create(mContext, R.raw.neworder)
mediaPlayer?.start()
mediaPlayer?.setOnCompletionListener {
resetAudioVolume()
}
}
private fun playNewOrderFromNet(url : String) {
try {
stopPlayMedia()
setMaxAudioVolume()
mediaPlayer = MediaPlayer()
mediaPlayer?.setAudioStreamType(AudioManager.STREAM_MUSIC) // 设置音频流类型
mediaPlayer?.setDataSource(url) // 设置音频文件的 URL
mediaPlayer?.prepareAsync() // 异步准备音频
// 准备完成后开始播放
mediaPlayer?.setOnPreparedListener { obj : MediaPlayer -> obj.start() }
mediaPlayer?.setOnErrorListener { mp : MediaPlayer?, what : Int, extra : Int ->
playNewOrder()
false
}
mediaPlayer?.setOnCompletionListener {
resetAudioVolume()
}
} catch (e : Exception) {
playNewOrder()
LogUtil.print("播放新订单失败", e)
}
}
fun stopPlayMedia() {
ThreadUtils.runOnUiThread {
if (null != mediaPlayer) {
mediaPlayer?.stop()
mediaPlayer?.release()
mediaPlayer = null
}
}
}
fun speechNewOrderSound(content : String?) {
if (content.isNullOrBlank()) {
speechNewOrderLooper("您有新的中道救援订单,请尽快接单!") { playNewOrder() }
return
}
val localResourceDao = RoomHelper.db?.localResourceDao()
val localUrlResource = localResourceDao?.getLocalResourceByName(content)
if (localUrlResource != null && ! localUrlResource.resourceUrl.isNullOrBlank()) {
speechNewOrderLooper(content) {
playNewOrderFromNet(localUrlResource.resourceUrl ?: "")
}
LogUtil.print("handlerNewOrderVoice", "播放本地语音");
return
}
val appNewOrderVoiceRequest = AppNewOrderVoiceRequest(content)
RetrofitHelper.getDefaultService().getVoiceUrl(appNewOrderVoiceRequest)
.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
.subscribe(object : BaseObserver<String>() {
override fun doSuccess(it : String?) {
if (it == null) {
speechNewOrderLooper(content) { playNewOrder() }
return
}
localResourceDao?.insert(LocalResourceBean(resourceName = content,
resourceType = 1,
resourceUrl = it))
speechNewOrderLooper(content) { playNewOrderFromNet(it) }
}
override fun doFailure(code : Int, msg : String?) {
speechNewOrderLooper("您有新的中道救援订单,请尽快接单!") { playNewOrder() }
}
})
}
private fun speechNewOrderLooper(content : String, play : () -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
val startTime = System.currentTimeMillis()
while (System.currentTimeMillis() - startTime < 1000 * 60 * 3 && GlobalData.isHandlerNewOrder == false) {
play()
delay(250L * content.length)
}
}
}
}

View File

@ -0,0 +1,81 @@
package com.za.common.speech
import android.annotation.SuppressLint
import android.content.Context
import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener
import com.za.common.log.LogUtil
import java.util.Locale
@SuppressLint("StaticFieldLeak")
object TTSManager {
private var tts: TextToSpeech? = null
private var context: Context? = null
private var listener: OnTTSListener? = null
fun init(context: Context?, onTTSListener: OnTTSListener) {
this.context = context
this.listener = onTTSListener
initTTS()
}
private fun initTTS() {
tts = TextToSpeech(context) { status ->
if (status == TextToSpeech.SUCCESS) {
val result = tts?.setLanguage(Locale.getDefault())
if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
LogUtil.print("TTS", "Language not supported")
listener?.onTTSFailed("Language not supported")
} else {
LogUtil.print("TTS", "TTS initialized successfully")
listener?.onTTSInitialized()
}
} else {
LogUtil.print("TTS", "Initialization failed")
listener?.onTTSFailed("Initialization failed")
}
}
tts?.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
override fun onStart(utteranceId: String) {
LogUtil.print("TTS", "Speech started")
}
override fun onDone(utteranceId: String) {
LogUtil.print("TTS", "Speech completed")
listener?.onTTSSuccess()
}
override fun onError(utteranceId: String) {
LogUtil.print("TTS", "Speech error")
listener?.onTTSFailed("Speech error")
}
})
}
fun speak(text: String?) {
if (tts != null && tts?.isSpeaking == false) {
tts?.speak(text, TextToSpeech.QUEUE_FLUSH, null, "uniqueId")
}
}
fun stop() {
tts?.takeIf { it.isSpeaking }?.stop()
}
fun shutdown() {
tts?.apply {
stop()
shutdown()
}
tts = null
}
}
interface OnTTSListener {
fun onTTSInitialized()
fun onTTSSuccess()
fun onTTSFailed(errorMessage: String?)
}