1 /* Copyright (C) 2018 Olga Yakovleva <yakovleva.o.v@gmail.com> */ 2 3 /* This program is free software: you can redistribute it and/or modify */ 4 /* it under the terms of the GNU Lesser General Public License as published by */ 5 /* the Free Software Foundation, either version 3 of the License, or */ 6 /* (at your option) any later version. */ 7 8 /* This program is distributed in the hope that it will be useful, */ 9 /* but WITHOUT ANY WARRANTY; without even the implied warranty of */ 10 /* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */ 11 /* GNU Lesser General Public License for more details. */ 12 13 /* You should have received a copy of the GNU Lesser General Public License */ 14 /* along with this program. If not, see <http://www.gnu.org/licenses/>. */ 15 16 package com.github.olga_yakovleva.rhvoice.android; 17 18 import android.os.Bundle; 19 import androidx.fragment.app.Fragment; 20 import android.media.MediaPlayer; 21 import android.speech.tts.TextToSpeech; 22 import android.content.Context; 23 import java.util.Map; 24 import java.util.HashMap; 25 import android.content.res.AssetFileDescriptor; 26 import java.io.IOException; 27 import android.util.Log; 28 import android.os.Handler; 29 import android.speech.tts.UtteranceProgressListener; 30 import android.media.AudioAttributes; 31 import android.media.AudioManager; 32 import android.os.Build; 33 import android.media.AudioFocusRequest; 34 35 public final class PlayerFragment extends Fragment 36 { 37 private static final String TAG="RHVoice.PlayerFragment"; 38 39 private final MediaPlayer.OnCompletionListener playerDoneListener=new MediaPlayer.OnCompletionListener() 40 { 41 @Override 42 public void onCompletion(MediaPlayer mp) 43 { 44 if(BuildConfig.DEBUG) 45 Log.v(TAG,"Playback completed"); 46 playerState.onStop(); 47 } 48 }; 49 50 private final MediaPlayer.OnErrorListener playerErrorListener=new MediaPlayer.OnErrorListener() 51 { 52 @Override 53 public boolean onError(MediaPlayer mp,int what,int extra) 54 { 55 if(BuildConfig.DEBUG) 56 Log.e(TAG,"Error: "+what+", "+extra); 57 playerState.reset(); 58 return true; 59 } 60 }; 61 62 private final TextToSpeech.OnInitListener ttsInitListener=new TextToSpeech.OnInitListener() 63 { 64 @Override 65 public void onInit(int status) 66 { 67 ttsState.onInit(status); 68 } 69 }; 70 71 private final UtteranceProgressListener ttsProgressListener=new UtteranceProgressListener() 72 { 73 @Override 74 public void onStart(String uttId) 75 { 76 } 77 78 @Override 79 public void onDone(String uttId) 80 { 81 ttsHandler.post(new TTSDoneEvent(uttId)); 82 } 83 84 @Override 85 public void onError(String uttId) 86 { 87 if(BuildConfig.DEBUG) 88 Log.w(TAG,"TTS error in utt "+uttId); 89 ttsHandler.post(new TTSErrorEvent(uttId)); 90 } 91 92 @Override 93 public void onError(String uttId,int code) 94 { 95 onError(uttId); 96 } 97 }; 98 99 private class AudioFocusListener implements AudioManager.OnAudioFocusChangeListener 100 { 101 @Override onAudioFocusChange(int change)102 public void onAudioFocusChange(int change) 103 { 104 switch(change) 105 { 106 case AudioManager.AUDIOFOCUS_LOSS: 107 playerState.stop(); 108 ttsState.stop(); 109 break; 110 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: 111 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: 112 playerState.pause(); 113 ttsState.pause(); 114 break; 115 case AudioManager.AUDIOFOCUS_GAIN: 116 playerState.resume(); 117 ttsState.resume(); 118 break; 119 default: 120 break; 121 } 122 } 123 } 124 125 private AudioManager audioManager; 126 private AudioAttributes audioAttrs; 127 private final Map<String,Map<String,Integer>> resIdCache=new HashMap<String,Map<String,Integer>>(); 128 private VoicePack playerVoice; 129 private MediaPlayer player; 130 private PlayerState playerState=new UninitializedPlayerState(); 131 private VoicePack ttsVoice; 132 private TextToSpeech tts; 133 private long ttsUttId; 134 private TTSState ttsState=new UninitializedTTSState(); 135 private Handler ttsHandler; 136 private AudioFocusListener audioFocusListener; 137 private AudioFocusRequest audioFocusRequest; 138 doGetResId(String name,String type)139 private int doGetResId(String name,String type) 140 { 141 Context context=getActivity(); 142 if(BuildConfig.DEBUG) 143 Log.v(TAG,"Looking for resource: name="+name+", type="+type); 144 int id=context.getResources().getIdentifier(name,type,context.getPackageName()); 145 if(BuildConfig.DEBUG) 146 { 147 if(id==0) 148 Log.w(TAG,"Resource not found"); 149 } 150 return id; 151 } 152 getResId(String name,String type)153 private int getResId(String name,String type) 154 { 155 Map<String,Integer> typeCache=resIdCache.get(type); 156 if(typeCache!=null) 157 { 158 Integer id0=typeCache.get(name); 159 if(id0!=null) 160 return id0; 161 } 162 int id=doGetResId(name,type); 163 if(typeCache==null) 164 { 165 typeCache=new HashMap<String,Integer>(); 166 resIdCache.put(type,typeCache); 167 } 168 typeCache.put(name,id); 169 return id; 170 } 171 getDemoResId(VoicePack v)172 private int getDemoResId(VoicePack v) 173 { 174 String name="demo_"+v.getId(); 175 return getResId(name,"raw"); 176 } 177 getTestResId(VoicePack v)178 private int getTestResId(VoicePack v) 179 { 180 String name="test_"+v.getLanguage().getCode(); 181 return getResId(name,"string"); 182 } 183 abandonAudioFocus()184 private void abandonAudioFocus() 185 { 186 if(audioFocusListener==null) 187 return; 188 if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O) 189 { 190 audioManager.abandonAudioFocusRequest(audioFocusRequest); 191 audioFocusRequest=null; 192 } 193 else 194 audioManager.abandonAudioFocus(audioFocusListener); 195 audioFocusListener=null; 196 } 197 requestAudioFocus()198 private boolean requestAudioFocus() 199 { 200 if(audioFocusListener!=null) 201 return true; 202 final int dur=AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK; 203 audioFocusListener=new AudioFocusListener(); 204 boolean hasFocus=false; 205 int res=0; 206 if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O) 207 { 208 AudioFocusRequest.Builder b=new AudioFocusRequest.Builder(dur); 209 b.setAcceptsDelayedFocusGain(false); 210 b.setAudioAttributes(audioAttrs); 211 b.setOnAudioFocusChangeListener(audioFocusListener); 212 b.setWillPauseWhenDucked(true); 213 audioFocusRequest=b.build(); 214 res=audioManager.requestAudioFocus(audioFocusRequest); 215 } 216 else 217 { 218 res=audioManager.requestAudioFocus(audioFocusListener,AudioManager.STREAM_MUSIC,dur); 219 } 220 hasFocus=(res==AudioManager.AUDIOFOCUS_REQUEST_GRANTED); 221 if(!hasFocus) 222 { 223 audioFocusListener=null; 224 if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O) 225 audioFocusRequest=null; 226 } 227 return hasFocus; 228 } 229 230 private abstract class PlayerState 231 { refreshUI()232 protected void refreshUI() 233 { 234 AvailableVoicesFragment frag=(AvailableVoicesFragment)(getActivity().getSupportFragmentManager().findFragmentByTag("voices")); 235 if(frag!=null) 236 { 237 if(playerVoice==null) 238 throw new IllegalStateException(); 239 frag.refresh(playerVoice,VoiceViewChange.PLAYING); 240 } 241 } 242 stop()243 public void stop() 244 { 245 } 246 release()247 public void release() 248 { 249 } 250 onStop()251 public void onStop() 252 { 253 } 254 isPlaying()255 public boolean isPlaying() 256 { 257 return false; 258 } 259 onOpen(VoicePack v)260 protected void onOpen(VoicePack v) 261 { 262 playerVoice=v; 263 if(!requestAudioFocus()) 264 { 265 playerState=new StoppedPlayerState(); 266 return; 267 } 268 player.start(); 269 playerState=new PlayingPlayerState(); 270 refreshUI(); 271 } 272 play(VoicePack v)273 public abstract void play(VoicePack v); 274 canPlay(VoicePack v)275 public boolean canPlay(VoicePack v) 276 { 277 return (getDemoResId(v)!=0); 278 } 279 isPlaying(VoicePack v)280 public boolean isPlaying(VoicePack v) 281 { 282 return (isPlaying()&&playerVoice==v); 283 } 284 reset()285 public void reset() 286 { 287 } 288 pause()289 public void pause() 290 { 291 } 292 resume()293 public void resume() 294 { 295 } 296 } 297 298 private final class UninitializedPlayerState extends PlayerState 299 { 300 @Override play(VoicePack v)301 public void play(VoicePack v) 302 { 303 player=new MediaPlayer(); 304 if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP) 305 player.setAudioAttributes(audioAttrs); 306 player.setOnErrorListener(playerErrorListener); 307 player.setOnCompletionListener(playerDoneListener); 308 playerState=new ResetPlayerState(); 309 playerState.play(v); 310 } 311 } 312 313 private abstract class CreatedPlayerState extends PlayerState 314 { 315 @Override release()316 public void release() 317 { 318 player.release(); 319 player=null; 320 playerState=new UninitializedPlayerState(); 321 } 322 323 @Override reset()324 public void reset() 325 { 326 player.reset(); 327 playerState=new ResetPlayerState(); 328 } 329 } 330 331 private class PlayingPlayerState extends CreatedPlayerState 332 { 333 @Override release()334 public void release() 335 { 336 stop(); 337 super.release(); 338 } 339 340 @Override stop()341 public void stop() 342 { 343 player.stop(); 344 onStop(); 345 playerState.reset(); 346 } 347 348 @Override onStop()349 public void onStop() 350 { 351 abandonAudioFocus(); 352 playerState=new StoppedPlayerState(); 353 refreshUI(); 354 } 355 356 @Override isPlaying()357 public boolean isPlaying() 358 { 359 return true; 360 } 361 362 @Override play(VoicePack v)363 public void play(VoicePack v) 364 { 365 stop(); 366 playerState.play(v); 367 } 368 369 @Override reset()370 public void reset() 371 { 372 super.reset(); 373 refreshUI(); 374 } 375 376 @Override pause()377 public void pause() 378 { 379 player.pause(); 380 playerState=new PausedPlayerState(); 381 } 382 } 383 384 private final class PausedPlayerState extends PlayingPlayerState 385 { 386 @Override resume()387 public void resume() 388 { 389 player.start(); 390 playerState=new PlayingPlayerState(); 391 } 392 } 393 394 private final class StoppedPlayerState extends CreatedPlayerState 395 { 396 @Override play(VoicePack v)397 public void play(VoicePack v) 398 { 399 if(playerVoice==v) 400 { 401 if(BuildConfig.DEBUG) 402 Log.v(TAG,"Request to play the same demo, just restarting"); 403 onOpen(v); 404 return; 405 } 406 reset(); 407 playerState.play(v); 408 } 409 } 410 411 private final class ResetPlayerState extends CreatedPlayerState 412 { 413 @Override play(VoicePack v)414 public void play(VoicePack v) 415 { 416 int demoId=getDemoResId(v); 417 if(demoId==0) 418 return; 419 AssetFileDescriptor afd=null; 420 try 421 { 422 afd=getActivity().getResources().openRawResourceFd(demoId); 423 player.setDataSource(afd.getFileDescriptor(),afd.getStartOffset(),afd.getLength()); 424 player.prepare(); 425 onOpen(v); 426 } 427 catch(IOException e) 428 { 429 if(BuildConfig.DEBUG) 430 Log.e(TAG,"Unable to set new data source",e); 431 if(afd!=null) 432 player.reset(); 433 return; 434 } 435 finally 436 { 437 if(afd!=null) 438 { 439 try 440 { 441 afd.close(); 442 } 443 catch(IOException e) 444 { 445 } 446 } 447 } 448 } 449 } 450 451 private abstract class TTSUttEvent implements Runnable 452 { 453 private final String eventUttId; 454 TTSUttEvent(String id)455 public TTSUttEvent(String id) 456 { 457 eventUttId=id; 458 } 459 onEvent()460 abstract void onEvent(); 461 462 @Override run()463 public void run() 464 { 465 if(String.valueOf(ttsUttId).equals(eventUttId)) 466 onEvent(); 467 } 468 } 469 470 private class TTSDoneEvent extends TTSUttEvent 471 { TTSDoneEvent(String id)472 public TTSDoneEvent(String id) 473 { 474 super(id); 475 } 476 477 @Override onEvent()478 void onEvent() 479 { 480 ttsState.onStop(); 481 } 482 } 483 484 private class TTSErrorEvent extends TTSUttEvent 485 { TTSErrorEvent(String id)486 public TTSErrorEvent(String id) 487 { 488 super(id); 489 } 490 491 @Override onEvent()492 void onEvent() 493 { 494 ttsState.onStop(); 495 } 496 } 497 498 private abstract class TTSState 499 { refreshUI()500 protected void refreshUI() 501 { 502 AvailableVoicesFragment frag=(AvailableVoicesFragment)(getActivity().getSupportFragmentManager().findFragmentByTag("voices")); 503 if(frag!=null) 504 { 505 if(ttsVoice==null) 506 throw new IllegalStateException(); 507 frag.refresh(ttsVoice,VoiceViewChange.PLAYING); 508 } 509 } 510 511 release()512 public void release() 513 { 514 } 515 play(VoicePack v)516 public void play(VoicePack v) 517 { 518 } 519 stop()520 public void stop() 521 { 522 } 523 onInit(int status)524 public void onInit(int status) 525 { 526 } 527 onStop()528 public void onStop() 529 { 530 } 531 canPlay(VoicePack v)532 public boolean canPlay(VoicePack v) 533 { 534 return (getTestResId(v)!=0); 535 } 536 isPlaying()537 public boolean isPlaying() 538 { 539 return false; 540 } 541 isPlaying(VoicePack v)542 public boolean isPlaying(VoicePack v) 543 { 544 return (isPlaying()&&(ttsVoice==v)); 545 } 546 pause()547 public void pause() 548 { 549 } 550 resume()551 public void resume() 552 { 553 } 554 } 555 556 private final class UninitializedTTSState extends TTSState 557 { 558 @Override play(VoicePack v)559 public void play(VoicePack v) 560 { 561 ttsVoice=v; 562 ttsState=new InitializingToPlayTTSState(); 563 tts=new TextToSpeech(getActivity(),ttsInitListener,getActivity().getPackageName()); 564 refreshUI(); 565 } 566 } 567 568 private abstract class CreatedTTSState extends TTSState 569 { 570 @Override release()571 public void release() 572 { 573 tts.shutdown(); 574 tts=null; 575 ttsState=new UninitializedTTSState(); 576 } 577 doPlay(VoicePack v)578 boolean doPlay(VoicePack v) 579 { 580 int resId=getTestResId(v); 581 if(resId==0) 582 return false; 583 if(!requestAudioFocus()) 584 return false; 585 ttsVoice=v; 586 String msg=getString(resId); 587 HashMap<String,String> params=new HashMap<String,String>(); 588 params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID,String.valueOf(ttsUttId)); 589 params.put(RHVoiceService.KEY_PARAM_TEST_VOICE,v.getName()); 590 int res=tts.speak(msg,TextToSpeech.QUEUE_FLUSH,params); 591 if(res!=TextToSpeech.SUCCESS) 592 { 593 abandonAudioFocus(); 594 return false; 595 } 596 ttsState=new PlayingTTSState(); 597 return true; 598 } 599 } 600 601 private class InitializingTTSState extends CreatedTTSState 602 { doOnInit()603 protected void doOnInit() 604 { 605 tts.setOnUtteranceProgressListener(ttsProgressListener); 606 ttsState=new InitializedTTSState(); 607 } 608 doOnError()609 protected void doOnError() 610 { 611 release(); 612 } 613 614 @Override onInit(int status)615 public void onInit(int status) 616 { 617 if(status==TextToSpeech.SUCCESS) 618 doOnInit(); 619 else 620 doOnError(); 621 } 622 623 @Override play(VoicePack v)624 public void play(VoicePack v) 625 { 626 ttsVoice=v; 627 ttsState=new InitializingToPlayTTSState(); 628 refreshUI(); 629 } 630 } 631 632 private final class InitializingToPlayTTSState extends InitializingTTSState 633 { 634 @Override doOnInit()635 protected void doOnInit() 636 { 637 super.doOnInit(); 638 ttsState.play(ttsVoice); 639 } 640 641 @Override doOnError()642 protected void doOnError() 643 { 644 super.doOnError(); 645 refreshUI(); 646 } 647 648 @Override isPlaying()649 public boolean isPlaying() 650 { 651 return true; 652 } 653 654 @Override stop()655 public void stop() 656 { 657 ttsState=new InitializingTTSState(); 658 refreshUI(); 659 } 660 661 @Override play(VoicePack v)662 public void play(VoicePack v) 663 { 664 stop(); 665 ttsState.play(v); 666 } 667 668 @Override release()669 public void release() 670 { 671 stop(); 672 super.release(); 673 } 674 } 675 676 private final class InitializedTTSState extends CreatedTTSState 677 { 678 @Override play(VoicePack v)679 public void play(VoicePack v) 680 { 681 if(doPlay(v)) 682 refreshUI(); 683 } 684 } 685 686 private class PlayingTTSState extends CreatedTTSState 687 { 688 @Override isPlaying()689 public boolean isPlaying() 690 { 691 return true; 692 } 693 694 @Override play(VoicePack v)695 public void play(VoicePack v) 696 { 697 stop(); 698 ttsState.play(v); 699 } 700 701 @Override onStop()702 public void onStop() 703 { 704 ++ttsUttId; 705 abandonAudioFocus(); 706 ttsState=new InitializedTTSState(); 707 refreshUI(); 708 } 709 710 @Override stop()711 public void stop() 712 { 713 tts.stop(); 714 onStop(); 715 } 716 717 @Override release()718 public void release() 719 { 720 stop(); 721 super.release(); 722 } 723 724 @Override pause()725 public void pause() 726 { 727 tts.stop(); 728 ttsState=new PausedTTSState(); 729 } 730 } 731 732 private final class PausedTTSState extends PlayingTTSState 733 { 734 @Override stop()735 public void stop() 736 { 737 onStop(); 738 } 739 740 @Override resume()741 public void resume() 742 { 743 ++ttsUttId; 744 if(doPlay(ttsVoice)) 745 return; 746 ttsState=new InitializedTTSState(); 747 refreshUI(); 748 } 749 } 750 751 @Override onCreate(Bundle state)752 public void onCreate(Bundle state) 753 { 754 super.onCreate(state); 755 ttsHandler=new Handler(); 756 audioManager=(AudioManager)(getActivity().getSystemService(Context.AUDIO_SERVICE)); 757 if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP) 758 audioAttrs=(new AudioAttributes.Builder()).setUsage(AudioAttributes.USAGE_MEDIA).setContentType(AudioAttributes.CONTENT_TYPE_SPEECH).build(); 759 } 760 761 @Override onStop()762 public void onStop() 763 { 764 super.onStop(); 765 playerState.release(); 766 ttsState.release(); 767 } 768 play(VoicePack v)769 public void play(VoicePack v) 770 { 771 stopPlayback(); 772 if(v.getEnabled(getActivity())&&v.isInstalled(getActivity())) 773 ttsState.play(v); 774 else 775 playerState.play(v); 776 } 777 canPlay(VoicePack v)778 public boolean canPlay(VoicePack v) 779 { 780 return (playerState.canPlay(v)&&ttsState.canPlay(v)); 781 } 782 isPlaying(VoicePack v)783 public boolean isPlaying(VoicePack v) 784 { 785 return (playerState.isPlaying(v)||ttsState.isPlaying(v)); 786 } 787 stopPlayback()788 public void stopPlayback() 789 { 790 playerState.stop(); 791 ttsState.stop(); 792 } 793 } 794