• 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 
166     private static final int TONE_RESOURCE_ID_UNDEFINED = -1;
167 
168     private static final int RELATIVE_VOLUME_EMERGENCY = 100;
169     private static final int RELATIVE_VOLUME_HIPRI = 80;
170     private static final int RELATIVE_VOLUME_LOPRI = 50;
171     private static final int RELATIVE_VOLUME_UNDEFINED = -1;
172 
173     // Buffer time (in msec) to add on to the tone timeout value. Needed mainly when the timeout
174     // value for a tone is exact duration of the tone itself.
175     private static final int TIMEOUT_BUFFER_MILLIS = 20;
176 
177     // The tone state.
178     private static final int STATE_OFF = 0;
179     private static final int STATE_ON = 1;
180     private static final int STATE_STOPPED = 2;
181 
182     /**
183      * Keeps count of the number of actively playing tones so that we can notify CallAudioManager
184      * when we need focus and when it can be release. This should only be manipulated from the main
185      * thread.
186      */
187     private static int sTonesPlaying = 0;
188 
189     private final CallAudioManager mCallAudioManager;
190     private final CallAudioRoutePeripheralAdapter mCallAudioRoutePeripheralAdapter;
191 
192     private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
193 
194     /** The ID of the tone to play. */
195     private final int mToneId;
196 
197     /** Current state of the tone player. */
198     private int mState;
199 
200     /** For tones which are not generated using ToneGenerator. */
201     private MediaPlayerAdapter mToneMediaPlayer = null;
202 
203     /** Telecom lock object. */
204     private final TelecomSystem.SyncRoot mLock;
205 
206     private Session mSession;
207     private final Object mSessionLock = new Object();
208 
209     private final ToneGeneratorFactory mToneGenerator;
210     private final MediaPlayerFactory mMediaPlayerFactory;
211     private final AudioManagerAdapter mAudioManagerAdapter;
212 
213     /**
214      * Initializes the tone player. Private; use the {@link Factory} to create tone players.
215      *
216      * @param toneId ID of the tone to play, see TONE_* constants.
217      */
InCallTonePlayer( int toneId, CallAudioManager callAudioManager, CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter, TelecomSystem.SyncRoot lock, ToneGeneratorFactory toneGeneratorFactory, MediaPlayerFactory mediaPlayerFactor, AudioManagerAdapter audioManagerAdapter)218     private InCallTonePlayer(
219             int toneId,
220             CallAudioManager callAudioManager,
221             CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter,
222             TelecomSystem.SyncRoot lock,
223             ToneGeneratorFactory toneGeneratorFactory,
224             MediaPlayerFactory mediaPlayerFactor,
225             AudioManagerAdapter audioManagerAdapter) {
226         mState = STATE_OFF;
227         mToneId = toneId;
228         mCallAudioManager = callAudioManager;
229         mCallAudioRoutePeripheralAdapter = callAudioRoutePeripheralAdapter;
230         mLock = lock;
231         mToneGenerator = toneGeneratorFactory;
232         mMediaPlayerFactory = mediaPlayerFactor;
233         mAudioManagerAdapter = audioManagerAdapter;
234     }
235 
236     /** {@inheritDoc} */
237     @Override
run()238     public void run() {
239         try {
240             synchronized (mSessionLock) {
241                 if (mSession != null) {
242                     Log.continueSession(mSession, "ICTP.r");
243                     mSession = null;
244                 }
245             }
246             Log.d(this, "run(toneId = %s)", mToneId);
247 
248             final int toneType;  // Passed to ToneGenerator.startTone.
249             final int toneVolume;  // Passed to the ToneGenerator constructor.
250             final int toneLengthMillis;
251             final int mediaResourceId; // The resourceId of the tone to play.  Used for media-based
252                                       // tones.
253 
254             switch (mToneId) {
255                 case TONE_BUSY:
256                     // TODO: CDMA-specific tones
257                     toneType = ToneGenerator.TONE_SUP_BUSY;
258                     toneVolume = RELATIVE_VOLUME_HIPRI;
259                     toneLengthMillis = 4000;
260                     mediaResourceId = TONE_RESOURCE_ID_UNDEFINED;
261                     break;
262                 case TONE_CALL_ENDED:
263                     // Don't use tone generator
264                     toneType = ToneGenerator.TONE_UNKNOWN;
265                     toneVolume = RELATIVE_VOLUME_UNDEFINED;
266                     toneLengthMillis = 0;
267 
268                     // Use a tone resource file for a more rich, full-bodied tone experience.
269                     mediaResourceId = R.raw.endcall;
270                     break;
271                 case TONE_OTA_CALL_ENDED:
272                     // TODO: fill in
273                     throw new IllegalStateException("OTA Call ended NYI.");
274                 case TONE_CALL_WAITING:
275                     toneType = ToneGenerator.TONE_SUP_CALL_WAITING;
276                     toneVolume = RELATIVE_VOLUME_HIPRI;
277                     toneLengthMillis = Integer.MAX_VALUE - TIMEOUT_BUFFER_MILLIS;
278                     mediaResourceId = TONE_RESOURCE_ID_UNDEFINED;
279                     break;
280                 case TONE_CDMA_DROP:
281                     toneType = ToneGenerator.TONE_CDMA_CALLDROP_LITE;
282                     toneVolume = RELATIVE_VOLUME_LOPRI;
283                     toneLengthMillis = 375;
284                     mediaResourceId = TONE_RESOURCE_ID_UNDEFINED;
285                     break;
286                 case TONE_CONGESTION:
287                     toneType = ToneGenerator.TONE_SUP_CONGESTION;
288                     toneVolume = RELATIVE_VOLUME_HIPRI;
289                     toneLengthMillis = 4000;
290                     mediaResourceId = TONE_RESOURCE_ID_UNDEFINED;
291                     break;
292                 case TONE_INTERCEPT:
293                     toneType = ToneGenerator.TONE_CDMA_ABBR_INTERCEPT;
294                     toneVolume = RELATIVE_VOLUME_LOPRI;
295                     toneLengthMillis = 500;
296                     mediaResourceId = TONE_RESOURCE_ID_UNDEFINED;
297                     break;
298                 case TONE_OUT_OF_SERVICE:
299                     toneType = ToneGenerator.TONE_CDMA_CALLDROP_LITE;
300                     toneVolume = RELATIVE_VOLUME_LOPRI;
301                     toneLengthMillis = 375;
302                     mediaResourceId = TONE_RESOURCE_ID_UNDEFINED;
303                     break;
304                 case TONE_REDIAL:
305                     toneType = ToneGenerator.TONE_CDMA_ALERT_AUTOREDIAL_LITE;
306                     toneVolume = RELATIVE_VOLUME_LOPRI;
307                     toneLengthMillis = 5000;
308                     mediaResourceId = TONE_RESOURCE_ID_UNDEFINED;
309                     break;
310                 case TONE_REORDER:
311                     toneType = ToneGenerator.TONE_CDMA_REORDER;
312                     toneVolume = RELATIVE_VOLUME_HIPRI;
313                     toneLengthMillis = 4000;
314                     mediaResourceId = TONE_RESOURCE_ID_UNDEFINED;
315                     break;
316                 case TONE_RING_BACK:
317                     toneType = ToneGenerator.TONE_SUP_RINGTONE;
318                     toneVolume = RELATIVE_VOLUME_HIPRI;
319                     toneLengthMillis = Integer.MAX_VALUE - TIMEOUT_BUFFER_MILLIS;
320                     mediaResourceId = TONE_RESOURCE_ID_UNDEFINED;
321                     break;
322                 case TONE_UNOBTAINABLE_NUMBER:
323                     toneType = ToneGenerator.TONE_SUP_ERROR;
324                     toneVolume = RELATIVE_VOLUME_HIPRI;
325                     toneLengthMillis = 4000;
326                     mediaResourceId = TONE_RESOURCE_ID_UNDEFINED;
327                     break;
328                 case TONE_VOICE_PRIVACY:
329                     // TODO: fill in.
330                     throw new IllegalStateException("Voice privacy tone NYI.");
331                 case TONE_VIDEO_UPGRADE:
332                     // Similar to the call waiting tone, but does not repeat.
333                     toneType = ToneGenerator.TONE_SUP_CALL_WAITING;
334                     toneVolume = RELATIVE_VOLUME_HIPRI;
335                     toneLengthMillis = 4000;
336                     mediaResourceId = TONE_RESOURCE_ID_UNDEFINED;
337                     break;
338                 default:
339                     throw new IllegalStateException("Bad toneId: " + mToneId);
340             }
341 
342             int stream = AudioManager.STREAM_VOICE_CALL;
343             if (mCallAudioRoutePeripheralAdapter.isBluetoothAudioOn()) {
344                 stream = AudioManager.STREAM_BLUETOOTH_SCO;
345             }
346 
347             if (toneType != ToneGenerator.TONE_UNKNOWN) {
348                 playToneGeneratorTone(stream, toneVolume, toneType, toneLengthMillis);
349             } else if (mediaResourceId != TONE_RESOURCE_ID_UNDEFINED) {
350                 playMediaTone(stream, mediaResourceId);
351             }
352         } finally {
353             cleanUpTonePlayer();
354             Log.endSession();
355         }
356     }
357 
358     /**
359      * Play a tone generated by the {@link ToneGenerator}.
360      * @param stream The stream on which the tone will be played.
361      * @param toneVolume The volume of the tone.
362      * @param toneType The type of tone to play.
363      * @param toneLengthMillis How long to play the tone.
364      */
playToneGeneratorTone(int stream, int toneVolume, int toneType, int toneLengthMillis)365     private void playToneGeneratorTone(int stream, int toneVolume, int toneType,
366             int toneLengthMillis) {
367         ToneGenerator toneGenerator = null;
368         try {
369             // If the ToneGenerator creation fails, just continue without it. It is a local audio
370             // signal, and is not as important.
371             try {
372                 toneGenerator = mToneGenerator.get(stream, toneVolume);
373             } catch (RuntimeException e) {
374                 Log.w(this, "Failed to create ToneGenerator.", e);
375                 return;
376             }
377 
378             Log.i(this, "playToneGeneratorTone: toneType=%d", toneType);
379             // TODO: Certain CDMA tones need to check the ringer-volume state before
380             // playing. See CallNotifier.InCallTonePlayer.
381 
382             // TODO: Some tones play through the end of a call so we need to inform
383             // CallAudioManager that we want focus the same way that Ringer does.
384 
385             synchronized (this) {
386                 if (mState != STATE_STOPPED) {
387                     mState = STATE_ON;
388                     toneGenerator.startTone(toneType);
389                     try {
390                         Log.v(this, "Starting tone %d...waiting for %d ms.", mToneId,
391                                 toneLengthMillis + TIMEOUT_BUFFER_MILLIS);
392                         wait(toneLengthMillis + TIMEOUT_BUFFER_MILLIS);
393                     } catch (InterruptedException e) {
394                         Log.w(this, "wait interrupted", e);
395                     }
396                 }
397             }
398             mState = STATE_OFF;
399         } finally {
400             if (toneGenerator != null) {
401                 toneGenerator.release();
402             }
403         }
404     }
405 
406     /**
407      * Plays an audio-file based media tone.
408      * @param stream The audio stream on which to play the tone.
409      * @param toneResourceId The resource ID of the tone to play.
410      */
playMediaTone(int stream, int toneResourceId)411     private void playMediaTone(int stream, int toneResourceId) {
412         synchronized (this) {
413             if (mState != STATE_STOPPED) {
414                 mState = STATE_ON;
415             }
416             Log.i(this, "playMediaTone: toneResourceId=%d", toneResourceId);
417             AudioAttributes attributes = new AudioAttributes.Builder()
418                     .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
419                     .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
420                     .setLegacyStreamType(stream)
421                     .build();
422             mToneMediaPlayer = mMediaPlayerFactory.get(toneResourceId, attributes);
423             mToneMediaPlayer.setLooping(false);
424             int durationMillis = mToneMediaPlayer.getDuration();
425             final CountDownLatch toneLatch = new CountDownLatch(1);
426             mToneMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
427                 @Override
428                 public void onCompletion(MediaPlayer mp) {
429                     Log.i(this, "playMediaTone: toneResourceId=%d completed.", toneResourceId);
430                     synchronized (this) {
431                         mState = STATE_OFF;
432                     }
433                     mToneMediaPlayer.release();
434                     mToneMediaPlayer = null;
435                     toneLatch.countDown();
436                 }
437             });
438             mToneMediaPlayer.start();
439             try {
440                 // Wait for the tone to stop playing; timeout at 2x the length of the file just to
441                 // be on the safe side.
442                 toneLatch.await(durationMillis * 2, TimeUnit.MILLISECONDS);
443             } catch (InterruptedException ie) {
444                 Log.e(this, ie, "playMediaTone: tone playback interrupted.");
445             }
446         }
447 
448     }
449 
450     @VisibleForTesting
startTone()451     public boolean startTone() {
452         // Skip playing the end call tone if the volume is silenced.
453         if (mToneId == TONE_CALL_ENDED && !mAudioManagerAdapter.isVolumeOverZero()) {
454             Log.i(this, "startTone: skip end-call tone as device is silenced.");
455             return false;
456         }
457 
458         sTonesPlaying++;
459         if (sTonesPlaying == 1) {
460             mCallAudioManager.setIsTonePlaying(true);
461         }
462 
463         synchronized (mSessionLock) {
464             if (mSession != null) {
465                 Log.cancelSubsession(mSession);
466             }
467             mSession = Log.createSubsession();
468         }
469 
470         super.start();
471         return true;
472     }
473 
474     @Override
start()475     public void start() {
476         Log.w(this, "Do not call the start method directly; use startTone instead.");
477     }
478 
479     /**
480      * Stops the tone.
481      */
482     @VisibleForTesting
stopTone()483     public void stopTone() {
484         synchronized (this) {
485             if (mState == STATE_ON) {
486                 Log.d(this, "Stopping the tone %d.", mToneId);
487                 notify();
488             }
489             mState = STATE_STOPPED;
490         }
491     }
492 
493     @VisibleForTesting
cleanup()494     public void cleanup() {
495         sTonesPlaying = 0;
496     }
497 
cleanUpTonePlayer()498     private void cleanUpTonePlayer() {
499         // Release focus on the main thread.
500         mMainThreadHandler.post(new Runnable("ICTP.cUTP", mLock) {
501             @Override
502             public void loggedRun() {
503                 if (sTonesPlaying == 0) {
504                     Log.wtf(this, "Over-releasing focus for tone player.");
505                 } else if (--sTonesPlaying == 0) {
506                     mCallAudioManager.setIsTonePlaying(false);
507                 }
508             }
509         }.prepare());
510     }
511 }
512