Initial Contribution
[android/platform/packages/apps/Phone.git] / src / com / android / phone / CallNotifier.java
1 /*
2  * Copyright (C) 2006 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.phone;
18
19 import com.android.internal.telephony.Call;
20 import com.android.internal.telephony.CallerInfo;
21 import com.android.internal.telephony.CallerInfoAsyncQuery;
22 import com.android.internal.telephony.Connection;
23 import com.android.internal.telephony.Phone;
24 import com.android.internal.telephony.gsm.GSMPhone;
25
26 import android.content.Context;
27 import android.media.AudioManager;
28 import android.media.ToneGenerator;
29 import android.os.AsyncResult;
30 import android.os.Handler;
31 import android.os.Message;
32 import android.os.SystemClock;
33 import android.os.SystemProperties;
34 import android.provider.CallLog;
35 import android.provider.CallLog.Calls;
36 import android.provider.Checkin;
37 import android.provider.Settings;
38 import android.telephony.PhoneStateListener;
39 import android.telephony.TelephonyManager;
40 import android.util.Log;
41
42
43 /**
44  * Stub which listens for phone state changes and decides whether it is worth
45  * telling the user what just happened.
46  */
47 public class CallNotifier extends Handler
48         implements CallerInfoAsyncQuery.OnQueryCompleteListener {
49     private static final String TAG = PhoneApp.LOG_TAG;
50
51     // this debug flag is now attached to the "userdebuggable" builds
52     // to keep useful logging available.
53     private static final boolean DBG =
54             (SystemProperties.getInt("ro.debuggable", 0) == 1);
55
56     // Strings used with Checkin.logEvent().
57     private static final String PHONE_UI_EVENT_RINGER_QUERY_ELAPSED =
58         "using default incoming call behavior";
59     private static final String PHONE_UI_EVENT_MULTIPLE_QUERY =
60         "multiple incoming call queries attempted";
61
62     // query timeout period, about 0.5 sec.
63     private static final int RINGTONE_QUERY_WAIT_TIME = 500;
64
65     // values used to track the query state
66     private static final int CALLERINFO_QUERY_READY = 0;
67     private static final int CALLERINFO_QUERYING = -1;
68
69     // the state of the CallerInfo Query.
70     private int mCallerInfoQueryState;
71
72     // object used to synchronize access to mCallerInfoQueryState
73     private Object mCallerInfoQueryStateGuard = new Object();
74
75     // Event used to indicate a query timeout.
76     private static final int RINGER_CUSTOM_RINGTONE_QUERY_COMPLETE = 100;
77
78     // Events from the Phone object:
79     private static final int PHONE_STATE_CHANGED = 1;
80     private static final int PHONE_NEW_RINGING_CONNECTION = 2;
81     private static final int PHONE_DISCONNECT = 3;
82     private static final int PHONE_UNKNOWN_CONNECTION_APPEARED = 4;
83     private static final int PHONE_INCOMING_RING = 5;
84     // Events generated internally:
85     private static final int PHONE_MWI_CHANGED = 6;
86     private static final int PHONE_BATTERY_LOW = 7;
87
88     private PhoneApp mApplication;
89     private Phone mPhone;
90     private InCallScreen mCallScreen;
91     private Ringer mRinger;
92     private BluetoothHandsfree mBluetoothHandsfree;
93     private boolean mSilentRingerRequested;
94
95     public CallNotifier(PhoneApp app, Phone phone, Ringer ringer,
96                         BluetoothHandsfree btMgr) {
97         mApplication = app;
98
99         mPhone = phone;
100         mPhone.registerForNewRingingConnection(this, PHONE_NEW_RINGING_CONNECTION, null);
101         mPhone.registerForPhoneStateChanged(this, PHONE_STATE_CHANGED, null);
102         mPhone.registerForDisconnect(this, PHONE_DISCONNECT, null);
103         mPhone.registerForUnknownConnection(this, PHONE_UNKNOWN_CONNECTION_APPEARED, null);
104         mPhone.registerForIncomingRing(this, PHONE_INCOMING_RING, null);
105         mRinger = ringer;
106         mBluetoothHandsfree = btMgr;
107
108         TelephonyManager telephonyManager = (TelephonyManager)app.getSystemService(
109                 Context.TELEPHONY_SERVICE);
110         telephonyManager.listen(mPhoneStateListener,
111                 PhoneStateListener.LISTEN_MESSAGE_WAITING_INDICATOR
112                 | PhoneStateListener.LISTEN_CALL_FORWARDING_INDICATOR);
113     }
114
115     @Override
116     public void handleMessage(Message msg) {
117         switch (msg.what) {
118             case PHONE_NEW_RINGING_CONNECTION:
119                 if (DBG) log("RINGING... (new)");
120                 onNewRingingConnection((AsyncResult) msg.obj);
121                 mSilentRingerRequested = false;
122                 break;
123
124             case PHONE_INCOMING_RING:
125                 // repeat the ring when requested by the RIL, and when the user has NOT
126                 // specifically requested silence.
127                 if (msg.obj != null && ((AsyncResult) msg.obj).result != null &&
128                         ((GSMPhone)((AsyncResult) msg.obj).result).getState() == Phone.State.RINGING
129                         && mSilentRingerRequested == false) {
130                     if (DBG) log("RINGING... ");
131                     mRinger.ring();
132                 } else {
133                     if (DBG) log("RING before NEW_RING, skipping");
134                 }
135                 break;
136
137             case PHONE_STATE_CHANGED:
138                 onPhoneStateChanged((AsyncResult) msg.obj);
139                 break;
140
141             case PHONE_DISCONNECT:
142                 if (DBG) log("DISCONNECT");
143                 onDisconnect((AsyncResult) msg.obj);
144                 break;
145
146             case PHONE_UNKNOWN_CONNECTION_APPEARED:
147                 onUnknownConnectionAppeared((AsyncResult) msg.obj);
148                 break;
149
150                 // timed out, go directly to playing the ringtone.
151             case RINGER_CUSTOM_RINGTONE_QUERY_COMPLETE:
152                 if (DBG) log("time out event detected for callerinfo query");
153                 onCustomRingQueryComplete();
154                 break;
155
156             case PHONE_MWI_CHANGED:
157                 onMwiChanged(mPhone.getMessageWaitingIndicator());
158                 break;
159
160             case PHONE_BATTERY_LOW:
161                 onBatteryLow();
162                 break;
163
164             default:
165                 // super.handleMessage(msg);
166         }
167     }
168
169     PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
170         @Override
171         public void onMessageWaitingIndicatorChanged(boolean mwi) {
172             onMwiChanged(mwi);
173         }
174
175         @Override
176         public void onCallForwardingIndicatorChanged(boolean cfi) {
177             onCfiChanged(cfi);
178         }
179     };
180
181     private void onNewRingingConnection(AsyncResult r) {
182         Connection c = (Connection) r.result;
183         if (DBG) log("onNewRingingConnection()... connection: " + c);
184         PhoneApp app = PhoneApp.getInstance();
185
186         // Incoming calls are totally ignored if the device isn't provisioned yet
187         boolean provisioned = Settings.System.getInt(mPhone.getContext().getContentResolver(),
188                 Settings.System.DEVICE_PROVISIONED, 0) != 0;
189         if (!provisioned) {
190             Log.i(TAG, "CallNotifier: rejecting incoming call because device isn't provisioned");
191             // Send the caller straight to voicemail, just like
192             // "rejecting" an incoming call.
193             PhoneUtils.hangupRingingCall(mPhone);
194             return;
195         }
196
197         if (c != null && c.isRinging()) {
198             Call.State state = c.getState();
199             // State will be either INCOMING or WAITING.
200             if (DBG) log("- connection is ringing!  state = " + state);
201             // if (DBG) PhoneUtils.dumpCallState(mPhone);
202
203             // No need to do any service state checks here (like for
204             // "emergency mode"), since in those states the SIM won't let
205             // us get incoming connections in the first place.
206
207             // TODO: Consider sending out a serialized broadcast Intent here
208             // (maybe "ACTION_NEW_INCOMING_CALL"), *before* starting the
209             // ringer and going to the in-call UI.  The intent should contain
210             // the caller-id info for the current connection, and say whether
211             // it would be a "call waiting" call or a regular ringing call.
212             // If anybody consumed the broadcast, we'd bail out without
213             // ringing or bringing up the in-call UI.
214             //
215             // This would give 3rd party apps a chance to listen for (and
216             // intercept) new ringing connections.  An app could reject the
217             // incoming call by consuming the broadcast and doing nothing, or
218             // it could "pick up" the call (without any action by the user!)
219             // by firing off an ACTION_ANSWER intent.
220             //
221             // We'd need to protect this with a new "intercept incoming calls"
222             // system permission.
223
224             // - don't ring for call waiting connections
225             // - do this before showing the incoming call panel
226             if (state == Call.State.INCOMING) {
227                 PhoneUtils.setAudioControlState(PhoneUtils.AUDIO_RINGING);
228                 startIncomingCallQuery(c);
229             } else {
230                 if (DBG) log("- starting call waiting tone...");
231                 new InCallTonePlayer(InCallTonePlayer.TONE_CALL_WAITING).start();
232                 // The InCallTonePlayer will automatically stop playing (and
233                 // clean itself up) after playing the tone.
234
235                 // TODO: alternatively, consider starting an
236                 // InCallTonePlayer with an "unlimited" tone length, and
237                 // manually stop it later when the ringing call either (a)
238                 // gets answered, or (b) gets disconnected.
239
240                 // in this case, just fall through like before, and call
241                 // PhoneUtils.showIncomingCallUi
242                 PhoneUtils.showIncomingCallUi();
243             }
244         }
245
246         // Obtain the wake lock. Since the keepScreenOn() call tracks the state
247         // of the wake lock, it is ok to make the call here as well as in
248         // InCallScreen.onPhoneStateChanged().
249         if (DBG) log("Holding wake lock on new incoming connection.");
250         mApplication.keepScreenOn(true);
251
252         if (DBG) log("- onNewRingingConnection() done.");
253     }
254
255     /**
256      * Helper method to manage the start of incoming call queries
257      */
258     private void startIncomingCallQuery(Connection c) {
259         // TODO: cache the custom ringer object so that subsequent
260         // calls will not need to do this query work.  We can keep
261         // the MRU ringtones in memory.  We'll still need to hit
262         // the database to get the callerinfo to act as a key,
263         // but at least we can save the time required for the
264         // Media player setup.  The only issue with this is that
265         // we may need to keep an eye on the resources the Media
266         // player uses to keep these ringtones around.
267
268         // make sure we're in a state where we can be ready to
269         // query a ringtone uri.
270         boolean shouldStartQuery = false;
271         synchronized (mCallerInfoQueryStateGuard) {
272             if (mCallerInfoQueryState == CALLERINFO_QUERY_READY) {
273                 mCallerInfoQueryState = CALLERINFO_QUERYING;
274                 shouldStartQuery = true;
275             }
276         }
277         if (shouldStartQuery) {
278             // create a custom ringer using the default ringer first
279             mRinger.setCustomRingtoneUri(Settings.System.DEFAULT_RINGTONE_URI);
280
281             // query the callerinfo to try to get the ringer.
282             PhoneUtils.CallerInfoToken cit = PhoneUtils.startGetCallerInfo(
283                     mPhone.getContext(), c, this, this);
284
285             // if this has already been queried then just ring, otherwise
286             // we wait for the alloted time before ringing.
287             if (cit.isFinal) {
288                 if (DBG) log("callerinfo already up to date, using available data");
289                 onQueryComplete(0, this, cit.currentInfo);
290             } else {
291                 if (DBG) log("starting query and setting timeout message.");
292                 sendEmptyMessageDelayed(RINGER_CUSTOM_RINGTONE_QUERY_COMPLETE,
293                         RINGTONE_QUERY_WAIT_TIME);
294             }
295             // calls to PhoneUtils.showIncomingCallUi will come after the
296             // queries are complete (or timeout).
297         } else {
298             // This should never happen; its the case where an incoming call
299             // arrives at the same time that the query is still being run,
300             // and before the timeout window has closed.
301             Checkin.logEvent(mPhone.getContext().getContentResolver(),
302                     Checkin.Events.Tag.PHONE_UI,
303                     PHONE_UI_EVENT_MULTIPLE_QUERY);
304
305             // In this case, just log the request and ring.
306             if (DBG) log("request to ring arrived while query is running.");
307             mRinger.ring();
308
309             // in this case, just fall through like before, and call
310             // PhoneUtils.showIncomingCallUi
311             PhoneUtils.showIncomingCallUi();
312         }
313     }
314
315     /**
316      * Continuation of the onNewRingingConnection method call,
317      * encompassing ringing and any successive actions.
318      */
319     private void onCustomRingQueryComplete() {
320         boolean isQueryExecutionTimeExpired = false;
321         synchronized (mCallerInfoQueryStateGuard) {
322             if (mCallerInfoQueryState == CALLERINFO_QUERYING) {
323                 mCallerInfoQueryState = CALLERINFO_QUERY_READY;
324                 isQueryExecutionTimeExpired = true;
325             }
326         }
327         if (isQueryExecutionTimeExpired) {
328             // There may be a problem with the query here, since the
329             // default ringtone is playing instead of the custom one.
330             Checkin.logEvent(mPhone.getContext().getContentResolver(),
331                     Checkin.Events.Tag.PHONE_UI,
332                     PHONE_UI_EVENT_RINGER_QUERY_ELAPSED);
333         }
334
335         // ring, either with the queried ringtone or default one.
336         mRinger.ring();
337
338         // now display the UI.
339         PhoneUtils.showIncomingCallUi();
340     }
341
342     private void onUnknownConnectionAppeared(AsyncResult r) {
343         Phone.State state = mPhone.getState();
344
345         if (state == Phone.State.OFFHOOK) {
346             // basically do onPhoneStateChanged + displayCallScreen
347             onPhoneStateChanged(r);
348             PhoneApp app = PhoneApp.getInstance();
349             app.displayCallScreen();
350         }
351     }
352
353     private void onPhoneStateChanged(AsyncResult r) {
354         Phone.State state = mPhone.getState();
355
356         // Turn status bar notifications on or off depending upon the state
357         // of the phone.  Notification Alerts (audible or vibrating) should
358         // be on if and only if the phone is IDLE.
359         NotificationMgr.getDefault().getStatusBarMgr()
360                 .enableNotificationAlerts(state == Phone.State.IDLE);
361
362         if (state == Phone.State.OFFHOOK) {
363             PhoneUtils.setAudioControlState(PhoneUtils.AUDIO_OFFHOOK);
364             if (DBG) log("onPhoneStateChanged: OFF HOOK");
365
366             // if the call screen is showing, let it handle the event,
367             // otherwise handle it here.
368             if (!mApplication.isShowingCallScreen()) {
369                 mApplication.setScreenTimeout(PhoneApp.ScreenTimeoutDuration.DEFAULT);
370                 mApplication.keepScreenOn(false);
371             }
372
373             if (mBluetoothHandsfree != null) {
374                 mBluetoothHandsfree.audioOn();
375             }
376
377             // Since we're now in-call, the Ringer should definitely *not*
378             // be ringing any more.  (This is just a sanity-check; we
379             // already stopped the ringer explicitly back in
380             // PhoneUtils.answerCall(), before the call to phone.acceptCall().)
381             // TODO: Confirm that this call really *is* unnecessary, and if so,
382             // remove it!
383             mRinger.stopRing();
384
385             // put a icon in the status bar
386             NotificationMgr.getDefault().updateInCallNotification();
387         }
388     }
389
390     /**
391      * Implemented for CallerInfoAsyncQuery.OnQueryCompleteListener interface.
392      * refreshes the CallCard data when it called.  If called with this
393      * class itself, it is assumed that we have been waiting for the ringtone
394      * and direct to voicemail settings to update.
395      */
396     public void onQueryComplete(int token, Object cookie, CallerInfo ci){
397         if (cookie instanceof Long) {
398             if (DBG) log("callerinfo query complete, posting missed call notification");
399
400             NotificationMgr.getDefault().notifyMissedCall(ci.name, ci.phoneNumber,
401                     ci.phoneLabel, ((Long) cookie).longValue());
402         } else if (cookie instanceof CallNotifier){
403             if (DBG) log("callerinfo query complete, updating data");
404
405             // get rid of the timeout messages
406             removeMessages(RINGER_CUSTOM_RINGTONE_QUERY_COMPLETE);
407
408             boolean isQueryExecutionTimeOK = false;
409             synchronized (mCallerInfoQueryStateGuard) {
410                 if (mCallerInfoQueryState == CALLERINFO_QUERYING) {
411                     mCallerInfoQueryState = CALLERINFO_QUERY_READY;
412                     isQueryExecutionTimeOK = true;
413                 }
414             }
415             //if we're in the right state
416             if (isQueryExecutionTimeOK) {
417
418                 // send directly to voicemail.
419                 if (ci.shouldSendToVoicemail) {
420                     if (DBG) log("send to voicemail flag detected. hanging up.");
421                     PhoneUtils.hangupRingingCall(mPhone);
422                     return;
423                 }
424
425                 // set the ringtone uri to prepare for the ring.
426                 if (ci.contactRingtoneUri != null) {
427                     if (DBG) log("custom ringtone found, setting up ringer.");
428                     Ringer r = ((CallNotifier) cookie).mRinger;
429                     r.setCustomRingtoneUri(ci.contactRingtoneUri);
430                 }
431                 // ring, and other post-ring actions.
432                 onCustomRingQueryComplete();
433             }
434         }
435     }
436
437     private void onDisconnect(AsyncResult r) {
438         if (DBG) log("onDisconnect()...  phone state: " + mPhone.getState());
439         if (mPhone.getState() == Phone.State.IDLE) {
440             PhoneUtils.setAudioControlState(PhoneUtils.AUDIO_IDLE);
441         }
442
443         Connection c = (Connection) r.result;
444         if (DBG && c != null) {
445             log("- cause = " + c.getDisconnectCause()
446                 + ", incoming = " + c.isIncoming()
447                 + ", date = " + c.getCreateTime());
448         }
449
450         // Stop the ringer if it was ringing (for an incoming call that
451         // either disconnected by itself, or was rejected by the user.)
452         //
453         // TODO: We technically *shouldn't* stop the ringer if the
454         // foreground or background call disconnects while an incoming call
455         // is still ringing, but that's a really rare corner case.
456         // It's safest to just unconditionally stop the ringer here.
457         mRinger.stopRing();
458
459         // Check for the various tones we might need to play (thru the
460         // earpiece) after a call disconnects.
461         int toneToPlay = InCallTonePlayer.TONE_NONE;
462
463         // The "Busy" or "Congestion" tone is the highest priority:
464         if (c != null) {
465             Connection.DisconnectCause cause = c.getDisconnectCause();
466             if (cause == Connection.DisconnectCause.BUSY) {
467                 if (DBG) log("- need to play BUSY tone!");
468                 toneToPlay = InCallTonePlayer.TONE_BUSY;
469             } else if (cause == Connection.DisconnectCause.CONGESTION) {
470                 if (DBG) log("- need to play CONGESTION tone!");
471                 toneToPlay = InCallTonePlayer.TONE_CONGESTION;
472             }
473         }
474
475         // If we don't need to play BUSY or CONGESTION, then play the
476         // "call ended" tone if this was a "regular disconnect" (i.e. a
477         // normal call where one end or the other hung up) *and* this
478         // disconnect event caused the phone to become idle.  (In other
479         // words, we *don't* play the sound if one call hangs up but
480         // there's still an active call on the other line.)
481         // TODO: We may eventually want to disable this via a preference.
482         if ((toneToPlay == InCallTonePlayer.TONE_NONE)
483             && (mPhone.getState() == Phone.State.IDLE)
484             && (c != null)) {
485             Connection.DisconnectCause cause = c.getDisconnectCause();
486             if ((cause == Connection.DisconnectCause.NORMAL)  // remote hangup
487                 || (cause == Connection.DisconnectCause.LOCAL)) {  // local hangup
488                 if (DBG) log("- need to play CALL_ENDED tone!");
489                 toneToPlay = InCallTonePlayer.TONE_CALL_ENDED;
490             }
491         }
492
493         if (mPhone.getState() == Phone.State.IDLE) {
494             // Don't reset the audio mode or bluetooth/speakerphone state
495             // if we still need to let the user hear a tone through the earpiece.
496             if (toneToPlay == InCallTonePlayer.TONE_NONE) {
497                 resetAudioStateAfterDisconnect();
498             }
499
500             NotificationMgr.getDefault().cancelCallInProgressNotification();
501
502             // if the call screen never called finish (it's still in activity
503             // history, call finish() on it so it can be removed from the
504             // activity history
505             if (mCallScreen != null) {
506                 if (DBG) log("onDisconnect: force mCallScreen.finish()");
507                 mCallScreen.finish();
508             }
509         }
510
511         if (c != null) {
512             String number = c.getAddress();
513             boolean isPrivateNumber = false; // TODO: need API for isPrivate()
514             long date = c.getCreateTime();
515             long duration = c.getDurationMillis();
516             Connection.DisconnectCause cause = c.getDisconnectCause();
517
518             // Set the "type" to be displayed in the call log (see constants in CallLog.Calls)
519             int callLogType;
520             if (c.isIncoming()) {
521                 callLogType = (cause == Connection.DisconnectCause.INCOMING_MISSED ?
522                                CallLog.Calls.MISSED_TYPE :
523                                CallLog.Calls.INCOMING_TYPE);
524             } else {
525                 callLogType = CallLog.Calls.OUTGOING_TYPE;
526             }
527             if (DBG) log("- callLogType: " + callLogType + ", UserData: " + c.getUserData());
528
529             // get the callerinfo object and then log the call with it.
530             {
531                 Object o = c.getUserData();
532                 CallerInfo ci;
533                 if ((o == null) || (o instanceof CallerInfo)){
534                     ci = (CallerInfo) o;
535                 } else {
536                     ci = ((PhoneUtils.CallerInfoToken) o).currentInfo;
537                 }
538                 Calls.addCall(ci, mApplication, number, isPrivateNumber,
539                         callLogType, date, (int) duration / 1000);
540             }
541
542             if (callLogType == CallLog.Calls.MISSED_TYPE) {
543                 // Show the "Missed call" notification.
544                 // (Note we *don't* do this if this was an incoming call that
545                 // the user deliberately rejected.)
546
547                 PhoneUtils.CallerInfoToken info =
548                     PhoneUtils.startGetCallerInfo(mApplication, c, this, new Long(date));
549                 if (info != null) {
550                     // at this point, we've requested to start a query, but it makes no
551                     // sense to log this missed call until the query comes back.
552                     if (DBG) log("onDisconnect: Querying for CallerInfo on missed call...");
553                     if (info.isFinal) {
554                         // it seems that the query we have actually is up to date.
555                         // send the notification then.
556                         CallerInfo ci = info.currentInfo;
557                         NotificationMgr.getDefault().notifyMissedCall(ci.name, ci.phoneNumber,
558                                 ci.phoneLabel, date);
559                     }
560                 } else {
561                     // getCallerInfo() can return null in rare cases, like if we weren't
562                     // able to get a valid phone number out of the specified Connection.
563                     Log.w(TAG, "onDisconnect: got null CallerInfo for Connection " + c);
564                 }
565             }
566
567             // Possibly play a "post-disconnect tone" thru the earpiece.
568             // We do this here, rather than from the InCallScreen
569             // activity, since we need to do this even if you're not in
570             // the Phone UI at the moment the connection ends.
571             if (toneToPlay != InCallTonePlayer.TONE_NONE) {
572                 if (DBG) log("- starting post-disconnect tone (" + toneToPlay + ")...");
573                 new InCallTonePlayer(toneToPlay).start();
574                 // The InCallTonePlayer will automatically stop playing (and
575                 // clean itself up) after a few seconds.
576
577                 // TODO: alternatively, we could start an InCallTonePlayer
578                 // here with an "unlimited" tone length,
579                 // and manually stop it later when this connection truly goes
580                 // away.  (The real connection over the network was closed as soon
581                 // as we got the BUSY message.  But our telephony layer keeps the
582                 // connection open for a few extra seconds so we can show the
583                 // "busy" indication to the user.  We could stop the busy tone
584                 // when *that* connection's "disconnect" event comes in.)
585             }
586
587             if (mPhone.getState() == Phone.State.IDLE) {
588                 // Release screen wake locks if the in-call screen is not
589                 // showing. Otherwise, let the in-call screen handle this because
590                 // it needs to show the call ended screen for a couple of
591                 // seconds.
592                 if (!mApplication.isShowingCallScreen()) {
593                     mApplication.setScreenTimeout(PhoneApp.ScreenTimeoutDuration.DEFAULT);
594                     mApplication.keepScreenOn(false);
595                 }
596             }
597         }
598     }
599
600     /**
601      * Resets the audio mode and speaker state when a call ends.
602      */
603     private void resetAudioStateAfterDisconnect() {
604         if (DBG) log("resetAudioStateAfterDisconnect()...");
605
606         if (mBluetoothHandsfree != null) {
607             mBluetoothHandsfree.audioOff();
608         }
609
610         if (PhoneUtils.isSpeakerOn(mPhone.getContext())) {
611             PhoneUtils.turnOnSpeaker(mPhone.getContext(), false);
612         }
613
614         PhoneUtils.setAudioMode(mPhone.getContext(), AudioManager.MODE_NORMAL);
615     }
616
617     private void onMwiChanged(boolean visible) {
618         if (DBG) log("onMwiChanged(): " + visible);
619         NotificationMgr.getDefault().updateMwi(visible);
620     }
621
622     /**
623      * Posts a delayed PHONE_MWI_CHANGED event, to schedule a "retry" for a
624      * failed NotificationMgr.updateMwi() call.
625      */
626     /* package */ void sendMwiChangedDelayed(long delayMillis) {
627         Message message = Message.obtain(this, PHONE_MWI_CHANGED);
628         sendMessageDelayed(message, delayMillis);
629     }
630
631     private void onCfiChanged(boolean visible) {
632         if (DBG) log("onCfiChanged(): " + visible);
633         NotificationMgr.getDefault().updateCfi(visible);
634     }
635
636     void setCallScreen(InCallScreen callScreen) {
637         if (DBG) log("setCallScreen: " + callScreen);
638         mCallScreen = callScreen;
639     }
640
641     /**
642      * Indicates whether or not this ringer is ringing.
643      */
644     boolean isRinging() {
645         return mRinger.isRinging();
646     }
647
648     /**
649      * Stops the current ring, and tells the notifier that future
650      * ring requests should be ignored.
651      */
652     void silenceRinger() {
653         mSilentRingerRequested = true;
654         mRinger.stopRing();
655     }
656
657     /**
658      * Posts a PHONE_BATTERY_LOW event, causing us to play a warning
659      * tone if the user is in-call.
660      */
661     /* package */ void sendBatteryLow() {
662         Message message = Message.obtain(this, PHONE_BATTERY_LOW);
663         sendMessage(message);
664     }
665
666     private void onBatteryLow() {
667         if (DBG) log("onBatteryLow()...");
668
669         // Play the "low battery" warning tone, only if the user is
670         // in-call.  (The test here is exactly the opposite of the test in
671         // StatusBarPolicy.updateBattery(), where we bring up the "low
672         // battery warning" dialog only if the user is NOT in-call.)
673         if (mPhone.getState() != Phone.State.IDLE) {
674             new InCallTonePlayer(InCallTonePlayer.TONE_BATTERY_LOW).start();
675         }
676     }
677
678
679     /**
680      * Helper class to play tones through the earpiece (or speaker / BT)
681      * during a call, using the ToneGenerator.
682      *
683      * To use, just instantiate a new InCallTonePlayer
684      * (passing in the TONE_* constant for the tone you want)
685      * and start() it.
686      *
687      * When we're done playing the tone, if the phone is idle at that
688      * point, we'll reset the audio routing and speaker state.
689      * (That means that for tones that get played *after* a call
690      * disconnects, like "busy" or "congestion" or "call ended", you
691      * should NOT call resetAudioStateAfterDisconnect() yourself.
692      * Instead, just start the InCallTonePlayer, which will automatically
693      * defer the resetAudioStateAfterDisconnect() call until the tone
694      * finishes playing.)
695      */
696     private class InCallTonePlayer extends Thread {
697         private int mToneId;
698
699         // The possible tones we can play.
700         public static final int TONE_NONE = 0;
701         public static final int TONE_CALL_WAITING = 1;
702         public static final int TONE_BUSY = 2;
703         public static final int TONE_CONGESTION = 3;
704         public static final int TONE_BATTERY_LOW = 4;
705         public static final int TONE_CALL_ENDED = 5;
706
707         // The tone volume relative to other sounds in the stream
708         private static final int TONE_RELATIVE_VOLUME_HIPRI = 80;
709         private static final int TONE_RELATIVE_VOLUME_LOPRI = 50;
710
711         InCallTonePlayer(int toneId) {
712             super();
713             mToneId = toneId;
714         }
715
716         @Override
717         public void run() {
718             if (DBG) log("InCallTonePlayer.run(toneId = " + mToneId + ")...");
719
720             int toneType;  // passed to ToneGenerator.startTone()
721             int toneVolume;  // passed to the ToneGenerator constructor
722             int toneLengthMillis;
723             switch (mToneId) {
724                 case TONE_CALL_WAITING:
725                     toneType = ToneGenerator.TONE_SUP_CALL_WAITING;
726                     toneVolume = TONE_RELATIVE_VOLUME_HIPRI;
727                     toneLengthMillis = 5000;
728                     break;
729                 case TONE_BUSY:
730                     toneType = ToneGenerator.TONE_SUP_BUSY;
731                     toneVolume = TONE_RELATIVE_VOLUME_HIPRI;
732                     toneLengthMillis = 4000;
733                     break;
734                 case TONE_CONGESTION:
735                     toneType = ToneGenerator.TONE_SUP_CONGESTION;
736                     toneVolume = TONE_RELATIVE_VOLUME_HIPRI;
737                     toneLengthMillis = 4000;
738                     break;
739                 case TONE_BATTERY_LOW:
740                     // For now, use ToneGenerator.TONE_PROP_ACK (two quick
741                     // beeps).  TODO: is there some other ToneGenerator
742                     // tone that would be more appropriate here?  Or
743                     // should we consider adding a new custom tone?
744                     toneType = ToneGenerator.TONE_PROP_ACK;
745                     toneVolume = TONE_RELATIVE_VOLUME_HIPRI;
746                     toneLengthMillis = 1000;
747                     break;
748                 case TONE_CALL_ENDED:
749                     toneType = ToneGenerator.TONE_PROP_PROMPT;
750                     toneVolume = TONE_RELATIVE_VOLUME_LOPRI;
751                     toneLengthMillis = 2000;
752                     break;
753                 default:
754                     throw new IllegalArgumentException("Bad toneId: " + mToneId);
755             }
756
757             // If the mToneGenerator creation fails, just continue without it.  It is
758             // a local audio signal, and is not as important.
759             ToneGenerator toneGenerator;
760             try {
761                 toneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, toneVolume);
762                 // if (DBG) log("- created toneGenerator: " + toneGenerator);
763             } catch (RuntimeException e) {
764                 Log.w(TAG, "InCallTonePlayer: Exception caught while creating ToneGenerator: " + e);
765                 toneGenerator = null;
766             }
767
768             // Using the ToneGenerator (with the CALL_WAITING / BUSY /
769             // CONGESTION tones at least), the ToneGenerator itself knows
770             // the right pattern of tones to play; we do NOT need to
771             // manually start/stop each individual tone, or manually
772             // insert the correct delay between tones.  (We just start it
773             // and let it run for however long we want the tone pattern to
774             // continue.)
775             //
776             // TODO: When we stop the ToneGenerator in the middle of a
777             // "tone pattern", it sounds bad if we cut if off while the
778             // tone is actually playing.  Consider adding API to the
779             // ToneGenerator to say "stop at the next silent part of the
780             // pattern", or simply "play the pattern N times and then
781             // stop."
782
783             if (toneGenerator != null) {
784                 toneGenerator.startTone(toneType);
785                 SystemClock.sleep(toneLengthMillis);
786                 toneGenerator.stopTone();
787
788                 // if (DBG) log("- InCallTonePlayer: done playing.");
789                 toneGenerator.release();
790             }
791
792             // Finally, do the same cleanup we otherwise would have done
793             // in onDisconnect().
794             //
795             // (But watch out: do NOT do this if the phone is in use,
796             // since some of our tones get played *during* a call (like
797             // CALL_WAITING and BATTERY_LOW) and we definitely *don't*
798             // want to reset the audio mode / speaker / bluetooth after
799             // playing those!
800             // This call is really here for use with tones that get played
801             // *after* a call disconnects, like "busy" or "congestion" or
802             // "call ended", where the phone has already become idle but
803             // we need to defer the resetAudioStateAfterDisconnect() call
804             // till the tone finishes playing.)
805             if (mPhone.getState() == Phone.State.IDLE) {
806                 resetAudioStateAfterDisconnect();
807             }
808         }
809     }
810
811
812     private void log(String msg) {
813         Log.d(TAG, "[CallNotifier] " + msg);
814     }
815 }