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