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