I am creating a text-to-speech app and want a dialog to be displayed to the speaker when the tts object is speaking and automatically hide itself when the finished. Anybody got a way to do this? Below is where I'm at so far, any ideas?
private lateinit var textToSpeech: TextToSpeech
private lateinit var alertDialogBuilder: AlertDialog.Builder
class MainActivity : AppCompatActivity() {
#RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
alertDialogBuilder = AlertDialog.Builder(this)
textToSpeech = TextToSpeech(applicationContext,
TextToSpeech.OnInitListener {})
}
#RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun speakToMe(view: View) {
alertDialogBuilder.setMessage("I'm speaking!")
alertDialogBuilder.show()
val charSeq = "Well hello there!" as CharSequence
textToSpeech.speak(charSeq, TextToSpeech.QUEUE_FLUSH, null, "")
while (!textToSpeech.isSpeaking){
// alertDialogBuilder.dismiss() or some other fun I can't seem to find
}
}
}
You can't use while in this way from the main thread because it will lock up the UI and you can get an ANR (application not responding crash).
When you start the speech, give it an ID, and then add a listener to respond to the result Since the listener might be called on another thread, you have to use runOnUiThread or a coroutine to go back to the main thread to manipulate your UI again.
When you call dialogBuilder.show(), store the returned AlertDialog so you can close it later.
By the way, there is no reason to cast the String to a CharSequence. You can just pass it to the function and it's recognized as a CharSequence by the compiler.
fun speakToMe(view: View) {
alertDialogBuilder.setMessage("I'm speaking!")
val dialog = alertDialogBuilder.show()
val charSeq = "Well hello there!"
val id = "speechId"
textToSpeech.speak(charSeq, TextToSpeech.QUEUE_FLUSH, null, id)
textToSpeech.onUtteranceProgressListener = object: UtteranceProgressListener {
override fun onDone(utteranceId: String) {
if (id == utteranceId) {
runOnUiThread { dialog.dismiss() }
}
}
}
}
You need to add a progress listener to your tts like so:
val params = Bundle()
params.putString(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, text)
tts.setOnUtteranceProgressListener(object : UtteranceProgressListener(){
override fun onDone(p0: String?) {
//dismiss the AlertDialog here (has to run on UI Thread)
}
override fun onError(p0: String?) {
}
override fun onStart(p0: String?) {
//show the AlertDialog here (has to run on UI Thread)
}
})
tts.speak(text ,TextToSpeech.QUEUE_FLUSH, params,"UtterID")
BEAR IN MIND, the "UtterID" (which can be anything you want) is essential to the functionality of the listener.
Related
I would like to open a new activity when phoneViewModel and ScanViewModel are instantiated. They are instantiated by calling an async function InitialRead(). I'm logging each step, atm they are logged as done3 => done2 => done1
I would like to have them in this order:
done1 => done2 => done3
I have following code:
class MainBusinessActivity : AppCompatActivity() {
private lateinit var scanViewModel: ScanViewModel
private lateinit var phoneViewModel: PhoneViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_business)
}
private fun startEntitySetListActivity() = GlobalScope.async {
val sapServiceManager = (application as SAPWizardApplication).sapServiceManager
sapServiceManager?.openODataStore {
phoneViewModel = ViewModelProvider(this#MainBusinessActivity).get(PhoneViewModel::class.java).also {it.initialRead{Log.e("done", "done1")}}
scanViewModel = ViewModelProvider(this#MainBusinessActivity).get(ScanViewModel::class.java).also {it.initialRead{Log.e("done", "done2")}}
}
}
override fun onResume() {
super.onResume()
//startEntitySetListActivity()
runBlocking {
startEntitySetListActivity().await()
val intent = Intent(this#MainBusinessActivity, HomeActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
Log.e("done", "done3")
startActivity(intent)
}
}
}
What am I doing wrong? Can someone correct my code?
Never use runBlocking in an Android app. runBlocking completely defeats the purpose of using coroutines, and can lead to an ANR. You also probably should never use GlobalScope, which leads to UI leaks. You might possibly need it for some kind of long-running task that doesn't make sense to put in a service but doesn't have dependency on any UI components, but I can't think of any examples
You also shouldn't be instantiating your ViewModels in the background. That should be done in onCreate().
Make this function a suspend function, and it can break down the two tasks in the background simultaneously before returning.
Start your coroutine with lifecycleScope.
Assuming sapServiceManager?.openODataStore is an asynchronous task that takes a callback, you will need to wrap it in suspendCoroutine.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_business)
phoneViewModel = ViewModelProvider(this#MainBusinessActivity).get(PhoneViewModel::class.java)
scanViewModel = ViewModelProvider(this#MainBusinessActivity).get(ScanViewModel::class.java)
}
private suspend fun startEntitySetListActivity() = coroutineScope {
val sapServiceManager = (application as SAPWizardApplication).sapServiceManager
sapServiceManager ?: return
suspendCoroutine<Unit> { continuation ->
sapServiceManager.openODataStore { continuation.resume(Unit) }
}
listOf(
launch {
phoneViewModel.initialRead{Log.e("done", "done1")}
},
launch {
scanViewModel.initialRead{Log.e("done", "done2")}
}
).joinAll()
}
override fun onResume() {
super.onResume()
lifecycleScope.launch {
startEntitySetListActivity()
val intent = Intent(this#MainBusinessActivity, HomeActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
Log.e("done", "done3")
startActivity(intent)
}
}
In Google's official codelab about advanced-coroutines-codelab sample, they've used ConflatedBroadcastChannel to watch a variable/object change.
I've used the same technique in one of my side projects, and when resuming the listening activity, sometimes ConflatedBroadcastChannel fires it's recent value, causing the execution of flatMapLatest body without any change.
I think this is happening while the system collects the garbage since I can reproduce this issue by calling System.gc() from another activity.
Here's the code
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
val tvCount = findViewById<TextView>(R.id.tv_count)
viewModel.count.observe(this, Observer {
tvCount.text = it
Toast.makeText(this, "Incremented", Toast.LENGTH_LONG).show();
})
findViewById<Button>(R.id.b_inc).setOnClickListener {
viewModel.increment()
}
findViewById<Button>(R.id.b_detail).setOnClickListener {
startActivity(Intent(this, DetailActivity::class.java))
}
}
}
MainViewModel.kt
class MainViewModel : ViewModel() {
companion object {
val TAG = MainViewModel::class.java.simpleName
}
class IncrementRequest
private var tempCount = 0
private val requestChannel = ConflatedBroadcastChannel<IncrementRequest>()
val count = requestChannel
.asFlow()
.flatMapLatest {
tempCount++
Log.d(TAG, "Incrementing number to $tempCount")
flowOf("Number is $tempCount")
}
.asLiveData()
fun increment() {
requestChannel.offer(IncrementRequest())
}
}
DetailActivity.kt
class DetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_detail)
val button = findViewById<Button>(R.id.b_gc)
val timer = object : CountDownTimer(5000, 1000) {
override fun onFinish() {
button.isEnabled = true
button.text = "CALL SYSTEM.GC() AND CLOSE ACTIVITY"
}
override fun onTick(millisUntilFinished: Long) {
button.text = "${TimeUnit.MILLISECONDS.toSeconds(millisUntilFinished)} second(s)"
}
}
button.setOnClickListener {
System.gc()
finish()
}
timer.start()
}
}
Here's the full source code :
CoroutinesFlowTest.zip
Why is this happening?
What am I missing?
Quoting from the official response, (The simple and straightforward solution)
The problem here is that you are trying to use
ConflatedBroadcastChannel for events, while it is designed to
represent current state as shown in the codelab. Every time the
downstream LiveData is reactivated it receives the most recent state
and performs the incrementing action. Don't use
ConflatedBroadcastChannel for events.
To fix it, you can replace ConflatedBroadcastChannel with
BroadcastChannel<IncrementRequest>(1) (non-conflated channel, which is
Ok for events to use) and it'll work as you expect it too.
In addition to the answer of Kiskae:
This might not be your case, but you can try to use BroadcastChannel(1).asFlow().conflate on a receiver side, but in my case it led to a bug where the code on a receiver side didn't get triggered sometimes (I think because conflate works in a separate coroutine or something).
Or you can use a custom version of stateless ConflatedBroadcastChannel (found here).
class StatelessBroadcastChannel<T> constructor(
private val broadcast: BroadcastChannel<T> = ConflatedBroadcastChannel()
) : BroadcastChannel<T> by broadcast {
override fun openSubscription(): ReceiveChannel<T> = broadcast
.openSubscription()
.apply { poll() }
}
On Coroutine 1.4.2 and Kotlin 1.4.31
Without using live data
private var tempCount = 0
private val requestChannel = BroadcastChannel<IncrementRequest>(Channel.CONFLATED)
val count = requestChannel
.asFlow()
.flatMapLatest {
tempCount++
Log.d(TAG, "Incrementing number to $tempCount")
flowOf("Number is $tempCount")
}
Use Flow and Coroutine
lifecycleScope.launchWhenStarted {
viewModel.count.collect {
tvCount.text = it
Toast.makeText(this#MainActivity, "Incremented", Toast.LENGTH_SHORT).show()
}
}
Without using BroadcastChannel
private var tempCount = 0
private val requestChannel = MutableStateFlow("")
val count: StateFlow<String> = requestChannel
fun increment() {
tempCount += 1
requestChannel.value = "Number is $tempCount"
}
The reason is very simple, ViewModels can persist outside of the lifecycle of Activities. By moving to another activity and garbagecollecting you're disposing of the original MainActivity but keeping the original MainViewModel.
Then when you return from DetailActivity it recreates MainActivity but reuses the viewmodel, which still has the broadcastchannel with a last known value, triggering the callback when count.observe is called.
If you add logging to observe the onCreate and onDestroy methods of the activity you should see the lifecycle getting advanced, while the viewmodel should only be created once.
I had an Activity with a calculation and I extracted the functionality of that Activity in MVP pattern, for simplicity:
CalcActivity
CalcPresenter
Earlier I had all the calculation in one single CalcActivity. There I did some calculations in that activity:
private fun Calculator.doCalculation() {
this.complexCalcualtion(intArrayOf(1, 2, 3, 4, 5, 6), object : CalculationCallback {
override fun onSuccess(result: String) {
runOnUiThread {
result_textview.text = result
}
}
})
}
This whole doCalculation() is done on another thread I guess. I moved this method to the presenter and I wanted to forward result to view:
private fun Calculator.doCalculation() {
this.complexCalcualtion(intArrayOf(1, 2, 3, 4, 5, 6), object : CalculationCallback {
override fun onSuccess(result: String) {
view?.showResult(result)
}
})
}
But view? is never called since it is null in the CalculationCallback.onSuccess() and I cant see view there.
Also I do not have access to activity there, so I can not runOnUiThread there..
How can I forward my result back to view/activity?
You can deliver calculation result by LiveData. This tool is integrated with activity life cycle and your data will be delivered when activity (and it views) will be in active state.
You can implement your calculator like this:
class Calculator {
private val resultLiveData = MutableLiveData<String>().apply { value = "" }
// expose result as live data
val result: LiveData<String> = resultLiveData
fun calculate(input: String) {
// calculation in worker thread
// ...
// return result for live data observers
resultLiveData.postValue("$input CALCULATED!")
}
fun cancel() {
// depending on background worker
}
}
And use it in activity (or fragment)
class MyActivity : Activity() {
private val calculator = Calculator()
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
calculator.result.observe(this::getLifecycle) { result ->
// setup result in your view
text_view.text = result
}
}
override fun onStart() {
super.onStart()
calculator.calculate("Input data")
}
override fun onStop() {
super.onStop()
calculator.cancel()
}
}
In My project sometimes the created thread does not start as fast as it should be, This happens on a minimal occasions but mostly will happen on slow/older phones.
I my Thread like..
class DBThread(threadName: String) : HandlerThread(threadName) {
private var mWorkerHandler: Handler? = null
override fun onLooperPrepared() {
super.onLooperPrepared()
mWorkerHandler = Handler(looper)
}
fun createTask(task: Runnable) {
mWorkerHandler?.post(task)
}
}
and when i use it and call on activity..
//this will handle db queries on background and not on main ui thread
var mDbThread: DBThread = DBThread("dbThread")
//use this to interact to main ui thread from different thread
val mUiHandler = Handler()
var mDb: LocalDatabase? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mDbThread.start()
mDb = LocalDatabase.getInstance(this)
fetchAndSetList()
}
override fun onDestroy() {
super.onDestroy()
LocalDatabase.destroyInstance()
mDbThread.quitSafely()
}
private fun fetchAndSetList(){
mDbThread.createTask(Runnable {
val list = getList()
mUiHandler.post {
// this sometimes does not trigger
setList(list)
}
})
}
the function setList does not trigger on sometimes.
And so i did something like this.
fun createTask(task: Runnable) {
if(mWorkerHandler == null ){
createTask(task)
return
}
mWorkerHandler?.post(task)
}
the modified action seems to work however I'm not quite sure if this is a SAFE way to do this. Thank you in advance.
I think the reason why mWorkerhandler is null is because Thread.start will create the new VMThread and start the looper in the VMThread. The whole flow is asynchronous so when onLooperPrepared actually is called, it's too late because "fetchAndSetList" is already trying to use mWorkerHandler
The solution is create the handler outside of the HandlerThread:
Handler workerHandler;
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mDbThread.start()
workerHandler = new Handler(mDbThread.getLooper());
mDb = LocalDatabase.getInstance(this)
fetchAndSetList()
}
private fun fetchAndSetList(){
workerHandler.post(Runnable {
val list = getList()
mUiHandler.post {
// this sometimes does not trigger
setList(list)
}
})
}
My MainActivity implements the Observer class. I also have a class called ObservedObject that extends the Observable class.
Here is my custom Observable , called ObservedObject:
class ObservedObject(var value: Boolean) : Observable() {
init {
value = false
}
fun setVal(vals: Boolean) {
value = vals
setChanged()
notifyObservers()
}
fun printVal() {
Log.i("Value" , "" + value)
}
}
Here is my Application called SpeechApp which contains my ObservedObject (an Observable actually):
class SpeechApp: Application() {
var isDictionaryRead = ObservedObject(false)
override fun onCreate() {
super.onCreate()
wordslist = ArrayList()
Thread {
execute()
}.start()
}
fun execute() {
while (/* Condition */) {
//Log.i("Read" , line)
/*Does Something Here*/
}
isDictionaryRead.setVal(true)
}
}
In my MainActivity, I mainly have a dialog, that should be displayed after I have got the output after Speech Recognition. It will display as long as the value of isDictionaryRead doesn't change to true:
class MainActivity(private val REQ_CODE_SPEECH_INPUT: Int = 100) : AppCompatActivity() , Observer{
override fun update(o: Observable?, arg: Any?) {
(o as ObservedObject).printVal()
dialog.hide()
}
private lateinit var app : SpeechApp
private lateinit var dialog: MaterialDialog
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
dialog = MaterialDialog.Builder(this)
.title("Please Wait")
.content("Loading from the Dictionary")
.progress(true , 0)
.build()
app = application as SpeechApp
app.isDictionaryRead.addObserver(this)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.menu_speech, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
val id = item?.itemId
when(id) {
R.id.menu_option_speech -> {
invokeSpeech()
}
}
return super.onOptionsItemSelected(item)
}
private fun invokeSpeech() {
/* Does Something, Works Fine */
try {
startActivityForResult(intent , REQ_CODE_SPEECH_INPUT)
}
catch (ex: ActivityNotFoundException) {
/* Does Something */
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
REQ_CODE_SPEECH_INPUT -> {
if (resultCode == Activity.RESULT_OK && null != data) {
dialog.show()
}
}
}
super.onActivityResult(requestCode, resultCode, data)
}
}
Now the problem is, when the SpeechApp sets the value of isDictionaryRead to true, I expect it to call the MainActivity update() method, wherein I have given the code to hide the dialog. That particular code is not working, and my dialog box doesn't go away. Where am I going wrong?
PS. I've pushed my code to Github now, just in case anyone could help me where I am going wrong.
The only thing I can think of that would cause this problem is that the execute() thread that was started in SpeechApp.onCreate finished execution and called isDictionaryRead.setVal(true) before the activity could call app.isDictionaryRead.addObserver(this). As a result, notifyObservers is called before the activity even starts observing, and as a result it is not notified. Here's my proposed solution: Start the execute thread in the activity's onCreate method after adding it as an observer.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
dialog = MaterialDialog.Builder(this)
.title("Please Wait")
.content("Loading from the Dictionary")
.progress(true , 0)
.build()
app = application as SpeechApp
app.isDictionaryRead.addObserver(this)
app.asyncReadDictionary()
}
Then remove the thread call from SpeechApp.onCreate and use this instead
// in SpeechApp
fun asyncReadDictionary() {
if (!isDictionaryRead.value) {
Thread { execute() }.start()
}
}
private fun execute() {
while (/* Condition */) {
//Log.i("Read" , line)
/*Does Something Here*/
}
isDictionaryRead.value = true
}
Also, reimplement ObservableObject as follows
class ObservedObject : Observable() {
var value: Boolean = false
set(newValue) {
field = newValue
setChanged()
notifyObservers()
}
fun printVal() {
Log.i("Value" , "" + value)
}
}