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