1 /* Copyright (C) 2013, 2018, 2019, 2020  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 2.1 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.content.BroadcastReceiver;
19 import android.content.Context;
20 import android.content.Intent;
21 import android.content.IntentFilter;
22 import android.content.SharedPreferences;
23 import android.media.AudioFormat;
24 import android.os.Build;
25 import android.os.Bundle;
26 import android.os.Handler;
27 import android.preference.PreferenceManager;
28 import android.speech.tts.SynthesisCallback;
29 import android.speech.tts.SynthesisRequest;
30 import android.speech.tts.TextToSpeech;
31 import android.speech.tts.TextToSpeechService;
32 import android.speech.tts.Voice;
33 import android.text.TextUtils;
34 import android.util.Log;
35 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
36 import com.github.olga_yakovleva.rhvoice.*;
37 import com.github.olga_yakovleva.rhvoice.LanguageInfo;
38 import java.io.File;
39 import java.nio.ByteBuffer;
40 import java.nio.ByteOrder;
41 import java.util.ArrayList;
42 import java.util.HashMap;
43 import java.util.HashSet;
44 import java.util.List;
45 import java.util.Locale;
46 import java.util.Map;
47 import java.util.Set;
48 import java.util.regex.Matcher;
49 import java.util.regex.Pattern;
50 
51 public final class RHVoiceService extends TextToSpeechService
52 {
53     public static final String KEY_PARAM_TEST_VOICE="com.github.olga_yakovleva.rhvoice.android.param_test_voice";
54 
55     private static final String TAG="RHVoiceTTSService";
56 
57     private static final int[] languageSupportConstants={TextToSpeech.LANG_NOT_SUPPORTED,TextToSpeech.LANG_AVAILABLE,TextToSpeech.LANG_COUNTRY_AVAILABLE,TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE};
58     private static final Pattern DEFAULT_VOICE_NAME_PATTERN=Pattern.compile("^([a-z]{3})-default$");
59 
60     public static final String ACTION_CHECK_DATA="com.github.olga_yakovleva.rhvoice.android.action.service_check_data";
61     private final BroadcastReceiver dataStateReceiver=new BroadcastReceiver()
62         {
63             @Override
64             public void onReceive(Context context,Intent intent)
65             {
66                 if(BuildConfig.DEBUG)
67                     Log.v(TAG,"Checking data");
68                 List<String> oldPaths=paths;
69                 paths=Data.getPaths(context);
70                 boolean changed=!paths.equals(oldPaths);
71                 if(BuildConfig.DEBUG)
72                     Log.v(TAG,"Paths changed: "+changed);
73                 if(changed)
74                     initialize();
75 }
76         };
77 
78     private final BroadcastReceiver packageReceiver=new OnPackageReceiver();
79 
80     private static interface SettingValueTranslator
81     {
load(SharedPreferences prefs,String key)82         public Object load(SharedPreferences prefs,String key);
translate(Object value)83         public String translate(Object value);
84 }
85 
86     private static final SettingValueTranslator prosodySettingValueTranslator=new SettingValueTranslator()
87         {
88             public Object load(SharedPreferences prefs,String key)
89             {
90                 return prefs.getString(key,"100");
91 }
92 
93             public String translate(Object value)
94             {
95                 try
96                     {
97                         int n=Integer.parseInt((String)value);
98                         float f=n/100.0f;
99                         return String.valueOf(f);
100 }
101                 catch(NumberFormatException e)
102                     {
103                         return "1";
104 }
105 }
106         };
107 
108     private static final SettingValueTranslator yesSettingValueTranslator=new SettingValueTranslator()
109         {
110             public Object load(SharedPreferences prefs,String key)
111             {
112                 Boolean value=prefs.getBoolean(key,true);
113                 return value;
114             }
115 
116             public String translate(Object value)
117             {
118                 return String.valueOf(value).toLowerCase();
119             }
120         };
121 
122     private static final SettingValueTranslator qualitySettingValueTranslator=new SettingValueTranslator()
123         {
124             public Object load(SharedPreferences prefs,String key)
125             {
126                 return prefs.getString(key,"std");
127 }
128 
129             public String translate(Object value)
130             {
131                 return value.toString();
132 }
133         };
134 
135     private static class MappedSetting
136     {
137         public final String prefKey;
138         public Object prefValue;
139         public final String nativeKey;
140         public final SettingValueTranslator valueTranslator;
141 
MappedSetting(String prefKey,String nativeKey,SettingValueTranslator valueTranslator)142         public MappedSetting(String prefKey,String nativeKey,SettingValueTranslator valueTranslator)
143         {
144             this.prefKey=prefKey;
145             this.nativeKey=nativeKey;
146             this.valueTranslator=valueTranslator;
147 }
148 }
149 
150     private static class Tts
151     {
152         public TTSEngine engine=null;
153         public List<AndroidVoiceInfo> voices;
154         public Map<String,LanguageInfo> languageIndex;
155         public Map<String,AndroidVoiceInfo> voiceIndex;
156         public List<MappedSetting> mappedSettings;
157 
Tts()158         public Tts()
159         {
160             voices=new ArrayList<AndroidVoiceInfo>();
161             languageIndex=new HashMap<String,LanguageInfo>();
162             voiceIndex=new HashMap<String,AndroidVoiceInfo>();
163             mappedSettings=new ArrayList<MappedSetting>();
164 }
165 
Tts(Tts other,boolean passEngine)166         public Tts(Tts other,boolean passEngine)
167         {
168             this.voices=other.voices;
169             this.languageIndex=other.languageIndex;
170             this.voiceIndex=other.voiceIndex;
171             this.mappedSettings=other.mappedSettings;
172             if(!passEngine)
173                 return;
174             this.engine=other.engine;
175             other.engine=null;
176 }
177     }
178 
179     private static class TtsManager
180         {
181             private Tts tts;
182             private boolean done;
183 
reset(Tts newTts)184             public synchronized void reset(Tts newTts)
185             {
186                 if(newTts==null)
187                     throw new IllegalArgumentException();
188                 if(tts!=null&&tts.engine!=null)
189                     tts.engine.shutdown();
190                 tts=newTts;
191             }
192 
destroy()193             public synchronized void destroy()
194             {
195                 done=true;
196                 if(tts!=null&&tts.engine!=null)
197                     tts.engine.shutdown();
198                 tts=null;
199             }
200 
acquireForSynthesis()201             public synchronized Tts acquireForSynthesis()
202             {
203                 if(done)
204                     return null;
205                 if(tts==null)
206                     return null;
207                 if(tts.engine==null)
208                     return null;
209                 if(tts.voices.isEmpty())
210                     return null;
211                 Tts result=new Tts(tts,true);
212                 return result;
213 }
214 
release(Tts usedTts)215             public synchronized void release(Tts usedTts)
216             {
217                 if(usedTts==null||usedTts.engine==null)
218                     throw new IllegalArgumentException();
219                 if(done||tts.engine!=null)
220                     usedTts.engine.shutdown();
221                 else
222                     tts=new Tts(usedTts,true);
223 }
224 
get()225             public synchronized Tts get()
226             {
227                 if(tts==null)
228                     return null;
229                 return new Tts(tts,false);
230 }
231     }
232 
233     private final TtsManager ttsManager=new TtsManager();
234     private volatile AndroidVoiceInfo currentVoice;
235     private volatile boolean speaking=false;
236     private List<String> paths=new ArrayList<String>();
237     private Handler handler;
238 
239     private class Player implements TTSClient
240     {
241         private SynthesisCallback callback;
242         private int sampleRate;
243 
Player(SynthesisCallback callback)244         public Player(SynthesisCallback callback)
245         {
246             this.callback=callback;
247         }
248 
setSampleRate(int sr)249         public boolean setSampleRate(int sr)
250         {
251             if(sampleRate!=0)
252                 return true;
253             sampleRate=sr;
254             callback.start(sampleRate,AudioFormat.ENCODING_PCM_16BIT,1);
255             return true;
256 }
257 
playSpeech(short[] samples)258         public boolean playSpeech(short[] samples)
259         {
260             if(!speaking)
261                 return false;
262             if(BuildConfig.DEBUG&&sampleRate==0)
263                 throw new IllegalStateException();
264             final ByteBuffer buffer=ByteBuffer.allocate(samples.length*2);
265             buffer.order(ByteOrder.LITTLE_ENDIAN);
266             buffer.asShortBuffer().put(samples);
267             final byte[] bytes=buffer.array();
268             final int size=callback.getMaxBufferSize();
269             int offset=0;
270             int count;
271             while(offset<bytes.length)
272                 {
273                     if(!speaking)
274                         return false;
275                     count=Math.min(size,bytes.length-offset);
276                     if(callback.audioAvailable(bytes,offset,count)!=TextToSpeech.SUCCESS)
277                         return false;
278                     offset+=count;
279                 }
280             return true;
281         }
282     };
283 
284     static private class Candidate
285     {
286         public AndroidVoiceInfo voice;
287         public int score;
288 
Candidate()289         public Candidate()
290         {
291             voice=null;
292             score=0;
293         }
294 
Candidate(AndroidVoiceInfo voice,String language,String country,String variant)295         public Candidate(AndroidVoiceInfo voice,String language,String country,String variant)
296         {
297             this.voice=voice;
298             score=voice.getSupportLevel(language,country,variant);
299         }
300     }
301 
302     private static class LanguageSettings
303     {
304         public AndroidVoiceInfo voice;
305         public boolean detect;
306     }
307 
logLanguage(String language,String country,String variant)308     private void logLanguage(String language,String country,String variant)
309     {
310         Log.v(TAG,"Language: "+language);
311         if(TextUtils.isEmpty(country))
312             return;
313         Log.v(TAG,"Country: "+country);
314         if(TextUtils.isEmpty(variant))
315             return;
316         Log.v(TAG,"Variant: "+variant);
317 }
318 
getLanguageSettings(Tts tts)319     private Map<String,LanguageSettings> getLanguageSettings(Tts tts)
320     {
321         Map<String,LanguageSettings> result=new HashMap<String,LanguageSettings>();
322         SharedPreferences prefs=PreferenceManager.getDefaultSharedPreferences(this);
323         for(String language: tts.languageIndex.keySet())
324             {
325                 LanguageSettings settings=new LanguageSettings();
326                 String prefVoice=prefs.getString("language."+language+".voice",null);
327                 for(AndroidVoiceInfo voice: tts.voices)
328                     {
329                         if(!voice.getSource().getLanguage().getAlpha3Code().equals(language))
330                             continue;
331                         String voiceName=voice.getSource().getName();
332                         if(settings.voice==null)
333                             {
334                                 settings.voice=voice;
335                                 if(prefVoice==null)
336                                     break;
337                             }
338                         if(voiceName.equals(prefVoice))
339                             {
340                                 settings.voice=voice;
341                                 break;
342                             }
343                     }
344                 settings.detect=prefs.getBoolean("language."+language+".detect",true);
345                 result.put(language,settings);
346             }
347         return result;
348     }
349 
parseDefaultVoiceName(Tts tts,String name)350     private String parseDefaultVoiceName(Tts tts,String name)
351     {
352         Matcher matcher=DEFAULT_VOICE_NAME_PATTERN.matcher(name);
353         if(!matcher.find())
354             return null;
355         String code=matcher.group(1);
356         if(!tts.languageIndex.containsKey(code))
357             return null;
358         return code;
359 }
360 
findBestVoice(Tts tts,String language,String country,String variant,String voiceName,boolean testing,Map<String,LanguageSettings> languageSettings)361     private Candidate findBestVoice(Tts tts,String language,String country,String variant,String voiceName,boolean testing,Map<String,LanguageSettings> languageSettings)
362     {
363         if(!TextUtils.isEmpty(voiceName))
364             {
365                 AndroidVoiceInfo voice=tts.voiceIndex.get(voiceName.toLowerCase());
366                 if(voice!=null)
367                     {
368                         if(testing)
369                             {
370                                 Candidate c=new Candidate();
371                                 c.voice=voice;
372                                 c.score=3;
373                                 return c;
374                             }
375                         else
376                             return new Candidate(voice,language,country,"");
377                     }
378                 else if(testing)
379                     return new Candidate();
380 }
381         Candidate best=new Candidate();
382         for(AndroidVoiceInfo voice: tts.voices)
383             {
384                 Candidate candidate=new Candidate(voice,language,country,variant);
385                 if(candidate.score>best.score||best.voice==null)
386                     best=candidate;
387             }
388         if(!TextUtils.isEmpty(variant)&&best.score==3)
389             return best;
390         if(best.voice==null)
391             best.voice=tts.voices.get(0);
392         LanguageSettings settings=null;
393         if(languageSettings!=null)
394             settings=languageSettings.get(best.voice.getLanguage());
395         if(settings==null)
396             return best;
397         if(settings.voice!=null)
398             best.voice=settings.voice;
399         return best;
400     }
401 
initialize()402     private void initialize()
403     {
404         if(BuildConfig.DEBUG)
405             Log.i(TAG,"Initializing the engine");
406         if(paths.isEmpty())
407             {
408                 Log.w(TAG,"No voice data");
409                 return;
410 }
411         Tts tts=new Tts();
412         try
413             {
414                 File configDir=Config.getDir(this);
415                 tts.engine=new TTSEngine("",configDir.getAbsolutePath(),paths,CoreLogger.instance);
416             }
417         catch(Exception e)
418             {
419                 if(BuildConfig.DEBUG)
420                     Log.e(TAG,"Error during engine initialization",e);
421                 return;
422             }
423         List<VoiceInfo> engineVoices=tts.engine.getVoices();
424         if(engineVoices.isEmpty())
425             {
426                 if(BuildConfig.DEBUG)
427                     Log.w(TAG,"No voices");
428                 tts.engine.shutdown();
429                 return;
430 }
431         for(VoiceInfo engineVoice: engineVoices)
432             {
433                 AndroidVoiceInfo nextVoice=new AndroidVoiceInfo(engineVoice);
434                 if(BuildConfig.DEBUG)
435                     Log.i(TAG,"Found voice "+nextVoice.toString());
436                 tts.voices.add(nextVoice);
437                 tts.voiceIndex.put(nextVoice.getName().toLowerCase(),nextVoice);
438                 LanguageInfo engineLanguage=engineVoice.getLanguage();
439                 tts.languageIndex.put(engineLanguage.getAlpha3Code(),engineLanguage);
440             }
441         for(String lang: tts.languageIndex.keySet())
442             {
443                 tts.mappedSettings.add(new MappedSetting("language."+lang+".volume","languages."+lang+".default_volume",prosodySettingValueTranslator));
444                 tts.mappedSettings.add(new MappedSetting("language."+lang+".rate","languages."+lang+".default_rate",prosodySettingValueTranslator));
445                 tts.mappedSettings.add(new MappedSetting("language."+lang+".use_pseudo_english","languages."+lang+".use_pseudo_english",yesSettingValueTranslator));
446 }
447         tts.mappedSettings.add(new MappedSetting("quality","quality",qualitySettingValueTranslator));
448         ttsManager.reset(tts);
449     }
450 
registerPackageReceiver()451     private void registerPackageReceiver()
452     {
453         IntentFilter filter=new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
454         filter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED);
455         filter.addDataScheme("package");
456         registerReceiver(packageReceiver,filter);
457 }
458 
459     private class VoiceInstaller implements Runnable
460     {
461         private final String languageCode;
462         private final String countryCode;
463 
VoiceInstaller(String languageCode,String countryCode)464         public VoiceInstaller(String languageCode,String countryCode)
465         {
466             this.languageCode=languageCode;
467             this.countryCode=countryCode;
468 }
469 
run()470         public void run()
471         {
472             LanguagePack lang=Data.findMatchingLanguage(languageCode,countryCode);
473             if(lang==null)
474                 return;
475             for(VoicePack voice: lang.getVoices())
476                 {
477                     if(voice.getEnabled(RHVoiceService.this))
478                         return;
479 }
480             VoicePack voice=lang.getDefaultVoice();
481             voice.setEnabled(RHVoiceService.this,true);
482 }
483 }
484 
485     @Override
onCreate()486     public void onCreate()
487     {
488         if(BuildConfig.DEBUG)
489             Log.i(TAG,"Starting the service");
490         handler=new Handler();
491         paths=Data.getPaths(this);
492         Data.scheduleSync(this,false);
493         LocalBroadcastManager.getInstance(this).registerReceiver(dataStateReceiver,new IntentFilter(ACTION_CHECK_DATA));
494         registerPackageReceiver();
495         initialize();
496         super.onCreate();
497     }
498 
499     @Override
onDestroy()500     public void onDestroy()
501     {
502         super.onDestroy();
503         unregisterReceiver(packageReceiver);
504         LocalBroadcastManager.getInstance(this).unregisterReceiver(dataStateReceiver);
505         ttsManager.destroy();
506     }
507 
508     @Override
onGetLanguage()509     protected String[] onGetLanguage()
510     {
511         if(BuildConfig.DEBUG)
512             Log.v(TAG,"onGetLanguage called");
513         String[] result={"rus","RUS",""};
514         AndroidVoiceInfo voice=currentVoice;
515         if(voice==null)
516             {
517                 Tts tts=ttsManager.get();
518                 if(tts!=null)
519                     {
520                         Locale locale=Locale.getDefault();
521                         Candidate bestMatch=findBestVoice(tts,locale.getISO3Language(),locale.getISO3Country(),"","",false,getLanguageSettings(tts));
522                         if(bestMatch.voice!=null)
523                             voice=bestMatch.voice;
524                     }
525             }
526         if(voice==null)
527             return result;
528         result[0]=voice.getLanguage();
529         result[1]=voice.getCountry();
530         if(BuildConfig.DEBUG)
531             Log.v(TAG,"onGetLanguage returns "+result[0]+"-"+result[1]);
532         return result;
533     }
534 
535     @Override
onIsLanguageAvailable(String language,String country,String variant)536     protected int onIsLanguageAvailable(String language,String country,String variant)
537     {
538         if(BuildConfig.DEBUG)
539             {
540                 Log.v(TAG,"onIsLanguageAvailable called");
541                 logLanguage(language,country,variant);
542 }
543         Tts tts=ttsManager.get();
544         if(tts==null)
545             {
546                 if(BuildConfig.DEBUG)
547                     Log.w(TAG,"Not initialized yet");
548                 return TextToSpeech.LANG_NOT_SUPPORTED;
549             }
550         Candidate bestMatch=findBestVoice(tts,language,country,variant,"",false,null);
551         int result=languageSupportConstants[bestMatch.score];
552         if(BuildConfig.DEBUG)
553             Log.v(TAG,"Result: "+result);
554         return result;
555     }
556 
557     @Override
onLoadLanguage(String language,String country,String variant)558     protected int onLoadLanguage(String language,String country,String variant)
559     {
560         if(BuildConfig.DEBUG)
561             Log.v(TAG,"onLoadLanguage called");
562         return onIsLanguageAvailable(language,country,variant);
563     }
564 
565     @Override
onStop()566     protected void onStop()
567     {
568         speaking=false;
569     }
570 
applyMappedSettings(Tts tts)571     private void applyMappedSettings(Tts tts)
572     {
573         SharedPreferences prefs=PreferenceManager.getDefaultSharedPreferences(this);
574         Object oldPrefValue;
575         String nativeValue;
576         for(MappedSetting setting: tts.mappedSettings)
577             {
578                 oldPrefValue=setting.prefValue;
579                 setting.prefValue=setting.valueTranslator.load(prefs,setting.prefKey);
580                 if(oldPrefValue!=null&&oldPrefValue.equals(setting.prefValue))
581                     continue;
582                 nativeValue=setting.valueTranslator.translate(setting.prefValue);
583                 tts.engine.configure(setting.nativeKey,nativeValue);
584 }
585 }
586 
587     @Override
onSynthesizeText(SynthesisRequest request,SynthesisCallback callback)588     protected void onSynthesizeText(SynthesisRequest request,SynthesisCallback callback)
589     {
590         if(BuildConfig.DEBUG)
591             {
592                 Log.v(TAG,"onSynthesize called");
593                 logLanguage(request.getLanguage(),request.getCountry(),request.getVariant());
594             }
595         boolean testing=false;
596                 Bundle requestParams=request.getParams();
597                 if(requestParams!=null&&requestParams.containsKey(KEY_PARAM_TEST_VOICE))
598                     testing=true;
599         Tts tts=ttsManager.acquireForSynthesis();
600         if(tts==null)
601             {
602                 if(BuildConfig.DEBUG)
603                     Log.w(TAG,"Not initialized yet");
604                 if(!testing)
605                     handler.post(new VoiceInstaller(request.getLanguage(),request.getCountry()));
606                 callback.error();
607                 return;
608             }
609         try
610             {
611                 speaking=true;
612                 String language=request.getLanguage();
613                 String country=request.getCountry();
614                 String variant=request.getVariant();
615                 Map<String,LanguageSettings> languageSettings=getLanguageSettings(tts);
616                 String voiceName="";
617                 if(testing)
618                     voiceName=requestParams.getString(KEY_PARAM_TEST_VOICE);
619                 else if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.LOLLIPOP)
620                     {
621                         voiceName=request.getVoiceName();
622                         if(!TextUtils.isEmpty(voiceName))
623                             {
624                                 if(BuildConfig.DEBUG)
625                                     Log.v(TAG,"Voice name: "+voiceName);
626                                 String code=parseDefaultVoiceName(tts,voiceName);
627                                 if(code!=null)
628                                     {
629                                         if(BuildConfig.DEBUG)
630                                             Log.v(TAG,"Default voice for "+code);
631                                         language=code;
632                                         country="";
633                                         variant="";
634                                         voiceName="";
635 }
636                             }
637                     }
638                 final Candidate bestMatch=findBestVoice(tts,language,country,variant,voiceName,testing,languageSettings);
639                 if(bestMatch.voice==null)
640                     {
641                         if(BuildConfig.DEBUG)
642                             Log.e(TAG,"Unsupported language");
643                         if(!testing)
644                             handler.post(new VoiceInstaller(language,country));
645                         callback.error();
646                         return;
647                     }
648                 if(BuildConfig.DEBUG)
649                     Log.v(TAG,"Selected voice: "+bestMatch.voice.getSource().getName());
650                 currentVoice=bestMatch.voice;
651                 StringBuilder voiceProfileSpecBuilder=new StringBuilder();
652                 voiceProfileSpecBuilder.append(bestMatch.voice.getSource().getName());
653                 for(Map.Entry<String,LanguageSettings> entry: languageSettings.entrySet())
654                     {
655                         if(entry.getKey().equals(bestMatch.voice.getLanguage()))
656                             continue;
657                         if(entry.getValue().detect)
658                             {
659                                 String name=entry.getValue().voice.getSource().getName();
660                                 voiceProfileSpecBuilder.append("+").append(name);
661                             }
662                     }
663                 String profileSpec=voiceProfileSpecBuilder.toString();
664                 if(BuildConfig.DEBUG)
665                     Log.v(TAG,"Synthesizing the following text: "+request.getText());
666                 int rate=request.getSpeechRate();
667                 if(BuildConfig.DEBUG)
668                     Log.v(TAG,"rate="+rate);
669                 int pitch=request.getPitch();
670                 if(BuildConfig.DEBUG)
671                     Log.v(TAG,"pitch="+pitch);
672                 if(BuildConfig.DEBUG)
673                     Log.v(TAG,"Profile: "+profileSpec);
674                 applyMappedSettings(tts);
675                 final SynthesisParameters params=new SynthesisParameters();
676                 params.setVoiceProfile(profileSpec);
677                 params.setRate(((double)rate)/100.0);
678                 params.setPitch(((double)pitch)/100.0);
679                 final Player player=new Player(callback);
680                 tts.engine.speak(request.getText(),params,player);
681                 player.setSampleRate(24000);
682                 callback.done();
683             }
684         catch(RHVoiceException e)
685             {
686                 if(BuildConfig.DEBUG)
687                     Log.e(TAG,"Synthesis error",e);
688                 callback.error();
689             }
690         finally
691             {
692                 speaking=false;
693                 ttsManager.release(tts);
694             }
695     }
696 
697     @Override
onGetDefaultVoiceNameFor(String language,String country,String variant)698     public String onGetDefaultVoiceNameFor(String language,String country,String variant)
699     {
700         if(BuildConfig.DEBUG)
701             {
702                 Log.v(TAG,"onGetDefaultVoiceNameFor called");
703                     logLanguage(language,country,variant);
704                     }
705         Tts tts=ttsManager.get();
706         if(tts==null)
707             return null;
708         if(!tts.languageIndex.containsKey(language))
709             return null;
710         String name=language+"-default";
711         if(BuildConfig.DEBUG)
712             Log.v(TAG,"Voice name: "+name);
713         return name;
714 }
715 
716     @Override
onGetVoices()717     public List<Voice> onGetVoices()
718     {
719         if(BuildConfig.DEBUG)
720             Log.v(TAG,"onGetVoices called");
721         List<Voice> result=new ArrayList<Voice>();
722         Tts tts=ttsManager.get();
723         if(tts==null)
724             return result;
725         Voice v=null;
726         for(AndroidVoiceInfo voice: tts.voices)
727             {
728                 v=voice.getAndroidVoice();
729                 if(BuildConfig.DEBUG)
730                     Log.v(TAG,"Voice: "+v.toString());
731                 result.add(v);
732             }
733         for(LanguageInfo lang: tts.languageIndex.values())
734             {
735                 Locale loc=new Locale(lang.getAlpha2Code(),lang.getAlpha2CountryCode());
736                 v=new Voice(lang.getAlpha3Code()+"-default",loc,Voice.QUALITY_NORMAL,Voice.LATENCY_NORMAL,false,new HashSet<String>());
737                 if(BuildConfig.DEBUG)
738                     Log.v(TAG,"Default voice: "+v.toString());
739                 result.add(v);
740 }
741         return result;
742     }
743 
744     @Override
onIsValidVoiceName(String name)745     public int onIsValidVoiceName(String name)
746     {
747         if(BuildConfig.DEBUG)
748             Log.v(TAG,"onIsValidVoiceName called for name "+name);
749         Tts tts=ttsManager.get();
750         if(tts==null)
751             return TextToSpeech.ERROR;
752         String code=parseDefaultVoiceName(tts,name);
753         if(code!=null)
754             {
755                 if(BuildConfig.DEBUG)
756                     Log.v(TAG,"Default voice name for "+code);
757                 return TextToSpeech.SUCCESS;
758 }
759         if(tts.voiceIndex.containsKey(name.toLowerCase()))
760             {
761                 if(BuildConfig.DEBUG)
762                     Log.v(TAG,"Voice found");
763                 return TextToSpeech.SUCCESS;
764             }
765         else
766             {
767                 if(BuildConfig.DEBUG)
768                     Log.v(TAG,"Voice not found");
769                 return TextToSpeech.ERROR;
770             }
771 }
772 
773     @Override
onLoadVoice(String name)774     public int onLoadVoice(String name)
775     {
776         if(BuildConfig.DEBUG)
777             Log.v(TAG,"onLoadVoice called with voice name "+name);
778         return onIsValidVoiceName(name);
779 }
780 }
781