• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2014, The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of 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,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.telecom;
18 
19 import android.annotation.Nullable;
20 import android.content.Context;
21 import android.media.AudioAttributes;
22 import android.media.AudioManager;
23 import android.media.MediaPlayer;
24 import android.media.ToneGenerator;
25 import android.net.Uri;
26 import android.os.Handler;
27 import android.os.Looper;
28 import android.telecom.Log;
29 import android.telecom.Logging.Runnable;
30 import android.telecom.Logging.Session;
31 
32 import com.android.internal.annotations.VisibleForTesting;
33 import com.android.server.telecom.flags.FeatureFlags;
34 
35 import java.util.concurrent.CountDownLatch;
36 import java.util.concurrent.TimeUnit;
37 import java.util.concurrent.atomic.AtomicInteger;
38 
39 /**
40  * Play a call-related tone (ringback, busy signal, etc.) either through ToneGenerator, or using a
41  * media resource file.
42  * To use, create an instance using InCallTonePlayer.Factory (passing in the TONE_* constant for
43  * the tone you want) and start() it. Implemented on top of {@link Thread} so that the tone plays in
44  * its own thread.
45  */
46 public class InCallTonePlayer extends Thread {
47 
48     /**
49      * Factory used to create InCallTonePlayers. Exists to aid with testing mocks.
50      */
51     public static class Factory {
52         private CallAudioManager mCallAudioManager;
53         private final CallAudioRoutePeripheralAdapter mCallAudioRoutePeripheralAdapter;
54         private final TelecomSystem.SyncRoot mLock;
55         private final ToneGeneratorFactory mToneGeneratorFactory;
56         private final MediaPlayerFactory mMediaPlayerFactory;
57         private final AudioManagerAdapter mAudioManagerAdapter;
58         private final FeatureFlags mFeatureFlags;
59         private final Looper mLooper;
60 
Factory(CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter, TelecomSystem.SyncRoot lock, ToneGeneratorFactory toneGeneratorFactory, MediaPlayerFactory mediaPlayerFactory, AudioManagerAdapter audioManagerAdapter, FeatureFlags flags, Looper looper)61         public Factory(CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter,
62                 TelecomSystem.SyncRoot lock, ToneGeneratorFactory toneGeneratorFactory,
63                 MediaPlayerFactory mediaPlayerFactory, AudioManagerAdapter audioManagerAdapter,
64                 FeatureFlags flags, Looper looper) {
65             mCallAudioRoutePeripheralAdapter = callAudioRoutePeripheralAdapter;
66             mLock = lock;
67             mToneGeneratorFactory = toneGeneratorFactory;
68             mMediaPlayerFactory = mediaPlayerFactory;
69             mAudioManagerAdapter = audioManagerAdapter;
70             mFeatureFlags = flags;
71             mLooper = looper;
72         }
73 
setCallAudioManager(CallAudioManager callAudioManager)74         public void setCallAudioManager(CallAudioManager callAudioManager) {
75             mCallAudioManager = callAudioManager;
76         }
77 
createPlayer(Call call, int tone)78         public InCallTonePlayer createPlayer(Call call, int tone) {
79             return new InCallTonePlayer(call, tone, mCallAudioManager,
80                     mCallAudioRoutePeripheralAdapter, mLock, mToneGeneratorFactory,
81                     mMediaPlayerFactory, mAudioManagerAdapter, mFeatureFlags, mLooper);
82         }
83     }
84 
85     public interface ToneGeneratorFactory {
get(int streamType, int volume)86         ToneGenerator get (int streamType, int volume);
87     }
88 
89     public interface MediaPlayerAdapter {
setLooping(boolean isLooping)90         void setLooping(boolean isLooping);
setOnCompletionListener(MediaPlayer.OnCompletionListener listener)91         void setOnCompletionListener(MediaPlayer.OnCompletionListener listener);
start()92         void start();
release()93         void release();
getDuration()94         int getDuration();
95     }
96 
97     public static class MediaPlayerAdapterImpl implements MediaPlayerAdapter {
98         private MediaPlayer mMediaPlayer;
99 
100         /**
101          * Create new media player adapter backed by a real mediaplayer.
102          * Note: Its possible for the mediaplayer to be null if
103          * {@link MediaPlayer#create(Context, Uri)} fails for some reason; in this case we can
104          * continue but not bother playing the audio.
105          * @param mediaPlayer The media player.
106          */
MediaPlayerAdapterImpl(@ullable MediaPlayer mediaPlayer)107         public MediaPlayerAdapterImpl(@Nullable MediaPlayer mediaPlayer) {
108             mMediaPlayer = mediaPlayer;
109         }
110 
111         @Override
setLooping(boolean isLooping)112         public void setLooping(boolean isLooping) {
113             if (mMediaPlayer != null) {
114                 mMediaPlayer.setLooping(isLooping);
115             }
116         }
117 
118         @Override
setOnCompletionListener(MediaPlayer.OnCompletionListener listener)119         public void setOnCompletionListener(MediaPlayer.OnCompletionListener listener) {
120             if (mMediaPlayer != null) {
121                 mMediaPlayer.setOnCompletionListener(listener);
122             }
123         }
124 
125         @Override
start()126         public void start() {
127             if (mMediaPlayer != null) {
128                 mMediaPlayer.start();
129             }
130         }
131 
132         @Override
release()133         public void release() {
134             if (mMediaPlayer != null) {
135                 mMediaPlayer.release();
136             }
137         }
138 
139         @Override
getDuration()140         public int getDuration() {
141             if (mMediaPlayer != null) {
142                 return mMediaPlayer.getDuration();
143             }
144             return 0;
145         }
146     }
147 
148     public interface MediaPlayerFactory {
get(int resourceId, AudioAttributes attributes)149         MediaPlayerAdapter get (int resourceId, AudioAttributes attributes);
150     }
151 
152     public interface AudioManagerAdapter {
isVolumeOverZero()153         boolean isVolumeOverZero();
154     }
155 
156     // The possible tones that we can play.
157     public static final int TONE_INVALID = 0;
158     public static final int TONE_BUSY = 1;
159     public static final int TONE_CALL_ENDED = 2;
160     public static final int TONE_OTA_CALL_ENDED = 3;
161     public static final int TONE_CALL_WAITING = 4;
162     public static final int TONE_CDMA_DROP = 5;
163     public static final int TONE_CONGESTION = 6;
164     public static final int TONE_INTERCEPT = 7;
165     public static final int TONE_OUT_OF_SERVICE = 8;
166     public static final int TONE_REDIAL = 9;
167     public static final int TONE_REORDER = 10;
168     public static final int TONE_RING_BACK = 11;
169     public static final int TONE_UNOBTAINABLE_NUMBER = 12;
170     public static final int TONE_VOICE_PRIVACY = 13;
171     public static final int TONE_VIDEO_UPGRADE = 14;
172     public static final int TONE_RTT_REQUEST = 15;
173     public static final int TONE_IN_CALL_QUALITY_NOTIFICATION = 16;
174 
175     private static final int TONE_RESOURCE_ID_UNDEFINED = -1;
176 
177     private static final int RELATIVE_VOLUME_EMERGENCY = 100;
178     private static final int RELATIVE_VOLUME_HIPRI = 80;
179     private static final int RELATIVE_VOLUME_LOPRI = 30;
180     private static final int RELATIVE_VOLUME_UNDEFINED = -1;
181 
182     // Buffer time (in msec) to add on to the tone timeout value. Needed mainly when the timeout
183     // value for a tone is exact duration of the tone itself.
184     private static final int TIMEOUT_BUFFER_MILLIS = 20;
185 
186     // The tone state.
187     private static final int STATE_OFF = 0;
188     private static final int STATE_ON = 1;
189     private static final int STATE_STOPPED = 2;
190 
191     // Invalid audio stream
192     private static final int STREAM_INVALID = -1;
193 
194     /**
195      * Keeps count of the number of actively playing tones so that we can notify CallAudioManager
196      * when we need focus and when it can be release. This should only be manipulated from the main
197      * thread.
198      */
199     private static AtomicInteger sTonesPlaying = new AtomicInteger(0);
200 
201     private final CallAudioManager mCallAudioManager;
202     private final CallAudioRoutePeripheralAdapter mCallAudioRoutePeripheralAdapter;
203 
204     private final Handler mMainThreadHandler;
205 
206     /** The ID of the tone to play. */
207     private final int mToneId;
208 
209     /** Current state of the tone player. */
210     private int mState;
211 
212     /** For tones which are not generated using ToneGenerator. */
213     private MediaPlayerAdapter mToneMediaPlayer = null;
214 
215     /** Telecom lock object. */
216     private final TelecomSystem.SyncRoot mLock;
217 
218     private Session mSession;
219     private final Object mSessionLock = new Object();
220 
221     private final Call mCall;
222     private final ToneGeneratorFactory mToneGenerator;
223     private final MediaPlayerFactory mMediaPlayerFactory;
224     private final AudioManagerAdapter mAudioManagerAdapter;
225     private final FeatureFlags mFeatureFlags;
226 
227     /**
228      * Latch used for awaiting on playback, which may be interrupted if the tone is stopped from
229      * outside the playback.
230      */
231     private final CountDownLatch mPlaybackLatch = new CountDownLatch(1);
232 
233     /**
234      * Initializes the tone player. Private; use the {@link Factory} to create tone players.
235      *
236      * @param toneId ID of the tone to play, see TONE_* constants.
237      */
InCallTonePlayer( Call call, int toneId, CallAudioManager callAudioManager, CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter, TelecomSystem.SyncRoot lock, ToneGeneratorFactory toneGeneratorFactory, MediaPlayerFactory mediaPlayerFactor, AudioManagerAdapter audioManagerAdapter, FeatureFlags flags, Looper looper)238     private InCallTonePlayer(
239             Call call,
240             int toneId,
241             CallAudioManager callAudioManager,
242             CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter,
243             TelecomSystem.SyncRoot lock,
244             ToneGeneratorFactory toneGeneratorFactory,
245             MediaPlayerFactory mediaPlayerFactor,
246             AudioManagerAdapter audioManagerAdapter,
247             FeatureFlags flags,
248             Looper looper) {
249         mCall = call;
250         mState = STATE_OFF;
251         mToneId = toneId;
252         mCallAudioManager = callAudioManager;
253         mCallAudioRoutePeripheralAdapter = callAudioRoutePeripheralAdapter;
254         mLock = lock;
255         mToneGenerator = toneGeneratorFactory;
256         mMediaPlayerFactory = mediaPlayerFactor;
257         mAudioManagerAdapter = audioManagerAdapter;
258         mFeatureFlags = flags;
259         mMainThreadHandler = new Handler(looper);
260     }
261 
262     /** {@inheritDoc} */
263     @Override
run()264     public void run() {
265         try {
266             synchronized (mSessionLock) {
267                 if (mSession != null) {
268                     Log.continueSession(mSession, "ICTP.r");
269                     mSession = null;
270                 }
271             }
272             Log.d(this, "run(toneId = %s)", mToneId);
273 
274             final int toneType;  // Passed to ToneGenerator.startTone.
275             final int toneVolume;  // Passed to the ToneGenerator constructor.
276             final int toneLengthMillis;
277             final int mediaResourceId; // The resourceId of the tone to play.  Used for media-based
278                                       // tones.
279 
280             switch (mToneId) {
281                 case TONE_BUSY:
282                     // TODO: CDMA-specific tones
283                     toneType = ToneGenerator.TONE_SUP_BUSY;
284                     toneVolume = RELATIVE_VOLUME_HIPRI;
285                     toneLengthMillis = 4000;
286                     mediaResourceId = TONE_RESOURCE_ID_UNDEFINED;
287                     break;
288                 case TONE_CALL_ENDED:
289                     // Don't use tone generator
290                     toneType = ToneGenerator.TONE_UNKNOWN;
291                     toneVolume = RELATIVE_VOLUME_UNDEFINED;
292                     toneLengthMillis = 0;
293 
294                     // Use a tone resource file for a more rich, full-bodied tone experience.
295                     mediaResourceId = R.raw.endcall;
296                     break;
297                 case TONE_OTA_CALL_ENDED:
298                     // TODO: fill in
299                     throw new IllegalStateException("OTA Call ended NYI.");
300                 case TONE_CALL_WAITING:
301                     toneType = ToneGenerator.TONE_SUP_CALL_WAITING;
302                     toneVolume = RELATIVE_VOLUME_HIPRI;
303                     toneLengthMillis = Integer.MAX_VALUE - TIMEOUT_BUFFER_MILLIS;
304                     mediaResourceId = TONE_RESOURCE_ID_UNDEFINED;
305                     break;
306                 case TONE_CDMA_DROP:
307                     toneType = ToneGenerator.TONE_CDMA_CALLDROP_LITE;
308                     toneVolume = RELATIVE_VOLUME_LOPRI;
309                     toneLengthMillis = 375;
310                     mediaResourceId = TONE_RESOURCE_ID_UNDEFINED;
311                     break;
312                 case TONE_CONGESTION:
313                     toneType = ToneGenerator.TONE_SUP_CONGESTION;
314                     toneVolume = RELATIVE_VOLUME_HIPRI;
315                     toneLengthMillis = 4000;
316                     mediaResourceId = TONE_RESOURCE_ID_UNDEFINED;
317                     break;
318                 case TONE_INTERCEPT:
319                     toneType = ToneGenerator.TONE_CDMA_ABBR_INTERCEPT;
320                     toneVolume = RELATIVE_VOLUME_LOPRI;
321                     toneLengthMillis = 500;
322                     mediaResourceId = TONE_RESOURCE_ID_UNDEFINED;
323                     break;
324                 case TONE_OUT_OF_SERVICE:
325                     toneType = ToneGenerator.TONE_CDMA_CALLDROP_LITE;
326                     toneVolume = RELATIVE_VOLUME_LOPRI;
327                     toneLengthMillis = 375;
328                     mediaResourceId = TONE_RESOURCE_ID_UNDEFINED;
329                     break;
330                 case TONE_REDIAL:
331                     toneType = ToneGenerator.TONE_CDMA_ALERT_AUTOREDIAL_LITE;
332                     toneVolume = RELATIVE_VOLUME_LOPRI;
333                     toneLengthMillis = 5000;
334                     mediaResourceId = TONE_RESOURCE_ID_UNDEFINED;
335                     break;
336                 case TONE_REORDER:
337                     toneType = ToneGenerator.TONE_CDMA_REORDER;
338                     toneVolume = RELATIVE_VOLUME_HIPRI;
339                     toneLengthMillis = 4000;
340                     mediaResourceId = TONE_RESOURCE_ID_UNDEFINED;
341                     break;
342                 case TONE_RING_BACK:
343                     toneType = ToneGenerator.TONE_SUP_RINGTONE;
344                     toneVolume = RELATIVE_VOLUME_HIPRI;
345                     toneLengthMillis = Integer.MAX_VALUE - TIMEOUT_BUFFER_MILLIS;
346                     mediaResourceId = TONE_RESOURCE_ID_UNDEFINED;
347                     break;
348                 case TONE_UNOBTAINABLE_NUMBER:
349                     toneType = ToneGenerator.TONE_SUP_ERROR;
350                     toneVolume = RELATIVE_VOLUME_HIPRI;
351                     toneLengthMillis = 4000;
352                     mediaResourceId = TONE_RESOURCE_ID_UNDEFINED;
353                     break;
354                 case TONE_VOICE_PRIVACY:
355                     // TODO: fill in.
356                     throw new IllegalStateException("Voice privacy tone NYI.");
357                 case TONE_VIDEO_UPGRADE:
358                 case TONE_RTT_REQUEST:
359                     // Similar to the call waiting tone, but does not repeat.
360                     toneType = ToneGenerator.TONE_SUP_CALL_WAITING;
361                     toneVolume = RELATIVE_VOLUME_HIPRI;
362                     toneLengthMillis = 4000;
363                     mediaResourceId = TONE_RESOURCE_ID_UNDEFINED;
364                     break;
365                 case TONE_IN_CALL_QUALITY_NOTIFICATION:
366                     // Don't use tone generator
367                     toneType = ToneGenerator.TONE_UNKNOWN;
368                     toneVolume = RELATIVE_VOLUME_UNDEFINED;
369                     toneLengthMillis = 0;
370 
371                     // Use a tone resource file for a more rich, full-bodied tone experience.
372                     mediaResourceId = R.raw.InCallQualityNotification;
373                     break;
374                 default:
375                     throw new IllegalStateException("Bad toneId: " + mToneId);
376             }
377 
378             int stream = getStreamType(toneType);
379             if (toneType != ToneGenerator.TONE_UNKNOWN) {
380                 playToneGeneratorTone(stream, toneVolume, toneType, toneLengthMillis);
381             } else if (mediaResourceId != TONE_RESOURCE_ID_UNDEFINED) {
382                 playMediaTone(stream, mediaResourceId);
383             }
384         } finally {
385             cleanUpTonePlayer();
386             Log.endSession();
387         }
388     }
389 
390     /**
391      * @param toneType The ToneGenerator tone type
392      * @return The ToneGenerator stream type
393      */
getStreamType(int toneType)394     private int getStreamType(int toneType) {
395         if (mFeatureFlags.useStreamVoiceCallTones()) {
396             return AudioManager.STREAM_VOICE_CALL;
397         }
398 
399         int stream = AudioManager.STREAM_VOICE_CALL;
400         if (mCallAudioRoutePeripheralAdapter.isBluetoothAudioOn()) {
401             stream = AudioManager.STREAM_BLUETOOTH_SCO;
402         }
403         if (toneType != ToneGenerator.TONE_UNKNOWN) {
404             if (stream == AudioManager.STREAM_BLUETOOTH_SCO) {
405                 // Override audio stream for BT le device and hearing aid device
406                 if (mCallAudioRoutePeripheralAdapter.isLeAudioDeviceOn()
407                         || mCallAudioRoutePeripheralAdapter.isHearingAidDeviceOn()) {
408                     stream = AudioManager.STREAM_VOICE_CALL;
409                 }
410             }
411         }
412         return stream;
413     }
414 
415     /**
416      * Play a tone generated by the {@link ToneGenerator}.
417      * @param stream The stream on which the tone will be played.
418      * @param toneVolume The volume of the tone.
419      * @param toneType The type of tone to play.
420      * @param toneLengthMillis How long to play the tone.
421      */
playToneGeneratorTone(int stream, int toneVolume, int toneType, int toneLengthMillis)422     private void playToneGeneratorTone(int stream, int toneVolume, int toneType,
423             int toneLengthMillis) {
424         ToneGenerator toneGenerator = null;
425         try {
426             // If the ToneGenerator creation fails, just continue without it. It is a local audio
427             // signal, and is not as important.
428             try {
429                 toneGenerator = mToneGenerator.get(stream, toneVolume);
430             } catch (RuntimeException e) {
431                 Log.w(this, "Failed to create ToneGenerator.", e);
432                 return;
433             }
434 
435             Log.i(this, "playToneGeneratorTone: toneType=%d", toneType);
436 
437             mState = STATE_ON;
438             toneGenerator.startTone(toneType);
439             try {
440                 Log.v(this, "Starting tone %d...waiting for %d ms.", mToneId,
441                         toneLengthMillis + TIMEOUT_BUFFER_MILLIS);
442                 if (mPlaybackLatch.await(toneLengthMillis + TIMEOUT_BUFFER_MILLIS,
443                         TimeUnit.MILLISECONDS)) {
444                     Log.i(this, "playToneGeneratorTone: tone playback stopped.");
445                 }
446             } catch (InterruptedException e) {
447                 Log.w(this, "playToneGeneratorTone: wait interrupted", e);
448             }
449             // Redundant; don't want anyone re-using at this point.
450             mState = STATE_STOPPED;
451         } finally {
452             if (toneGenerator != null) {
453                 toneGenerator.release();
454             }
455         }
456     }
457 
458     /**
459      * Plays an audio-file based media tone.
460      * @param stream The audio stream on which to play the tone.
461      * @param toneResourceId The resource ID of the tone to play.
462      */
playMediaTone(int stream, int toneResourceId)463     private void playMediaTone(int stream, int toneResourceId) {
464         mState = STATE_ON;
465         Log.i(this, "playMediaTone: toneResourceId=%d", toneResourceId);
466         AudioAttributes attributes = new AudioAttributes.Builder()
467                 .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
468                 .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
469                 .setLegacyStreamType(stream)
470                 .build();
471         mToneMediaPlayer = mMediaPlayerFactory.get(toneResourceId, attributes);
472         mToneMediaPlayer.setLooping(false);
473         int durationMillis = mToneMediaPlayer.getDuration();
474         mToneMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
475             @Override
476             public void onCompletion(MediaPlayer mp) {
477                 Log.i(InCallTonePlayer.this, "playMediaTone: toneResourceId=%d completed.",
478                         toneResourceId);
479                 mPlaybackLatch.countDown();
480             }
481         });
482 
483         try {
484             mToneMediaPlayer.start();
485             // Wait for the tone to stop playing; timeout at 2x the length of the file just to
486             // be on the safe side.  Playback can also be stopped via stopTone().
487             if (mPlaybackLatch.await(durationMillis * 2, TimeUnit.MILLISECONDS)) {
488                 Log.i(this, "playMediaTone: tone playback stopped.");
489             }
490         } catch (InterruptedException ie) {
491             Log.e(this, ie, "playMediaTone: tone playback interrupted.");
492         } finally {
493             // Redundant; don't want anyone re-using at this point.
494             mState = STATE_STOPPED;
495             mToneMediaPlayer.release();
496             mToneMediaPlayer = null;
497         }
498     }
499 
500     @VisibleForTesting
startTone()501     public boolean startTone() {
502         // Tone already done; don't allow re-used
503         if (mState == STATE_STOPPED) {
504             return false;
505         }
506 
507         if (sTonesPlaying.incrementAndGet() == 1) {
508             mCallAudioManager.setIsTonePlaying(mCall, true);
509         }
510 
511         synchronized (mSessionLock) {
512             if (mSession != null) {
513                 Log.cancelSubsession(mSession);
514             }
515             mSession = Log.createSubsession();
516         }
517 
518         super.start();
519         return true;
520     }
521 
522     @Override
start()523     public void start() {
524         Log.w(this, "Do not call the start method directly; use startTone instead.");
525     }
526 
527     /**
528      * Stops the tone.
529      */
530     @VisibleForTesting
stopTone()531     public void stopTone() {
532         Log.i(this, "stopTone: Stopping the tone %d.", mToneId);
533         // Notify the playback to end early.
534         mPlaybackLatch.countDown();
535 
536         mState = STATE_STOPPED;
537     }
538 
539     @VisibleForTesting
cleanup()540     public void cleanup() {
541         sTonesPlaying.set(0);
542     }
543 
cleanUpTonePlayer()544     private void cleanUpTonePlayer() {
545         Log.d(this, "cleanUpTonePlayer(): posting cleanup");
546         // Release focus on the main thread.
547         mMainThreadHandler.post(new Runnable("ICTP.cUTP", mLock) {
548             @Override
549             public void loggedRun() {
550                 int newToneCount = sTonesPlaying.updateAndGet( t -> Math.max(0, --t));
551 
552                 if (newToneCount == 0) {
553                     Log.i(InCallTonePlayer.this,
554                             "cleanUpTonePlayer(): tonesPlaying=%d, tone completed", newToneCount);
555                     if (mCallAudioManager != null) {
556                         mCallAudioManager.setIsTonePlaying(mCall, false);
557                     } else {
558                         Log.w(InCallTonePlayer.this,
559                                 "cleanUpTonePlayer(): mCallAudioManager is null!");
560                     }
561                 } else {
562                     Log.i(InCallTonePlayer.this,
563                             "cleanUpTonePlayer(): tonesPlaying=%d; still playing", newToneCount);
564                 }
565             }
566         }.prepare());
567     }
568 }
569