blob: bc24560ba0c094d551f4c6ed10da77d15c0efff0 [file] [log] [blame]
/*
* Copyright 2014, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.telecomm;
import android.content.Context;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnErrorListener;
import android.media.MediaPlayer.OnPreparedListener;
import android.media.RingtoneManager;
import android.net.Uri;
import android.telecomm.CallState;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import java.io.IOException;
import java.util.List;
/**
* Controls ringing and vibration for incoming calls.
*/
final class Ringer extends CallsManagerListenerBase implements OnErrorListener, OnPreparedListener {
// States for the Ringer.
/** Actively playing the ringer. */
private static final int RINGING = 1;
/** Ringer currently stopped. */
private static final int STOPPED = 2;
/** {@link #mMediaPlayer} is preparing, expected to ring once prepared. */
private static final int PREPARING_WITH_RING = 3;
/** {@link #mMediaPlayer} is preparing, expected to stop once prepared. */
private static final int PREPARING_WITH_STOP = 4;
/**
* The current state of the ringer.
*/
private int mState = STOPPED;
/** The active media player for the ringer. */
private MediaPlayer mMediaPlayer;
/*
* Used to keep ordering of unanswered incoming calls. The existence of multiple call services
* means that there can easily exist multiple incoming calls and explicit ordering is useful for
* maintaining the proper state of the ringer.
*/
private final List<String> mUnansweredCallIds = Lists.newLinkedList();
@Override
public void onCallAdded(Call call) {
if (call.isIncoming() && call.getState() == CallState.RINGING) {
if (mUnansweredCallIds.contains(call.getId())) {
Log.wtf(this, "New ringing call is already in list of unanswered calls");
}
mUnansweredCallIds.add(call.getId());
if (mUnansweredCallIds.size() == 1) {
// Start the ringer if we are the top-most incoming call (the only one in this
// case).
startRinging();
}
}
}
@Override
public void onCallRemoved(Call call) {
removeFromUnansweredCallIds(call.getId());
}
@Override
public void onCallStateChanged(Call call, CallState oldState, CallState newState) {
if (newState != CallState.RINGING) {
removeFromUnansweredCallIds(call.getId());
}
}
@Override
public void onIncomingCallAnswered(Call call) {
onRespondedToIncomingCall(call);
}
@Override
public void onIncomingCallRejected(Call call) {
onRespondedToIncomingCall(call);
}
private void onRespondedToIncomingCall(Call call) {
// Only stop the ringer if this call is the top-most incoming call.
if (!mUnansweredCallIds.isEmpty() && mUnansweredCallIds.get(0).equals(call.getId())) {
stopRinging();
}
}
/**
* Removes the specified call from the list of unanswered incoming calls and updates the ringer
* based on the new state of {@link #mUnansweredCallIds}. Safe to call with a call ID that
* is not present in the list of incoming calls.
*
* @param callId The ID of the call.
*/
private void removeFromUnansweredCallIds(String callId) {
if (mUnansweredCallIds.remove(callId)) {
if (mUnansweredCallIds.isEmpty()) {
stopRinging();
} else {
startRinging();
}
}
}
/**
* Starts the vibration, ringer, and/or call-waiting tone.
* TODO(santoscordon): vibration and call-waiting tone.
*/
private void startRinging() {
// Check if we are muted before playing the ringer.
if (getAudioManager().getStreamVolume(AudioManager.STREAM_RING) > 0) {
moveToState(RINGING);
} else {
Log.d(this, "Ringer play skipped due to muted volume.");
}
}
/**
* Stops the vibration, ringer, and/or call-waiting tone.
*/
private void stopRinging() {
moveToState(STOPPED);
}
/**
* Handles asynchronous media player "prepared" response by playing the ringer if we are
* still expected to or uninitializing it if we've been asked to stop.
*
* {@inheritDoc}
*/
@Override
public void onPrepared(MediaPlayer mediaPlayer) {
Preconditions.checkState(mMediaPlayer == null);
// See {@link #moveToState} for state transitions.
if (PREPARING_WITH_RING == mState) {
Log.i(this, "Playing the ringer.");
setRingerAudioMode();
mMediaPlayer = mediaPlayer;
mMediaPlayer.start();
setState(RINGING);
} else if (PREPARING_WITH_STOP == mState) {
mediaPlayer.release();
setState(STOPPED);
}
}
/** {@inheritDoc} */
@Override
public boolean onError(MediaPlayer mediaPlayer, int what, int extra) {
Log.i(this, "Mediaplayer failed to initialize. What: %d, extra: %d.", what, extra);
resetMediaPlayer();
setState(STOPPED);
return true;
}
/**
* Transitions the state of the ringer. State machine below. Any missing arrows imply that the
* state remains the same (e.g., (r) on RING state keeps it at RING state).
*
* +----------------(s)----------------------------+
* | |
* +----------------(e)-------+ |
* | | |
* +-> STOPPED -(r)-> PREPARING_WITH_RING +-(p)-> RING
* ^ ^ |
* | | |
* (p,e) (r) |
* | | +-(s)-> PREPARING_WITH_STOP
* | | | |
* | +----------------------+ |
* +---------------------------------------------+
*
* STOPPED - Ringer completely stopped, like its initial state.
* PREPARING_TO_RING - Media player preparing asynchronously to start ringing.
* RINGING - The ringtone is currently playing.
* PREPARING_TO_STOP - Media player is still preparing, but we've already been asked to stop.
*
* (r) - {@link #startRinging}
* (s) - {@link #stopRinging}
* (p) - {@link #onPrepared}
* (e) - {@link #onError}
*/
private void moveToState(int newState) {
// Only this method sets PREPARING_* states.
Preconditions.checkState(newState == RINGING || newState == STOPPED);
if (newState == mState) {
return;
}
if (RINGING == newState) {
if (STOPPED == mState) {
// If we are stopped, we need to preparing the media player and wait for it to
// start the ring. New state set by prepareForRinging.
if (prepareForRinging()) {
setState(PREPARING_WITH_RING);
}
} else if (PREPARING_WITH_STOP == mState) {
// We are currently preparing the media player, but expect it to put the ringer into
// stop once prepared...change that to ring.
setState(PREPARING_WITH_RING);
}
} else if (STOPPED == newState) {
if (RINGING == mState) {
// We are currently ringing, so just stop it.
stopPlayingRinger();
setState(STOPPED);
} else if (PREPARING_WITH_RING == mState) {
// We are preparing the media player, make sure that when it is finished, it moves
// to STOPPED instead of ringing.
setState(PREPARING_WITH_STOP);
}
}
}
/**
* Sets the ringer state and checks the current thread.
*
* @param newState The new state to set.
*/
private void setState(int newState) {
ThreadUtil.checkOnMainThread();
Log.v(this, "setState, %d -> %d", mState, newState);
mState = newState;
}
/**
* Starts media player's asynchronous prepare. Response returned in either {@link #onError} or
* {@link #onPrepared}.
*
* @return True if the prepare was successfully started.
*/
private boolean prepareForRinging() {
Log.i(this, "Preparing the ringer.");
Uri ringtoneUri = getCurrentRingtoneUri();
if (ringtoneUri == null) {
Log.e(this, null, "Ringtone not set.");
return false;
}
MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setOnErrorListener(this);
mediaPlayer.setOnPreparedListener(this);
mediaPlayer.setAudioStreamType(AudioManager.STREAM_RING);
try {
mediaPlayer.setDataSource(TelecommApp.getInstance(), ringtoneUri);
mediaPlayer.prepareAsync();
return true;
} catch (IOException e) {
mediaPlayer.reset();
mediaPlayer.release();
Log.e(this, e, "Failed to initialize media player for ringer: %s.", ringtoneUri);
return false;
}
}
/**
* Stops and uninitializes the media player.
*/
private void stopPlayingRinger() {
Preconditions.checkNotNull(mMediaPlayer);
Log.i(this, "Stopping the ringer.");
resetMediaPlayer();
unsetRingerAudioMode();
}
/**
* Stops and uninitializes the media player.
*/
private void resetMediaPlayer() {
if (mMediaPlayer != null) {
// Ringtone.java does not do stop() before release, but it's safer to do so and none of
// the documentation suggests that stop() should be skipped.
mMediaPlayer.stop();
mMediaPlayer.release();
mMediaPlayer = null;
}
}
/**
* @return The default ringtone Uri.
*/
private Uri getCurrentRingtoneUri() {
return RingtoneManager.getActualDefaultRingtoneUri(
TelecommApp.getInstance(), RingtoneManager.TYPE_RINGTONE);
}
/**
* Sets the audio mode for playing the ringtone.
*/
private void setRingerAudioMode() {
AudioManager audioManager = getAudioManager();
audioManager.requestAudioFocusForCall(
AudioManager.STREAM_RING, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
audioManager.setMode(AudioManager.MODE_RINGTONE);
}
/**
* Returns the audio mode to the normal state after ringing.
*/
private void unsetRingerAudioMode() {
AudioManager audioManager = getAudioManager();
audioManager.setMode(AudioManager.MODE_NORMAL);
audioManager.abandonAudioFocusForCall();
}
/**
* Returns the system audio manager.
*/
private AudioManager getAudioManager() {
return (AudioManager) TelecommApp.getInstance().getSystemService(Context.AUDIO_SERVICE);
}
}