1 package org.coolreader.crengine; 2 3 import android.content.BroadcastReceiver; 4 import android.content.Context; 5 import android.content.Intent; 6 import android.content.IntentFilter; 7 import android.graphics.drawable.BitmapDrawable; 8 import android.media.AudioManager; 9 import android.os.Build; 10 import android.os.Bundle; 11 import android.speech.tts.TextToSpeech; 12 import android.speech.tts.UtteranceProgressListener; 13 import android.speech.tts.Voice; 14 import android.util.Log; 15 import android.view.Gravity; 16 import android.view.KeyEvent; 17 import android.view.LayoutInflater; 18 import android.view.View; 19 import android.view.ViewGroup; 20 import android.view.WindowManager; 21 import android.widget.ImageButton; 22 import android.widget.PopupWindow; 23 import android.widget.SeekBar; 24 import android.widget.SeekBar.OnSeekBarChangeListener; 25 import android.widget.TextView; 26 27 import com.s_trace.motion_watchdog.HandlerThread; 28 import com.s_trace.motion_watchdog.MotionWatchdogHandler; 29 30 import org.coolreader.CoolReader; 31 import org.coolreader.R; 32 import org.coolreader.tts.TTSControlBinder; 33 import org.coolreader.tts.TTSControlService; 34 import org.coolreader.tts.TTSControlServiceAccessor; 35 36 import java.util.ArrayList; 37 import java.util.HashMap; 38 import java.util.Locale; 39 import java.util.Map; 40 import java.util.Set; 41 42 public class TTSToolbarDlg implements Settings { 43 public static final Logger log = L.create("ttssrv"); 44 45 private static final String CR3_UTTERANCE_ID = "cr3UtteranceId"; 46 private static final int MAX_CONTINUOUS_ERRORS = 3; 47 48 private final PopupWindow mWindow; 49 private final CoolReader mCoolReader; 50 private final ReaderView mReaderView; 51 private String mBookTitle; 52 private TextToSpeech mTTS; 53 private TTSControlServiceAccessor mTTSControl; 54 private ImageButton mPlayPauseButton; 55 private TextView mVolumeTextView; 56 private TextView mSpeedTextView; 57 private SeekBar mSbSpeed; 58 private SeekBar mSbVolume; 59 private HandlerThread mMotionWatchdog; 60 private boolean changedPageMode; 61 private int mContinuousErrors = 0; 62 private Runnable mOnCloseListener; 63 private boolean mClosed; 64 private Selection mCurrentSelection; 65 private boolean isSpeaking; 66 private Runnable mOnStopRunnable; 67 private int mMotionTimeout; 68 private boolean mAutoSetDocLang; 69 private String mForcedLanguage; 70 private String mForcedVoice; 71 private int mTTSSpeedPercent = 50; // 50% (normal) 72 73 74 BroadcastReceiver mTTSControlButtonReceiver = new BroadcastReceiver() { 75 @Override 76 public void onReceive(Context context, Intent intent) { 77 String action = intent.getAction(); 78 log.d("received action: " + action); 79 if (null != action) { 80 switch (action) { 81 case TTSControlService.TTS_CONTROL_ACTION_PLAY_PAUSE: 82 toggleStartStop(); 83 break; 84 case TTSControlService.TTS_CONTROL_ACTION_NEXT: 85 if ( isSpeaking ) { 86 stop(() -> { 87 isSpeaking = true; 88 moveSelection( ReaderCommand.DCMD_SELECT_NEXT_SENTENCE ); 89 }); 90 } else 91 moveSelection( ReaderCommand.DCMD_SELECT_NEXT_SENTENCE ); 92 break; 93 case TTSControlService.TTS_CONTROL_ACTION_PREV: 94 if ( isSpeaking ) { 95 stop(() -> { 96 isSpeaking = true; 97 moveSelection( ReaderCommand.DCMD_SELECT_PREV_SENTENCE ); 98 }); 99 } else 100 moveSelection( ReaderCommand.DCMD_SELECT_PREV_SENTENCE ); 101 break; 102 case TTSControlService.TTS_CONTROL_ACTION_DONE: 103 stopAndClose(); 104 break; 105 } 106 } 107 } 108 }; 109 showDialog( CoolReader coolReader, ReaderView readerView, TextToSpeech tts)110 static public TTSToolbarDlg showDialog( CoolReader coolReader, ReaderView readerView, TextToSpeech tts) { 111 TTSToolbarDlg dlg = new TTSToolbarDlg(coolReader, readerView, tts); 112 log.d("popup: " + dlg.mWindow.getWidth() + "x" + dlg.mWindow.getHeight()); 113 return dlg; 114 } 115 setOnCloseListener(Runnable handler)116 public void setOnCloseListener(Runnable handler) { 117 mOnCloseListener = handler; 118 } 119 stopAndClose()120 public void stopAndClose() { 121 if (mClosed) 122 return; 123 isSpeaking = false; 124 mClosed = true; 125 BackgroundThread.instance().executeGUI(() -> { 126 stop(); 127 mCoolReader.unregisterReceiver(mTTSControlButtonReceiver); 128 if (null != mTTSControl) 129 mTTSControl.unbind(); 130 Intent intent = new Intent(mCoolReader, TTSControlService.class); 131 mCoolReader.stopService(intent); 132 restoreReaderMode(); 133 mReaderView.clearSelection(); 134 if (mOnCloseListener != null) 135 mOnCloseListener.run(); 136 if ( mWindow.isShowing() ) 137 mWindow.dismiss(); 138 mReaderView.save(); 139 }); 140 } 141 setReaderMode()142 private void setReaderMode() { 143 String oldViewSetting = mReaderView.getSetting( ReaderView.PROP_PAGE_VIEW_MODE ); 144 if ( "1".equals(oldViewSetting) ) { 145 changedPageMode = true; 146 mReaderView.setViewModeNonPermanent(ViewMode.SCROLL); 147 } 148 moveSelection( ReaderCommand.DCMD_SELECT_FIRST_SENTENCE ); 149 } 150 restoreReaderMode()151 private void restoreReaderMode() { 152 if ( changedPageMode ) { 153 mReaderView.setViewModeNonPermanent(ViewMode.PAGES); 154 } 155 } 156 moveSelection( ReaderCommand cmd )157 private void moveSelection( ReaderCommand cmd ) 158 { 159 mReaderView.moveSelection(cmd, 0, new ReaderView.MoveSelectionCallback() { 160 161 @Override 162 public void onNewSelection(Selection selection) { 163 log.d("onNewSelection: " + selection.text); 164 mCurrentSelection = selection; 165 if ( isSpeaking ) 166 say(mCurrentSelection); 167 } 168 169 @Override 170 public void onFail() { 171 log.e("fail()"); 172 stop(); 173 //mCurrentSelection = null; 174 } 175 }); 176 } 177 say( Selection selection )178 private void say( Selection selection ) { 179 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 180 Bundle bundle = new Bundle(); 181 bundle.putInt(TextToSpeech.Engine.KEY_PARAM_STREAM, AudioManager.STREAM_MUSIC); 182 mTTS.speak(selection.text, TextToSpeech.QUEUE_ADD, bundle, CR3_UTTERANCE_ID); 183 } else { 184 HashMap<String, String> params = new HashMap<String, String>(); 185 params.put(TextToSpeech.Engine.KEY_PARAM_STREAM, String.valueOf(AudioManager.STREAM_MUSIC)); 186 params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, CR3_UTTERANCE_ID); 187 mTTS.speak(selection.text, TextToSpeech.QUEUE_ADD, params); 188 } 189 runInTTSControlService(tts -> tts.notifyPlay(mBookTitle, selection.text)); 190 } 191 start()192 private void start() { 193 if ( mCurrentSelection ==null ) 194 return; 195 startMotionWatchdog(); 196 isSpeaking = true; 197 say(mCurrentSelection); 198 } 199 startMotionWatchdog()200 private void startMotionWatchdog(){ 201 String TAG = "MotionWatchdog"; 202 log.d("startMotionWatchdog() enter"); 203 204 if (mMotionTimeout == 0) { 205 Log.d(TAG, "startMotionWatchdog() early exit - timeout is 0"); 206 return; 207 } 208 209 mMotionWatchdog = new HandlerThread("MotionWatchdog"); 210 mMotionWatchdog.start(); 211 new MotionWatchdogHandler(this, mCoolReader, mMotionWatchdog, mMotionTimeout); 212 Log.d(TAG, "startMotionWatchdog() exit"); 213 } 214 stop()215 private void stop() { 216 stop(null); 217 } 218 stop(Runnable runnable)219 private void stop(Runnable runnable) { 220 isSpeaking = false; 221 mOnStopRunnable = runnable; 222 if ( mTTS.isSpeaking() ) { 223 mTTS.stop(); 224 } 225 if (mMotionWatchdog != null) { 226 mMotionWatchdog.interrupt(); 227 } 228 } 229 pause()230 public void pause() { 231 if (isSpeaking) 232 toggleStartStop(); 233 } 234 toggleStartStop()235 private void toggleStartStop() { 236 if ( isSpeaking ) { 237 mPlayPauseButton.setImageResource(Utils.resolveResourceIdByAttr(mCoolReader, R.attr.ic_media_play_drawable, R.drawable.ic_media_play)); 238 runInTTSControlService(tts -> tts.notifyPause(mBookTitle)); 239 stop(); 240 } else { 241 if (null != mCurrentSelection) { 242 mPlayPauseButton.setImageResource(Utils.resolveResourceIdByAttr(mCoolReader, R.attr.ic_media_pause_drawable, R.drawable.ic_media_pause)); 243 runInTTSControlService(tts -> tts.notifyPlay(mBookTitle, mCurrentSelection.text)); 244 start(); 245 } 246 } 247 } 248 runInTTSControlService(TTSControlBinder.Callback callback)249 private void runInTTSControlService(TTSControlBinder.Callback callback) { 250 if (null == mTTSControl) { 251 mTTSControl = new TTSControlServiceAccessor(mCoolReader); 252 } 253 mTTSControl.bind(callback); 254 } 255 changeTTS(TextToSpeech tts)256 public void changeTTS(TextToSpeech tts) { 257 pause(); 258 mTTS = tts; 259 setupTTSVoice(); 260 setupTTSHandlers(); 261 } 262 263 /** 264 * Convert speech speed percentage to speech rate value. 265 * @param percent speech rate percentage 266 * @return speech rate value 267 * 268 * 0% - 0.30 269 * 10% - 0.44 270 * 20% - 0.58 271 * 30% - 0.72 272 * 40% - 0.86 273 * 50% - 1.00 274 * 60% - 1.50 275 * 70% - 2.00 276 * 80% - 2.50 277 * 90% - 3.00 278 * 100%- 3.50 279 */ speechRateFromPercent(int percent)280 private float speechRateFromPercent(int percent) { 281 float rate; 282 if ( percent < 50 ) 283 rate = 0.3f + 0.7f * percent / 50f; 284 else 285 rate = 1.0f + 2.5f * (percent - 50) / 50f; 286 return rate; 287 } 288 setAppSettings(Properties newSettings, Properties oldSettings)289 public void setAppSettings(Properties newSettings, Properties oldSettings) { 290 log.v("setAppSettings()"); 291 BackgroundThread.ensureGUI(); 292 if (oldSettings == null) 293 oldSettings = new Properties(); 294 Properties changedSettings = newSettings.diff(oldSettings); 295 for (Map.Entry<Object, Object> entry : changedSettings.entrySet()) { 296 String key = (String) entry.getKey(); 297 String value = (String) entry.getValue(); 298 processAppSetting(key, value); 299 } 300 // Apply settings 301 setupTTSVoice(); 302 mTTS.setSpeechRate(speechRateFromPercent(mTTSSpeedPercent)); 303 mSbSpeed.setProgress(mTTSSpeedPercent); 304 } 305 processAppSetting(String key, String value)306 private void processAppSetting(String key, String value) { 307 boolean flg = "1".equals(value); 308 switch (key) { 309 case PROP_APP_MOTION_TIMEOUT: 310 mMotionTimeout = Utils.parseInt(value, 0, 0, 100); 311 mMotionTimeout = mMotionTimeout * 60 * 1000; // Convert minutes to msecs 312 break; 313 case PROP_APP_TTS_SPEED: 314 mTTSSpeedPercent = Utils.parseInt(value, 50, 0, 100); 315 break; 316 case PROP_APP_TTS_ENGINE: 317 // handled in CoolReader 318 break; 319 case PROP_APP_TTS_USE_DOC_LANG: 320 mAutoSetDocLang = flg; 321 break; 322 case PROP_APP_TTS_FORCE_LANGUAGE: 323 mForcedLanguage = value; 324 break; 325 case PROP_APP_TTS_VOICE: 326 mForcedVoice = value; 327 break; 328 } 329 } 330 setupTTSVoice()331 private void setupTTSVoice() { 332 if (mAutoSetDocLang) { 333 // set language for TTS based on book's language 334 log.d("Setting language according book's language"); 335 Locale locale = null; 336 BookInfo bookInfo = mReaderView.getBookInfo(); 337 if (null != bookInfo) { 338 FileInfo fileInfo = bookInfo.getFileInfo(); 339 if (null != fileInfo) { 340 log.d("book language is \"" + fileInfo.language + "\""); 341 if (null != fileInfo.language && fileInfo.language.length() > 0) { 342 locale = new Locale(fileInfo.language); 343 } 344 } 345 } 346 if (null != locale) { 347 log.d("trying to set TTS language to \"" + locale.getDisplayLanguage() + "\""); 348 mTTS.setLanguage(locale); 349 } else { 350 log.e("Failed to detect book's language, using system default!"); 351 } 352 } else { 353 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) { 354 // Update voices list 355 Set<Voice> voices; 356 if (null != mTTS) 357 voices = mTTS.getVoices(); 358 else 359 voices = null; 360 // Filter voices for given language 361 log.d("Trying to find voice for language \"" + mForcedLanguage + "\""); 362 Voice sel_voice = null; 363 if (null != voices && null != mForcedLanguage && mForcedLanguage.length() > 0) { 364 ArrayList<Voice> acceptable_voices = new ArrayList<>(); 365 for (Voice voice : voices) { 366 Locale locale = voice.getLocale(); 367 if (mForcedLanguage.toLowerCase().equals(locale.toString().toLowerCase())) { 368 acceptable_voices.add(voice); 369 } 370 } 371 if (acceptable_voices.size() > 0) { 372 // Select one specific voice 373 boolean found = false; 374 for (Voice voice : acceptable_voices) { 375 if (voice.getName().equals(mForcedVoice)) 376 { 377 sel_voice = voice; 378 found = true; 379 break; 380 } 381 } 382 if (found) { 383 log.d("Voice \"" + mForcedVoice + "\" is found"); 384 } else { 385 sel_voice = acceptable_voices.get(0); 386 log.e("Voice \"" + mForcedVoice + "\" NOT found, using \"" + sel_voice.getName() + "\""); 387 } 388 } 389 } 390 if (sel_voice != null) { 391 log.d("Setting voice: " + sel_voice.getName()); 392 mTTS.setVoice(sel_voice); 393 } else { 394 log.e("Failed to find voice for language \"" + mForcedLanguage + "\"!"); 395 } 396 } 397 } 398 399 } 400 setupTTSHandlers()401 private void setupTTSHandlers() { 402 if (null != mTTS) { 403 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { 404 mTTS.setOnUtteranceCompletedListener(utteranceId -> { 405 if (null != mOnStopRunnable) { 406 mOnStopRunnable.run(); 407 mOnStopRunnable = null; 408 } else { 409 if ( isSpeaking ) 410 moveSelection( ReaderCommand.DCMD_SELECT_NEXT_SENTENCE ); 411 } 412 }); 413 } else { 414 mTTS.setOnUtteranceProgressListener(new UtteranceProgressListener() { 415 @Override 416 public void onStart(String utteranceId) { 417 // nothing... 418 } 419 420 @Override 421 public void onDone(String utteranceId) { 422 if (null != mOnStopRunnable) { 423 mOnStopRunnable.run(); 424 mOnStopRunnable = null; 425 } else { 426 if ( isSpeaking ) 427 moveSelection( ReaderCommand.DCMD_SELECT_NEXT_SENTENCE ); 428 } 429 mContinuousErrors = 0; 430 } 431 432 @Override 433 public void onError(String utteranceId) { 434 log.e("TTS error"); 435 mContinuousErrors++; 436 if (mContinuousErrors > MAX_CONTINUOUS_ERRORS) { 437 BackgroundThread.instance().executeGUI(() -> { 438 toggleStartStop(); 439 mCoolReader.showToast(R.string.tts_failed); 440 }); 441 } else { 442 if (null != mOnStopRunnable) { 443 mOnStopRunnable.run(); 444 mOnStopRunnable = null; 445 } else { 446 if ( isSpeaking ) 447 moveSelection( ReaderCommand.DCMD_SELECT_NEXT_SENTENCE ); 448 } 449 } 450 } 451 452 // API 21 453 @Override 454 public void onError(String utteranceId, int errorCode) { 455 log.e("TTS error, code=" + errorCode); 456 mContinuousErrors++; 457 if (mContinuousErrors > MAX_CONTINUOUS_ERRORS) { 458 BackgroundThread.instance().executeGUI(() -> { 459 toggleStartStop(); 460 mCoolReader.showToast(R.string.tts_failed); 461 }); 462 } else { 463 if (null != mOnStopRunnable) { 464 mOnStopRunnable.run(); 465 mOnStopRunnable = null; 466 } else { 467 if ( isSpeaking ) 468 moveSelection( ReaderCommand.DCMD_SELECT_NEXT_SENTENCE ); 469 } 470 } 471 } 472 473 // API 23 474 @Override 475 public void onStop(String utteranceId, boolean interrupted) { 476 if (null != mOnStopRunnable) { 477 mOnStopRunnable.run(); 478 mOnStopRunnable = null; 479 } 480 } 481 482 // API 24 483 public void onAudioAvailable(String utteranceId, byte[] audio) { 484 // nothing... 485 } 486 487 // API 24 488 public void onBeginSynthesis(String utteranceId, 489 int sampleRateInHz, 490 int audioFormat, 491 int channelCount) { 492 // nothing... 493 } 494 }); 495 } 496 } 497 } 498 TTSToolbarDlg(CoolReader coolReader, ReaderView readerView, TextToSpeech tts)499 public TTSToolbarDlg(CoolReader coolReader, ReaderView readerView, TextToSpeech tts) { 500 mCoolReader = coolReader; 501 mReaderView = readerView; 502 View anchor = readerView.getSurface(); 503 mTTS = tts; 504 setupTTSHandlers(); 505 506 //Context context = mCoolReader.getApplicationContext(); 507 Context context = anchor.getContext(); 508 LayoutInflater inflater = LayoutInflater.from(context); 509 View panel = inflater.inflate(R.layout.tts_toolbar, null); 510 panel.measure(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); 511 512 mPlayPauseButton = panel.findViewById(R.id.tts_play_pause); 513 mPlayPauseButton.setImageResource(Utils.resolveResourceIdByAttr(mCoolReader, R.attr.ic_media_play_drawable, R.drawable.ic_media_play)); 514 ImageButton backButton = panel.findViewById(R.id.tts_back); 515 ImageButton forwardButton = panel.findViewById(R.id.tts_forward); 516 ImageButton stopButton = panel.findViewById(R.id.tts_stop); 517 ImageButton optionsButton = panel.findViewById(R.id.tts_options); 518 519 mWindow = new PopupWindow( context ); 520 mWindow.setBackgroundDrawable(new BitmapDrawable()); 521 mPlayPauseButton.setOnClickListener(v -> toggleStartStop()); 522 backButton.setOnClickListener(v -> { 523 if ( isSpeaking ) { 524 stop(() -> { 525 isSpeaking = true; 526 moveSelection( ReaderCommand.DCMD_SELECT_PREV_SENTENCE ); 527 }); 528 } else 529 moveSelection( ReaderCommand.DCMD_SELECT_PREV_SENTENCE ); 530 }); 531 forwardButton.setOnClickListener(v -> { 532 if ( isSpeaking ) { 533 stop(() -> { 534 isSpeaking = true; 535 moveSelection( ReaderCommand.DCMD_SELECT_NEXT_SENTENCE ); 536 }); 537 } else 538 moveSelection( ReaderCommand.DCMD_SELECT_NEXT_SENTENCE ); 539 }); 540 optionsButton.setOnClickListener(v -> { 541 OptionsDialog dlg = new OptionsDialog(mCoolReader, OptionsDialog.Mode.TTS, null, null, mTTS); 542 dlg.show(); 543 }); 544 stopButton.setOnClickListener(v -> stopAndClose()); 545 panel.setFocusable(true); 546 panel.setEnabled(true); 547 panel.setOnKeyListener((v, keyCode, event) -> { 548 if ( event.getAction()==KeyEvent.ACTION_UP ) { 549 switch ( keyCode ) { 550 case KeyEvent.KEYCODE_VOLUME_DOWN: 551 case KeyEvent.KEYCODE_VOLUME_UP: 552 return true; 553 case KeyEvent.KEYCODE_BACK: 554 stopAndClose(); 555 return true; 556 // case KeyEvent.KEYCODE_DPAD_LEFT: 557 // case KeyEvent.KEYCODE_DPAD_UP: 558 // //mReaderView.findNext(pattern, true, caseInsensitive); 559 // return true; 560 // case KeyEvent.KEYCODE_DPAD_RIGHT: 561 // case KeyEvent.KEYCODE_DPAD_DOWN: 562 // //mReaderView.findNext(pattern, false, caseInsensitive); 563 // return true; 564 } 565 } else if ( event.getAction()==KeyEvent.ACTION_DOWN ) { 566 switch ( keyCode ) { 567 case KeyEvent.KEYCODE_VOLUME_DOWN: { 568 int p = mSbVolume.getProgress() - 5; 569 if ( p<0 ) 570 p = 0; 571 mSbVolume.setProgress(p); 572 return true; 573 } 574 case KeyEvent.KEYCODE_VOLUME_UP: 575 int p = mSbVolume.getProgress() + 5; 576 if ( p>100 ) 577 p = 100; 578 mSbVolume.setProgress(p); 579 return true; 580 } 581 if ( keyCode == KeyEvent.KEYCODE_BACK) { 582 return true; 583 } 584 } 585 return false; 586 }); 587 588 mWindow.setOnDismissListener(() -> { 589 if ( !mClosed) 590 stopAndClose(); 591 }); 592 593 mWindow.setBackgroundDrawable(new BitmapDrawable()); 594 mWindow.setWidth(WindowManager.LayoutParams.FILL_PARENT); 595 mWindow.setHeight(WindowManager.LayoutParams.WRAP_CONTENT); 596 mWindow.setFocusable(true); 597 mWindow.setTouchable(true); 598 mWindow.setOutsideTouchable(true); 599 mWindow.setContentView(panel); 600 601 int [] location = new int[2]; 602 anchor.getLocationOnScreen(location); 603 604 mWindow.showAtLocation(anchor, Gravity.TOP | Gravity.CENTER_HORIZONTAL, location[0], location[1] + anchor.getHeight() - panel.getHeight()); 605 606 setReaderMode(); 607 608 // setup speed && volume seek bars 609 int volume = mCoolReader.getVolume(); 610 mVolumeTextView = panel.findViewById(R.id.tts_lbl_volume); 611 mVolumeTextView.setText(String.format(Locale.getDefault(), "%s (%d%%)", context.getString(R.string.tts_volume), volume)); 612 mSpeedTextView = panel.findViewById(R.id.tts_lbl_speed); 613 mSpeedTextView.setText(String.format(Locale.getDefault(), "%s (x%.2f)", context.getString(R.string.tts_rate), speechRateFromPercent(50))); 614 615 mSbSpeed = panel.findViewById(R.id.tts_sb_speed); 616 mSbVolume = panel.findViewById(R.id.tts_sb_volume); 617 618 mSbSpeed.setMax(100); 619 mSbSpeed.setProgress(50); 620 mSbVolume.setMax(100); 621 mSbVolume.setProgress(volume); 622 mSbSpeed.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { 623 @Override 624 public void onProgressChanged(SeekBar seekBar, int progress, 625 boolean fromUser) { 626 // round to a multiple of 5 627 int roundedVal = 5*(progress/5); 628 if (progress != roundedVal) { 629 mSbSpeed.setProgress(roundedVal); 630 return; 631 } 632 mTTSSpeedPercent = progress; 633 mTTS.setSpeechRate(speechRateFromPercent(mTTSSpeedPercent)); 634 mSpeedTextView.setText(String.format(Locale.getDefault(), "%s (x%.2f)", context.getString(R.string.tts_rate), speechRateFromPercent(progress))); 635 } 636 637 @Override 638 public void onStartTrackingTouch(SeekBar seekBar) { 639 } 640 641 @Override 642 public void onStopTrackingTouch(SeekBar seekBar) { 643 mCoolReader.setSetting(PROP_APP_TTS_SPEED, String.valueOf(mTTSSpeedPercent), false); 644 } 645 }); 646 mSbVolume.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { 647 @Override 648 public void onProgressChanged(SeekBar seekBar, int progress, 649 boolean fromUser) { 650 mCoolReader.setVolume(progress); 651 mVolumeTextView.setText(String.format(Locale.getDefault(), "%s (%d%%)", context.getString(R.string.tts_volume), progress)); 652 } 653 654 @Override 655 public void onStartTrackingTouch(SeekBar seekBar) { 656 } 657 658 @Override 659 public void onStopTrackingTouch(SeekBar seekBar) { 660 } 661 }); 662 663 BookInfo bookInfo = mReaderView.getBookInfo(); 664 if (null != bookInfo) { 665 FileInfo fileInfo = bookInfo.getFileInfo(); 666 if (null != fileInfo) { 667 mBookTitle = fileInfo.title; 668 } 669 } 670 if (null == mBookTitle) 671 mBookTitle = ""; 672 673 // Start the foreground service to make this app also foreground, 674 // even if the main activity is in the background. 675 // https://developer.android.com/about/versions/oreo/background#services 676 Intent intent = new Intent(coolReader, TTSControlService.class); 677 Bundle data = new Bundle(); 678 data.putString("bookTitle", mBookTitle); 679 intent.putExtras(data); 680 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) 681 coolReader.startForegroundService(intent); 682 else 683 coolReader.startService(intent); 684 IntentFilter filter = new IntentFilter(); 685 filter.addAction(TTSControlService.TTS_CONTROL_ACTION_PLAY_PAUSE); 686 filter.addAction(TTSControlService.TTS_CONTROL_ACTION_NEXT); 687 filter.addAction(TTSControlService.TTS_CONTROL_ACTION_PREV); 688 filter.addAction(TTSControlService.TTS_CONTROL_ACTION_DONE); 689 mCoolReader.registerReceiver(mTTSControlButtonReceiver, filter); 690 691 panel.requestFocus(); 692 } 693 694 } 695