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