blob: bc24560ba0c094d551f4c6ed10da77d15c0efff0 [file] [log] [blame]
Santos Cordon4e9fffe2014-03-04 18:13:41 -08001/*
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
17package com.android.telecomm;
18
Santos Cordonbe322e92014-03-17 16:56:13 -070019import android.content.Context;
20import android.media.AudioManager;
21import android.media.MediaPlayer;
22import android.media.MediaPlayer.OnErrorListener;
23import android.media.MediaPlayer.OnPreparedListener;
Santos Cordon57a680f2014-03-07 14:48:11 -080024import android.media.RingtoneManager;
Santos Cordonbe322e92014-03-17 16:56:13 -070025import android.net.Uri;
Sailesh Nepal810735e2014-03-18 18:15:46 -070026import android.telecomm.CallState;
Santos Cordonbe322e92014-03-17 16:56:13 -070027
28import com.google.common.base.Preconditions;
Sailesh Nepal810735e2014-03-18 18:15:46 -070029import com.google.common.collect.Lists;
Santos Cordonbe322e92014-03-17 16:56:13 -070030
31import java.io.IOException;
Sailesh Nepal810735e2014-03-18 18:15:46 -070032import java.util.List;
Santos Cordon57a680f2014-03-07 14:48:11 -080033
Santos Cordon4e9fffe2014-03-04 18:13:41 -080034/**
35 * Controls ringing and vibration for incoming calls.
Santos Cordon4e9fffe2014-03-04 18:13:41 -080036 */
Sailesh Nepal810735e2014-03-18 18:15:46 -070037final class Ringer extends CallsManagerListenerBase implements OnErrorListener, OnPreparedListener {
Santos Cordonbe322e92014-03-17 16:56:13 -070038 // States for the Ringer.
39 /** Actively playing the ringer. */
40 private static final int RINGING = 1;
Santos Cordon4e9fffe2014-03-04 18:13:41 -080041
Santos Cordonbe322e92014-03-17 16:56:13 -070042 /** Ringer currently stopped. */
43 private static final int STOPPED = 2;
Santos Cordon57a680f2014-03-07 14:48:11 -080044
Santos Cordonbe322e92014-03-17 16:56:13 -070045 /** {@link #mMediaPlayer} is preparing, expected to ring once prepared. */
46 private static final int PREPARING_WITH_RING = 3;
47
48 /** {@link #mMediaPlayer} is preparing, expected to stop once prepared. */
49 private static final int PREPARING_WITH_STOP = 4;
Santos Cordon57a680f2014-03-07 14:48:11 -080050
51 /**
Santos Cordonbe322e92014-03-17 16:56:13 -070052 * The current state of the ringer.
Santos Cordon57a680f2014-03-07 14:48:11 -080053 */
Santos Cordonbe322e92014-03-17 16:56:13 -070054 private int mState = STOPPED;
Santos Cordon57a680f2014-03-07 14:48:11 -080055
Santos Cordonbe322e92014-03-17 16:56:13 -070056 /** The active media player for the ringer. */
57 private MediaPlayer mMediaPlayer;
Santos Cordon57a680f2014-03-07 14:48:11 -080058
Sailesh Nepal810735e2014-03-18 18:15:46 -070059 /*
60 * Used to keep ordering of unanswered incoming calls. The existence of multiple call services
61 * means that there can easily exist multiple incoming calls and explicit ordering is useful for
62 * maintaining the proper state of the ringer.
63 */
64 private final List<String> mUnansweredCallIds = Lists.newLinkedList();
65
66 @Override
67 public void onCallAdded(Call call) {
68 if (call.isIncoming() && call.getState() == CallState.RINGING) {
69 if (mUnansweredCallIds.contains(call.getId())) {
70 Log.wtf(this, "New ringing call is already in list of unanswered calls");
71 }
72 mUnansweredCallIds.add(call.getId());
73 if (mUnansweredCallIds.size() == 1) {
74 // Start the ringer if we are the top-most incoming call (the only one in this
75 // case).
76 startRinging();
77 }
78 }
79 }
80
81 @Override
82 public void onCallRemoved(Call call) {
83 removeFromUnansweredCallIds(call.getId());
84 }
85
86 @Override
87 public void onCallStateChanged(Call call, CallState oldState, CallState newState) {
88 if (newState != CallState.RINGING) {
89 removeFromUnansweredCallIds(call.getId());
90 }
91 }
92
93 @Override
94 public void onIncomingCallAnswered(Call call) {
95 onRespondedToIncomingCall(call);
96 }
97
98 @Override
99 public void onIncomingCallRejected(Call call) {
100 onRespondedToIncomingCall(call);
101 }
102
103 private void onRespondedToIncomingCall(Call call) {
104 // Only stop the ringer if this call is the top-most incoming call.
105 if (!mUnansweredCallIds.isEmpty() && mUnansweredCallIds.get(0).equals(call.getId())) {
106 stopRinging();
107 }
108 }
109
110 /**
111 * Removes the specified call from the list of unanswered incoming calls and updates the ringer
112 * based on the new state of {@link #mUnansweredCallIds}. Safe to call with a call ID that
113 * is not present in the list of incoming calls.
114 *
115 * @param callId The ID of the call.
116 */
117 private void removeFromUnansweredCallIds(String callId) {
118 if (mUnansweredCallIds.remove(callId)) {
119 if (mUnansweredCallIds.isEmpty()) {
120 stopRinging();
121 } else {
122 startRinging();
123 }
124 }
125 }
126
Santos Cordon4e9fffe2014-03-04 18:13:41 -0800127 /**
128 * Starts the vibration, ringer, and/or call-waiting tone.
Santos Cordonbe322e92014-03-17 16:56:13 -0700129 * TODO(santoscordon): vibration and call-waiting tone.
Santos Cordon4e9fffe2014-03-04 18:13:41 -0800130 */
Sailesh Nepal810735e2014-03-18 18:15:46 -0700131 private void startRinging() {
Santos Cordonbe322e92014-03-17 16:56:13 -0700132 // Check if we are muted before playing the ringer.
133 if (getAudioManager().getStreamVolume(AudioManager.STREAM_RING) > 0) {
134 moveToState(RINGING);
135 } else {
136 Log.d(this, "Ringer play skipped due to muted volume.");
137 }
Santos Cordon4e9fffe2014-03-04 18:13:41 -0800138 }
139
140 /**
141 * Stops the vibration, ringer, and/or call-waiting tone.
142 */
Sailesh Nepal810735e2014-03-18 18:15:46 -0700143 private void stopRinging() {
Santos Cordonbe322e92014-03-17 16:56:13 -0700144 moveToState(STOPPED);
Santos Cordon57a680f2014-03-07 14:48:11 -0800145 }
146
147 /**
Santos Cordonbe322e92014-03-17 16:56:13 -0700148 * Handles asynchronous media player "prepared" response by playing the ringer if we are
149 * still expected to or uninitializing it if we've been asked to stop.
150 *
151 * {@inheritDoc}
Santos Cordon57a680f2014-03-07 14:48:11 -0800152 */
Santos Cordonbe322e92014-03-17 16:56:13 -0700153 @Override
154 public void onPrepared(MediaPlayer mediaPlayer) {
155 Preconditions.checkState(mMediaPlayer == null);
Santos Cordon57a680f2014-03-07 14:48:11 -0800156
Santos Cordonbe322e92014-03-17 16:56:13 -0700157 // See {@link #moveToState} for state transitions.
158 if (PREPARING_WITH_RING == mState) {
159 Log.i(this, "Playing the ringer.");
160 setRingerAudioMode();
161 mMediaPlayer = mediaPlayer;
162 mMediaPlayer.start();
163 setState(RINGING);
164 } else if (PREPARING_WITH_STOP == mState) {
165 mediaPlayer.release();
166 setState(STOPPED);
167 }
168 }
169
170 /** {@inheritDoc} */
171 @Override
172 public boolean onError(MediaPlayer mediaPlayer, int what, int extra) {
173 Log.i(this, "Mediaplayer failed to initialize. What: %d, extra: %d.", what, extra);
174 resetMediaPlayer();
175 setState(STOPPED);
176 return true;
177 }
178
179 /**
180 * Transitions the state of the ringer. State machine below. Any missing arrows imply that the
181 * state remains the same (e.g., (r) on RING state keeps it at RING state).
182 *
183 * +----------------(s)----------------------------+
184 * | |
185 * +----------------(e)-------+ |
186 * | | |
187 * +-> STOPPED -(r)-> PREPARING_WITH_RING +-(p)-> RING
188 * ^ ^ |
189 * | | |
190 * (p,e) (r) |
191 * | | +-(s)-> PREPARING_WITH_STOP
192 * | | | |
193 * | +----------------------+ |
194 * +---------------------------------------------+
195 *
196 * STOPPED - Ringer completely stopped, like its initial state.
197 * PREPARING_TO_RING - Media player preparing asynchronously to start ringing.
198 * RINGING - The ringtone is currently playing.
199 * PREPARING_TO_STOP - Media player is still preparing, but we've already been asked to stop.
200 *
201 * (r) - {@link #startRinging}
202 * (s) - {@link #stopRinging}
203 * (p) - {@link #onPrepared}
204 * (e) - {@link #onError}
205 */
206 private void moveToState(int newState) {
207 // Only this method sets PREPARING_* states.
208 Preconditions.checkState(newState == RINGING || newState == STOPPED);
209
210 if (newState == mState) {
211 return;
212 }
213
214 if (RINGING == newState) {
215 if (STOPPED == mState) {
216 // If we are stopped, we need to preparing the media player and wait for it to
217 // start the ring. New state set by prepareForRinging.
218 if (prepareForRinging()) {
219 setState(PREPARING_WITH_RING);
Santos Cordon57a680f2014-03-07 14:48:11 -0800220 }
Santos Cordonbe322e92014-03-17 16:56:13 -0700221 } else if (PREPARING_WITH_STOP == mState) {
222 // We are currently preparing the media player, but expect it to put the ringer into
223 // stop once prepared...change that to ring.
224 setState(PREPARING_WITH_RING);
Santos Cordon57a680f2014-03-07 14:48:11 -0800225 }
Santos Cordonbe322e92014-03-17 16:56:13 -0700226 } else if (STOPPED == newState) {
227 if (RINGING == mState) {
228 // We are currently ringing, so just stop it.
229 stopPlayingRinger();
230 setState(STOPPED);
231 } else if (PREPARING_WITH_RING == mState) {
232 // We are preparing the media player, make sure that when it is finished, it moves
233 // to STOPPED instead of ringing.
234 setState(PREPARING_WITH_STOP);
235 }
Santos Cordon57a680f2014-03-07 14:48:11 -0800236 }
Santos Cordon57a680f2014-03-07 14:48:11 -0800237 }
238
239 /**
Santos Cordonbe322e92014-03-17 16:56:13 -0700240 * Sets the ringer state and checks the current thread.
Santos Cordon57a680f2014-03-07 14:48:11 -0800241 *
Santos Cordonbe322e92014-03-17 16:56:13 -0700242 * @param newState The new state to set.
Santos Cordon57a680f2014-03-07 14:48:11 -0800243 */
Santos Cordonbe322e92014-03-17 16:56:13 -0700244 private void setState(int newState) {
245 ThreadUtil.checkOnMainThread();
246 Log.v(this, "setState, %d -> %d", mState, newState);
247 mState = newState;
248 }
249
250 /**
251 * Starts media player's asynchronous prepare. Response returned in either {@link #onError} or
252 * {@link #onPrepared}.
253 *
254 * @return True if the prepare was successfully started.
255 */
256 private boolean prepareForRinging() {
257 Log.i(this, "Preparing the ringer.");
258
259 Uri ringtoneUri = getCurrentRingtoneUri();
260 if (ringtoneUri == null) {
261 Log.e(this, null, "Ringtone not set.");
262 return false;
Santos Cordon57a680f2014-03-07 14:48:11 -0800263 }
264
Santos Cordonbe322e92014-03-17 16:56:13 -0700265 MediaPlayer mediaPlayer = new MediaPlayer();
266 mediaPlayer.setOnErrorListener(this);
267 mediaPlayer.setOnPreparedListener(this);
268 mediaPlayer.setAudioStreamType(AudioManager.STREAM_RING);
269
270 try {
271 mediaPlayer.setDataSource(TelecommApp.getInstance(), ringtoneUri);
272 mediaPlayer.prepareAsync();
273 return true;
274 } catch (IOException e) {
275 mediaPlayer.reset();
276 mediaPlayer.release();
277
278 Log.e(this, e, "Failed to initialize media player for ringer: %s.", ringtoneUri);
279 return false;
280 }
281 }
282
283 /**
284 * Stops and uninitializes the media player.
285 */
286 private void stopPlayingRinger() {
287 Preconditions.checkNotNull(mMediaPlayer);
288 Log.i(this, "Stopping the ringer.");
289
290 resetMediaPlayer();
291 unsetRingerAudioMode();
292 }
293
294 /**
295 * Stops and uninitializes the media player.
296 */
297 private void resetMediaPlayer() {
298 if (mMediaPlayer != null) {
299 // Ringtone.java does not do stop() before release, but it's safer to do so and none of
300 // the documentation suggests that stop() should be skipped.
301 mMediaPlayer.stop();
302 mMediaPlayer.release();
303 mMediaPlayer = null;
304 }
305 }
306
307 /**
308 * @return The default ringtone Uri.
309 */
310 private Uri getCurrentRingtoneUri() {
311 return RingtoneManager.getActualDefaultRingtoneUri(
312 TelecommApp.getInstance(), RingtoneManager.TYPE_RINGTONE);
313 }
314
315 /**
316 * Sets the audio mode for playing the ringtone.
317 */
318 private void setRingerAudioMode() {
319 AudioManager audioManager = getAudioManager();
320 audioManager.requestAudioFocusForCall(
321 AudioManager.STREAM_RING, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
322 audioManager.setMode(AudioManager.MODE_RINGTONE);
323 }
324
325 /**
326 * Returns the audio mode to the normal state after ringing.
327 */
328 private void unsetRingerAudioMode() {
329 AudioManager audioManager = getAudioManager();
330 audioManager.setMode(AudioManager.MODE_NORMAL);
331 audioManager.abandonAudioFocusForCall();
332 }
333
334 /**
335 * Returns the system audio manager.
336 */
337 private AudioManager getAudioManager() {
338 return (AudioManager) TelecommApp.getInstance().getSystemService(Context.AUDIO_SERVICE);
Santos Cordon4e9fffe2014-03-04 18:13:41 -0800339 }
340}