diff --git a/app/build.gradle b/app/build.gradle index 1e8aefb..b62be49 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -26,7 +26,12 @@ android { } buildFeatures { viewBinding true + compose true } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.3" + } + namespace 'com.elishaazaria.sayboard' packagingOptions { @@ -63,10 +68,10 @@ dependencies { implementation group: 'com.alphacephei', name: 'vosk-android', version: '0.3.32' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'com.google.android.material:material:1.9.0' - implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' - implementation 'androidx.navigation:navigation-fragment:2.7.1' - implementation 'androidx.navigation:navigation-ui:2.7.1' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.2' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2' + implementation 'androidx.navigation:navigation-fragment:2.7.2' + implementation 'androidx.navigation:navigation-ui:2.7.2' implementation 'androidx.preference:preference:1.2.1' implementation 'org.greenrobot:eventbus:3.3.1' @@ -79,5 +84,51 @@ dependencies { compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+ - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0' + + implementation 'dev.patrickgold.jetpref:jetpref-datastore-model:0.1.0-beta14' + implementation 'dev.patrickgold.jetpref:jetpref-datastore-ui:0.1.0-beta14' + implementation 'dev.patrickgold.jetpref:jetpref-material-ui:0.1.0-beta14' + + def composeBom = platform('androidx.compose:compose-bom:2023.08.00') + implementation composeBom + androidTestImplementation composeBom + + // Choose one of the following: + // Material Design 3 + implementation 'androidx.compose.material3:material3' + // or Material Design 2 +// implementation 'androidx.compose.material:material' + // or skip Material Design and build directly on top of foundational components +// implementation 'androidx.compose.foundation:foundation' + // or only import the main APIs for the underlying toolkit systems, + // such as input and measurement/layout +// implementation 'androidx.compose.ui:ui' + + // Android Studio Preview support + implementation 'androidx.compose.ui:ui-tooling-preview' + debugImplementation 'androidx.compose.ui:ui-tooling' + + // UI Tests + androidTestImplementation 'androidx.compose.ui:ui-test-junit4' + debugImplementation 'androidx.compose.ui:ui-test-manifest' + + // Optional - Included automatically by material, only add when you need + // the icons but not the material library (e.g. when using Material3 or a + // custom design system based on Foundation) +// implementation 'androidx.compose.material:material-icons-core' + // Optional - Add full set of material icons + implementation 'androidx.compose.material:material-icons-extended' + // Optional - Add window size utils + implementation 'androidx.compose.material3:material3-window-size-class' + + // Optional - Integration with activities + implementation 'androidx.activity:activity-compose:1.7.2' + // Optional - Integration with ViewModels + implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2' + // Optional - Integration with LiveData + implementation 'androidx.compose.runtime:runtime-livedata' + // Optional - Integration with RxJava + implementation 'androidx.compose.runtime:runtime-rxjava2' + } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9d07c87..3091a42 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> + diff --git a/app/src/main/java/com/elishaazaria/sayboard/AppPrefs.kt b/app/src/main/java/com/elishaazaria/sayboard/AppPrefs.kt new file mode 100644 index 0000000..1303c4b --- /dev/null +++ b/app/src/main/java/com/elishaazaria/sayboard/AppPrefs.kt @@ -0,0 +1,90 @@ +package com.elishaazaria.sayboard + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import com.elishaazaria.sayboard.data.KeepScreenAwakeMode +import dev.patrickgold.jetpref.datastore.JetPref +import dev.patrickgold.jetpref.datastore.model.PreferenceModel + +// Defining a getter function for easy retrieval of the AppPrefs model. +// You can name this however you want, the convention is PreferenceModel +fun sayboardPreferenceModel() = JetPref.getOrCreatePreferenceModel(AppPrefs::class, ::AppPrefs) + +// Defining a preference model for our app prefs +// The name we give here is the file name of the preferences and is saved +// within the app's `jetpref_datastore` directory. +class AppPrefs : PreferenceModel("example-app-preferences") { + val logicKeepScreenAwake = enum( + key = "e_keep_screen_awake", + default = KeepScreenAwakeMode.NEVER + ) + + val logicListenImmediately = boolean( + key = "b_listen_immediately", + default = false + ) + + val logicAutoSwitchBack = boolean( + key = "b_auto_switch_back_ime", + default = false + ) + + val logicWeakRefToModel = boolean( + key = "b_weak_ref_to_model", + default = false + ) + + val uiDayForegroundMaterialYou = boolean( + key = "b_day_foreground_material_you", + default = false + ) + val uiDayForeground = int( + key = "c_day_foreground_color", + default = Color(0xFF377A00).toArgb() + ) + val uiDayBackground = int( + key = "c_day_background_color", + default = Color(0xFFFFFFFF).toArgb() + ) + + val uiNightForegroundMaterialYou = boolean( + key = "b_night_foreground_material_you", + default = false + ) + val uiNightForeground = int( + key = "c_night_foreground_color", + default = Color(0xFF66BB6A).toArgb() + ) + val uiNightBackground = int( + key = "c_night_background_color", + default = Color(0xFF000000).toArgb() + ) + + val uiKeyboardHeightPortrait = float( + key = "f_keyboard_height_portrait", + default = 0.3f + ) + + val uiKeyboardHeightLandscape = float( + key = "f_keyboard_height_landscape", + default = 0.5f + ) + + + val showExampleGroup = boolean( + key = "show_example_group", + default = true, + ) + val boxSizePortrait = int( + key = "box_size_portrait", + default = 40, + ) + val boxSizeLandscape = int( + key = "box_size_landscape", + default = 20, + ) + val welcomeMessage = string( + key = "welcome_message", + default = "Hello world!", + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/SayboardApplication.kt b/app/src/main/java/com/elishaazaria/sayboard/SayboardApplication.kt index 4a33e0d..f5c217a 100644 --- a/app/src/main/java/com/elishaazaria/sayboard/SayboardApplication.kt +++ b/app/src/main/java/com/elishaazaria/sayboard/SayboardApplication.kt @@ -2,11 +2,24 @@ package com.elishaazaria.sayboard import android.app.Application import com.elishaazaria.sayboard.AppCtx.setAppCtx +import dev.patrickgold.jetpref.datastore.JetPref import org.greenrobot.eventbus.EventBus class SayboardApplication : Application() { + private val prefs by sayboardPreferenceModel() override fun onCreate() { super.onCreate() + + // Optionally initialize global JetPref configs. This must be done before + // any preference datastore is initialized! + JetPref.configure( + saveIntervalMs = 500, + encodeDefaultValues = true, + ) + + // Initialize your datastore here (required) + prefs.initializeBlocking(this) + EventBus.builder().logNoSubscriberMessages(false).sendNoSubscriberEvent(false) .installDefaultEventBus() setAppCtx(this) diff --git a/app/src/main/java/com/elishaazaria/sayboard/SettingsActivity.kt b/app/src/main/java/com/elishaazaria/sayboard/SettingsActivity.kt index c59b67c..4c74d99 100644 --- a/app/src/main/java/com/elishaazaria/sayboard/SettingsActivity.kt +++ b/app/src/main/java/com/elishaazaria/sayboard/SettingsActivity.kt @@ -1,48 +1,156 @@ package com.elishaazaria.sayboard +import android.Manifest +import android.content.Intent import android.os.Bundle -import android.view.View -import androidx.appcompat.app.AppCompatActivity -import androidx.navigation.NavController -import androidx.navigation.fragment.NavHostFragment -import androidx.navigation.ui.AppBarConfiguration -import androidx.navigation.ui.NavigationUI.setupActionBarWithNavController -import androidx.navigation.ui.NavigationUI.setupWithNavController -import com.elishaazaria.sayboard.databinding.ActivitySettingsBinding - -class SettingsActivity : AppCompatActivity() { - private var binding: ActivitySettingsBinding? = null - private var navController: NavController? = null +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.app.ActivityCompat +import androidx.lifecycle.MutableLiveData +import com.elishaazaria.sayboard.data.LocalModel +import com.elishaazaria.sayboard.ui.GrantPermissionUi +import com.elishaazaria.sayboard.ui.LogicSettingsUi +import com.elishaazaria.sayboard.ui.ModelsSettingsUi +import com.elishaazaria.sayboard.ui.UISettingsUi +import java.util.Locale + +class SettingsActivity : ComponentActivity() { + + private val micGranted = MutableLiveData(true) + private val imeGranted = MutableLiveData(true) + + private val modelSettingsUi = ModelsSettingsUi(this) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Tools.createNotificationChannel(this) - binding = ActivitySettingsBinding.inflate( - layoutInflater - ) - setContentView(binding!!.root) - val navHostFragment = - supportFragmentManager.findFragmentById(R.id.nav_host_fragment_activity_settings) as NavHostFragment? - navController = navHostFragment!!.navController - if (Tools.isMicrophonePermissionGranted(this) && Tools.isIMEEnabled(this)) { - permissionsGranted() - } else { - navController!!.navigate(R.id.navigation_setup) - binding!!.navView.visibility = View.GONE + + checkPermissions() + + modelSettingsUi.onCreate() + + setContent { + val micGrantedState = micGranted.observeAsState(true) + val imeGrantedState = imeGranted.observeAsState(true) + if (micGrantedState.value && imeGrantedState.value) { + + MainUi() + } else { + GrantPermissionUi(mic = micGrantedState, ime = imeGrantedState, requestMic = { + ActivityCompat.requestPermissions( + this, arrayOf( + Manifest.permission.RECORD_AUDIO + ), PERMISSIONS_REQUEST_RECORD_AUDIO + ) + }) { + startActivity(Intent("android.settings.INPUT_METHOD_SETTINGS")) + } + } + } + } + + @Composable + private fun MainUi() { + val tabs = listOf("Models", "UI", "Logic") + var selectedIndex by remember { + mutableIntStateOf(0) } + + Scaffold(bottomBar = { + NavigationBar() { + tabs.forEachIndexed { index, tab -> + NavigationBarItem( + selected = index == selectedIndex, + onClick = { selectedIndex = index }, + icon = { + when (index) { + 0 -> Icon( + imageVector = Icons.Default.Home, + contentDescription = null + ) + + 1 -> Icon( + painter = painterResource(id = R.drawable.ic_baseline_color_lens_24), + contentDescription = null + ) + + 2 -> Icon( + imageVector = Icons.Default.Settings, + contentDescription = null + ) + } + }, label = { + Text(text = tab) + }) + } + } + }, floatingActionButton = { + if (selectedIndex == 0) { + modelSettingsUi.Fab() + } + }) { + Box(modifier = Modifier + .padding(it) + .padding(10.dp)) { + when (selectedIndex) { + 0 -> modelSettingsUi.Content() + 1 -> UISettingsUi() + 2 -> LogicSettingsUi() + } + } + } + } + + private fun checkPermissions() { + micGranted.postValue(Tools.isMicrophonePermissionGranted(this)) + imeGranted.postValue(Tools.isIMEEnabled(this)) + } + + override fun onStart() { + super.onStart() + modelSettingsUi.onStart() + } + + override fun onStop() { + modelSettingsUi.onStop() + super.onStop() + } + + override fun onResume() { + super.onResume() + checkPermissions() + modelSettingsUi.onResume() + } + + companion object { + /* Used to handle permission request */ + private const val PERMISSIONS_REQUEST_RECORD_AUDIO = 1 } - fun permissionsGranted() { - binding!!.navView.visibility = View.VISIBLE - // Passing each menu ID as a set of Ids because each - // menu should be considered as top level destinations. - val appBarConfiguration: AppBarConfiguration = AppBarConfiguration.Builder( // R.id.navigation_setup, - R.id.navigation_models, - R.id.navigation_ui, - R.id.navigation_logic - ) - .build() - setupActionBarWithNavController(this, navController!!, appBarConfiguration) - setupWithNavController(binding!!.navView, navController!!) - navController!!.navigate(R.id.navigation_models) + @Preview + @Composable + fun DefaultPreview() { + modelSettingsUi.models.postValue(listOf(LocalModel("abc/def", Locale.ENGLISH, "english"))) + MainUi() } } \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/Tools.kt b/app/src/main/java/com/elishaazaria/sayboard/Tools.kt index b8273a7..a037a82 100644 --- a/app/src/main/java/com/elishaazaria/sayboard/Tools.kt +++ b/app/src/main/java/com/elishaazaria/sayboard/Tools.kt @@ -12,11 +12,12 @@ import androidx.core.content.ContextCompat import com.elishaazaria.sayboard.Constants.getModelsDirectory import com.elishaazaria.sayboard.data.LocalModel import com.elishaazaria.sayboard.data.ModelLink -import com.elishaazaria.sayboard.settingsfragments.modelsfragment.ModelsAdapterLocalData import java.io.File import java.util.* object Tools { + const val VOSK_SERVER_ENABLED = false + @JvmStatic fun isMicrophonePermissionGranted(activity: Activity): Boolean { val permissionCheck = ContextCompat.checkSelfPermission( @@ -38,35 +39,6 @@ object Tools { return false } - // public static void downloadModelFromLink(ModelLink model, OnDownloadStatusListener listener, Context context) { - // String serverFilePath = model.link; - // - // File tempFolder = Constants.getTemporaryDownloadLocation(context); - // if (!tempFolder.exists()) { - // tempFolder.mkdirs(); - // } - // - // String fileName = model.link.substring(model.link.lastIndexOf('/') + 1); // file name - // File tempFile = new File(tempFolder, fileName); - // - // String localPath = tempFile.getAbsolutePath(); - // - // File modelFolder = Constants.getDirectoryForModel(context, model.locale); - // - // if (!modelFolder.exists()) { - // modelFolder.mkdirs(); - // } - // - // String unzipPath = modelFolder.getAbsolutePath(); - // - // DownloadRequest downloadRequest = new DownloadRequest(serverFilePath, localPath, true); - // downloadRequest.setRequiresUnzip(true); - // downloadRequest.setDeleteZipAfterExtract(true); - // downloadRequest.setUnzipAtFilePath(unzipPath); - // - // FileDownloader downloader = FileDownloader.getInstance(listener); - // downloader.download(downloadRequest, context); - // } @JvmStatic fun deleteModel(model: LocalModel, context: Context?) { val modelFile = File(model.path) @@ -75,7 +47,7 @@ object Tools { @JvmStatic fun deleteRecursive(fileOrDirectory: File) { - if (fileOrDirectory.isDirectory) for (child in fileOrDirectory.listFiles()) deleteRecursive( + if (fileOrDirectory.isDirectory) for (child in fileOrDirectory.listFiles()!!) deleteRecursive( child ) fileOrDirectory.delete() @@ -85,11 +57,11 @@ object Tools { val localeMap: MutableMap> = HashMap() val modelsDir = getModelsDirectory(context!!) if (!modelsDir.exists()) return localeMap - for (localeFolder in modelsDir.listFiles()) { + for (localeFolder in modelsDir.listFiles()!!) { if (!localeFolder.isDirectory) continue val locale = Locale.forLanguageTag(localeFolder.name) val models: MutableList = ArrayList() - for (modelFolder in localeFolder.listFiles()) { + for (modelFolder in localeFolder.listFiles()!!) { if (!modelFolder.isDirectory) continue val name = modelFolder.name val model = LocalModel(modelFolder.absolutePath, locale, name) @@ -105,10 +77,10 @@ object Tools { val models: MutableList = ArrayList() val modelsDir = getModelsDirectory(context!!) if (!modelsDir.exists()) return models - for (localeFolder in modelsDir.listFiles()) { + for (localeFolder in modelsDir.listFiles()!!) { if (!localeFolder.isDirectory) continue val locale = Locale.forLanguageTag(localeFolder.name) - for (modelFolder in localeFolder.listFiles()) { + for (modelFolder in localeFolder.listFiles()!!) { if (!modelFolder.isDirectory) continue val name = modelFolder.name val model = LocalModel(modelFolder.absolutePath, locale, name) @@ -118,34 +90,6 @@ object Tools { return models } - @JvmStatic - fun getModelsData(context: Context?): List { - val data: MutableList = ArrayList() - val installedModels = getInstalledModelsMap(context) - for (link in ModelLink.values()) { - var found = false - if (installedModels.containsKey(link.locale)) { - val localeModels = installedModels[link.locale]!! - for (i in localeModels.indices) { - val model = localeModels[i] - if (model.filename == link.filename) { - data.add(ModelsAdapterLocalData(link, model)) - localeModels.removeAt(i) - found = true - break - } - } - } - if (!found) data.add(ModelsAdapterLocalData(link)) - } - for (models in installedModels.values) { - for (model in models) { - data.add(ModelsAdapterLocalData(model)) - } - } - return data - } - @JvmStatic fun getModelForLink(modelLink: ModelLink, context: Context?): LocalModel? { val modelsDir = getModelsDirectory(context!!) diff --git a/app/src/main/java/com/elishaazaria/sayboard/data/LocalModel.kt b/app/src/main/java/com/elishaazaria/sayboard/data/LocalModel.kt index 5813f56..0d96e63 100644 --- a/app/src/main/java/com/elishaazaria/sayboard/data/LocalModel.kt +++ b/app/src/main/java/com/elishaazaria/sayboard/data/LocalModel.kt @@ -3,21 +3,7 @@ package com.elishaazaria.sayboard.data import java.io.Serializable import java.util.* -class LocalModel(val path: String?, val locale: Locale?, val filename: String?) : Serializable { - override fun equals(o: Any?): Boolean { - if (this === o) return true - if (o == null || javaClass != o.javaClass) return false - val model = o as LocalModel - if (path != model.path) return false - return if (locale != model.locale) false else filename == model.filename - } - - override fun hashCode(): Int { - var result = path?.hashCode() ?: 0 - result = 31 * result + (locale?.hashCode() ?: 0) - result = 31 * result + (filename?.hashCode() ?: 0) - return result - } +data class LocalModel(val path: String, val locale: Locale, val filename: String) : Serializable { companion object { fun serialize(model: LocalModel): String { diff --git a/app/src/main/java/com/elishaazaria/sayboard/data/SimpleEnums.kt b/app/src/main/java/com/elishaazaria/sayboard/data/SimpleEnums.kt new file mode 100644 index 0000000..e7fe087 --- /dev/null +++ b/app/src/main/java/com/elishaazaria/sayboard/data/SimpleEnums.kt @@ -0,0 +1,28 @@ +package com.elishaazaria.sayboard.data + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.elishaazaria.sayboard.R +import dev.patrickgold.jetpref.datastore.ui.listPrefEntries + +enum class KeepScreenAwakeMode { + NEVER, WHEN_LISTENING, WHEN_OPEN; + + companion object { + @Composable + fun listEntries() = listPrefEntries { + entry( + key = NEVER, + label = stringResource(id = R.string.p_keep_screen_awake_mode_never), + ) + entry( + key = WHEN_LISTENING, + label = stringResource(id = R.string.p_keep_screen_awake_mode_when_listening), + ) + entry( + key = WHEN_OPEN, + label = stringResource(id = R.string.p_keep_screen_awake_mode_when_open), + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/downloader/FileDownloadService.kt b/app/src/main/java/com/elishaazaria/sayboard/downloader/FileDownloadService.kt index 98fcac9..3d43250 100644 --- a/app/src/main/java/com/elishaazaria/sayboard/downloader/FileDownloadService.kt +++ b/app/src/main/java/com/elishaazaria/sayboard/downloader/FileDownloadService.kt @@ -1,9 +1,12 @@ package com.elishaazaria.sayboard.downloader +import android.Manifest import android.app.Service import android.content.Intent +import android.content.pm.PackageManager import android.os.IBinder import android.util.Log +import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.elishaazaria.sayboard.Constants @@ -11,9 +14,11 @@ import com.elishaazaria.sayboard.Constants.getDirectoryForModel import com.elishaazaria.sayboard.Constants.getTemporaryDownloadLocation import com.elishaazaria.sayboard.Constants.getTemporaryUnzipLocation import com.elishaazaria.sayboard.R +import com.elishaazaria.sayboard.Tools import com.elishaazaria.sayboard.downloader.messages.* import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode import java.io.* import java.net.URL import java.util.* @@ -27,8 +32,11 @@ class FileDownloadService : Service() { private var currentModel: ModelInfo? = null private val queuedModels: Queue = LinkedList() private var currentState = State.NONE - private var downloadProgress = 0 - private var unzipProgress = 0 + private var downloadProgress = 0f + private var unzipProgress = 0f + + private var interrupt = false + override fun onCreate() { super.onCreate() EventBus.getDefault().register(this) @@ -47,8 +55,8 @@ class FileDownloadService : Service() { ?: return START_NOT_STICKY Log.d(TAG, "Got message $modelInfo") queuedModels.add(modelInfo) - executor.execute { main() } sendEnqueued(modelInfo) + executor.execute { main() } startForeground(notificationId, notificationBuilder.build()) return START_NOT_STICKY } @@ -57,8 +65,8 @@ class FileDownloadService : Service() { currentModel = queuedModels.poll() val currentModelS = currentModel ?: return Log.d(TAG, "Started processing $currentModelS") - downloadProgress = 0 - unzipProgress = 0 + downloadProgress = 0f + unzipProgress = 0f setState(State.NONE) val downloadLocation = getTemporaryDownloadLocation( applicationContext, currentModelS.filename @@ -74,6 +82,10 @@ class FileDownloadService : Service() { mainEnd() return } + if (interrupt) { + interrupted(downloadLocation) + return + } Log.d(TAG, "Finished downloading") try { unzipFile(downloadLocation) @@ -83,6 +95,9 @@ class FileDownloadService : Service() { mainEnd() return } + if (interrupt) { + interrupted(downloadLocation) + } Log.d(TAG, "Finished unzipping") downloadLocation.delete() setState(State.FINISHED) @@ -90,9 +105,29 @@ class FileDownloadService : Service() { mainEnd() } + private fun interrupted(downloadLocation: File) { + if (downloadLocation.exists()) { + downloadLocation.delete() + } + val unzipFolder = getTemporaryUnzipLocation(this) + if (unzipFolder.exists()) { + Tools.deleteRecursive(unzipFolder) + } + + Log.d(TAG, "Download Canceled") + EventBus.getDefault() + .post( + CancelFinished(currentModel!!) + ) + updateNotification() + + interrupt = false + mainEnd() + } + private fun mainEnd() { - downloadProgress = 0 - unzipProgress = 0 + downloadProgress = 0f + unzipProgress = 0f currentState = State.NONE currentModel = null if (queuedModels.isEmpty()) stopForeground(false) @@ -106,22 +141,24 @@ class FileDownloadService : Service() { urlConnection.connect() val lengthOfFile = urlConnection.contentLength Log.d("TAG", "Length of file: $lengthOfFile") - setDownloadProgress(0) + setDownloadProgress(0f) val input: InputStream = BufferedInputStream(url.openStream()) val output: OutputStream = FileOutputStream(downloadLocation) val data = ByteArray(1024) // 1mb - var total: Long = 0 + var total: Float = 0f var count: Int - while (input.read(data).also { count = it } != -1) { - total += count.toLong() - setDownloadProgress((total * PROGRESS_MAX / lengthOfFile).toInt()) + while (input.read(data).also { count = it } != -1 && !interrupt) { + total += count + setDownloadProgress(total / lengthOfFile) output.write(data, 0, count) } output.flush() output.close() input.close() - setDownloadProgress(PROGRESS_MAX) - setState(State.DOWNLOAD_FINISHED) + if (!interrupt) { + setDownloadProgress(1f) + setState(State.DOWNLOAD_FINISHED) + } } @Throws(IOException::class) @@ -139,13 +176,13 @@ class FileDownloadService : Service() { downloadLocation, currentUnzipFolder, unzipDestination - ) { d: Double -> setUnzipProgress((d * PROGRESS_MAX).toInt()) } - setUnzipProgress(PROGRESS_MAX) + ) { d: Double -> setUnzipProgress(d.toFloat()) } + setUnzipProgress(1f) setState(State.UNZIP_FINISHED) } - private var lastDownloadProgress = 0 - private var lastUnzipProgress = 0 + private var lastDownloadProgress = 0f + private var lastUnzipProgress = 0f private var lastState: State = State.NONE private var lastUpdateTime: Long = 0 private fun updateNotification() { @@ -154,8 +191,8 @@ class FileDownloadService : Service() { val currentTime = System.currentTimeMillis() if (currentTime - lastUpdateTime < minUpdateTime) { if (lastState == currentState) { // it's a progress update - if (!(downloadProgress == PROGRESS_MAX && unzipProgress == 0) && - unzipProgress != PROGRESS_MAX + if (!(downloadProgress == 1f && unzipProgress == 0f) && + unzipProgress != 1f ) { // it's not the last progress update lastUpdateTime = currentTime return @@ -166,21 +203,36 @@ class FileDownloadService : Service() { when (currentState) { State.NONE -> notificationBuilder.setContentText(getString(R.string.notification_download_content_unknown)) .setProgress(0, 0, true) + State.DOWNLOAD_STARTED, State.DOWNLOAD_FINISHED -> notificationBuilder.setContentText( getString(R.string.notification_download_content_downloading) ) - .setProgress(PROGRESS_MAX, downloadProgress, false) + .setProgress(PROGRESS_MAX, (downloadProgress * PROGRESS_MAX).toInt(), false) + State.UNZIP_STARTED, State.UNZIP_FINISHED -> notificationBuilder.setContentText( getString(R.string.notification_download_content_unzipping) ) - .setProgress(PROGRESS_MAX, unzipProgress, false) + .setProgress(PROGRESS_MAX, (unzipProgress * PROGRESS_MAX).toInt(), false) + State.FINISHED -> notificationBuilder.setContentText(getString(R.string.notification_download_content_finished)) .setProgress(0, 0, false) + State.ERROR -> notificationBuilder.setContentText(getString(R.string.notification_download_content_error)) .setProgress(0, 0, false) - State.QUEUED -> TODO() + + State.CANCELED -> notificationBuilder.setContentText("Downloading Canceled") + .setProgress(0, 0, false) + + else -> {} + } + + if (ActivityCompat.checkSelfPermission( + this, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + ) { + notificationManager.notify(notificationId, notificationBuilder.build()) } - notificationManager.notify(notificationId, notificationBuilder.build()) lastDownloadProgress = downloadProgress lastUnzipProgress = unzipProgress lastState = currentState @@ -192,13 +244,13 @@ class FileDownloadService : Service() { updateNotification() } - private fun setDownloadProgress(progress: Int) { + private fun setDownloadProgress(progress: Float) { downloadProgress = progress EventBus.getDefault().post(DownloadProgress(currentModel!!, downloadProgress)) updateNotification() } - private fun setUnzipProgress(progress: Int) { + private fun setUnzipProgress(progress: Float) { unzipProgress = progress EventBus.getDefault().post(UnzipProgress(currentModel!!, unzipProgress)) updateNotification() @@ -214,10 +266,34 @@ class FileDownloadService : Service() { EventBus.getDefault().post(DownloadState(modelInfo, State.QUEUED)) } - @Subscribe + @Subscribe(threadMode = ThreadMode.MAIN) fun handleStatusQuery(event: StatusQuery?) { EventBus.getDefault() - .post(Status(currentModel!!, queuedModels, downloadProgress, unzipProgress, currentState)) + .post(Status(currentModel, queuedModels, downloadProgress, unzipProgress, currentState)) + } + + @Subscribe + fun handleCancelPending(event: CancelPending) { + if (queuedModels.remove(event.info)) { + EventBus.getDefault() + .post( + Status( + currentModel, + queuedModels, + downloadProgress, + unzipProgress, + currentState + ) + ) + } + } + + @Subscribe + fun handleCancelCurrent(event: CancelCurrent) { + if (currentModel == event.info) { + interrupt = true + setState(State.CANCELED) + } } override fun onBind(intent: Intent): IBinder? { diff --git a/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/CancelCurrent.kt b/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/CancelCurrent.kt new file mode 100644 index 0000000..424b279 --- /dev/null +++ b/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/CancelCurrent.kt @@ -0,0 +1,3 @@ +package com.elishaazaria.sayboard.downloader.messages + +data class CancelCurrent(val info: ModelInfo) \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/CancelFinished.kt b/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/CancelFinished.kt new file mode 100644 index 0000000..f32c553 --- /dev/null +++ b/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/CancelFinished.kt @@ -0,0 +1,3 @@ +package com.elishaazaria.sayboard.downloader.messages + +data class CancelFinished(val info: ModelInfo) \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/CancelPending.kt b/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/CancelPending.kt new file mode 100644 index 0000000..993db5d --- /dev/null +++ b/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/CancelPending.kt @@ -0,0 +1,3 @@ +package com.elishaazaria.sayboard.downloader.messages + +data class CancelPending(val info: ModelInfo) \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/DownloadError.kt b/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/DownloadError.kt index 6740c36..b8544fa 100644 --- a/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/DownloadError.kt +++ b/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/DownloadError.kt @@ -1,3 +1,3 @@ package com.elishaazaria.sayboard.downloader.messages -class DownloadError(val info: ModelInfo, val message: String) \ No newline at end of file +data class DownloadError(val info: ModelInfo, val message: String) \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/DownloadProgress.kt b/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/DownloadProgress.kt index dd14ba1..f33eb5a 100644 --- a/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/DownloadProgress.kt +++ b/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/DownloadProgress.kt @@ -1,3 +1,3 @@ package com.elishaazaria.sayboard.downloader.messages -class DownloadProgress(val info: ModelInfo, val progress: Int) \ No newline at end of file +data class DownloadProgress(val info: ModelInfo, val progress: Float) \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/DownloadState.kt b/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/DownloadState.kt index ed4f6e8..bdbc27f 100644 --- a/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/DownloadState.kt +++ b/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/DownloadState.kt @@ -1,3 +1,3 @@ package com.elishaazaria.sayboard.downloader.messages -class DownloadState(val info: ModelInfo, val state: State) \ No newline at end of file +data class DownloadState(val info: ModelInfo, val state: State) \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/ModelInfo.kt b/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/ModelInfo.kt index a713738..50fba3c 100644 --- a/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/ModelInfo.kt +++ b/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/ModelInfo.kt @@ -2,12 +2,12 @@ package com.elishaazaria.sayboard.downloader.messages import java.util.* -class ModelInfo(val url: String, val filename: String, val locale: Locale) { - override fun toString(): String { - return "ModelInfo{" + - "url='" + url + '\'' + - ", filename='" + filename + '\'' + - ", locale=" + locale + - '}' - } +data class ModelInfo(val url: String, val filename: String, val locale: Locale) { +// override fun toString(): String { +// return "ModelInfo{" + +// "url='" + url + '\'' + +// ", filename='" + filename + '\'' + +// ", locale=" + locale + +// '}' +// } } \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/State.kt b/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/State.kt index 809a4de..1fd2546 100644 --- a/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/State.kt +++ b/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/State.kt @@ -1,5 +1,5 @@ package com.elishaazaria.sayboard.downloader.messages enum class State { - NONE, QUEUED, DOWNLOAD_STARTED, DOWNLOAD_FINISHED, UNZIP_STARTED, UNZIP_FINISHED, FINISHED, ERROR + NONE, QUEUED, DOWNLOAD_STARTED, DOWNLOAD_FINISHED, UNZIP_STARTED, UNZIP_FINISHED, FINISHED, ERROR, CANCELED } \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/Status.kt b/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/Status.kt index 01cef2f..34547a7 100644 --- a/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/Status.kt +++ b/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/Status.kt @@ -2,10 +2,10 @@ package com.elishaazaria.sayboard.downloader.messages import java.util.* -class Status( - val current: ModelInfo, +data class Status( + val current: ModelInfo?, val queued: Queue, - val downloadProgress: Int, - val unzipProgress: Int, + val downloadProgress: Float, + val unzipProgress: Float, val state: State ) \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/UnzipProgress.kt b/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/UnzipProgress.kt index 871e298..c4fba7b 100644 --- a/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/UnzipProgress.kt +++ b/app/src/main/java/com/elishaazaria/sayboard/downloader/messages/UnzipProgress.kt @@ -1,3 +1,3 @@ package com.elishaazaria.sayboard.downloader.messages -class UnzipProgress(val info: ModelInfo, val progress: Int) \ No newline at end of file +data class UnzipProgress(val info: ModelInfo, val progress: Float) \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/ime/IME.kt b/app/src/main/java/com/elishaazaria/sayboard/ime/IME.kt index 42823f0..5fc7896 100644 --- a/app/src/main/java/com/elishaazaria/sayboard/ime/IME.kt +++ b/app/src/main/java/com/elishaazaria/sayboard/ime/IME.kt @@ -32,14 +32,15 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import com.elishaazaria.sayboard.BuildConfig import com.elishaazaria.sayboard.R -import com.elishaazaria.sayboard.preferences.LogicPreferences -import com.elishaazaria.sayboard.preferences.LogicPreferences.isAutoSwitchBack -import com.elishaazaria.sayboard.preferences.LogicPreferences.keepScreenAwake +import com.elishaazaria.sayboard.data.KeepScreenAwakeMode +import com.elishaazaria.sayboard.sayboardPreferenceModel import org.vosk.LibVosk import org.vosk.LogLevel import org.vosk.android.RecognitionListener class IME : InputMethodService(), RecognitionListener, LifecycleOwner { + private val prefs by sayboardPreferenceModel() + private val lifecycleRegistry = LifecycleRegistry(this) private lateinit var editorInfo: EditorInfo private lateinit var viewManager: ViewManager @@ -82,7 +83,7 @@ class IME : InputMethodService(), RecognitionListener, LifecycleOwner { } modelManager.initializeRecognizer() viewManager.refresh() - setKeepScreenOn(keepScreenAwake == LogicPreferences.KEEP_SCREEN_AWAKE_WHEN_OPEN) + setKeepScreenOn(prefs.logicKeepScreenAwake.get() == KeepScreenAwakeMode.WHEN_OPEN) } override fun onFinishInputView(finishingInput: Boolean) { @@ -90,7 +91,7 @@ class IME : InputMethodService(), RecognitionListener, LifecycleOwner { // text input has ended setKeepScreenOn(false) modelManager.stop() - if (isAutoSwitchBack) { + if (prefs.logicAutoSwitchBack.get()) { // switch back actionManager.switchToLastIme(false) } @@ -103,20 +104,17 @@ class IME : InputMethodService(), RecognitionListener, LifecycleOwner { if (modelManager.isRunning) { if (modelManager.isPaused) { modelManager.pause(false) - if (keepScreenAwake == LogicPreferences.KEEP_SCREEN_AWAKE_WHEN_LISTENING) setKeepScreenOn( - true - ) + if (prefs.logicKeepScreenAwake.get() == KeepScreenAwakeMode.WHEN_LISTENING) + setKeepScreenOn(true) } else { modelManager.pause(true) - if (keepScreenAwake == LogicPreferences.KEEP_SCREEN_AWAKE_WHEN_LISTENING) setKeepScreenOn( - false - ) + if (prefs.logicKeepScreenAwake.get() == KeepScreenAwakeMode.WHEN_LISTENING) + setKeepScreenOn(false) } } else { modelManager.start() - if (keepScreenAwake == LogicPreferences.KEEP_SCREEN_AWAKE_WHEN_LISTENING) setKeepScreenOn( - true - ) + if (prefs.logicKeepScreenAwake.get() == KeepScreenAwakeMode.WHEN_LISTENING) + setKeepScreenOn(true) } } @@ -281,11 +279,11 @@ class IME : InputMethodService(), RecognitionListener, LifecycleOwner { override fun onError(e: Exception) { viewManager.errorMessageLD.postValue(R.string.mic_error_recognizer_error) - viewManager.stateLD.postValue(ViewManager.Companion.STATE_ERROR) + viewManager.stateLD.postValue(ViewManager.STATE_ERROR) } override fun onTimeout() { - viewManager.stateLD.postValue(ViewManager.Companion.STATE_PAUSED) + viewManager.stateLD.postValue(ViewManager.STATE_PAUSED) } companion object { diff --git a/app/src/main/java/com/elishaazaria/sayboard/ime/ModelManager.kt b/app/src/main/java/com/elishaazaria/sayboard/ime/ModelManager.kt index 33fb434..5a586f3 100644 --- a/app/src/main/java/com/elishaazaria/sayboard/ime/ModelManager.kt +++ b/app/src/main/java/com/elishaazaria/sayboard/ime/ModelManager.kt @@ -5,18 +5,18 @@ import android.content.pm.PackageManager import androidx.core.app.ActivityCompat import androidx.lifecycle.Observer import com.elishaazaria.sayboard.R +import com.elishaazaria.sayboard.Tools import com.elishaazaria.sayboard.ime.recognizers.RecognizerSource import com.elishaazaria.sayboard.ime.recognizers.providers.RecognizerSourceProvider import com.elishaazaria.sayboard.ime.recognizers.providers.VoskLocalProvider import com.elishaazaria.sayboard.ime.recognizers.providers.VoskServerProvider -import com.elishaazaria.sayboard.preferences.LogicPreferences.isListenImmediately -import com.elishaazaria.sayboard.preferences.LogicPreferences.isWeakRefModel -import com.elishaazaria.sayboard.preferences.ModelPreferences +import com.elishaazaria.sayboard.sayboardPreferenceModel import java.io.IOException import java.util.concurrent.Executor import java.util.concurrent.Executors class ModelManager(private val ime: IME, private val viewManager: ViewManager) { + private val prefs by sayboardPreferenceModel() private var speechService: MySpeechService? = null var isRunning = false private set @@ -28,7 +28,7 @@ class ModelManager(private val ime: IME, private val viewManager: ViewManager) { fun initializeRecognizer() { if (recognizerSources.size == 0) return val onLoaded = Observer { r: RecognizerSource? -> - if (isListenImmediately) { + if (prefs.logicListenImmediately.get()) { start() // execute after initialize } } @@ -39,7 +39,7 @@ class ModelManager(private val ime: IME, private val viewManager: ViewManager) { } private fun stopRecognizerSource() { - currentRecognizerSource.close(isWeakRefModel) + currentRecognizerSource.close(prefs.logicWeakRefToModel.get()) currentRecognizerSource.stateLD.removeObserver(viewManager) } @@ -57,7 +57,7 @@ class ModelManager(private val ime: IME, private val viewManager: ViewManager) { if (isRunning || speechService != null) { speechService!!.stop() } - viewManager.stateLD.postValue(ViewManager.Companion.STATE_LISTENING) + viewManager.stateLD.postValue(ViewManager.STATE_LISTENING) try { val recognizer = currentRecognizerSource.recognizer if (ActivityCompat.checkSelfPermission( @@ -71,7 +71,7 @@ class ModelManager(private val ime: IME, private val viewManager: ViewManager) { speechService!!.startListening(ime) } catch (e: IOException) { viewManager.errorMessageLD.postValue(R.string.mic_error_mic_in_use) - viewManager.stateLD.postValue(ViewManager.Companion.STATE_ERROR) + viewManager.stateLD.postValue(ViewManager.STATE_ERROR) } isRunning = true } @@ -80,7 +80,7 @@ class ModelManager(private val ime: IME, private val viewManager: ViewManager) { init { sourceProviders.add(VoskLocalProvider(ime)) - if (ModelPreferences.VOSK_SERVER_ENABLED) { + if (Tools.VOSK_SERVER_ENABLED) { sourceProviders.add(VoskServerProvider()) } for (provider in sourceProviders) { @@ -88,7 +88,7 @@ class ModelManager(private val ime: IME, private val viewManager: ViewManager) { } if (recognizerSources.size == 0) { viewManager.errorMessageLD.postValue(R.string.mic_error_no_recognizers) - viewManager.stateLD.postValue(ViewManager.Companion.STATE_ERROR) + viewManager.stateLD.postValue(ViewManager.STATE_ERROR) } else { currentRecognizerSourceIndex = 0 initializeRecognizer() @@ -100,9 +100,9 @@ class ModelManager(private val ime: IME, private val viewManager: ViewManager) { speechService!!.setPause(checked) pausedState = checked if (checked) { - viewManager.stateLD.postValue(ViewManager.Companion.STATE_PAUSED) + viewManager.stateLD.postValue(ViewManager.STATE_PAUSED) } else { - viewManager.stateLD.postValue(ViewManager.Companion.STATE_LISTENING) + viewManager.stateLD.postValue(ViewManager.STATE_LISTENING) } } else { pausedState = false diff --git a/app/src/main/java/com/elishaazaria/sayboard/ime/ViewManager.kt b/app/src/main/java/com/elishaazaria/sayboard/ime/ViewManager.kt index a2be8d6..605bc8f 100644 --- a/app/src/main/java/com/elishaazaria/sayboard/ime/ViewManager.kt +++ b/app/src/main/java/com/elishaazaria/sayboard/ime/ViewManager.kt @@ -12,17 +12,16 @@ import android.widget.ImageButton import android.widget.TextView import androidx.appcompat.content.res.AppCompatResources import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat import androidx.core.widget.TextViewCompat import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import com.elishaazaria.sayboard.R import com.elishaazaria.sayboard.ime.recognizers.RecognizerState -import com.elishaazaria.sayboard.preferences.UIPreferences.getBackgroundColor -import com.elishaazaria.sayboard.preferences.UIPreferences.getForegroundColor -import com.elishaazaria.sayboard.preferences.UIPreferences.screenHeightLandscape -import com.elishaazaria.sayboard.preferences.UIPreferences.screenHeightPortrait +import com.elishaazaria.sayboard.sayboardPreferenceModel class ViewManager(private val ime: IME) : Observer { + private val prefs by sayboardPreferenceModel() lateinit var root: ConstraintLayout private set private lateinit var micButton: ImageButton @@ -84,8 +83,26 @@ class ViewManager(private val ime: IME) : Observer { private fun setUpTheme() { val dark = ime.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES - val foreground = getForegroundColor(dark, ime) - val background = getBackgroundColor(dark) + + val foreground = if (dark) { + if (prefs.uiNightForegroundMaterialYou.get()) { + ContextCompat.getColor(ime, R.color.materialYouForeground) + } else { + prefs.uiNightForeground.get() + } + } else { + if (prefs.uiDayForegroundMaterialYou.get()) { + ContextCompat.getColor(ime, R.color.materialYouForeground) + } else { + prefs.uiDayForeground.get() + } + } + val background = if (dark) { + prefs.uiNightBackground.get() + } else { + prefs.uiDayBackground.get() + } + if (currentForeground == foreground && currentBackground == background) return currentForeground = foreground currentBackground = background @@ -107,9 +124,9 @@ class ViewManager(private val ime: IME) : Observer { val screenHeight = ime.resources.displayMetrics.heightPixels val percent: Float percent = if (landscape) { - screenHeightLandscape + prefs.uiKeyboardHeightLandscape.get() } else { - screenHeightPortrait + prefs.uiKeyboardHeightPortrait.get() } val height = (percent * screenHeight).toInt() Log.d("ViewManager", "Screen height: $screenHeight, height: $height") @@ -138,21 +155,25 @@ class ViewManager(private val ime: IME) : Observer { icon = R.drawable.ic_settings_voice enabled = false } + STATE_READY, STATE_PAUSED -> { text = R.string.mic_info_ready icon = R.drawable.ic_mic_none enabled = true } + STATE_LISTENING -> { text = R.string.mic_info_recording icon = R.drawable.ic_mic enabled = true } + STATE_ERROR -> { text = R.string.mic_info_error icon = R.drawable.ic_mic_off enabled = false } + else -> return } resultView.setText(text) @@ -188,6 +209,7 @@ class ViewManager(private val ime: IME) : Observer { RecognizerState.CLOSED, RecognizerState.NONE -> stateLD.setValue( STATE_INITIAL ) + RecognizerState.LOADING -> stateLD.setValue(STATE_LOADING) RecognizerState.READY -> stateLD.setValue(STATE_READY) RecognizerState.IN_RAM -> stateLD.setValue(STATE_PAUSED) diff --git a/app/src/main/java/com/elishaazaria/sayboard/ime/recognizers/providers/VoskServerProvider.kt b/app/src/main/java/com/elishaazaria/sayboard/ime/recognizers/providers/VoskServerProvider.kt index 9e4c713..30ed4df 100644 --- a/app/src/main/java/com/elishaazaria/sayboard/ime/recognizers/providers/VoskServerProvider.kt +++ b/app/src/main/java/com/elishaazaria/sayboard/ime/recognizers/providers/VoskServerProvider.kt @@ -1,13 +1,11 @@ package com.elishaazaria.sayboard.ime.recognizers.providers import com.elishaazaria.sayboard.ime.recognizers.RecognizerSource -import com.elishaazaria.sayboard.ime.recognizers.VoskServer -import com.elishaazaria.sayboard.preferences.ModelPreferences.voskServers class VoskServerProvider : RecognizerSourceProvider { override fun loadSources(recognizerSources: MutableList) { - for (voskServer in voskServers) { - recognizerSources.add(VoskServer(voskServer)) - } +// for (voskServer in voskServers) { +// recognizerSources.add(VoskServer(voskServer)) +// } } } \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/preferences/LogicPreferences.kt b/app/src/main/java/com/elishaazaria/sayboard/preferences/LogicPreferences.kt deleted file mode 100644 index be9f169..0000000 --- a/app/src/main/java/com/elishaazaria/sayboard/preferences/LogicPreferences.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.elishaazaria.sayboard.preferences - -import com.elishaazaria.sayboard.AppCtx.getStringRes -import com.elishaazaria.sayboard.AppCtx.getBoolRes -import com.elishaazaria.sayboard.AppCtx.appCtx -import com.elishaazaria.sayboard.AppCtx.getIntegerRes -import com.elishaazaria.sayboard.preferences.MyPreferences -import com.elishaazaria.sayboard.AppCtx -import com.elishaazaria.sayboard.R -import com.elishaazaria.sayboard.preferences.LogicPreferences -import com.elishaazaria.sayboard.data.VoskServerData -import android.content.SharedPreferences -import android.os.Build -import com.elishaazaria.sayboard.preferences.UIPreferences -import androidx.core.content.ContextCompat - -object LogicPreferences { - const val KEEP_SCREEN_AWAKE_NEVER = 0 - const val KEEP_SCREEN_AWAKE_WHEN_LISTENING = 1 - const val KEEP_SCREEN_AWAKE_WHEN_OPEN = 2 - @JvmStatic - val keepScreenAwake: Int - get() { - val `val` = MyPreferences.sharedPref - .getString( - getStringRes(R.string.pref_logic_keep_screen_awake_l), - getStringRes(R.string.pref_keep_awake_default) - ) - if (`val` == getStringRes(R.string.value_keep_awake_never)) return KEEP_SCREEN_AWAKE_NEVER - if (`val` == getStringRes(R.string.value_keep_awake_when_listening)) return KEEP_SCREEN_AWAKE_WHEN_LISTENING - return if (`val` == getStringRes(R.string.value_keep_awake_when_open)) KEEP_SCREEN_AWAKE_WHEN_OPEN else -1 - } - val isListenImmediately: Boolean - get() = MyPreferences.sharedPref.getBoolean( - getStringRes(R.string.pref_logic_listen_immediately_b), - getBoolRes(R.bool.pref_listen_immediately_default) - ) - val isAutoSwitchBack: Boolean - get() = MyPreferences.sharedPref.getBoolean( - getStringRes(R.string.pref_logic_auto_switch_back_b), - getBoolRes(R.bool.pref_auto_switch_back_default) - ) - val isWeakRefModel: Boolean - get() = MyPreferences.sharedPref.getBoolean( - getStringRes(R.string.pref_logic_weak_ref_model_b), - getBoolRes(R.bool.pref_weak_ref_model_default) - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/preferences/ModelPreferences.kt b/app/src/main/java/com/elishaazaria/sayboard/preferences/ModelPreferences.kt deleted file mode 100644 index a77427b..0000000 --- a/app/src/main/java/com/elishaazaria/sayboard/preferences/ModelPreferences.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.elishaazaria.sayboard.preferences - -import com.elishaazaria.sayboard.AppCtx.getStringRes -import com.elishaazaria.sayboard.AppCtx.getBoolRes -import com.elishaazaria.sayboard.AppCtx.appCtx -import com.elishaazaria.sayboard.AppCtx.getIntegerRes -import com.elishaazaria.sayboard.preferences.MyPreferences -import com.elishaazaria.sayboard.AppCtx -import com.elishaazaria.sayboard.R -import com.elishaazaria.sayboard.preferences.LogicPreferences -import com.elishaazaria.sayboard.data.VoskServerData -import android.content.SharedPreferences -import android.os.Build -import com.elishaazaria.sayboard.preferences.UIPreferences -import androidx.core.content.ContextCompat -import java.util.* -import kotlin.collections.HashSet - -object ModelPreferences { - const val VOSK_SERVER_ENABLED = false - @JvmStatic - var voskServers: List - get() { - val set = MyPreferences.sharedPref.getStringSet( - getStringRes(R.string.pref_models_vosk_servers_set), - null - ) ?: return ArrayList() - val list = ArrayList() - for (stringData in set) { - val data = VoskServerData.deserialize(stringData) - if (data != null) { - list.add(data) - } - } - Collections.sort(list) - return list - } - set(servers) { - val set: MutableSet = HashSet() - for (data in servers) { - set.add(VoskServerData.serialize(data)) - } - MyPreferences.sharedPref.edit() - .putStringSet(getStringRes(R.string.pref_models_vosk_servers_set), set).apply() - } - - fun addToVoskServers(data: VoskServerData) { - val set: MutableSet = HashSet( - MyPreferences.sharedPref.getStringSet( - getStringRes(R.string.pref_models_vosk_servers_set), - HashSet() - ) - ) - set.add(VoskServerData.serialize(data)) - MyPreferences.sharedPref.edit() - .putStringSet(getStringRes(R.string.pref_models_vosk_servers_set), set).apply() - } - - fun removeFromVoskServers(data: VoskServerData) { - val set: MutableSet = HashSet( - MyPreferences.sharedPref.getStringSet( - getStringRes(R.string.pref_models_vosk_servers_set), - HashSet() - ) - ) - set.remove(VoskServerData.serialize(data)) - MyPreferences.sharedPref.edit() - .putStringSet(getStringRes(R.string.pref_models_vosk_servers_set), set).apply() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/preferences/MyPreferences.kt b/app/src/main/java/com/elishaazaria/sayboard/preferences/MyPreferences.kt deleted file mode 100644 index 1cdd777..0000000 --- a/app/src/main/java/com/elishaazaria/sayboard/preferences/MyPreferences.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.elishaazaria.sayboard.preferences - -import android.content.Context -import android.content.SharedPreferences -import com.elishaazaria.sayboard.AppCtx.appCtx -import com.elishaazaria.sayboard.AppCtx.getStringRes -import com.elishaazaria.sayboard.R - -object MyPreferences { - private var mySharedPref: SharedPreferences? = null - - val sharedPref: SharedPreferences - get() { - if (mySharedPref == null) { - mySharedPref = appCtx!!.getSharedPreferences( - getStringRes(R.string.main_shared_pref), Context.MODE_PRIVATE - ) - } - return mySharedPref!! - } -} \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/preferences/UIPreferences.kt b/app/src/main/java/com/elishaazaria/sayboard/preferences/UIPreferences.kt deleted file mode 100644 index 88f1495..0000000 --- a/app/src/main/java/com/elishaazaria/sayboard/preferences/UIPreferences.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.elishaazaria.sayboard.preferences - -import android.content.Context -import android.os.Build -import androidx.core.content.ContextCompat -import com.elishaazaria.sayboard.AppCtx.getBoolRes -import com.elishaazaria.sayboard.AppCtx.getIntegerRes -import com.elishaazaria.sayboard.AppCtx.getStringRes -import com.elishaazaria.sayboard.R - -object UIPreferences { - fun isForegroundMaterialYou(dark: Boolean): Boolean { - return if (!dark) { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && MyPreferences.sharedPref - .getBoolean( - getStringRes(R.string.pref_ui_light_foreground_material_you_b), - getBoolRes(R.bool.pref_light_foreground_material_you_default) - ) - } else { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && MyPreferences.sharedPref - .getBoolean( - getStringRes(R.string.pref_ui_dark_foreground_material_you), - getBoolRes(R.bool.pref_dark_foreground_material_you_default) - ) - } - } - - @JvmStatic - fun getForegroundColor(dark: Boolean, ctx: Context?): Int { - if (isForegroundMaterialYou(dark)) { - return ContextCompat.getColor(ctx!!, R.color.materialYouForeground) - } - return if (!dark) { - MyPreferences.sharedPref - .getInt( - getStringRes(R.string.pref_ui_light_foreground_c), - getIntegerRes(R.integer.pref_light_foreground_color_default) - ) - } else { - MyPreferences.sharedPref - .getInt( - getStringRes(R.string.pref_ui_dark_foreground_c), - getIntegerRes(R.integer.pref_dark_foreground_color_default) - ) - } - } - - @JvmStatic - fun getBackgroundColor(dark: Boolean): Int { - return if (!dark) { - MyPreferences.sharedPref - .getInt( - getStringRes(R.string.pref_ui_light_background_c), - getIntegerRes(R.integer.pref_light_background_color_default) - ) - } else { - MyPreferences.sharedPref - .getInt( - getStringRes(R.string.pref_ui_dark_background_c), - getIntegerRes(R.integer.pref_dark_background_color_default) - ) - } - } - - @JvmStatic - val screenHeightLandscape: Float - get() = MyPreferences.sharedPref.getInt( - getStringRes(R.string.pref_ui_keyboard_height_landscape_i), - getIntegerRes(R.integer.pref_keyboard_height_landscape_default) - ) / getIntegerRes(R.integer.keyboard_height_max).toFloat() - @JvmStatic - val screenHeightPortrait: Float - get() = MyPreferences.sharedPref.getInt( - getStringRes(R.string.pref_ui_keyboard_height_portrait_i), - getIntegerRes(R.integer.pref_keyboard_height_portrait_default) - ) / getIntegerRes(R.integer.keyboard_height_max).toFloat() -} \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/LogicSettingsFragment.kt b/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/LogicSettingsFragment.kt deleted file mode 100644 index 9b6ac8c..0000000 --- a/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/LogicSettingsFragment.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.elishaazaria.sayboard.settingsfragments - -import android.os.Bundle -import androidx.preference.PreferenceFragmentCompat -import com.elishaazaria.sayboard.AppCtx.getStringRes -import com.elishaazaria.sayboard.R - -class LogicSettingsFragment : PreferenceFragmentCompat() { - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - val preferenceManager = preferenceManager - preferenceManager.sharedPreferencesName = getStringRes(R.string.main_shared_pref) - setPreferencesFromResource(R.xml.logic_preferences, rootKey) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/ModelsFragment.kt b/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/ModelsFragment.kt deleted file mode 100644 index 0d33153..0000000 --- a/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/ModelsFragment.kt +++ /dev/null @@ -1,186 +0,0 @@ -package com.elishaazaria.sayboard.settingsfragments - -import android.os.Bundle -import android.view.* -import android.widget.ProgressBar -import android.widget.Toast -import androidx.core.view.MenuHost -import androidx.core.view.MenuProvider -import androidx.fragment.app.Fragment -import androidx.lifecycle.Lifecycle -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.elishaazaria.sayboard.R -import com.elishaazaria.sayboard.Tools.getModelForLink -import com.elishaazaria.sayboard.Tools.getModelsData -import com.elishaazaria.sayboard.data.VoskServerData -import com.elishaazaria.sayboard.databinding.FragmentModelsBinding -import com.elishaazaria.sayboard.downloader.messages.* -import com.elishaazaria.sayboard.preferences.ModelPreferences -import com.elishaazaria.sayboard.settingsfragments.modelsfragment.* -import com.elishaazaria.sayboard.settingsfragments.modelsfragment.ModelsAdapter.ItemClickListener -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode -import java.net.URI - -class ModelsFragment : Fragment(), ItemClickListener, MenuProvider { - private lateinit var binding: FragmentModelsBinding - private lateinit var adapter: ModelsAdapter - private lateinit var progressBar: ProgressBar - private lateinit var recyclerView: RecyclerView - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = FragmentModelsBinding.inflate(inflater, container, false) - val root: View = binding.root - recyclerView = binding.recyclerView - progressBar = binding.progressBar - recyclerView.layoutManager = LinearLayoutManager(context) - val dataProvider: AdapterDataProvider = object : AdapterDataProvider { - override val data: List - get() { - val list = ArrayList() - if (ModelPreferences.VOSK_SERVER_ENABLED) { - for (data in ModelPreferences.voskServers) { - list.add(ModelsAdapterServerData(data)) - } - } - list.addAll(getModelsData(context)) - return list - } - } - adapter = ModelsAdapter(requireContext(), dataProvider) - adapter.setClickListener(this) - recyclerView.adapter = adapter - val activity: MenuHost = requireActivity() - activity.addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) - return root - } - - override fun onItemClick(view: View, position: Int, data: ModelsAdapterData) {} - override fun onButtonClicked(view: View, position: Int, data: ModelsAdapterData) { - data.buttonClicked(adapter, requireContext()) - } - - fun getAdapterDataForModel(modelInfo: ModelInfo): ModelsAdapterLocalData? { - for (i in 0 until adapter.size()) { - val data = adapter.getItem(i) - if (data is ModelsAdapterLocalData) { - val localData = data - if (localData.filename == modelInfo.filename) { - return localData - } - } - } - return null - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onState(state: DownloadState) { - val current = getAdapterDataForModel(state.info) ?: return - when (state.state) { - State.DOWNLOAD_STARTED -> { - progressBar.visibility = View.VISIBLE - current.downloading() - adapter.changed(current) - } - State.FINISHED -> { - progressBar.visibility = View.GONE - val model = getModelForLink(current.modelLink!!, context) - if (model != null) current.wasInstalled(model) - adapter.changed(current) - } - State.ERROR -> { - progressBar.visibility = View.GONE - Toast.makeText( - context, - "Download failed for " + current.filename, - Toast.LENGTH_SHORT - ).show() - current.downloadCanceled() - adapter.changed(current) - } - State.QUEUED -> { - current.wasQueued() - adapter.changed(current) - } - else -> {} - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onStatus(status: Status) { - onState(DownloadState(status.current, status.state)) - when (status.state) { - State.DOWNLOAD_STARTED -> onDownloadProgress( - DownloadProgress( - status.current, - status.downloadProgress - ) - ) - State.UNZIP_STARTED -> onUnzipProgress( - UnzipProgress( - status.current, - status.unzipProgress - ) - ) - else -> {} - } - for (modelInfo in status.queued) { - onState(DownloadState(modelInfo, State.QUEUED)) - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onDownloadProgress(progress: DownloadProgress) { - progressBar.progress = progress.progress - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onUnzipProgress(progress: UnzipProgress) { - progressBar.secondaryProgress = progress.progress - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onDownloadError(error: DownloadError?) { -// Toast.makeText(getContext(), error.message, Toast.LENGTH_SHORT).show(); - } - - override fun onStart() { - super.onStart() - EventBus.getDefault().register(this) - EventBus.getDefault().post(StatusQuery()) - } - - override fun onStop() { - EventBus.getDefault().unregister(this) - super.onStop() - } - - override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { - if (ModelPreferences.VOSK_SERVER_ENABLED) { - menuInflater.inflate(R.menu.models_fragment_menu, menu) - } - } - - override fun onMenuItemSelected(menuItem: MenuItem): Boolean { - if (ModelPreferences.VOSK_SERVER_ENABLED) { - if (menuItem.title === getString(R.string.menu_models_add_server)) { - AddVoskServerDialogFragment { add: Boolean, uri: URI? -> - if (add && uri != null) { - ModelPreferences.addToVoskServers(VoskServerData(uri, null)) - adapter.reload() - } - }.show(requireActivity().supportFragmentManager, "AddVoskServerDialogFragment") - } - } - return false - } - - companion object { - private const val TAG = "ModelsFragment" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/SetupFragment.kt b/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/SetupFragment.kt deleted file mode 100644 index 9a7d1ea..0000000 --- a/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/SetupFragment.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.elishaazaria.sayboard.settingsfragments - -import android.Manifest -import android.content.Intent -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.activity.result.contract.ActivityResultContracts -import androidx.core.app.ActivityCompat -import androidx.fragment.app.Fragment -import com.elishaazaria.sayboard.R -import com.elishaazaria.sayboard.SettingsActivity -import com.elishaazaria.sayboard.Tools.isIMEEnabled -import com.elishaazaria.sayboard.Tools.isMicrophonePermissionGranted -import com.elishaazaria.sayboard.databinding.FragmentSetupBinding - -class SetupFragment : Fragment() { - private var binding: FragmentSetupBinding? = null - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - binding = FragmentSetupBinding.inflate(inflater, container, false) - val root: View = binding!!.root - registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean? -> reloadButtons() } - binding!!.microphonePermission.setOnClickListener { v: View? -> - ActivityCompat.requestPermissions( - requireActivity(), arrayOf( - Manifest.permission.RECORD_AUDIO - ), PERMISSIONS_REQUEST_RECORD_AUDIO - ) - } - binding!!.enableKeyboard.setOnClickListener { v: View? -> startActivity(Intent("android.settings.INPUT_METHOD_SETTINGS")) } - -// InputMethodManager imeManager = (InputMethodManager) requireActivity().getApplicationContext().getSystemService(Context.INPUT_METHOD_SERVICE); -// binding.openImeSwitcher.setOnClickListener(v -> imeManager.showInputMethodPicker()); - reloadButtons() - return root - } - - override fun onDestroyView() { - super.onDestroyView() - binding = null - } - - private fun reloadButtons() { - val permissionGranted = isMicrophonePermissionGranted(requireActivity()) - binding!!.microphonePermission.text = - getString(if (permissionGranted) R.string.mic_permission_granted else R.string.mic_permission_not_granted) - binding!!.microphonePermission.isEnabled = !permissionGranted - val keyboardInEnabledList = isIMEEnabled(requireActivity()) - binding!!.enableKeyboard.text = - getString(if (keyboardInEnabledList) R.string.keyboard_enabled else R.string.keyboard_not_enabled) - binding!!.enableKeyboard.isEnabled = !keyboardInEnabledList - if (permissionGranted && keyboardInEnabledList) { - (requireActivity() as SettingsActivity).permissionsGranted() - } - } - - // @Override - // public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - // super.onRequestPermissionsResult(requestCode, permissions, grantResults); - // reloadButtons(); - // } - override fun onResume() { - super.onResume() - reloadButtons() - } - - companion object { - /* Used to handle permission request */ - private const val PERMISSIONS_REQUEST_RECORD_AUDIO = 1 - } -} \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/UIFragment.kt b/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/UIFragment.kt deleted file mode 100644 index 21737ca..0000000 --- a/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/UIFragment.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.elishaazaria.sayboard.settingsfragments - -import android.os.Bundle -import androidx.preference.Preference -import androidx.preference.PreferenceFragmentCompat -import com.elishaazaria.sayboard.AppCtx.getStringRes -import com.elishaazaria.sayboard.R -import com.rarepebble.colorpicker.ColorPreference - -class UIFragment : PreferenceFragmentCompat() { - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - val preferenceManager = preferenceManager - preferenceManager.sharedPreferencesName = getStringRes(R.string.main_shared_pref) - setPreferencesFromResource(R.xml.ui_preferences, rootKey) - } - - override fun onDisplayPreferenceDialog(preference: Preference) { - if (preference is ColorPreference) { - preference.showDialog(this, 0) - } else super.onDisplayPreferenceDialog(preference) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/modelsfragment/AdapterDataProvider.kt b/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/modelsfragment/AdapterDataProvider.kt deleted file mode 100644 index d3fa553..0000000 --- a/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/modelsfragment/AdapterDataProvider.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.elishaazaria.sayboard.settingsfragments.modelsfragment - -interface AdapterDataProvider { - val data: List -} \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/modelsfragment/AddVoskServerDialogFragment.kt b/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/modelsfragment/AddVoskServerDialogFragment.kt deleted file mode 100644 index 4eca6de..0000000 --- a/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/modelsfragment/AddVoskServerDialogFragment.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.elishaazaria.sayboard.settingsfragments.modelsfragment - -import android.annotation.SuppressLint -import android.app.AlertDialog -import android.app.Dialog -import android.content.DialogInterface -import android.os.Bundle -import android.util.Log -import android.view.View -import android.widget.EditText -import androidx.fragment.app.DialogFragment -import com.elishaazaria.sayboard.R -import java.net.URI -import java.net.URISyntaxException - -class AddVoskServerDialogFragment(private val callback: Callback) : DialogFragment() { - @SuppressLint("InflateParams") - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val dialogBuilder = AlertDialog.Builder(requireActivity()) - // Get the layout inflater - val inflater = requireActivity().layoutInflater - - // Inflate and set the layout for the dialog - // Pass null as the parent view because its going in the dialog layout - dialogBuilder.setView( - inflater.inflate( - R.layout.add_vosk_server_dialog, - null - ) - ) // Add action buttons - .setPositiveButton(R.string.dialog_add_vosk_server_add) { dialog: DialogInterface?, id: Int -> - val hostname = - (getDialog()!!.findViewById(R.id.hostname) as EditText).text.toString() - val portString = - (getDialog()!!.findViewById(R.id.port) as EditText).text.toString() - Log.d("VoskServerDialog", "$hostname:$portString") - try { - val port = portString.toInt() - callback.callback(true, URI(null, null, hostname, port, null, null, null)) - } catch (e: NumberFormatException) { - e.printStackTrace() - callback.callback(false, null) - } catch (e: URISyntaxException) { - e.printStackTrace() - callback.callback(false, null) - } - } - .setNegativeButton(R.string.dialog_add_vosk_server_cancel) { dialog: DialogInterface, id: Int -> dialog.cancel() } - return dialogBuilder.create() - } - - fun interface Callback { - fun callback(add: Boolean, uri: URI?) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/modelsfragment/ModelsAdapter.kt b/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/modelsfragment/ModelsAdapter.kt deleted file mode 100644 index 6cb587d..0000000 --- a/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/modelsfragment/ModelsAdapter.kt +++ /dev/null @@ -1,121 +0,0 @@ -package com.elishaazaria.sayboard.settingsfragments.modelsfragment - -import android.annotation.SuppressLint -import android.content.Context -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageButton -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import com.elishaazaria.sayboard.R - -class ModelsAdapter(context: Context, dataProvider: AdapterDataProvider) : - RecyclerView.Adapter() { - enum class DataState { - CLOUD, INSTALLED, DOWNLOADING, QUEUED - } - - private val context: Context - private val dataProvider: AdapterDataProvider - private val mData: MutableList - private val mInflater: LayoutInflater - private var mClickListener: ItemClickListener? = null - - // data is passed into the constructor - init { - mInflater = LayoutInflater.from(context) - this.context = context - this.dataProvider = dataProvider - mData = ArrayList(dataProvider.data) - } - - // inflates the row layout from xml when needed - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val view = mInflater.inflate(R.layout.fragment_models_entry, parent, false) - return ViewHolder(view) - } - - // binds the data to the TextView in each row - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val data = mData[position] - holder.titleTextView.text = data.title - holder.subtitleTextView.text = data.subtitle - holder.downloadButton.setImageResource(data.imageRes) - holder.data = data - } - - // total number of rows - override fun getItemCount(): Int { - return mData.size - } - - // stores and recycles views as they are scrolled off screen - inner class ViewHolder internal constructor(itemView: View) : - RecyclerView.ViewHolder(itemView) { - var titleTextView: TextView - var subtitleTextView: TextView - var downloadButton: ImageButton - var data: ModelsAdapterData? = null - - init { - titleTextView = itemView.findViewById(R.id.titleTextView) - subtitleTextView = itemView.findViewById(R.id.subtitleTextView) - downloadButton = itemView.findViewById(R.id.downloadButton) - downloadButton.setOnClickListener { view: View -> onButtonClick(view) } - itemView.setOnClickListener { view: View -> onClick(view) } - } - - private fun onClick(view: View) { - if (mClickListener != null) mClickListener!!.onItemClick(view, adapterPosition, data!!) - } - - private fun onButtonClick(view: View) { - if (mClickListener != null) { - mClickListener!!.onButtonClicked(view, adapterPosition, data!!) - } - } - } - - // convenience method for getting data at click position - fun getItem(index: Int): ModelsAdapterData { - return mData[index] - } - - fun size(): Int { - return mData.size - } - - fun changed(data: ModelsAdapterData?): Boolean { - val index = mData.indexOf(data) - if (index == -1) return false - notifyItemChanged(index) - return true - } - - fun removed(data: ModelsAdapterData?): Boolean { - val index = mData.indexOf(data) - if (index == -1) return false - mData.removeAt(index) - notifyItemRemoved(index) - return true - } - - @SuppressLint("NotifyDataSetChanged") - fun reload() { - mData.clear() - mData.addAll(dataProvider.data) - notifyDataSetChanged() - } - - // allows clicks events to be caught - fun setClickListener(itemClickListener: ItemClickListener?) { - mClickListener = itemClickListener - } - - // parent activity will implement this method to respond to click events - interface ItemClickListener { - fun onItemClick(view: View, position: Int, data: ModelsAdapterData) - fun onButtonClicked(view: View, position: Int, data: ModelsAdapterData) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/modelsfragment/ModelsAdapterData.kt b/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/modelsfragment/ModelsAdapterData.kt deleted file mode 100644 index 1ab2ad5..0000000 --- a/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/modelsfragment/ModelsAdapterData.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.elishaazaria.sayboard.settingsfragments.modelsfragment - -import android.content.Context - -interface ModelsAdapterData { - val title: String - val subtitle: String - val imageRes: Int - fun buttonClicked(adapter: ModelsAdapter, context: Context) -} \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/modelsfragment/ModelsAdapterLocalData.kt b/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/modelsfragment/ModelsAdapterLocalData.kt deleted file mode 100644 index d3bed22..0000000 --- a/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/modelsfragment/ModelsAdapterLocalData.kt +++ /dev/null @@ -1,107 +0,0 @@ -package com.elishaazaria.sayboard.settingsfragments.modelsfragment - -import android.content.Context -import com.elishaazaria.sayboard.R -import com.elishaazaria.sayboard.Tools.deleteModel -import com.elishaazaria.sayboard.data.LocalModel -import com.elishaazaria.sayboard.data.ModelLink -import com.elishaazaria.sayboard.downloader.FileDownloader -import com.elishaazaria.sayboard.settingsfragments.modelsfragment.ModelsAdapter.DataState -import java.util.* - -class ModelsAdapterLocalData : ModelsAdapterData { - var modelLink: ModelLink? - private set - var model: LocalModel? - private set - var state: DataState - private set - - constructor(modelLink: ModelLink?) { - this.modelLink = modelLink - model = null - state = DataState.CLOUD - } - - constructor(model: LocalModel?) { - modelLink = null - this.model = model - state = DataState.INSTALLED - } - - constructor(modelLink: ModelLink?, model: LocalModel?) { - this.modelLink = modelLink - this.model = model - state = DataState.INSTALLED - } - - val filename: String - get() = if (modelLink != null) { - modelLink!!.filename - } else if (model != null) { - model!!.filename!! - } else { - "Undefined" - } - val locale: Locale - get() = if (model != null) { - model!!.locale!! - } else if (modelLink != null) { - modelLink!!.locale - } else Locale.forLanguageTag("und") - - fun wasInstalled(model: LocalModel?) { - this.model = model - state = DataState.INSTALLED - } - - fun wasDeleted(): Boolean { - model = null - state = DataState.CLOUD - return modelLink == null - } - - fun wasQueued() { - state = DataState.QUEUED - } - - fun downloading() { - state = DataState.DOWNLOADING - } - - fun downloadCanceled() { - if (state == DataState.DOWNLOADING) state = DataState.CLOUD - } - - override val title: String - get() = locale.displayName - override val subtitle: String - get() = filename - override val imageRes: Int - get() { - return when (state) { - DataState.CLOUD -> R.drawable.ic_download - DataState.INSTALLED -> R.drawable.ic_delete - DataState.DOWNLOADING -> R.drawable.ic_downloading - DataState.QUEUED -> R.drawable.ic_add_circle_outline - } - return 0 - } - - override fun buttonClicked(adapter: ModelsAdapter, context: Context) { - when (state) { - DataState.CLOUD -> FileDownloader.downloadModel(modelLink!!, context) - DataState.INSTALLED -> { - deleteModel(model!!, context) - val removed = wasDeleted() - if (removed) { - adapter.removed(this) - } else { - adapter.changed(this) - } - } - DataState.DOWNLOADING -> {} - DataState.QUEUED -> {} - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/modelsfragment/ModelsAdapterServerData.kt b/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/modelsfragment/ModelsAdapterServerData.kt deleted file mode 100644 index ecc8953..0000000 --- a/app/src/main/java/com/elishaazaria/sayboard/settingsfragments/modelsfragment/ModelsAdapterServerData.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.elishaazaria.sayboard.settingsfragments.modelsfragment - -import android.content.Context -import com.elishaazaria.sayboard.R -import com.elishaazaria.sayboard.data.VoskServerData -import com.elishaazaria.sayboard.preferences.ModelPreferences - -class ModelsAdapterServerData(private val data: VoskServerData) : ModelsAdapterData { - override val title: String - get() = data.uri.toString() - override val subtitle: String - get() = "vosk server" - override val imageRes: Int - get() = R.drawable.ic_delete - - override fun buttonClicked(adapter: ModelsAdapter, context: Context) { - ModelPreferences.removeFromVoskServers(data) - adapter.removed(this) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/ui/ColorPickerPreference.kt b/app/src/main/java/com/elishaazaria/sayboard/ui/ColorPickerPreference.kt new file mode 100644 index 0000000..bace630 --- /dev/null +++ b/app/src/main/java/com/elishaazaria/sayboard/ui/ColorPickerPreference.kt @@ -0,0 +1,113 @@ +package com.elishaazaria.sayboard.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import dev.patrickgold.jetpref.datastore.model.PreferenceData +import dev.patrickgold.jetpref.datastore.model.PreferenceModel +import dev.patrickgold.jetpref.datastore.ui.PreferenceUiScope +import dev.patrickgold.jetpref.material.ui.ExperimentalJetPrefMaterialUi +import dev.patrickgold.jetpref.material.ui.JetPrefColorPicker +import dev.patrickgold.jetpref.material.ui.JetPrefListItem +import dev.patrickgold.jetpref.material.ui.rememberJetPrefColorPickerState + +@OptIn(ExperimentalJetPrefMaterialUi::class, ExperimentalMaterial3Api::class) +@Composable +fun PreferenceUiScope.ColorPickerPreference( + pref: PreferenceData, + title: String, + modifier: Modifier = Modifier, + summary: String? = null, + enabled: Boolean = true +) { + var setColor by remember { + mutableStateOf(Color(pref.get())) + } + var selectedColor by remember { + mutableStateOf(Color(pref.get())) + } + val colorPickerState = rememberJetPrefColorPickerState(initColor = selectedColor) + var showDialog by remember { + mutableStateOf(false) + } + JetPrefListItem( + modifier = + modifier.clickable( + enabled = enabled, + role = Role.Button, + onClick = { + showDialog = true + }, + ), + icon = { + Spacer( + modifier = Modifier + .background(Color.Black) + .padding(1.dp) + .background(setColor) + .size(40.dp) + ) + }, + text = title, + secondaryText = summary, + enabled = enabled + ) + + if (showDialog) { + AlertDialog( + onDismissRequest = { + showDialog = false + }) { + Card { + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Text(text = title, fontSize = 20.sp) + JetPrefColorPicker(state = colorPickerState, + onColorChange = { + selectedColor = it + }) + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Spacer(modifier = Modifier.weight(1f)) + Button(onClick = { + showDialog = false + }) { + Text(text = "Cancel") + } + Button(onClick = { + showDialog = false + setColor = selectedColor + pref.set(selectedColor.toArgb()) + }) { + Text(text = "Select") + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/ui/GrantPermissionsUi.kt b/app/src/main/java/com/elishaazaria/sayboard/ui/GrantPermissionsUi.kt new file mode 100644 index 0000000..8bb0dcb --- /dev/null +++ b/app/src/main/java/com/elishaazaria/sayboard/ui/GrantPermissionsUi.kt @@ -0,0 +1,41 @@ +package com.elishaazaria.sayboard.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +public fun GrantPermissionUi( + mic: State, + ime: State, + requestMic: () -> Unit, + requestIme: () -> Unit +) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxSize(), + ) { + Button(onClick = requestMic, enabled = mic.value) { + if (mic.value) { + Text(text = "Grant Microphone Permission") + } else { + Text(text = "Microphone Permission Granted") + } + + } + Button(onClick = requestIme, enabled = ime.value) { + if (ime.value) { + Text(text = "Enable IME") + } else { + Text(text = "IME enabled") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/ui/LogicSettingsUi.kt b/app/src/main/java/com/elishaazaria/sayboard/ui/LogicSettingsUi.kt new file mode 100644 index 0000000..7710ba1 --- /dev/null +++ b/app/src/main/java/com/elishaazaria/sayboard/ui/LogicSettingsUi.kt @@ -0,0 +1,40 @@ +package com.elishaazaria.sayboard.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.elishaazaria.sayboard.R +import com.elishaazaria.sayboard.data.KeepScreenAwakeMode +import com.elishaazaria.sayboard.sayboardPreferenceModel +import dev.patrickgold.jetpref.datastore.ui.ListPreference +import dev.patrickgold.jetpref.datastore.ui.ScrollablePreferenceLayout +import dev.patrickgold.jetpref.datastore.ui.SwitchPreference + +@Composable +fun LogicSettingsUi() = ScrollablePreferenceLayout(sayboardPreferenceModel()) { + ListPreference( + listPref = prefs.logicKeepScreenAwake, + title = stringResource(id = R.string.other_keep_screen_awake_title), + entries = KeepScreenAwakeMode.listEntries() + ) + SwitchPreference( + pref = prefs.logicListenImmediately, + title = stringResource(id = R.string.other_listen_immediately_title), + summary = stringResource( + id = R.string.other_listen_immediately_summery + ) + ) + SwitchPreference( + pref = prefs.logicAutoSwitchBack, + title = stringResource(id = R.string.other_auto_switch_back_title), + summary = stringResource( + id = R.string.other_auto_switch_back_summery + ) + ) + SwitchPreference( + pref = prefs.logicWeakRefToModel, + title = stringResource(id = R.string.other_weak_ref_to_model_title), + summary = stringResource( + id = R.string.other_weak_ref_to_model_summery + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/ui/ModelsSettingsUi.kt b/app/src/main/java/com/elishaazaria/sayboard/ui/ModelsSettingsUi.kt new file mode 100644 index 0000000..776ddd5 --- /dev/null +++ b/app/src/main/java/com/elishaazaria/sayboard/ui/ModelsSettingsUi.kt @@ -0,0 +1,381 @@ +package com.elishaazaria.sayboard.ui + +import android.util.Log +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Cancel +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Download +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.ShapeDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.DialogProperties +import androidx.lifecycle.MutableLiveData +import com.elishaazaria.sayboard.SettingsActivity +import com.elishaazaria.sayboard.Tools +import com.elishaazaria.sayboard.data.LocalModel +import com.elishaazaria.sayboard.data.ModelLink +import com.elishaazaria.sayboard.downloader.FileDownloader +import com.elishaazaria.sayboard.downloader.messages.CancelCurrent +import com.elishaazaria.sayboard.downloader.messages.CancelFinished +import com.elishaazaria.sayboard.downloader.messages.CancelPending +import com.elishaazaria.sayboard.downloader.messages.DownloadError +import com.elishaazaria.sayboard.downloader.messages.DownloadProgress +import com.elishaazaria.sayboard.downloader.messages.DownloadState +import com.elishaazaria.sayboard.downloader.messages.ModelInfo +import com.elishaazaria.sayboard.downloader.messages.State +import com.elishaazaria.sayboard.downloader.messages.Status +import com.elishaazaria.sayboard.downloader.messages.StatusQuery +import com.elishaazaria.sayboard.downloader.messages.UnzipProgress +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode + +data class DownloadModelProgress( + val info: ModelInfo, + var state: State, var progress: Float +) + +class ModelsSettingsUi(private val activity: SettingsActivity) { + val models = MutableLiveData>(mutableListOf()) + + private val modelsPendingDownloadLD = MutableLiveData>(mutableListOf()) + private val modelsPendingDownload = mutableListOf() + + private val currentDownloadingModel = MutableLiveData() + + fun onCreate() { + reloadModels() + } + + private fun reloadModels() { + models.postValue(Tools.getInstalledModelsList(activity)) + } + + + @Composable + fun Content() { + val modelsState = models.observeAsState(listOf()) + val modelsPendingDownload = modelsPendingDownloadLD.observeAsState(mutableListOf()) + val currentDownloadingModel = currentDownloadingModel.observeAsState() + + LazyColumn(verticalArrangement = Arrangement.spacedBy(10.dp)) { + item { + currentDownloadingModel.value?.let { current -> + Card { + Row( + modifier = Modifier.padding(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text(text = current.info.locale.displayName, fontSize = 20.sp) + Text(text = current.info.url, fontSize = 12.sp) + val stateText = when (current.state) { + State.NONE -> "Unknown" + State.QUEUED -> "Pending" + State.DOWNLOAD_STARTED -> "Downloading" + State.DOWNLOAD_FINISHED -> "Download Finished" + State.UNZIP_STARTED -> "Unzipping" + State.UNZIP_FINISHED -> "Unzipping Finished" + State.FINISHED -> "Finished" + State.ERROR -> "Error" + State.CANCELED -> "Canceled" + } + + Text( + text = "State: $stateText", fontSize = 14.sp + ) + } + FilledIconButton(onClick = { + EventBus.getDefault() + .post( + CancelCurrent(current.info) + ) + }, shape = ShapeDefaults.Medium) { + Icon( + imageVector = Icons.Default.Cancel, + contentDescription = null + ) + } + } + LinearProgressIndicator(current.progress) + } + } + } + items(items = modelsPendingDownload.value) { + Card { + Row( + modifier = Modifier.padding(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text(text = it.locale.displayName, fontSize = 20.sp) + Text(text = it.url, fontSize = 12.sp) + Text(text = "State: Pending Download", fontSize = 14.sp) + } + FilledIconButton(onClick = { + EventBus.getDefault() + .post( + CancelPending(it) + ) + }, shape = ShapeDefaults.Medium) { + Icon(imageVector = Icons.Default.Cancel, contentDescription = null) + } + } + } + } + + items(items = modelsState.value) { lm -> + Card { + Row( + modifier = Modifier.padding(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text(text = lm.locale.displayName ?: "null", fontSize = 20.sp) + Text(text = lm.path, fontSize = 12.sp) + } + FilledIconButton(onClick = { + Tools.deleteModel(lm, activity) + reloadModels() + }, shape = ShapeDefaults.Medium) { + Icon(imageVector = Icons.Default.Delete, contentDescription = null) + } + } + } + } + } + } + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + fun Fab() { + var showDownloadDialog by remember { + mutableStateOf(false) + } + + Column( + verticalArrangement = Arrangement.spacedBy(10.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + FloatingActionButton(onClick = { + showDownloadDialog = true + }) { + Icon(imageVector = Icons.Default.Download, contentDescription = null) + } + } + + if (showDownloadDialog) { + AlertDialog(onDismissRequest = { + showDownloadDialog = false + }, properties = DialogProperties(usePlatformDefaultWidth = false)) { + Box( + Modifier + .clip(RectangleShape) + .fillMaxWidth() + .background(Color.White) + .padding(10.dp) + ) { + Column(modifier = Modifier.fillMaxWidth()) { + Text(text = "Download Model", fontSize = 30.sp) + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(10.dp), + modifier = Modifier.weight(1f) + ) { + items(items = ModelLink.values()) { ml -> + Card(onClick = { + showDownloadDialog = false + FileDownloader.downloadModel(ml, activity) + }, modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(10.dp)) { + Text(text = ml.locale.displayName) + Text(text = ml.link, fontSize = 10.sp) + } + } + } + } + Box(modifier = Modifier.fillMaxWidth()) { + Button(onClick = { + showDownloadDialog = false + }, modifier = Modifier.align(Alignment.CenterEnd)) { + Text(text = "Cancel") + } + } + } + } + } + } + } + + private fun updateCurrentDownloading(info: ModelInfo) { + val currentModel = currentDownloadingModel.value + + if (currentModel == null) { + currentDownloadingModel.value = DownloadModelProgress( + info, + State.NONE, + 0f + ) + } else if (currentModel.info != info) { + val newCurrent = modelsPendingDownload.find { it == currentModel.info } + if (newCurrent == null) { + currentDownloadingModel.value = DownloadModelProgress( + info, + State.NONE, + 0f + ) + } else { + modelsPendingDownload.remove(newCurrent) + modelsPendingDownloadLD.postValue(modelsPendingDownload.toList()) + currentDownloadingModel.value = DownloadModelProgress( + newCurrent, + State.NONE, + 0f + ) + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onAddToDownload(info: ModelInfo) { + Log.d(TAG, "onAddToDownload($info)") + + if (modelsPendingDownload.contains(info)) return + modelsPendingDownload.add(info) + modelsPendingDownloadLD.postValue(modelsPendingDownload.toList()) + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onState(state: DownloadState) { + Log.d(TAG, "onState($state)") + when (state.state) { + State.QUEUED -> onAddToDownload(state.info) + State.DOWNLOAD_STARTED, State.NONE -> { + modelsPendingDownload.remove(state.info) + modelsPendingDownloadLD.postValue(modelsPendingDownload.toList()) + updateCurrentDownloading(state.info) + val current = currentDownloadingModel.value!! + current.state = state.state + currentDownloadingModel.value = current + } + + State.FINISHED -> { + currentDownloadingModel.value = null + reloadModels() + } + + else -> { + updateCurrentDownloading(state.info) + val current = currentDownloadingModel.value!! + current.state = state.state + currentDownloadingModel.value = current + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onStatus(status: Status) { + Log.d(TAG, "onStatus($status)") + if (status.current != null) { + onState(DownloadState(status.current, status.state)) + when (status.state) { + State.DOWNLOAD_STARTED -> onDownloadProgress( + DownloadProgress( + status.current, + status.downloadProgress + ) + ) + + State.UNZIP_STARTED -> onUnzipProgress( + UnzipProgress( + status.current, + status.unzipProgress + ) + ) + + else -> {} + } + } + for (modelInfo in status.queued) { + onState(DownloadState(modelInfo, State.QUEUED)) + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onDownloadProgress(progress: DownloadProgress) { +// Log.d(TAG, "onDownloadProgress($progress)") + updateCurrentDownloading(progress.info) + val current = currentDownloadingModel.value!! + current.progress = progress.progress + currentDownloadingModel.postValue(current) + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onUnzipProgress(progress: UnzipProgress) { +// Log.d(TAG, "onUnzipProgress($progress)") + updateCurrentDownloading(progress.info) + + val current = currentDownloadingModel.value!! + current.progress = progress.progress + currentDownloadingModel.postValue(current) + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onCancelFinished(event: CancelFinished) { + if (currentDownloadingModel.value?.info == event.info) { + currentDownloadingModel.value = null + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onDownloadError(error: DownloadError?) { +// Toast.makeText(getContext(), error.message, Toast.LENGTH_SHORT).show(); + } + + fun onStart() { + EventBus.getDefault().register(this) + EventBus.getDefault().post(StatusQuery()) + } + + fun onStop() { + EventBus.getDefault().unregister(this) + } + + fun onResume() { + reloadModels() +// EventBus.getDefault().post(StatusQuery()) + } + + companion object { + private val TAG = "ModelsSettingUi" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/elishaazaria/sayboard/ui/UISettingsUi.kt b/app/src/main/java/com/elishaazaria/sayboard/ui/UISettingsUi.kt new file mode 100644 index 0000000..e76fd7c --- /dev/null +++ b/app/src/main/java/com/elishaazaria/sayboard/ui/UISettingsUi.kt @@ -0,0 +1,68 @@ +package com.elishaazaria.sayboard.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.elishaazaria.sayboard.R +import com.elishaazaria.sayboard.sayboardPreferenceModel +import dev.patrickgold.jetpref.datastore.model.observeAsState +import dev.patrickgold.jetpref.datastore.ui.DialogSliderPreference +import dev.patrickgold.jetpref.datastore.ui.ExperimentalJetPrefDatastoreUi +import dev.patrickgold.jetpref.datastore.ui.PreferenceGroup +import dev.patrickgold.jetpref.datastore.ui.ScrollablePreferenceLayout +import dev.patrickgold.jetpref.datastore.ui.SwitchPreference +import dev.patrickgold.jetpref.material.ui.ExperimentalJetPrefMaterialUi + +@OptIn(ExperimentalJetPrefMaterialUi::class, ExperimentalJetPrefDatastoreUi::class) +@Composable +fun UISettingsUi() = ScrollablePreferenceLayout(sayboardPreferenceModel()) { + PreferenceGroup(title = stringResource(id = R.string.ui_light_theme_header)) { + SwitchPreference( + pref = prefs.uiDayForegroundMaterialYou, + title = stringResource(id = R.string.ui_light_theme_foreground_material_you_title) + ) + + ColorPickerPreference( + pref = prefs.uiDayForeground, + title = stringResource(id = R.string.ui_light_theme_foreground_color_title), + enabled = !prefs.uiDayForegroundMaterialYou.observeAsState().value + ) + + ColorPickerPreference( + pref = prefs.uiDayBackground, + title = stringResource(id = R.string.ui_light_theme_background_color_title) + ) + } + PreferenceGroup(title = stringResource(id = R.string.ui_dark_theme_header)) { + SwitchPreference( + pref = prefs.uiNightForegroundMaterialYou, + title = stringResource(id = R.string.ui_dark_theme_foreground_material_you_title) + ) + + ColorPickerPreference( + pref = prefs.uiNightForeground, + title = stringResource(id = R.string.ui_dark_theme_foreground_color_title), + enabled = !prefs.uiNightForegroundMaterialYou.observeAsState().value + ) + + ColorPickerPreference( + pref = prefs.uiNightBackground, + title = stringResource(id = R.string.ui_dark_theme_background_color_title) + ) + } + PreferenceGroup(title = stringResource(id = R.string.ui_keyboard_header)) { + DialogSliderPreference( + pref = prefs.uiKeyboardHeightPortrait, + title = stringResource(id = R.string.other_keyboard_height_portrait_title), + min = 0.01f, + max = 1f, + stepIncrement = 0.01f + ) + DialogSliderPreference( + pref = prefs.uiKeyboardHeightLandscape, + title = stringResource(id = R.string.other_keyboard_height_landscape_title), + min = 0.01f, + max = 1f, + stepIncrement = 0.01f + ) + } +} \ No newline at end of file diff --git a/app/src/main/res/values/prefs.xml b/app/src/main/res/values/prefs.xml new file mode 100644 index 0000000..09f6442 --- /dev/null +++ b/app/src/main/res/values/prefs.xml @@ -0,0 +1,6 @@ + + + Never + When Listening + When Open + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8be1818..89fc31a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -80,8 +80,8 @@ Start listening immediately when the recognizer is ready. Auto switch back Automatically switch to previous keyboard whenever the keyboard is hidden. - Weak ref to model (when closed) - Allows the system to delete the model from RAM when the keyboard is closed if low on memory. + Weak ref to model (when closed) + Allows the system to delete the model from RAM when the keyboard is closed. Model download diff --git a/app/src/main/res/xml/logic_preferences.xml b/app/src/main/res/xml/logic_preferences.xml index fd83237..e3388d9 100644 --- a/app/src/main/res/xml/logic_preferences.xml +++ b/app/src/main/res/xml/logic_preferences.xml @@ -21,7 +21,7 @@ + android:summary="@string/other_weak_ref_to_model_summery" + android:title="@string/other_weak_ref_to_model_title" /> \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index f8360ed..00b58a1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,19 @@ -android.useAndroidX=true -android.enableJetifier=true +## 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. +# Default value: -Xmx1024m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -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 +#Thu Sep 28 14:52:36 IDT 2023 android.defaults.buildfeatures.buildconfig=true -android.nonTransitiveRClass=false +android.enableJetifier=true android.nonFinalResIds=false +android.nonTransitiveRClass=false +android.useAndroidX=true +org.gradle.jvmargs=-Xmx1536M