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