TtsService.java revision 28dbae7df43ee683ba1bf468ad9924092bb9c569
1/*
2 * Copyright (C) 2009 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16package android.tts;
17
18import android.app.Service;
19import android.content.ContentResolver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.SharedPreferences;
23import android.content.pm.PackageManager;
24import android.content.pm.PackageManager.NameNotFoundException;
25import android.media.MediaPlayer;
26import android.media.MediaPlayer.OnCompletionListener;
27import android.net.Uri;
28import android.os.IBinder;
29import android.os.RemoteCallbackList;
30import android.os.RemoteException;
31import android.preference.PreferenceManager;
32import android.speech.tts.ITts.Stub;
33import android.speech.tts.ITtsCallback;
34import android.speech.tts.TextToSpeech;
35import android.util.Log;
36import java.util.ArrayList;
37import java.util.Arrays;
38import java.util.HashMap;
39import java.util.concurrent.locks.ReentrantLock;
40
41/**
42 * @hide Synthesizes speech from text. This is implemented as a service so that
43 *       other applications can call the TTS without needing to bundle the TTS
44 *       in the build.
45 *
46 */
47public class TtsService extends Service implements OnCompletionListener {
48
49    private static class SpeechItem {
50        public static final int SPEECH = 0;
51        public static final int EARCON = 1;
52        public static final int SILENCE = 2;
53        public String mText = null;
54        public ArrayList<String> mParams = null;
55        public int mType = SPEECH;
56        public long mDuration = 0;
57
58        public SpeechItem(String text, ArrayList<String> params, int itemType) {
59            mText = text;
60            mParams = params;
61            mType = itemType;
62        }
63
64        public SpeechItem(long silenceTime) {
65            mDuration = silenceTime;
66        }
67    }
68
69    /**
70     * Contains the information needed to access a sound resource; the name of
71     * the package that contains the resource and the resID of the resource
72     * within that package.
73     */
74    private static class SoundResource {
75        public String mSourcePackageName = null;
76        public int mResId = -1;
77        public String mFilename = null;
78
79        public SoundResource(String packageName, int id) {
80            mSourcePackageName = packageName;
81            mResId = id;
82            mFilename = null;
83        }
84
85        public SoundResource(String file) {
86            mSourcePackageName = null;
87            mResId = -1;
88            mFilename = file;
89        }
90    }
91
92    private static final String ACTION = "android.intent.action.USE_TTS";
93    private static final String CATEGORY = "android.intent.category.TTS";
94    private static final String PKGNAME = "android.tts";
95
96    final RemoteCallbackList<android.speech.tts.ITtsCallback> mCallbacks = new RemoteCallbackList<ITtsCallback>();
97
98    private Boolean mIsSpeaking;
99    private ArrayList<SpeechItem> mSpeechQueue;
100    private HashMap<String, SoundResource> mEarcons;
101    private HashMap<String, SoundResource> mUtterances;
102    private MediaPlayer mPlayer;
103    private TtsService mSelf;
104
105    private ContentResolver mResolver;
106
107    private final ReentrantLock speechQueueLock = new ReentrantLock();
108    private final ReentrantLock synthesizerLock = new ReentrantLock();
109
110    private SynthProxy nativeSynth;
111
112    @Override
113    public void onCreate() {
114        super.onCreate();
115        Log.i("TTS", "TTS starting");
116
117        mResolver = getContentResolver();
118
119        String soLibPath = "/system/lib/libttspico.so";
120        nativeSynth = new SynthProxy(soLibPath);
121
122        mSelf = this;
123        mIsSpeaking = false;
124
125        mEarcons = new HashMap<String, SoundResource>();
126        mUtterances = new HashMap<String, SoundResource>();
127
128        mSpeechQueue = new ArrayList<SpeechItem>();
129        mPlayer = null;
130
131        setDefaultSettings();
132    }
133
134    @Override
135    public void onDestroy() {
136        super.onDestroy();
137        // Don't hog the media player
138        cleanUpPlayer();
139
140        nativeSynth.shutdown();
141
142        // Unregister all callbacks.
143        mCallbacks.kill();
144    }
145
146
147    private void setDefaultSettings() {
148
149        // TODO handle default language
150        setLanguage("eng", "USA", "");
151
152        // speech rate
153        setSpeechRate(getDefaultRate());
154
155    }
156
157
158    private boolean isDefaultEnforced() {
159        return (android.provider.Settings.Secure.getInt(mResolver,
160                    android.provider.Settings.Secure.TTS_USE_DEFAULTS,
161                    TextToSpeech.Engine.FALLBACK_TTS_USE_DEFAULTS)
162                == 1 );
163    }
164
165
166    private int getDefaultRate() {
167        return android.provider.Settings.Secure.getInt(mResolver,
168                android.provider.Settings.Secure.TTS_DEFAULT_RATE,
169                TextToSpeech.Engine.FALLBACK_TTS_DEFAULT_RATE);
170    }
171
172
173    private String getDefaultLanguage() {
174        String defaultLang = android.provider.Settings.Secure.getString(mResolver,
175                android.provider.Settings.Secure.TTS_DEFAULT_LANG);
176        if (defaultLang == null) {
177            return TextToSpeech.Engine.FALLBACK_TTS_DEFAULT_LANG;
178        } else {
179            return defaultLang;
180        }
181    }
182
183
184    private String getDefaultCountry() {
185        String defaultCountry = android.provider.Settings.Secure.getString(mResolver,
186                android.provider.Settings.Secure.TTS_DEFAULT_COUNTRY);
187        if (defaultCountry == null) {
188            return TextToSpeech.Engine.FALLBACK_TTS_DEFAULT_COUNTRY;
189        } else {
190            return defaultCountry;
191        }
192    }
193
194
195    private String getDefaultLocVariant() {
196        String defaultVar = android.provider.Settings.Secure.getString(mResolver,
197                android.provider.Settings.Secure.TTS_DEFAULT_VARIANT);
198        if (defaultVar == null) {
199            return TextToSpeech.Engine.FALLBACK_TTS_DEFAULT_VARIANT;
200        } else {
201            return defaultVar;
202        }
203    }
204
205
206    private void setSpeechRate(int rate) {
207        if (isDefaultEnforced()) {
208            nativeSynth.setSpeechRate(getDefaultRate());
209        } else {
210            nativeSynth.setSpeechRate(rate);
211        }
212    }
213
214
215    private void setPitch(int pitch) {
216        nativeSynth.setPitch(pitch);
217    }
218
219
220    private void setLanguage(String lang, String country, String variant) {
221        Log.v("TTS", "TtsService.setLanguage(" + lang + ", " + country + ", " + variant + ")");
222        if (isDefaultEnforced()) {
223            nativeSynth.setLanguage(getDefaultLanguage(), getDefaultCountry(),
224                    getDefaultLocVariant());
225        } else {
226            nativeSynth.setLanguage(lang, country, variant);
227        }
228    }
229
230
231    /**
232     * Adds a sound resource to the TTS.
233     *
234     * @param text
235     *            The text that should be associated with the sound resource
236     * @param packageName
237     *            The name of the package which has the sound resource
238     * @param resId
239     *            The resource ID of the sound within its package
240     */
241    private void addSpeech(String text, String packageName, int resId) {
242        mUtterances.put(text, new SoundResource(packageName, resId));
243    }
244
245    /**
246     * Adds a sound resource to the TTS.
247     *
248     * @param text
249     *            The text that should be associated with the sound resource
250     * @param filename
251     *            The filename of the sound resource. This must be a complete
252     *            path like: (/sdcard/mysounds/mysoundbite.mp3).
253     */
254    private void addSpeech(String text, String filename) {
255        mUtterances.put(text, new SoundResource(filename));
256    }
257
258    /**
259     * Adds a sound resource to the TTS as an earcon.
260     *
261     * @param earcon
262     *            The text that should be associated with the sound resource
263     * @param packageName
264     *            The name of the package which has the sound resource
265     * @param resId
266     *            The resource ID of the sound within its package
267     */
268    private void addEarcon(String earcon, String packageName, int resId) {
269        mEarcons.put(earcon, new SoundResource(packageName, resId));
270    }
271
272    /**
273     * Adds a sound resource to the TTS as an earcon.
274     *
275     * @param earcon
276     *            The text that should be associated with the sound resource
277     * @param filename
278     *            The filename of the sound resource. This must be a complete
279     *            path like: (/sdcard/mysounds/mysoundbite.mp3).
280     */
281    private void addEarcon(String earcon, String filename) {
282        mEarcons.put(earcon, new SoundResource(filename));
283    }
284
285    /**
286     * Speaks the given text using the specified queueing mode and parameters.
287     *
288     * @param text
289     *            The text that should be spoken
290     * @param queueMode
291     *            0 for no queue (interrupts all previous utterances), 1 for
292     *            queued
293     * @param params
294     *            An ArrayList of parameters. This is not implemented for all
295     *            engines.
296     */
297    private void speak(String text, int queueMode, ArrayList<String> params) {
298        if (queueMode == 0) {
299            stop();
300        }
301        mSpeechQueue.add(new SpeechItem(text, params, SpeechItem.SPEECH));
302        if (!mIsSpeaking) {
303            processSpeechQueue();
304        }
305    }
306
307    /**
308     * Plays the earcon using the specified queueing mode and parameters.
309     *
310     * @param earcon
311     *            The earcon that should be played
312     * @param queueMode
313     *            0 for no queue (interrupts all previous utterances), 1 for
314     *            queued
315     * @param params
316     *            An ArrayList of parameters. This is not implemented for all
317     *            engines.
318     */
319    private void playEarcon(String earcon, int queueMode,
320            ArrayList<String> params) {
321        if (queueMode == 0) {
322            stop();
323        }
324        mSpeechQueue.add(new SpeechItem(earcon, params, SpeechItem.EARCON));
325        if (!mIsSpeaking) {
326            processSpeechQueue();
327        }
328    }
329
330    /**
331     * Stops all speech output and removes any utterances still in the queue.
332     */
333    private void stop() {
334        Log.i("TTS", "Stopping");
335        mSpeechQueue.clear();
336
337        nativeSynth.stop();
338        mIsSpeaking = false;
339        if (mPlayer != null) {
340            try {
341                mPlayer.stop();
342            } catch (IllegalStateException e) {
343                // Do nothing, the player is already stopped.
344            }
345        }
346        Log.i("TTS", "Stopped");
347    }
348
349    public void onCompletion(MediaPlayer arg0) {
350        processSpeechQueue();
351    }
352
353    private void playSilence(long duration, int queueMode,
354            ArrayList<String> params) {
355        if (queueMode == 0) {
356            stop();
357        }
358        mSpeechQueue.add(new SpeechItem(duration));
359        if (!mIsSpeaking) {
360            processSpeechQueue();
361        }
362    }
363
364    private void silence(final long duration) {
365        class SilenceThread implements Runnable {
366            public void run() {
367                try {
368                    Thread.sleep(duration);
369                } catch (InterruptedException e) {
370                    e.printStackTrace();
371                } finally {
372                    processSpeechQueue();
373                }
374            }
375        }
376        Thread slnc = (new Thread(new SilenceThread()));
377        slnc.setPriority(Thread.MIN_PRIORITY);
378        slnc.start();
379    }
380
381    private void speakInternalOnly(final String text,
382            final ArrayList<String> params) {
383        class SynthThread implements Runnable {
384            public void run() {
385                boolean synthAvailable = false;
386                try {
387                    synthAvailable = synthesizerLock.tryLock();
388                    if (!synthAvailable) {
389                        Thread.sleep(100);
390                        Thread synth = (new Thread(new SynthThread()));
391                        synth.setPriority(Thread.MIN_PRIORITY);
392                        synth.start();
393                        return;
394                    }
395                    nativeSynth.speak(text);
396                } catch (InterruptedException e) {
397                    e.printStackTrace();
398                } finally {
399                    // This check is needed because finally will always run;
400                    // even if the
401                    // method returns somewhere in the try block.
402                    if (synthAvailable) {
403                        synthesizerLock.unlock();
404                    }
405                    processSpeechQueue();
406                }
407            }
408        }
409        Thread synth = (new Thread(new SynthThread()));
410        synth.setPriority(Thread.MIN_PRIORITY);
411        synth.start();
412    }
413
414    private SoundResource getSoundResource(SpeechItem speechItem) {
415        SoundResource sr = null;
416        String text = speechItem.mText;
417        if (speechItem.mType == SpeechItem.SILENCE) {
418            // Do nothing if this is just silence
419        } else if (speechItem.mType == SpeechItem.EARCON) {
420            sr = mEarcons.get(text);
421        } else {
422            sr = mUtterances.get(text);
423        }
424        return sr;
425    }
426
427    private void broadcastTtsQueueProcessingCompleted(){
428        Intent i = new Intent(Intent.ACTION_TTS_QUEUE_PROCESSING_COMPLETED);
429        sendBroadcast(i);
430    }
431
432    private void dispatchSpeechCompletedCallbacks(String mark) {
433        Log.i("TTS callback", "dispatch started");
434        // Broadcast to all clients the new value.
435        final int N = mCallbacks.beginBroadcast();
436        for (int i = 0; i < N; i++) {
437            try {
438                mCallbacks.getBroadcastItem(i).markReached(mark);
439            } catch (RemoteException e) {
440                // The RemoteCallbackList will take care of removing
441                // the dead object for us.
442            }
443        }
444        mCallbacks.finishBroadcast();
445        Log.i("TTS callback", "dispatch completed to " + N);
446    }
447
448    private void processSpeechQueue() {
449        boolean speechQueueAvailable = false;
450        try {
451            speechQueueAvailable = speechQueueLock.tryLock();
452            if (!speechQueueAvailable) {
453                return;
454            }
455            if (mSpeechQueue.size() < 1) {
456                mIsSpeaking = false;
457                broadcastTtsQueueProcessingCompleted();
458                return;
459            }
460
461            SpeechItem currentSpeechItem = mSpeechQueue.get(0);
462            mIsSpeaking = true;
463            SoundResource sr = getSoundResource(currentSpeechItem);
464            // Synth speech as needed - synthesizer should call
465            // processSpeechQueue to continue running the queue
466            Log.i("TTS processing: ", currentSpeechItem.mText);
467            if (sr == null) {
468                if (currentSpeechItem.mType == SpeechItem.SPEECH) {
469                    // TODO: Split text up into smaller chunks before accepting
470                    // them for processing.
471                    speakInternalOnly(currentSpeechItem.mText,
472                            currentSpeechItem.mParams);
473                } else {
474                    // This is either silence or an earcon that was missing
475                    silence(currentSpeechItem.mDuration);
476                }
477            } else {
478                cleanUpPlayer();
479                if (sr.mSourcePackageName == PKGNAME) {
480                    // Utterance is part of the TTS library
481                    mPlayer = MediaPlayer.create(this, sr.mResId);
482                } else if (sr.mSourcePackageName != null) {
483                    // Utterance is part of the app calling the library
484                    Context ctx;
485                    try {
486                        ctx = this.createPackageContext(sr.mSourcePackageName,
487                                0);
488                    } catch (NameNotFoundException e) {
489                        e.printStackTrace();
490                        mSpeechQueue.remove(0); // Remove it from the queue and
491                        // move on
492                        mIsSpeaking = false;
493                        return;
494                    }
495                    mPlayer = MediaPlayer.create(ctx, sr.mResId);
496                } else {
497                    // Utterance is coming from a file
498                    mPlayer = MediaPlayer.create(this, Uri.parse(sr.mFilename));
499                }
500
501                // Check if Media Server is dead; if it is, clear the queue and
502                // give up for now - hopefully, it will recover itself.
503                if (mPlayer == null) {
504                    mSpeechQueue.clear();
505                    mIsSpeaking = false;
506                    return;
507                }
508                mPlayer.setOnCompletionListener(this);
509                try {
510                    mPlayer.start();
511                } catch (IllegalStateException e) {
512                    mSpeechQueue.clear();
513                    mIsSpeaking = false;
514                    cleanUpPlayer();
515                    return;
516                }
517            }
518            if (mSpeechQueue.size() > 0) {
519                mSpeechQueue.remove(0);
520            }
521        } finally {
522            // This check is needed because finally will always run; even if the
523            // method returns somewhere in the try block.
524            if (speechQueueAvailable) {
525                speechQueueLock.unlock();
526            }
527        }
528    }
529
530    private void cleanUpPlayer() {
531        if (mPlayer != null) {
532            mPlayer.release();
533            mPlayer = null;
534        }
535    }
536
537    /**
538     * Synthesizes the given text using the specified queuing mode and
539     * parameters.
540     *
541     * @param text
542     *            The String of text that should be synthesized
543     * @param params
544     *            An ArrayList of parameters. The first element of this array
545     *            controls the type of voice to use.
546     * @param filename
547     *            The string that gives the full output filename; it should be
548     *            something like "/sdcard/myappsounds/mysound.wav".
549     * @return A boolean that indicates if the synthesis succeeded
550     */
551    private boolean synthesizeToFile(String text, ArrayList<String> params,
552            String filename, boolean calledFromApi) {
553        // Only stop everything if this is a call made by an outside app trying
554        // to
555        // use the API. Do NOT stop if this is a call from within the service as
556        // clearing the speech queue here would be a mistake.
557        if (calledFromApi) {
558            stop();
559        }
560        Log.i("TTS", "Synthesizing to " + filename);
561        boolean synthAvailable = false;
562        try {
563            synthAvailable = synthesizerLock.tryLock();
564            if (!synthAvailable) {
565                return false;
566            }
567            // Don't allow a filename that is too long
568            // TODO use platform constant
569            if (filename.length() > 250) {
570                return false;
571            }
572            nativeSynth.synthesizeToFile(text, filename);
573        } finally {
574            // This check is needed because finally will always run; even if the
575            // method returns somewhere in the try block.
576            if (synthAvailable) {
577                synthesizerLock.unlock();
578            }
579        }
580        Log.i("TTS", "Completed synthesis for " + filename);
581        return true;
582    }
583
584    @Override
585    public IBinder onBind(Intent intent) {
586        if (ACTION.equals(intent.getAction())) {
587            for (String category : intent.getCategories()) {
588                if (category.equals(CATEGORY)) {
589                    return mBinder;
590                }
591            }
592        }
593        return null;
594    }
595
596    private final android.speech.tts.ITts.Stub mBinder = new Stub() {
597
598        public void registerCallback(ITtsCallback cb) {
599            if (cb != null)
600                mCallbacks.register(cb);
601        }
602
603        public void unregisterCallback(ITtsCallback cb) {
604            if (cb != null)
605                mCallbacks.unregister(cb);
606        }
607
608        /**
609         * Speaks the given text using the specified queueing mode and
610         * parameters.
611         *
612         * @param text
613         *            The text that should be spoken
614         * @param queueMode
615         *            0 for no queue (interrupts all previous utterances), 1 for
616         *            queued
617         * @param params
618         *            An ArrayList of parameters. The first element of this
619         *            array controls the type of voice to use.
620         */
621        public void speak(String text, int queueMode, String[] params) {
622            ArrayList<String> speakingParams = new ArrayList<String>();
623            if (params != null) {
624                speakingParams = new ArrayList<String>(Arrays.asList(params));
625            }
626            mSelf.speak(text, queueMode, speakingParams);
627        }
628
629        /**
630         * Plays the earcon using the specified queueing mode and parameters.
631         *
632         * @param earcon
633         *            The earcon that should be played
634         * @param queueMode
635         *            0 for no queue (interrupts all previous utterances), 1 for
636         *            queued
637         * @param params
638         *            An ArrayList of parameters.
639         */
640        public void playEarcon(String earcon, int queueMode, String[] params) {
641            ArrayList<String> speakingParams = new ArrayList<String>();
642            if (params != null) {
643                speakingParams = new ArrayList<String>(Arrays.asList(params));
644            }
645            mSelf.playEarcon(earcon, queueMode, speakingParams);
646        }
647
648        /**
649         * Plays the silence using the specified queueing mode and parameters.
650         *
651         * @param duration
652         *            The duration of the silence that should be played
653         * @param queueMode
654         *            0 for no queue (interrupts all previous utterances), 1 for
655         *            queued
656         * @param params
657         *            An ArrayList of parameters.
658         */
659        public void playSilence(long duration, int queueMode, String[] params) {
660            ArrayList<String> speakingParams = new ArrayList<String>();
661            if (params != null) {
662                speakingParams = new ArrayList<String>(Arrays.asList(params));
663            }
664            mSelf.playSilence(duration, queueMode, speakingParams);
665        }
666
667        /**
668         * Stops all speech output and removes any utterances still in the
669         * queue.
670         */
671        public void stop() {
672            mSelf.stop();
673        }
674
675        /**
676         * Returns whether or not the TTS is speaking.
677         *
678         * @return Boolean to indicate whether or not the TTS is speaking
679         */
680        public boolean isSpeaking() {
681            return (mSelf.mIsSpeaking && (mSpeechQueue.size() < 1));
682        }
683
684        /**
685         * Adds a sound resource to the TTS.
686         *
687         * @param text
688         *            The text that should be associated with the sound resource
689         * @param packageName
690         *            The name of the package which has the sound resource
691         * @param resId
692         *            The resource ID of the sound within its package
693         */
694        public void addSpeech(String text, String packageName, int resId) {
695            mSelf.addSpeech(text, packageName, resId);
696        }
697
698        /**
699         * Adds a sound resource to the TTS.
700         *
701         * @param text
702         *            The text that should be associated with the sound resource
703         * @param filename
704         *            The filename of the sound resource. This must be a
705         *            complete path like: (/sdcard/mysounds/mysoundbite.mp3).
706         */
707        public void addSpeechFile(String text, String filename) {
708            mSelf.addSpeech(text, filename);
709        }
710
711        /**
712         * Adds a sound resource to the TTS as an earcon.
713         *
714         * @param earcon
715         *            The text that should be associated with the sound resource
716         * @param packageName
717         *            The name of the package which has the sound resource
718         * @param resId
719         *            The resource ID of the sound within its package
720         */
721        public void addEarcon(String earcon, String packageName, int resId) {
722            mSelf.addEarcon(earcon, packageName, resId);
723        }
724
725        /**
726         * Adds a sound resource to the TTS as an earcon.
727         *
728         * @param earcon
729         *            The text that should be associated with the sound resource
730         * @param filename
731         *            The filename of the sound resource. This must be a
732         *            complete path like: (/sdcard/mysounds/mysoundbite.mp3).
733         */
734        public void addEarconFile(String earcon, String filename) {
735            mSelf.addEarcon(earcon, filename);
736        }
737
738        /**
739         * Sets the speech rate for the TTS. Note that this will only have an
740         * effect on synthesized speech; it will not affect pre-recorded speech.
741         *
742         * @param speechRate
743         *            The speech rate that should be used
744         */
745        public void setSpeechRate(int speechRate) {
746            mSelf.setSpeechRate(speechRate);
747        }
748
749        /**
750         * Sets the pitch for the TTS. Note that this will only have an
751         * effect on synthesized speech; it will not affect pre-recorded speech.
752         *
753         * @param pitch
754         *            The pitch that should be used for the synthesized voice
755         */
756        public void setPitch(int pitch) {
757            mSelf.setPitch(pitch);
758        }
759
760        /**
761         * Sets the speech rate for the TTS, which affects the synthesized voice.
762         *
763         * @param lang  the three letter ISO language code.
764         * @param country  the three letter ISO country code.
765         * @param variant  the variant code associated with the country and language pair.
766         */
767        public void setLanguage(String lang, String country, String variant) {
768            mSelf.setLanguage(lang, country, variant);
769        }
770
771        /**
772         * Speaks the given text using the specified queueing mode and
773         * parameters.
774         *
775         * @param text
776         *            The String of text that should be synthesized
777         * @param params
778         *            An ArrayList of parameters. The first element of this
779         *            array controls the type of voice to use.
780         * @param filename
781         *            The string that gives the full output filename; it should
782         *            be something like "/sdcard/myappsounds/mysound.wav".
783         * @return A boolean that indicates if the synthesis succeeded
784         */
785        public boolean synthesizeToFile(String text, String[] params,
786                String filename) {
787            ArrayList<String> speakingParams = new ArrayList<String>();
788            if (params != null) {
789                speakingParams = new ArrayList<String>(Arrays.asList(params));
790            }
791            return mSelf.synthesizeToFile(text, speakingParams, filename, true);
792        }
793    };
794
795}
796