{
+ return rememberUpdatedState(if (enabled) contentColor else disabledContentColor)
+}
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/ui/widgets/SearchTextFieldInList.kt b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/ui/widgets/SearchTextFieldInList.kt
new file mode 100644
index 0000000000..51e277fad2
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/ui/widgets/SearchTextFieldInList.kt
@@ -0,0 +1,50 @@
+package com.k2fsa.sherpa.onnx.tts.engine.ui.widgets
+
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import com.k2fsa.sherpa.onnx.tts.engine.R
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+
+@Composable
+fun SearchTextFieldInList(
+ modifier: Modifier,
+ onSearch: (String) -> Unit
+) {
+ val keyboardController = LocalSoftwareKeyboardController.current
+
+ var search by rememberSaveable { mutableStateOf("") }
+
+ var text by rememberSaveable { mutableStateOf("") }
+ DenseOutlinedField(
+ modifier = modifier,
+ value = text, onValueChange = { text = it },
+ label = { Text(stringResource(id = R.string.search)) },
+ maxLines = 1,
+ keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
+ keyboardActions = KeyboardActions(
+ onDone = { keyboardController?.hide() }
+ )
+ )
+
+ LaunchedEffect(Unit) {
+ while (coroutineContext.isActive) {
+ delay(500)
+ if (text != search) {
+ search = text
+ onSearch(search)
+ }
+ }
+ }
+}
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/ui/widgets/SelectableCard.kt b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/ui/widgets/SelectableCard.kt
new file mode 100644
index 0000000000..de0d37ae2f
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/ui/widgets/SelectableCard.kt
@@ -0,0 +1,70 @@
+package com.k2fsa.sherpa.onnx.tts.engine.ui.widgets
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.selected
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.stateDescription
+import androidx.compose.ui.unit.dp
+import com.k2fsa.sherpa.onnx.tts.engine.R
+
+@Composable
+fun VerticalBar(
+ modifier: Modifier = Modifier,
+ enabled: Boolean,
+ color: Color = MaterialTheme.colorScheme.primary
+) {
+ val context = LocalContext.current
+ Box(
+ modifier = modifier
+ .background(
+ color = if (enabled) color else Color.Transparent,
+ shape = MaterialTheme.shapes.small
+ )
+ .width(4.dp)
+ .height(32.dp)
+ .semantics {
+ this.stateDescription = if (enabled) context.getString(R.string.enabled)
+ else ""
+ }
+ )
+}
+
+@Composable
+fun SelectableCard(
+ name: String,
+ selected: Boolean,
+ modifier: Modifier,
+ content: @Composable() (ColumnScope.() -> Unit)
+) {
+ val context = LocalContext.current
+ val color =
+ if (selected) MaterialTheme.colorScheme.primary else Color.Unspecified
+ Card(
+ modifier = modifier
+ .semantics {
+ this.stateDescription = "$name ${if (!selected) "not" else ""} selected"
+ this.selected = selected
+ },
+ colors = if (selected) CardDefaults.elevatedCardColors(
+ containerColor = MaterialTheme.colorScheme.primaryContainer.copy(
+ alpha = 0.5f
+ )
+ )
+ else CardDefaults.elevatedCardColors(),
+ border = if (selected) BorderStroke(1.dp, MaterialTheme.colorScheme.primary) else null,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/ui/widgets/TextCheckBox.kt b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/ui/widgets/TextCheckBox.kt
new file mode 100644
index 0000000000..3f51f68dd3
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/ui/widgets/TextCheckBox.kt
@@ -0,0 +1,44 @@
+package com.github.jing332.tts_server_android.compose.widgets
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.CheckboxColors
+import androidx.compose.material3.CheckboxDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.unit.dp
+import com.k2fsa.sherpa.onnx.tts.engine.utils.clickableRipple
+
+@Composable
+fun TextCheckBox(
+ modifier: Modifier = Modifier,
+ text: @Composable RowScope.() -> Unit,
+ checked: Boolean,
+ onCheckedChange: (Boolean) -> Unit,
+ colors: CheckboxColors = CheckboxDefaults.colors(),
+
+ horizontalArrangement: Arrangement.Horizontal = Arrangement.Center,
+ verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
+) {
+ Row(
+ modifier
+ .height(48.dp)
+ .clip(MaterialTheme.shapes.small)
+ .clickableRipple(role = Role.Checkbox) { onCheckedChange(!checked) },
+ verticalAlignment = verticalAlignment,
+ horizontalArrangement = horizontalArrangement,
+ ) {
+ Row(Modifier.padding(horizontal = 8.dp)) {
+ Checkbox(colors = colors, checked = checked, onCheckedChange = null)
+ text()
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/ui/widgets/TextFieldDialog.kt b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/ui/widgets/TextFieldDialog.kt
new file mode 100644
index 0000000000..3653c8280a
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/ui/widgets/TextFieldDialog.kt
@@ -0,0 +1,44 @@
+package com.k2fsa.sherpa.onnx.tts.engine.ui.widgets
+
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.res.stringResource
+
+@Composable
+fun TextFieldDialog(
+ title: String,
+ initialText: String,
+ onDismissRequest: () -> Unit,
+ onConfirm: (String) -> Unit
+) {
+ var textValue by rememberSaveable { mutableStateOf(initialText) }
+ AlertDialog(onDismissRequest = onDismissRequest,
+ title = {
+ Text(title)
+ },
+ text = {
+ OutlinedTextField(
+ value = textValue, onValueChange = { textValue = it },
+ )
+ },
+ dismissButton = {
+ TextButton(onClick = onDismissRequest) {
+ Text(stringResource(id = android.R.string.cancel))
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = {
+ onConfirm(textValue)
+ }) {
+ Text(stringResource(id = android.R.string.ok))
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/ui/widgets/Widgets.kt b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/ui/widgets/Widgets.kt
new file mode 100644
index 0000000000..073c4d8a99
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/ui/widgets/Widgets.kt
@@ -0,0 +1,84 @@
+package com.k2fsa.sherpa.onnx.tts.engine.ui.widgets
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.core.content.ContextCompat
+import com.google.accompanist.systemuicontroller.rememberSystemUiController
+
+
+@Composable
+fun SetupSystemBars() {
+ val systemUiController = rememberSystemUiController()
+ val useDarkIcons = !isSystemInDarkTheme()
+ SideEffect {
+ systemUiController.setSystemBarsColor(
+ color = Color.Transparent,
+ darkIcons = useDarkIcons,
+ )
+ }
+}
+
+@Composable
+fun BasicBroadcastReceiver(
+ intentFilter: IntentFilter,
+ onReceive: (Intent?) -> Unit,
+ onRegister: (BroadcastReceiver, Context) -> Unit,
+ onUnregister: (BroadcastReceiver, Context) -> Unit
+) {
+ val context = LocalContext.current
+ val currentReceive by rememberUpdatedState(onReceive)
+
+ DisposableEffect(context, intentFilter) {
+ val receiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ currentReceive(intent)
+ }
+ }
+ onRegister(receiver, context)
+
+ onDispose {
+ onUnregister(receiver, context)
+ }
+ }
+}
+
+/*@Composable
+fun LocalBroadcastReceiver(intentFilter: IntentFilter, onReceive: (Intent?) -> Unit) {
+ BasicBroadcastReceiver(
+ intentFilter,
+ onReceive,
+ { obj, context ->
+ LocalBroadcastManager.getInstance(context).registerReceiver(obj, intentFilter)
+ },
+ { obj, context -> LocalBroadcastManager.getInstance(context).unregisterReceiver(obj) }
+ )
+}*/
+
+@Composable
+fun SystemBroadcastReceiver(
+ intentFilter: IntentFilter,
+ onSystemEvent: (intent: Intent?) -> Unit
+) {
+ BasicBroadcastReceiver(
+ intentFilter = intentFilter, onReceive = onSystemEvent,
+ onRegister = { obj, context ->
+ ContextCompat.registerReceiver(
+ context,
+ obj,
+ intentFilter,
+ ContextCompat.RECEIVER_EXPORTED
+ )
+ },
+ onUnregister = { obj, context -> context.unregisterReceiver(obj) }
+ )
+}
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/ClipBoardUtils.kt b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/ClipBoardUtils.kt
new file mode 100644
index 0000000000..b9049596eb
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/ClipBoardUtils.kt
@@ -0,0 +1,96 @@
+package com.k2fsa.sherpa.onnx.tts.engine.utils
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.ClipboardManager.OnPrimaryClipChangedListener
+import android.content.Context
+import com.k2fsa.sherpa.onnx.tts.engine.App
+
+
+/**
+ *
+ * author: Blankj
+ * blog : http://blankj.com
+ * time : 2016/09/25
+ * desc : utils about clipboard
+
*
+ */
+object ClipboardUtils {
+ /**
+ * Copy the text to clipboard.
+ *
+ * The label equals name of package.
+ *
+ * @param text The text.
+ */
+ fun copyText(text: CharSequence?) {
+ val cm = App.instance.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ cm.setPrimaryClip(ClipData.newPlainText(App.instance.getPackageName(), text))
+ }
+
+ /**
+ * Copy the text to clipboard.
+ *
+ * @param label The label.
+ * @param text The text.
+ */
+ fun copyText(label: CharSequence?, text: CharSequence?) {
+ val cm = App.instance.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ cm.setPrimaryClip(ClipData.newPlainText(label, text))
+ }
+
+ /**
+ * Clear the clipboard.
+ */
+ fun clear() {
+ val cm = App.instance.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ cm.setPrimaryClip(ClipData.newPlainText(null, ""))
+ }
+
+ /**
+ * Return the label for clipboard.
+ *
+ * @return the label for clipboard
+ */
+ fun getLabel(): CharSequence {
+ val cm = App.instance
+ .getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val des = cm.primaryClipDescription ?: return ""
+ return des.label ?: return ""
+ }
+
+ /**
+ * Return the text for clipboard.
+ *
+ * @return the text for clipboard
+ */
+ val text: CharSequence
+ get() {
+ val cm =
+ App.instance.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val clip = cm.primaryClip
+ if (clip != null && clip.itemCount > 0) {
+ val text = clip.getItemAt(0).coerceToText(App.instance)
+ if (text != null) {
+ return text
+ }
+ }
+ return ""
+ }
+
+ /**
+ * Add the clipboard changed listener.
+ */
+ fun addChangedListener(listener: OnPrimaryClipChangedListener?) {
+ val cm = App.instance.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ cm.addPrimaryClipChangedListener(listener)
+ }
+
+ /**
+ * Remove the clipboard changed listener.
+ */
+ fun removeChangedListener(listener: OnPrimaryClipChangedListener?) {
+ val cm = App.instance.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ cm.removePrimaryClipChangedListener(listener)
+ }
+}
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/CompressUtils.kt b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/CompressUtils.kt
new file mode 100644
index 0000000000..d94067d72c
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/CompressUtils.kt
@@ -0,0 +1,81 @@
+package com.k2fsa.sherpa.onnx.tts.engine.utils
+
+import android.util.Log
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.withContext
+import org.apache.commons.compress.archivers.ArchiveEntry
+import org.apache.commons.compress.archivers.ArchiveInputStream
+import org.apache.commons.io.FileUtils
+import java.io.File
+import kotlin.coroutines.coroutineContext
+
+
+object CompressUtils {
+ const val TAG = "CompressUtils"
+
+ fun interface ProgressListener {
+ fun onEntryProgress(name: String, entrySize: Long, bytes: Long)
+ }
+
+ suspend fun ArchiveInputStream<*>.uncompress(
+ outputDir: String,
+ progressListener: ProgressListener
+ ) {
+ createFile(outputDir, "").mkdirs()
+ var totalBytes = 0L
+
+ var entry: ArchiveEntry
+ try {
+ while (nextEntry.also { entry = it } != null) {
+ totalBytes += entry.size
+ val file = createFile(outputDir, entry.name)
+
+ if (entry.isDirectory) {
+ file.mkdirs()
+ } else {
+ withContext(Dispatchers.IO) {
+ if (file.exists()) {
+ file.delete()
+ } else {
+ FileUtils.createParentDirectories(file)
+ file.createNewFile()
+ }
+ }
+
+ file.outputStream().use { out ->
+ val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
+ var bytes = 0L
+ var len = 0
+ while (read(buffer).also { len = it } != -1) {
+ if (!coroutineContext.isActive) {
+ throw CancellationException()
+ }
+
+ out.write(buffer, 0, len)
+ bytes += len
+ progressListener.onEntryProgress(entry.name, entry.size, bytes)
+ }
+ }
+ }
+ }
+ } catch (_: NullPointerException) {
+ }
+ }
+
+
+ private fun createFile(outputDir: String, name: String): File {
+ return File(outputDir + File.separator + name)
+ }
+
+ suspend fun deepGetFiles(file: File, onFile: suspend (File) -> Unit) {
+ if (file.isDirectory) {
+ file.listFiles()?.forEach {
+ deepGetFiles(it, onFile)
+ }
+ } else {
+ onFile(file)
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/Compressor.kt b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/Compressor.kt
new file mode 100644
index 0000000000..5e9422a69b
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/Compressor.kt
@@ -0,0 +1,157 @@
+package com.k2fsa.sherpa.onnx.tts.engine.utils
+
+import com.k2fsa.sherpa.onnx.tts.engine.utils.CompressUtils.uncompress
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.isActive
+import org.apache.commons.compress.archivers.ArchiveEntry
+import org.apache.commons.compress.archivers.ArchiveInputStream
+import org.apache.commons.compress.archivers.ArchiveOutputStream
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream
+import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
+import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream
+import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
+import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream
+import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream
+import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream
+import org.apache.commons.compress.compressors.xz.XZCompressorInputStream
+import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream
+import java.io.File
+import java.io.InputStream
+import java.io.OutputStream
+import kotlin.coroutines.coroutineContext
+
+object CompressorFactory {
+ val compressors = listOf(
+ TarBzip2Compressor(),
+ ZipCompressor(),
+ TarGzipCompressor(),
+ TarXzCompressor()
+ )
+
+ fun createCompressor(type: String): CompressorInterface? {
+ for (compressor in compressors) {
+ if (compressor.verifyType(type)) {
+ return compressor
+ }
+ }
+ return null
+ }
+}
+
+interface CompressorInterface {
+ fun verifyType(type: String): Boolean
+
+ suspend fun uncompress(
+ ins: InputStream,
+ outputDir: String,
+ progressListener: CompressUtils.ProgressListener
+ )
+
+ suspend fun compress(
+ dir: String,
+ ous: OutputStream,
+ progressListener: CompressUtils.ProgressListener
+ )
+}
+
+abstract class ImplCompressor(open val extName: List) :
+ CompressorInterface {
+ override fun verifyType(type: String): Boolean {
+ return extName.any { it.lowercase() == type.lowercase() }
+ }
+
+ abstract fun archiveInputStream(ins: InputStream): ArchiveInputStream<*>
+ open fun archiveOutputStream(outs: OutputStream): ArchiveOutputStream {
+ TODO()
+ }
+
+ override suspend fun uncompress(
+ ins: InputStream,
+ outputDir: String,
+ progressListener: CompressUtils.ProgressListener
+ ) {
+ archiveInputStream(ins).use { arIn ->
+ arIn.uncompress(outputDir, progressListener)
+ }
+ }
+
+ override suspend fun compress(
+ dir: String, ous: OutputStream, progressListener: CompressUtils.ProgressListener
+ ) {
+ archiveOutputStream(ous).use { it ->
+ CompressUtils.deepGetFiles(File(dir)) { file ->
+ if (!coroutineContext.isActive) throw CancellationException()
+
+ val entry = it.createArchiveEntry(file, file.relativeTo(File(dir)).path)!!
+ it.putArchiveEntry(entry)
+ file.inputStream().use { ins ->
+ val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
+ var len = 0
+ var bytes = 0
+ while (ins.read(buffer).also { len = it } != -1) {
+ if (!coroutineContext.isActive) throw CancellationException()
+
+ it.write(buffer, 0, len)
+ bytes += len
+ progressListener.onEntryProgress(entry.name, entry.size, bytes.toLong())
+ }
+ }
+ it.closeArchiveEntry()
+ }
+
+ }
+ }
+}
+
+class TarBzip2Compressor : ImplCompressor(listOf("tar.bz2", "tbz2")) {
+ override fun archiveInputStream(ins: InputStream): ArchiveInputStream<*> =
+ TarArchiveInputStream(BZip2CompressorInputStream(ins))
+}
+
+class ZipCompressor : ImplCompressor(listOf("zip")) {
+ override fun archiveInputStream(ins: InputStream): ArchiveInputStream<*> =
+ ZipArchiveInputStream(ins)
+
+ override fun archiveOutputStream(outs: OutputStream): ArchiveOutputStream {
+ return ZipArchiveOutputStream(outs)
+ }
+
+// override suspend fun compress(dir: String, ous: OutputStream) {
+// val file = File(dir)
+//
+// ZipArchiveOutputStream(ous).use { zipOus ->
+// CompressUtils.deepGetFiles(file) {
+// if (!coroutineContext.isActive) throw CancellationException()
+//
+// val relPath = it.relativeTo(file).path
+// Log.e("TAG", it.absolutePath)
+// val entry = zipOus.createArchiveEntry(it, relPath)
+// entry.size = it.length()
+// zipOus.putArchiveEntry(entry)
+// it.inputStream().use { ins ->
+// ins.copyTo(zipOus)
+// }
+// zipOus.closeArchiveEntry()
+// }
+// zipOus.finish()
+// }
+// }
+}
+
+class TarGzipCompressor : ImplCompressor(listOf("tar.gz", "tgz")) {
+ override fun archiveInputStream(ins: InputStream): ArchiveInputStream<*> =
+ TarArchiveInputStream(GzipCompressorInputStream(ins))
+
+ override fun archiveOutputStream(outs: OutputStream): ArchiveOutputStream =
+ TarArchiveOutputStream(GzipCompressorOutputStream(outs))
+}
+
+class TarXzCompressor : ImplCompressor(listOf("tar.xz", "txz")) {
+ override fun archiveInputStream(ins: InputStream): ArchiveInputStream<*> =
+ TarArchiveInputStream(XZCompressorInputStream(ins))
+
+ override fun archiveOutputStream(outs: OutputStream): ArchiveOutputStream =
+ TarArchiveOutputStream(XZCompressorOutputStream(outs))
+}
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/ExtensionUtils.kt b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/ExtensionUtils.kt
new file mode 100644
index 0000000000..7d7b16ff8f
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/ExtensionUtils.kt
@@ -0,0 +1,406 @@
+package com.k2fsa.sherpa.onnx.tts.engine.utils
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.app.Notification
+import android.app.Service
+import android.content.BroadcastReceiver
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.ServiceInfo
+import android.content.res.Resources
+import android.graphics.Rect
+import android.graphics.Typeface
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.SystemClock
+import android.text.Spanned
+import android.text.style.ForegroundColorSpan
+import android.text.style.StyleSpan
+import android.text.style.UnderlineSpan
+import android.util.DisplayMetrics
+import android.view.HapticFeedbackConstants
+import android.view.View
+import android.view.WindowInsets
+import android.view.WindowManager
+import android.view.WindowMetrics
+import androidx.activity.OnBackPressedCallback
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.core.content.ContextCompat
+import androidx.core.net.toUri
+import androidx.lifecycle.LifecycleOwner
+import androidx.navigation.NavController
+import androidx.navigation.NavDeepLinkRequest
+import androidx.navigation.NavDestination
+import androidx.navigation.NavHostController
+import androidx.navigation.NavOptions
+import androidx.navigation.Navigator
+
+
+@SuppressLint("RestrictedApi")
+fun NavController.navigate(
+ route: String,
+ argsBuilder: Bundle.() -> Unit = {},
+ navOptions: NavOptions? = null,
+ navigatorExtras: Navigator.Extras? = null
+) {
+ navigate(route, Bundle().apply(argsBuilder), navOptions, navigatorExtras)
+}
+
+/*
+* 可传递 Bundle 到 Navigation
+* */
+@SuppressLint("RestrictedApi")
+fun NavController.navigate(
+ route: String,
+ args: Bundle,
+ navOptions: NavOptions? = null,
+ navigatorExtras: Navigator.Extras? = null
+) {
+ val routeLink = NavDeepLinkRequest
+ .Builder
+ .fromUri(NavDestination.createRoute(route).toUri())
+ .build()
+
+ val deepLinkMatch = graph.matchDeepLink(routeLink)
+ if (deepLinkMatch != null) {
+ val destination = deepLinkMatch.destination
+ val id = destination.id
+ navigate(id, args, navOptions, navigatorExtras)
+ } else {
+ navigate(route, navOptions, navigatorExtras)
+ }
+}
+
+/**
+ * 单例并清空其他栈
+ */
+fun NavHostController.navigateSingleTop(
+ route: String,
+ args: Bundle? = null,
+ popUpToMain: Boolean = false
+) {
+ val navController = this
+ val navOptions = NavOptions.Builder()
+ .setLaunchSingleTop(true)
+ .apply {
+ if (popUpToMain) setPopUpTo(
+ navController.graph.startDestinationId,
+ inclusive = false,
+ saveState = true
+ )
+ }
+ .setRestoreState(true)
+ .build()
+ if (args == null)
+ navController.navigate(route, navOptions)
+ else
+ navController.navigate(route, args, navOptions)
+}
+
+fun Long.formatFileSize(context: Context): String =
+ android.text.format.Formatter.formatFileSize(context, this)
+
+fun FloatArray.toByteArray(): ByteArray {
+ // byteArray is actually a ShortArray
+ val byteArray = ByteArray(this.size * 2)
+ for (i in this.indices) {
+ val sample = (this[i] * 32767).toInt()
+ byteArray[2 * i] = sample.toByte()
+ byteArray[2 * i + 1] = (sample shr 8).toByte()
+ }
+ return byteArray
+}
+
+fun Service.startForegroundCompat(
+ notificationId: Int,
+ notification: Notification
+) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // A14
+ startForeground(
+ notificationId,
+ notification,
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
+ )
+ } else {
+ startForeground(notificationId, notification)
+ }
+}
+
+@Composable
+fun Modifier.simpleVerticalScrollbar(
+ state: LazyListState,
+ width: Dp = 8.dp,
+ color: Color = MaterialTheme.colorScheme.secondary
+): Modifier {
+ val targetAlpha = if (state.isScrollInProgress) 1f else 0f
+ val duration = if (state.isScrollInProgress) 150 else 500
+
+ val alpha by animateFloatAsState(
+ targetValue = targetAlpha,
+ animationSpec = tween(durationMillis = duration), label = ""
+ )
+
+ return drawWithContent {
+ drawContent()
+
+ val firstVisibleElementIndex = state.layoutInfo.visibleItemsInfo.firstOrNull()?.index
+ val needDrawScrollbar = state.isScrollInProgress || alpha > 0.0f
+
+ // Draw scrollbar if scrolling or if the animation is still running and lazy column has content
+ if (needDrawScrollbar && firstVisibleElementIndex != null) {
+ val elementHeight = this.size.height / state.layoutInfo.totalItemsCount
+
+ val scrollbarOffsetY =
+ firstVisibleElementIndex * elementHeight + state.firstVisibleItemScrollOffset / 4
+
+// val scrollbarOffsetY = firstVisibleElementIndex * elementHeight
+ val scrollbarHeight = state.layoutInfo.visibleItemsInfo.size * elementHeight
+
+ drawRect(
+ color = color,
+ topLeft = Offset(this.size.width - width.toPx(), scrollbarOffsetY),
+ size = Size(width.toPx(), scrollbarHeight),
+ alpha = alpha
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun Modifier.clickableRipple(
+ enabled: Boolean = true,
+ role: Role? = null,
+ onLongClick: (() -> Unit)? = null,
+ onLongClickLabel: String? = null,
+ onClickLabel: String? = null,
+ onClick: () -> Unit,
+) =
+ this.combinedClickable(
+ enabled = enabled,
+ role = role,
+ indication = rememberRipple(),
+ interactionSource = remember { MutableInteractionSource() },
+ onClickLabel = onClickLabel,
+ onClick = onClick,
+ onLongClick = onLongClick,
+ onLongClickLabel = onLongClickLabel,
+ )
+
+fun Spanned.toAnnotatedString(): AnnotatedString = buildAnnotatedString {
+ val spanned = this@toAnnotatedString
+ append(spanned.toString())
+ getSpans(0, spanned.length, Any::class.java).forEach { span ->
+ val start = getSpanStart(span)
+ val end = getSpanEnd(span)
+ when (span) {
+ is StyleSpan -> when (span.style) {
+ Typeface.BOLD -> addStyle(SpanStyle(fontWeight = FontWeight.Bold), start, end)
+ Typeface.ITALIC -> addStyle(SpanStyle(fontStyle = FontStyle.Italic), start, end)
+ Typeface.BOLD_ITALIC -> addStyle(
+ SpanStyle(
+ fontWeight = FontWeight.Bold,
+ fontStyle = FontStyle.Italic
+ ), start, end
+ )
+ }
+
+ is UnderlineSpan -> addStyle(
+ SpanStyle(textDecoration = TextDecoration.Underline),
+ start,
+ end
+ )
+
+ is ForegroundColorSpan -> addStyle(
+ SpanStyle(color = Color(span.foregroundColor)),
+ start,
+ end
+ )
+ }
+ }
+}
+
+fun Context.registerGlobalReceiver(
+ actions: List,
+ receiver: BroadcastReceiver
+) {
+ ContextCompat.registerReceiver(this, receiver, IntentFilter().apply {
+ actions.forEach { addAction(it) }
+ }, ContextCompat.RECEIVER_EXPORTED)
+}
+
+fun View.performLongPress() {
+ this.isHapticFeedbackEnabled = true
+ this.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
+}
+
+fun Context.startActivity(clz: Class<*>) {
+ startActivity(Intent(this, clz).apply { action = Intent.ACTION_VIEW })
+}
+
+fun Uri.grantReadPermission(contentResolver: ContentResolver) {
+ contentResolver.takePersistableUriPermission(
+ this,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION
+ )
+}
+
+fun Uri.grantReadWritePermission(contentResolver: ContentResolver) {
+ contentResolver.takePersistableUriPermission(
+ this,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ )
+}
+
+//fun Intent.getBinder(): IBinder? {
+// val bundle = getBundleExtra(KeyConst.KEY_BUNDLE)
+// return bundle?.getBinder(KeyConst.KEY_LARGE_DATA_BINDER)
+//}
+//
+//fun Intent.setBinder(binder: IBinder) {
+// putExtra(
+// KeyConst.KEY_BUNDLE,
+// Bundle().apply {
+// putBinder(KeyConst.KEY_LARGE_DATA_BINDER, binder)
+// })
+//}
+//
+//val Int.dp: Int get() = SizeUtils.dp2px(this.toFloat())
+//
+//val Int.px: Int get() = SizeUtils.px2dp(this.toFloat())
+
+
+/**
+ * 重启当前 Activity
+ */
+fun Activity.restart() {
+ finish()
+ ContextCompat.startActivity(this, intent, null)
+}
+
+val WindowManager.windowSize: DisplayMetrics
+ get() {
+ val displayMetrics = DisplayMetrics()
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ val windowMetrics: WindowMetrics = currentWindowMetrics
+ val insets = windowMetrics.windowInsets
+ .getInsetsIgnoringVisibility(WindowInsets.Type.systemBars())
+ displayMetrics.widthPixels = windowMetrics.bounds.width() - insets.left - insets.right
+ displayMetrics.heightPixels = windowMetrics.bounds.height() - insets.top - insets.bottom
+ } else {
+ @Suppress("DEPRECATION")
+ defaultDisplay.getMetrics(displayMetrics)
+ }
+ return displayMetrics
+ }
+
+@Suppress("DEPRECATION")
+val Activity.displayHeight: Int
+ get() {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ val windowMetrics = windowManager.currentWindowMetrics
+ val insets = windowMetrics.windowInsets.getInsetsIgnoringVisibility(
+ WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout()
+ )
+ windowMetrics.bounds.height() - insets.bottom - insets.top
+ } else
+ windowManager.defaultDisplay.height
+ }
+
+/**
+ * 点击防抖动
+ */
+fun View.clickWithThrottle(throttleTime: Long = 600L, action: (v: View) -> Unit) {
+ this.setOnClickListener(object : View.OnClickListener {
+ private var lastClickTime: Long = 0
+
+ override fun onClick(v: View) {
+ if (SystemClock.elapsedRealtime() - lastClickTime < throttleTime) return
+ else action(v)
+
+ lastClickTime = SystemClock.elapsedRealtime()
+ }
+ })
+}
+
+/**
+ * View 是否在屏幕上可见
+ */
+fun View.isVisibleOnScreen(): Boolean {
+ if (!isShown) {
+ return false
+ }
+ val actualPosition = Rect()
+ val isGlobalVisible = getGlobalVisibleRect(actualPosition)
+ val screenWidth = Resources.getSystem().displayMetrics.widthPixels
+ val screenHeight = Resources.getSystem().displayMetrics.heightPixels
+ val screen = Rect(0, 0, screenWidth, screenHeight)
+ return isGlobalVisible && Rect.intersects(actualPosition, screen)
+}
+
+
+/**
+ * 绑定返回键回调(建议使用该方法)
+ * @param owner Receive callbacks to a new OnBackPressedCallback when the given LifecycleOwner is at least started.
+ * This will automatically call addCallback(OnBackPressedCallback) and remove the callback as the lifecycle state changes. As a corollary, if your lifecycle is already at least started, calling this method will result in an immediate call to addCallback(OnBackPressedCallback).
+ * When the LifecycleOwner is destroyed, it will automatically be removed from the list of callbacks. The only time you would need to manually call OnBackPressedCallback.remove() is if you'd like to remove the callback prior to destruction of the associated lifecycle.
+ * @param onBackPressed 回调方法;返回true则表示消耗了按键事件,事件不会继续往下传递,相反返回false则表示没有消耗,事件继续往下传递
+ * @return 注册的回调对象,如果想要移除注册的回调,直接通过调用[OnBackPressedCallback.remove]方法即可。
+ */
+fun androidx.activity.ComponentActivity.addOnBackPressed(
+ owner: LifecycleOwner,
+ onBackPressed: () -> Boolean
+): OnBackPressedCallback {
+ return backPressedCallback(onBackPressed).also {
+ onBackPressedDispatcher.addCallback(owner, it)
+ }
+}
+
+/**
+ * 绑定返回键回调,未关联生命周期,建议使用关联生命周期的办法(尤其在fragment中使用,应该关联fragment的生命周期)
+ */
+fun androidx.activity.ComponentActivity.addOnBackPressed(onBackPressed: () -> Boolean): OnBackPressedCallback {
+ return backPressedCallback(onBackPressed).also {
+ onBackPressedDispatcher.addCallback(it)
+ }
+}
+
+private fun androidx.activity.ComponentActivity.backPressedCallback(onBackPressed: () -> Boolean): OnBackPressedCallback {
+ return object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ if (!onBackPressed()) {
+ isEnabled = false
+ onBackPressedDispatcher.onBackPressed()
+ isEnabled = true
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/FileUtils.kt b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/FileUtils.kt
new file mode 100644
index 0000000000..9a0b574725
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/FileUtils.kt
@@ -0,0 +1,28 @@
+package com.k2fsa.sherpa.onnx.tts.engine.utils
+
+import android.content.Context
+import android.net.Uri
+import androidx.documentfile.provider.DocumentFile
+import java.io.File
+
+
+fun Uri.fileName(context: Context): String {
+ var name: String? = null
+ scheme?.let {
+ when (it) {
+ "content" -> {
+ name = DocumentFile.fromSingleUri(context, this)?.name
+ }
+
+ "file" -> {
+ path?.let {
+ name = File(it).name
+ }
+ }
+
+ else -> {}
+ }
+ }
+
+ return name ?: ""
+}
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/HandlerUtils.kt b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/HandlerUtils.kt
new file mode 100644
index 0000000000..1dee382b13
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/HandlerUtils.kt
@@ -0,0 +1,51 @@
+@file:Suppress("unused")
+/* https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/utils/HandlerUtils.kt */
+package com.k2fsa.sherpa.onnx.tts.engine.utils
+
+import android.os.Build.VERSION.SDK_INT
+import android.os.Handler
+import android.os.Looper
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+
+/** This main looper cache avoids synchronization overhead when accessed repeatedly. */
+private val mainLooper: Looper = Looper.getMainLooper()
+
+private val mainThread: Thread = mainLooper.thread
+
+private val isMainThread: Boolean inline get() = mainThread === Thread.currentThread()
+
+fun buildMainHandler(): Handler {
+ return if (SDK_INT >= 28) Handler.createAsync(mainLooper) else try {
+ Handler::class.java.getDeclaredConstructor(
+ Looper::class.java,
+ Handler.Callback::class.java,
+ Boolean::class.javaPrimitiveType // async
+ ).newInstance(mainLooper, null, true)
+ } catch (ignored: NoSuchMethodException) {
+ // Hidden constructor absent. Fall back to non-async constructor.
+ Handler(mainLooper)
+ }
+}
+
+private val mainHandler by lazy { buildMainHandler() }
+
+fun runOnUI(function: () -> Unit) {
+ if (isMainThread) {
+ function()
+ } else {
+ mainHandler.post(function)
+ }
+}
+
+fun CoroutineScope.runOnIO(function: suspend () -> Unit) {
+ if (isMainThread) {
+ launch(IO) {
+ function()
+ }
+ } else {
+ runBlocking { function() }
+ }
+}
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/LocaleUtils.kt b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/LocaleUtils.kt
new file mode 100644
index 0000000000..71ee5009dd
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/LocaleUtils.kt
@@ -0,0 +1,77 @@
+package com.k2fsa.sherpa.onnx.tts.engine.utils
+
+import java.util.Locale
+
+object LocaleUtils {
+
+}
+
+val Locale.mustIso3Language: String
+ get() = try {
+ this.isO3Language
+ } catch (e: Exception) {
+ ""
+ }
+
+val Locale.mustIso3Country: String
+ get() = try {
+ this.isO3Country
+ } catch (e: Exception) {
+ ""
+ }
+
+val Locale.mustVariant: String
+ get() = try {
+ this.variant
+ } catch (e: Exception) {
+ ""
+ }
+
+fun Locale.toIso3Code(): String {
+ val lang = mustIso3Language
+ val country = mustIso3Country
+ val variant = mustVariant
+
+ return when {
+ lang.isNotEmpty() && country.isNotEmpty() && variant.isNotEmpty() -> "$lang-$country-$variant"
+ lang.isNotEmpty() && country.isNotEmpty() -> "$lang-$country"
+ lang.isNotEmpty() -> lang
+ else -> ""
+ }
+}
+
+fun String.toLocaleFromIso3(): Locale? {
+ return Locale.getAvailableLocales().find { it.toIso3Code() == this }?.let {
+ return it
+ }
+}
+
+fun Locale.equalsIso3(
+ iso3Lang: String,
+ iso3Country: String = "",
+ iso3Variant: String = ""
+): Boolean {
+ return this.mustIso3Language == iso3Lang &&
+ this.mustIso3Country == iso3Country &&
+ this.mustVariant == iso3Variant
+}
+
+fun String.toLocale(): Locale {
+ val parts = split("-")
+
+ return when (parts.size) {
+ 1 -> Locale(parts[0])
+ 2 -> Locale(parts[0], parts[1])
+ else -> Locale(this)
+ }
+}
+
+fun newLocaleFromCode(code: String): Locale = code.toLocale()
+
+fun Locale.toCode(): String {
+ return try {
+ "$language-$country"
+ } catch (e: Exception) {
+ language
+ }.trimEnd('-')
+}
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/NotificationUtils.kt b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/NotificationUtils.kt
new file mode 100644
index 0000000000..b5004124f8
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/NotificationUtils.kt
@@ -0,0 +1,80 @@
+package com.k2fsa.sherpa.onnx.tts.engine.utils
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import androidx.annotation.RequiresApi
+import com.k2fsa.sherpa.onnx.tts.engine.R
+import com.k2fsa.sherpa.onnx.tts.engine.app
+import com.k2fsa.sherpa.onnx.tts.engine.ui.MainActivity
+import java.util.concurrent.atomic.AtomicLong
+
+
+val pendingIntentFlags =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ PendingIntent.FLAG_UPDATE_CURRENT or
+ PendingIntent.FLAG_MUTABLE or
+ PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
+ } else {
+ PendingIntent.FLAG_UPDATE_CURRENT
+ }
+
+val notificationManager
+ get() = app.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+
+@Suppress("DEPRECATION")
+object NotificationUtils {
+ const val UNSPECIFIED_ID = -1
+
+ private val mAtomLong = AtomicLong(0)
+
+ @Synchronized
+ fun nextNotificationId(): Int = mAtomLong.incrementAndGet().toInt()
+
+ fun Context.notificationBuilder(channelId: String): Notification.Builder =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ Notification.Builder(this, channelId)
+ } else
+ Notification.Builder(this)
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ fun createChannel(
+ id: String, name: String, importance: Int = NotificationManager.IMPORTANCE_HIGH
+ ) {
+ val chan = NotificationChannel(id, name, importance)
+ chan.lightColor = android.graphics.Color.CYAN
+ chan.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
+ notificationManager.createNotificationChannel(chan)
+ }
+
+ fun Context.sendNotification(
+ notificationId: Int = nextNotificationId(),
+ channelId: String,
+ title: String,
+ content: String = "",
+ ) {
+ notificationManager.notify(
+ notificationId,
+ notificationBuilder(channelId).apply {
+ setContentTitle(title)
+ setContentText(content)
+ setSmallIcon(R.mipmap.ic_launcher)
+ setAutoCancel(true)
+ setContentIntent(
+ PendingIntent.getActivity(
+ this@sendNotification,
+ 0,
+ Intent(this@sendNotification, MainActivity::class.java),
+ pendingIntentFlags
+ )
+ )
+ }.build()
+ )
+ }
+}
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/PcmAudioPlayer.kt b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/PcmAudioPlayer.kt
new file mode 100644
index 0000000000..3149cf9398
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/PcmAudioPlayer.kt
@@ -0,0 +1,110 @@
+package com.k2fsa.sherpa.onnx.tts.engine.utils
+
+import android.media.AudioFormat
+import android.media.AudioManager
+import android.media.AudioTrack
+import android.media.AudioTrack.PLAYSTATE_PLAYING
+import android.util.Log
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import java.io.InputStream
+import kotlin.coroutines.coroutineContext
+
+class PcmAudioPlayer {
+ companion object {
+ private const val TAG = "AudioTrackPlayer"
+ }
+
+ private var audioTrack: AudioTrack? = null
+ private var currentSampleRate = 16000
+
+ @Suppress("DEPRECATION")
+ private fun createAudioTrack(sampleRate: Int = 16000): AudioTrack {
+ val mSampleRate = if (sampleRate == 0) 16000 else sampleRate
+ Log.d(TAG, "createAudioTrack: sampleRate=$mSampleRate")
+
+ val bufferSize = AudioTrack.getMinBufferSize(
+ mSampleRate,
+ AudioFormat.CHANNEL_OUT_MONO,
+ AudioFormat.ENCODING_PCM_16BIT
+ )
+ return AudioTrack(
+ AudioManager.STREAM_MUSIC,
+ mSampleRate,
+ AudioFormat.CHANNEL_OUT_MONO,
+ AudioFormat.ENCODING_PCM_16BIT,
+ bufferSize,
+ AudioTrack.MODE_STREAM
+ )
+ }
+
+ suspend fun play(inputStream: InputStream, sampleRate: Int = currentSampleRate) {
+ val bufferSize = AudioTrack.getMinBufferSize(
+ sampleRate,
+ AudioFormat.CHANNEL_OUT_MONO,
+ AudioFormat.ENCODING_PCM_16BIT
+ )
+ inputStream.readPcmChunk(chunkSize = bufferSize) { data ->
+ play(data, sampleRate)
+ }
+ }
+
+ @Synchronized
+ fun play(audioData: ByteArray, sampleRate: Int = currentSampleRate) {
+ if (currentSampleRate == sampleRate) {
+ audioTrack = audioTrack ?: createAudioTrack(sampleRate)
+ } else {
+ audioTrack?.stop()
+ audioTrack?.release()
+ audioTrack = createAudioTrack(sampleRate)
+ currentSampleRate = sampleRate
+ }
+
+ if (audioTrack!!.playState != PLAYSTATE_PLAYING) audioTrack!!.play()
+
+ audioTrack!!.write(audioData, 0, audioData.size)
+ println("play done..")
+ }
+
+
+ fun stop() {
+ audioTrack?.stop()
+ }
+
+ fun release() {
+ audioTrack?.release()
+ }
+
+ suspend fun InputStream.readPcmChunk(
+ bufferSize: Int = 4096,
+ chunkSize: Int = 2048,
+ onRead: suspend (ByteArray) -> Unit
+ ) {
+ var bufferFilledCount = 0
+ val buffer = ByteArray(bufferSize)
+
+ while (coroutineContext.isActive) {
+ val readLen =
+ this.read(buffer, bufferFilledCount, chunkSize - bufferFilledCount)
+ if (readLen == -1) {
+ if (bufferFilledCount > 0) {
+ val chunkData = buffer.copyOfRange(0, bufferFilledCount)
+ onRead.invoke(chunkData)
+ }
+ break
+ }
+ if (readLen == 0) {
+ delay(100)
+ continue
+ }
+
+ bufferFilledCount += readLen
+ if (bufferFilledCount >= chunkSize) {
+ val chunkData = buffer.copyOfRange(0, chunkSize)
+
+ onRead.invoke(chunkData)
+ bufferFilledCount = 0
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/ThrottleUtil.kt b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/ThrottleUtil.kt
new file mode 100644
index 0000000000..b5a08643b3
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/ThrottleUtil.kt
@@ -0,0 +1,20 @@
+package com.k2fsa.sherpa.onnx.tts.engine.utils
+
+import kotlinx.coroutines.*
+
+@OptIn(DelicateCoroutinesApi::class)
+class ThrottleUtil(private val scope: CoroutineScope = GlobalScope, val time: Long = 100L) {
+ var job: Job? = null
+
+ fun runAction(
+ dispatcher: CoroutineDispatcher = Dispatchers.Main,
+ action: suspend () -> Unit,
+ ) {
+ job?.cancel()
+ job = null
+ job = scope.launch(dispatcher) {
+ delay(time)
+ action.invoke()
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/ToastUtils.kt b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/ToastUtils.kt
new file mode 100644
index 0000000000..c402256e49
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/java/com/k2fsa/sherpa/onnx/tts/engine/utils/ToastUtils.kt
@@ -0,0 +1,49 @@
+@file:Suppress("unused")
+/* https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/utils/ToastUtils.kt */
+package com.k2fsa.sherpa.onnx.tts.engine.utils
+
+import android.content.Context
+import android.widget.Toast
+import androidx.annotation.StringRes
+import androidx.fragment.app.Fragment
+
+fun Context.toast(@StringRes message: Int, vararg args: Any) {
+ runOnUI {
+ kotlin.runCatching {
+ Toast.makeText(this, getString(message, *args), Toast.LENGTH_SHORT).show()
+ }
+ }
+}
+
+fun Context.toast(message: CharSequence?) {
+ runOnUI {
+ kotlin.runCatching {
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
+ }
+ }
+}
+
+fun Context.longToast(@StringRes message: Int, vararg args: Any) {
+ runOnUI {
+ kotlin.runCatching {
+ Toast.makeText(this, getString(message, *args), Toast.LENGTH_LONG).show()
+ }
+ }
+}
+
+fun Context.longToast(message: CharSequence?) {
+ runOnUI {
+ kotlin.runCatching {
+ Toast.makeText(this, message, Toast.LENGTH_LONG).show()
+ }
+ }
+}
+
+
+fun Fragment.toast(@StringRes message: Int) = requireActivity().toast(message)
+
+fun Fragment.toast(message: CharSequence) = requireActivity().toast(message)
+
+fun Fragment.longToast(@StringRes message: Int) = requireContext().longToast(message)
+
+fun Fragment.longToast(message: CharSequence) = requireContext().longToast(message)
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/jniLibs/arm64-v8a/.gitkeep b/android/SherpaOnnxTtsEngine-NG/app/src/main/jniLibs/arm64-v8a/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/jniLibs/armeabi-v7a/.gitkeep b/android/SherpaOnnxTtsEngine-NG/app/src/main/jniLibs/armeabi-v7a/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/jniLibs/x86/.gitkeep b/android/SherpaOnnxTtsEngine-NG/app/src/main/jniLibs/x86/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/jniLibs/x86_64/.gitkeep b/android/SherpaOnnxTtsEngine-NG/app/src/main/jniLibs/x86_64/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000000..5d37a4866b
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/res/layout/big_text_view.xml b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/layout/big_text_view.xml
new file mode 100644
index 0000000000..e3818e9ec6
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/layout/big_text_view.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000000..ef49c99170
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000000..ef49c99170
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000000..448417e328
Binary files /dev/null and b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000..289a155579
Binary files /dev/null and b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000000..71decc5448
Binary files /dev/null and b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000..0c35850800
Binary files /dev/null and b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000000..ce9ceeb492
Binary files /dev/null and b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000..b29b684a7d
Binary files /dev/null and b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000000..940bd7e3b8
Binary files /dev/null and b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000..2b30818b14
Binary files /dev/null and b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000000..23accc49f7
Binary files /dev/null and b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000000..1269b99109
Binary files /dev/null and b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/res/values-fa/strings.xml b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/values-fa/strings.xml
new file mode 100644
index 0000000000..9549c0ba73
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/values-fa/strings.xml
@@ -0,0 +1,87 @@
+
+ استماع
+ متن نمونه
+ وارد کردن بسته مدل
+ فهرست خالی است.
+ ویرایش
+ زبان
+ نام نمایش
+ فایل مدل Onnx
+ جستجو
+ در حال بارگیری
+ کپی شد
+ پیکربندی TTS تنظیم نشده است
+ افزودن
+ حذف
+ مدیر مدل
+ تنظیمات
+ حذف نوار جستجو
+ افزودن نوار جستجو
+ بستن
+ موتور
+ مدت زمان تخریب
+ %1$s دقیقه
+ مدل بهطور خودکار پس از اینکه برای مدت زمان مشخصی برای ذخیره حافظه در حال استفاده استفاده نشد، از بین میرود.
+ برای خروج دوباره به عقب فشار دهید
+ آیا میخواهید حذف کنید؟
+ گزینه های بیشتر
+ لغو انتخاب
+ فعال
+ غیرفعال
+ تغییر زبان
+ انتخاب را معکوس کنید
+ هیچ فایلی انتخاب نشده است
+ سرویس فشرده
+ درحال فشردهسازی
+ فشردهشدن به پایان رسید
+ افزودن مدلها
+ ❌ فقط [%1$s] فایل پشتیبانی میشود
+ ❌ دریافت نام فایل ممکن نیست
+ ❌ امکان اعطای مجوز خواندن وجود ندارد
+ کار پسزمینه اضافه شده است، لطفاً پیشرفت را از اعلان سیستم بررسی کنید.
+ در حال کپی کردن
+ واردات تکمیل شد
+ برای مرتبسازی بکشید
+ از %1$s تا %2$s
+ اجازه اعلان رد شده است، لطفاً تنظیمات را وارد کرده و آن را فعال کنید.
+ دانلود مدل
+ در حال بارگیری
+ حرکت فایلها
+ نصب کننده مدل
+ نصب مدل انجام نشد ❌
+ مدل نصب شد
+ وقت تمام شد
+ اندازه حافظه پنهان
+ حداکثر تعداد حافظه پنهان مدل
+ تعداد رشته ها
+ حداکثر تعداد رشتهها در هر مدل
+ کپی صدا
+ بلندگوی پیش فرض
+ بلندگو %1$s
+ مدیر صدا
+ افزودن صدا
+ مدل
+ خطا
+ صداهای تکراری
+ این مدل از بلندگوهای بیشتری پشتیبانی نمیکند
+ هیچ مدلی منتظر اضافه شدن به فهرست نیست
+ مرتبسازی
+ بر اساس نام
+ بر اساس مدل
+ لطفاً انتخاب کنید
+ "پیکربندی متن نمونه برای هر زبان"
+ ویرایش نام
+ بدون مدل، لطفاً از سمت راست بالا اضافه کنید.
+ صدایی وجود ندارد، لطفاً از سمت راست بالا اضافه کنید.
+ صدا
+ پراکسی Github
+ از آدرس اینترنتی پراکسی هنگام دانلود مدل ها از Github استفاده کنید
+ حذف مدل
+ حذف فایل مدل
+ ❌ مدل پیدا نشد: %1$s
+ صادرات
+ صادر نشد ❌
+ در حال فشرده سازی
+ صادرات به پایان رسید
+
+
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/res/values-zh/strings.xml b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/values-zh/strings.xml
new file mode 100644
index 0000000000..dc47b4b221
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/values-zh/strings.xml
@@ -0,0 +1,86 @@
+
+ 试听
+ 示例文本
+ 导入模型包
+ 列表为空。
+ 编辑
+ 语言
+ 显示名称
+ Onnx模型文件
+ 搜索
+ 加载中
+ 已复制
+ TTS配置未设置
+ 添加
+ 删除
+ 模型管理器
+ 设置
+ 滑动条删除
+ 滑动条添加
+ 关闭
+ 引擎
+ 超时销毁
+ %1$s 分钟
+ 模型在未使用一定时间后将自动销毁,以节省运行内存。
+ 再次按返回键退出
+ 您确定要删除吗?
+ 更多选项
+ 取消选择
+ 已启用
+ 已禁用
+ 更改语言
+ 反选
+ 未选择文件
+ 压缩服务
+ 解压中
+ 解压完成
+ 添加模型
+ ❌ 仅支持 [%1$s] 文件
+ ❌ 无法获取文件名
+ ❌ 无法授予读取权限
+ 后台任务已添加,请从系统通知中检查进度。
+ 复制中
+ 导入完成
+ 拖动进行排序
+ 从 %1$s 移动到 %2$s
+ 通知权限已拒绝,请转到设置并启用它。
+ 下载模型
+ 下载中
+ 移动文件中
+ 模型安装程序
+ 模型安装失败 ❌
+ 模型已安装
+ 超时
+ 缓存大小
+ 模型缓存的最大数量
+ 线程数
+ 每个模型的最大线程数
+ 复制语音
+ 默认发言人
+ 发言人 %1$s
+ 语音管理器
+ 添加语音
+ 模型
+ 错误
+ 重复的语音
+ 此模型不支持更多的发言人
+ 目录中没有等待添加的模型
+ 排序
+ 按名称排序
+ 按模型排序
+ 请选择
+ "为每种语言配置示例文本"
+ 编辑名称
+ 没有模型,请从右上角添加。
+ 没有语音,请从右上角添加。
+ 语音
+ Github代理
+ 从Github下载模型时使用代理URL
+ 删除模型
+ 删除模型文件
+ ❌ 未找到模型:%1$s
+ 导出
+ 导出失败 ❌
+ 压缩中
+ 导出完成
+
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/res/values/colors.xml b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..f8c6127d32
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/res/values/ic_launcher_background.xml b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000000..287a668472
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #0b62c2
+
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/res/values/strings.xml b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..5cd7b6e0b4
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/values/strings.xml
@@ -0,0 +1,87 @@
+
+ Next-gen Kaldi: TTS
+ Audition
+ Sample text
+ Import model package
+ List is empty.
+ Edit
+ Language
+ Display name
+ Onnx model file
+ Search
+ Loading
+ Copied
+ TTS Config not set
+ Add
+ Delete
+ Model manager
+ Settings
+ Seekbar remove
+ Seekbar add
+ Close
+ Engine
+ Timeout destruction
+ %1$s min
+ The model is automatically destroyed after it has not been used for a specified period of time to save running memory.
+ Press back again to exit
+ Do you want to delete?
+ More options
+ Cancel select
+ Enabled
+ Disabled
+ Change language
+ Invert select
+ No file selected
+ Compress service
+ Decompressing
+ Decompressing finished
+ Add models
+ ❌ Only [%1$s] files are supported
+ ❌ Unable get file name
+ ❌ Unable to grant read permission
+ The background task has been added, please check the progress from the system notification.
+ Copying
+ Import completed
+ Drag to sort
+ From %1$s to %2$s
+ Notification permission is denied, please goto settings and enable it.
+ Download model
+ Downloading
+ Moving files
+ Model installer
+ Model installation failed ❌
+ Model installed
+ Timed out
+ Cache size
+ Maximum number of model caches
+ Number of threads
+ Maximum number of threads per model
+ Copy voice
+ Default speaker
+ Speaker %1$s
+ Voice manager
+ Add voice
+ Model
+ Error
+ Duplicate voices
+ This model does not support more speakers
+ There are no models waiting to be added in the directory
+ Sort
+ By Name
+ By Model
+ Please select
+ "Configure sample text for each language "
+ Edit name
+ No model, please add from top-right.
+ No voice, please add from top-right.
+ Voice
+ Github proxy
+ Use proxy url when downloading models from Github
+ Delete model
+ Delete model file
+ ❌ Model not found: %1$s
+ Export
+ Export failed ❌
+ Compressing
+ Export finished
+
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/res/values/themes.xml b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000000..012ac97063
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/res/xml/backup_rules.xml b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000000..fa0f996d2c
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/res/xml/data_extraction_rules.xml b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000000..9ee9997b0b
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/main/res/xml/tts_engine.xml b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/xml/tts_engine.xml
new file mode 100644
index 0000000000..9ed8952cfd
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/main/res/xml/tts_engine.xml
@@ -0,0 +1,5 @@
+
+
+
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/app/src/test/java/com/k2fsa/sherpa/onnx/tts/engine/ExampleUnitTest.kt b/android/SherpaOnnxTtsEngine-NG/app/src/test/java/com/k2fsa/sherpa/onnx/tts/engine/ExampleUnitTest.kt
new file mode 100644
index 0000000000..0132285561
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/app/src/test/java/com/k2fsa/sherpa/onnx/tts/engine/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.k2fsa.sherpa.onnx.tts.engine
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/build.gradle.kts b/android/SherpaOnnxTtsEngine-NG/build.gradle.kts
new file mode 100644
index 0000000000..10fbe7a16a
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/build.gradle.kts
@@ -0,0 +1,8 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+plugins {
+ id("com.android.application") version "8.2.0" apply false
+ id("org.jetbrains.kotlin.android") version "1.9.22" apply false
+ id("org.jetbrains.kotlin.plugin.serialization") version "1.9.22" apply false
+
+}
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/gradle.properties b/android/SherpaOnnxTtsEngine-NG/gradle.properties
new file mode 100644
index 0000000000..3c5031eb7d
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/android/SherpaOnnxTtsEngine-NG/gradle/wrapper/gradle-wrapper.jar b/android/SherpaOnnxTtsEngine-NG/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000000..e708b1c023
Binary files /dev/null and b/android/SherpaOnnxTtsEngine-NG/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/android/SherpaOnnxTtsEngine-NG/gradle/wrapper/gradle-wrapper.properties b/android/SherpaOnnxTtsEngine-NG/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000000..83ff778c5b
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sun Dec 31 18:47:53 CST 2023
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/android/SherpaOnnxTtsEngine-NG/gradlew b/android/SherpaOnnxTtsEngine-NG/gradlew
new file mode 100644
index 0000000000..4f906e0c81
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/android/SherpaOnnxTtsEngine-NG/gradlew.bat b/android/SherpaOnnxTtsEngine-NG/gradlew.bat
new file mode 100644
index 0000000000..107acd32c4
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/android/SherpaOnnxTtsEngine-NG/images/1.jpg b/android/SherpaOnnxTtsEngine-NG/images/1.jpg
new file mode 100644
index 0000000000..ac6038727e
Binary files /dev/null and b/android/SherpaOnnxTtsEngine-NG/images/1.jpg differ
diff --git a/android/SherpaOnnxTtsEngine-NG/install-solib.sh b/android/SherpaOnnxTtsEngine-NG/install-solib.sh
new file mode 100644
index 0000000000..a9bc56a7d6
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/install-solib.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+curl -L https://github.com/k2-fsa/sherpa-onnx/releases/download/v1.9.10/sherpa-onnx-v1.9.10-android.tar.bz2 -o android.tar.bz2
+tar -jxvf android.tar.bz2
+rm -rf android.tar.bz2
+
+echo "Move jniLibs to app/src/main/"
+rm -rf app/src/main/jniLibs
+mv jniLibs app/src/main/
diff --git a/android/SherpaOnnxTtsEngine-NG/settings.gradle.kts b/android/SherpaOnnxTtsEngine-NG/settings.gradle.kts
new file mode 100644
index 0000000000..f621731cf8
--- /dev/null
+++ b/android/SherpaOnnxTtsEngine-NG/settings.gradle.kts
@@ -0,0 +1,18 @@
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ maven { setUrl("https://jitpack.io") }
+ }
+}
+
+rootProject.name = "SherpaOnnxTtsEngine"
+include(":app")