diff --git a/app/src/main/java/io/legado/app/data/api/IHttpPostApi.kt b/app/src/main/java/io/legado/app/data/api/IHttpPostApi.kt index 92b5f1cf3..17bf16ee8 100644 --- a/app/src/main/java/io/legado/app/data/api/IHttpPostApi.kt +++ b/app/src/main/java/io/legado/app/data/api/IHttpPostApi.kt @@ -45,9 +45,9 @@ interface IHttpPostApi { @FormUrlEncoded @POST - fun postMapByte( + fun postMapByteAsync( @Url url: String, @FieldMap(encoded = true) fieldMap: Map, @HeaderMap headers: Map - ): Call + ): Deferred> } diff --git a/app/src/main/java/io/legado/app/help/JsExtensions.kt b/app/src/main/java/io/legado/app/help/JsExtensions.kt index d321f57fb..754751b01 100644 --- a/app/src/main/java/io/legado/app/help/JsExtensions.kt +++ b/app/src/main/java/io/legado/app/help/JsExtensions.kt @@ -1,5 +1,6 @@ package io.legado.app.help +import android.util.Base64 import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.utils.EncoderUtils import io.legado.app.utils.MD5Utils @@ -33,6 +34,10 @@ object JsExtensions { return EncoderUtils.base64Encode(str) } + fun base64Encode(str: String, flags: Int = Base64.NO_WRAP): String? { + return EncoderUtils.base64Encode(str, flags) + } + fun strToMd5By32(str: String): String? { return MD5Utils.strToMd5By32(str) } diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt index b4bb0276e..c97fc7b98 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt @@ -1,6 +1,7 @@ package io.legado.app.model.analyzeRule import android.text.TextUtils +import android.util.Base64 import androidx.annotation.Keep import io.legado.app.constant.AppConst.SCRIPT_ENGINE import io.legado.app.constant.Pattern.JS_PATTERN @@ -580,6 +581,10 @@ class AnalyzeRule(private var book: BaseBook? = null) { return EncoderUtils.base64Encode(str) } + fun base64Encode(str: String, flags: Int = Base64.NO_WRAP): String? { + return EncoderUtils.base64Encode(str, flags) + } + fun strToMd5By32(str: String): String? { return MD5Utils.strToMd5By32(str) } diff --git a/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt b/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt index 9af47f861..8c46f11c8 100644 --- a/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt +++ b/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt @@ -101,7 +101,8 @@ abstract class BaseReadAloudService : BaseService(), return super.onStartCommand(intent, flags, startId) } - private fun newReadAloud(dataKey: String?, play: Boolean) { + @CallSuper + open fun newReadAloud(dataKey: String?, play: Boolean) { dataKey?.let { textChapter = IntentDataHelp.getData(dataKey) as? TextChapter textChapter?.let { textChapter -> diff --git a/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt b/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt index a797118d6..90f43f567 100644 --- a/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt +++ b/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt @@ -7,15 +7,16 @@ import io.legado.app.data.api.IHttpPostApi import io.legado.app.help.FileHelp import io.legado.app.help.IntentHelp import io.legado.app.help.http.HttpHelper +import io.legado.app.utils.LogUtils import io.legado.app.utils.getPrefInt import io.legado.app.utils.getPrefString import io.legado.app.utils.postEvent import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.Job +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.jetbrains.anko.toast import java.io.File +import java.io.FileDescriptor import java.io.FileInputStream import java.net.URLEncoder @@ -24,10 +25,14 @@ class HttpReadAloudService : BaseReadAloudService(), MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener { - private var mediaPlayer = MediaPlayer() + private val mediaPlayer = MediaPlayer() + private lateinit var ttsFolder: String + private var job: Job? = null + private var playingIndex = -1 override fun onCreate() { super.onCreate() + ttsFolder = cacheDir.absolutePath + File.separator + "bdTts" mediaPlayer.setOnErrorListener(this) mediaPlayer.setOnPreparedListener(this) mediaPlayer.setOnCompletionListener(this) @@ -38,18 +43,72 @@ class HttpReadAloudService : BaseReadAloudService(), mediaPlayer.release() } - private fun getAudioBody(): Map { + override fun newReadAloud(dataKey: String?, play: Boolean) { + mediaPlayer.reset() + job?.cancel() + playingIndex = -1 + super.newReadAloud(dataKey, play) + } + + override fun play() { + if (contentList.isEmpty()) return + if (nowSpeak == 0) { + downloadAudio() + } else { + val file = getSpeakFile(nowSpeak) + if (file.exists()) { + playAudio(FileInputStream(file).fd) + } + } + } + + private fun downloadAudio() { + job = launch(IO) { + FileHelp.deleteFile(ttsFolder) + for (index in 0 until contentList.size) { + if (isActive) { + val bytes = HttpHelper.getByteRetrofit("http://tts.baidu.com") + .create(IHttpPostApi::class.java) + .postMapByteAsync( + "http://tts.baidu.com/text2audio", + getAudioBody(contentList[index]), mapOf() + ).await() + .body() + if (bytes != null && isActive) { + val file = getSpeakFile(index) + file.writeBytes(bytes) + if (index == nowSpeak) { + playAudio(FileInputStream(file).fd) + } + } + } else { + break + } + } + } + } + + @Synchronized + private fun playAudio(fd: FileDescriptor) { + if (playingIndex != nowSpeak) { + playingIndex = nowSpeak + try { + mediaPlayer.reset() + mediaPlayer.setDataSource(fd) + mediaPlayer.prepareAsync() + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + private fun getSpeakFile(index: Int = nowSpeak): File { + return FileHelp.getFile("${ttsFolder}${File.separator}${index}.mp3") + } + + private fun getAudioBody(content: String): Map { return mapOf( - Pair( - "tex", - URLEncoder.encode( - URLEncoder.encode( - contentList[nowSpeak], - "UTF-8" - ), - "UTF-8" - ) - ), + Pair("tex", encodeTwo(content)), Pair("spd", ((getPrefInt("ttsSpeechRate", 25) + 5) / 5).toString()), Pair("per", getPrefString("ttsSpeechPer") ?: "0"), Pair("cuid", "baidu_speech_demo"), @@ -64,27 +123,11 @@ class HttpReadAloudService : BaseReadAloudService(), ) } - override fun play() { - if (contentList.isEmpty()) return - launch(IO) { - if (requestFocus()) { - val bytes = HttpHelper.getByteRetrofit("http://tts.baidu.com") - .create(IHttpPostApi::class.java) - .postMapByte("http://tts.baidu.com/text2audio", getAudioBody(), mapOf()) - .execute().body() - if (bytes == null) { - withContext(Main) { - toast("访问失败") - } - } else { - val file = - FileHelp.getFile(cacheDir.absolutePath + File.separator + "bdTts.mp3") - file.writeBytes(bytes) - mediaPlayer.reset() - mediaPlayer.setDataSource(FileInputStream(file).fd) - mediaPlayer.prepareAsync() - } - } + private fun encodeTwo(content: String): String { + return try { + URLEncoder.encode(URLEncoder.encode(content, "UTF-8"), "UTF-8") + } catch (e: Exception) { + " " } } @@ -99,6 +142,13 @@ class HttpReadAloudService : BaseReadAloudService(), } override fun upSpeechRate(reset: Boolean) { + job?.cancel() + mediaPlayer.reset() + for (i in 0 until nowSpeak) { + contentList.removeAt(0) + } + nowSpeak = 0 + playingIndex = -1 play() } @@ -138,8 +188,9 @@ class HttpReadAloudService : BaseReadAloudService(), } override fun onCompletion(mp: MediaPlayer?) { + LogUtils.d("播放完成", contentList[nowSpeak]) readAloudNumber += contentList[nowSpeak].length + 1 - if (nowSpeak < contentList.size) { + if (nowSpeak < contentList.lastIndex) { nowSpeak++ play() } else { diff --git a/app/src/main/java/io/legado/app/ui/filechooser/FileChooserDialog.kt b/app/src/main/java/io/legado/app/ui/filechooser/FileChooserDialog.kt new file mode 100644 index 000000000..6c1fc6fbd --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/filechooser/FileChooserDialog.kt @@ -0,0 +1,20 @@ +package io.legado.app.ui.filechooser + +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager + + +class FileChooserDialog : DialogFragment() { + + companion object { + const val tag = "FileChooserDialog" + + fun show(manager: FragmentManager) { + val fragment = + (manager.findFragmentByTag(tag) as? FileChooserDialog) ?: FileChooserDialog() + fragment.show(manager, tag) + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/EncoderUtils.kt b/app/src/main/java/io/legado/app/utils/EncoderUtils.kt index 8e5c03522..4858113e3 100644 --- a/app/src/main/java/io/legado/app/utils/EncoderUtils.kt +++ b/app/src/main/java/io/legado/app/utils/EncoderUtils.kt @@ -33,7 +33,7 @@ object EncoderUtils { } } - fun base64Encode(str: String): String? { - return Base64.encodeToString(str.toByteArray(), Base64.DEFAULT) + fun base64Encode(str: String, flags: Int = Base64.NO_WRAP): String? { + return Base64.encodeToString(str.toByteArray(), flags) } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/FileUtils.kt b/app/src/main/java/io/legado/app/utils/FileUtils.kt index 4626fe489..533504279 100644 --- a/app/src/main/java/io/legado/app/utils/FileUtils.kt +++ b/app/src/main/java/io/legado/app/utils/FileUtils.kt @@ -48,8 +48,8 @@ object FileUtils { val list = ArrayList() for (i in 0 until length) { - val storageValume = Array.get(invokeVolumeList, i)//得到StorageVolume对象 - val path = getPath.invoke(storageValume) as String + val storageVolume = Array.get(invokeVolumeList, i)//得到StorageVolume对象 + val path = getPath.invoke(storageVolume) as String list.add(path) } @@ -103,11 +103,8 @@ object FileUtils { val id = DocumentsContract.getDocumentId(uri) val split = id.split(":").dropLastWhile { it.isEmpty() }.toTypedArray() val type = split[0] - if ("raw".equals( - type, - ignoreCase = true - ) - ) { //处理某些机型(比如Goole Pixel )ID是raw:/storage/emulated/0/Download/c20f8664da05ab6b4644913048ea8c83.mp4 + if ("raw".equals(type, ignoreCase = true)) { + //处理某些机型(比如Google Pixel )ID是raw:/storage/emulated/0/Download/c20f8664da05ab6b4644913048ea8c83.mp4 return split[1] } @@ -122,12 +119,10 @@ object FileUtils { val type = split[0] var contentUri: Uri? = null - if ("image" == type) { - contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI - } else if ("video" == type) { - contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI - } else if ("audio" == type) { - contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + when (type) { + "image" -> contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI + "video" -> contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI + "audio" -> contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI } val selection = "_id=?" @@ -136,21 +131,17 @@ object FileUtils { return getDataColumn(context, contentUri, selection, selectionArgs) }// MediaProvider // DownloadsProvider - } else if ("content".equals(uri.scheme!!, ignoreCase = true)) { - + } else if ("content".equals(uri.scheme, ignoreCase = true)) { // Return the remote address - return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn( - context, - uri, - null, - null - ) + return if (isGooglePhotosUri(uri)) + uri.lastPathSegment + else + getDataColumn(context, uri, null, null) - } else if ("file".equals(uri.scheme!!, ignoreCase = true)) { + } else if ("file".equals(uri.scheme, ignoreCase = true)) { return uri.path }// File // MediaStore (and general) - return null }