2 * Copyright (C) 2006 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.phone;
19 import android.app.Notification;
20 import android.app.NotificationManager;
21 import android.app.PendingIntent;
22 import android.app.StatusBarManager;
23 import android.content.AsyncQueryHandler;
24 import android.content.ContentResolver;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.database.Cursor;
28 import android.media.AudioManager;
29 import android.net.Uri;
30 import android.os.Handler;
31 import android.os.IBinder;
32 import android.os.Message;
33 import android.os.SystemClock;
34 import android.provider.CallLog.Calls;
35 import android.provider.Contacts.Phones;
36 import android.telephony.PhoneNumberUtils;
37 import android.text.TextUtils;
38 import android.util.Log;
39 import android.widget.RemoteViews;
40 import android.widget.Toast;
42 import com.android.internal.R.drawable;
43 import com.android.internal.telephony.Call;
44 import com.android.internal.telephony.CallerInfo;
45 import com.android.internal.telephony.CallerInfoAsyncQuery;
46 import com.android.internal.telephony.Connection;
47 import com.android.internal.telephony.Phone;
51 * NotificationManager-related utility code for the Phone app.
53 public class NotificationMgr implements CallerInfoAsyncQuery.OnQueryCompleteListener{
54 private static final String LOG_TAG = PhoneApp.LOG_TAG;
55 private static final boolean DBG = false;
56 private static final int EVENT_ENHANCED_VP_ON = 1;
57 private static final int EVENT_ENHANCED_VP_OFF = 2;
59 // **Callback for enhanced voice privacy return value
60 private Handler mEnhancedVPHandler = new Handler() {
61 boolean enhancedVoicePrivacy = false;
63 public void handleMessage(Message msg) {
65 case EVENT_ENHANCED_VP_ON:
66 enhancedVoicePrivacy = true;
68 case EVENT_ENHANCED_VP_OFF:
69 enhancedVoicePrivacy = false;
72 // We should never reach this
74 updateInCallNotification(enhancedVoicePrivacy);
78 private static final String[] CALL_LOG_PROJECTION = new String[] {
87 static final int MISSED_CALL_NOTIFICATION = 1;
88 static final int IN_CALL_NOTIFICATION = 2;
89 static final int MMI_NOTIFICATION = 3;
90 static final int NETWORK_SELECTION_NOTIFICATION = 4;
91 static final int VOICEMAIL_NOTIFICATION = 5;
92 static final int CALL_FORWARD_NOTIFICATION = 6;
93 static final int DATA_DISCONNECTED_ROAMING_NOTIFICATION = 7;
94 static final int ECBM_NOTIFICATION = 8;
96 private static NotificationMgr sMe = null;
99 private Context mContext;
100 private NotificationManager mNotificationMgr;
101 private StatusBarManager mStatusBar;
102 private StatusBarMgr mStatusBarMgr;
103 private Toast mToast;
104 private IBinder mSpeakerphoneIcon;
105 private IBinder mMuteIcon;
107 // used to track the missed call counter, default to 0.
108 private int mNumberMissedCalls = 0;
110 // Currently-displayed resource IDs for some status bar icons (or zero
111 // if no notification is active):
112 private int mInCallResId;
114 // Retry params for the getVoiceMailNumber() call; see updateMwi().
115 private static final int MAX_VM_NUMBER_RETRIES = 5;
116 private static final int VM_NUMBER_RETRY_DELAY_MILLIS = 10000;
117 private int mVmNumberRetriesRemaining = MAX_VM_NUMBER_RETRIES;
119 // Query used to look up caller-id info for the "call log" notification.
120 private QueryHandler mQueryHandler = null;
121 private static final int CALL_LOG_TOKEN = -1;
122 private static final int CONTACT_TOKEN = -2;
124 NotificationMgr(Context context) {
126 mNotificationMgr = (NotificationManager)
127 context.getSystemService(Context.NOTIFICATION_SERVICE);
129 mStatusBar = (StatusBarManager) context.getSystemService(Context.STATUS_BAR_SERVICE);
131 PhoneApp app = PhoneApp.getInstance();
133 mPhone.registerForInCallVoicePrivacyOn(mEnhancedVPHandler, EVENT_ENHANCED_VP_ON, null);
134 mPhone.registerForInCallVoicePrivacyOff(mEnhancedVPHandler, EVENT_ENHANCED_VP_OFF, null);
137 static void init(Context context) {
138 sMe = new NotificationMgr(context);
140 // update the notifications that need to be touched at startup.
141 sMe.updateNotifications();
144 static NotificationMgr getDefault() {
149 * Class that controls the status bar. This class maintains a set
150 * of state and acts as an interface between the Phone process and
151 * the Status bar. All interaction with the status bar should be
152 * though the methods contained herein.
158 StatusBarMgr getStatusBarMgr() {
159 if (mStatusBarMgr == null) {
160 mStatusBarMgr = new StatusBarMgr();
162 return mStatusBarMgr;
166 * StatusBarMgr implementation
170 private boolean mIsNotificationEnabled = true;
171 private boolean mIsExpandedViewEnabled = true;
173 private StatusBarMgr () {
177 * Sets the notification state (enable / disable
178 * vibrating notifications) for the status bar,
179 * updates the status bar service if there is a change.
180 * Independent of the remaining Status Bar
181 * functionality, including icons and expanded view.
183 void enableNotificationAlerts(boolean enable) {
184 if (mIsNotificationEnabled != enable) {
185 mIsNotificationEnabled = enable;
191 * Sets the ability to expand the notifications for the
192 * status bar, updates the status bar service if there
193 * is a change. Independent of the remaining Status Bar
194 * functionality, including icons and notification
197 void enableExpandedView(boolean enable) {
198 if (mIsExpandedViewEnabled != enable) {
199 mIsExpandedViewEnabled = enable;
205 * Method to synchronize status bar state with our current
208 void updateStatusBar() {
209 int state = StatusBarManager.DISABLE_NONE;
211 if (!mIsExpandedViewEnabled) {
212 state |= StatusBarManager.DISABLE_EXPAND;
215 if (!mIsNotificationEnabled) {
216 state |= StatusBarManager.DISABLE_NOTIFICATION_ALERTS;
219 // send the message to the status bar manager.
220 if (DBG) log("updating status bar state: " + state);
221 mStatusBar.disable(state);
226 * Makes sure notifications are up to date.
228 void updateNotifications() {
229 if (DBG) log("begin querying call log");
231 // instantiate query handler
232 mQueryHandler = new QueryHandler(mContext.getContentResolver());
234 // setup query spec, look for all Missed calls that are new.
235 StringBuilder where = new StringBuilder("type=");
236 where.append(Calls.MISSED_TYPE);
237 where.append(" AND new=1");
240 mQueryHandler.startQuery(CALL_LOG_TOKEN, null, Calls.CONTENT_URI, CALL_LOG_PROJECTION,
241 where.toString(), null, Calls.DEFAULT_SORT_ORDER);
243 // synchronize the in call notification
244 if (mPhone.getState() != Phone.State.OFFHOOK) {
245 if (DBG) log("Phone is idle, canceling notification.");
248 if (DBG) log("Phone is offhook, updating notification.");
249 updateInCallNotification();
252 // Depend on android.app.StatusBarManager to be set to
253 // disable(DISABLE_NONE) upon startup. This will be the
254 // case even if the phone app crashes.
257 /** The projection to use when querying the phones table */
258 static final String[] PHONES_PROJECTION = new String[] {
264 * Class used to run asynchronous queries to re-populate
265 * the notifications we care about.
267 private class QueryHandler extends AsyncQueryHandler {
270 * Used to store relevant fields for the Missed Call
273 private class NotificationInfo {
275 public String number;
280 public QueryHandler(ContentResolver cr) {
285 * Handles the query results. There are really 2 steps to this,
286 * similar to what happens in RecentCallsListActivity.
287 * 1. Find the list of missed calls
288 * 2. For each call, run a query to retrieve the caller's name.
291 protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
292 // TODO: it would be faster to use a join here, but for the purposes
293 // of this small record set, it should be ok.
295 // Note that CursorJoiner is not useable here because the number
296 // comparisons are not strictly equals; the comparisons happen in
297 // the SQL function PHONE_NUMBERS_EQUAL, which is not available for
300 // Executing our own query is also feasible (with a join), but that
301 // will require some work (possibly destabilizing) in Contacts
304 // At this point, we will execute subqueries on each row just as
305 // RecentCallsListActivity.java does.
308 if (DBG) log("call log query complete.");
310 // initial call to retrieve the call list.
311 if (cursor != null) {
312 while (cursor.moveToNext()) {
313 // for each call in the call log list, create
314 // the notification object and query contacts
315 NotificationInfo n = getNotificationInfo (cursor);
317 if (DBG) log("query contacts for number: " + n.number);
319 mQueryHandler.startQuery(CONTACT_TOKEN, n,
320 Uri.withAppendedPath(Phones.CONTENT_FILTER_URL, n.number),
321 PHONES_PROJECTION, null, null, Phones.DEFAULT_SORT_ORDER);
324 if (DBG) log("closing call log cursor.");
329 if (DBG) log("contact query complete.");
331 // subqueries to get the caller name.
332 if ((cursor != null) && (cookie != null)){
333 NotificationInfo n = (NotificationInfo) cookie;
335 if (cursor.moveToFirst()) {
336 // we have contacts data, get the name.
337 if (DBG) log("contact :" + n.name + " found for phone: " + n.number);
338 n.name = cursor.getString(cursor.getColumnIndexOrThrow(Phones.NAME));
341 // send the notification
342 if (DBG) log("sending notification.");
343 notifyMissedCall(n.name, n.number, n.label, n.date);
345 if (DBG) log("closing contact cursor.");
354 * Factory method to generate a NotificationInfo object given a
355 * cursor from the call log table.
357 private final NotificationInfo getNotificationInfo(Cursor cursor) {
358 NotificationInfo n = new NotificationInfo();
360 n.number = cursor.getString(cursor.getColumnIndexOrThrow(Calls.NUMBER));
361 n.label = cursor.getString(cursor.getColumnIndexOrThrow(Calls.TYPE));
362 n.date = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.DATE));
364 // make sure we update the number depending upon saved values in
365 // CallLog.addCall(). If either special values for unknown or
366 // private number are detected, we need to hand off the message
367 // to the missed call notification.
368 if ( (n.number.equals(CallerInfo.UNKNOWN_NUMBER)) ||
369 (n.number.equals(CallerInfo.PRIVATE_NUMBER)) ||
370 (n.number.equals(CallerInfo.PAYPHONE_NUMBER)) ) {
374 if (DBG) log("NotificationInfo constructed for number: " + n.number);
381 * Displays a notification about a missed call.
383 * @param nameOrNumber either the contact name, or the phone number if no contact
384 * @param label the label of the number if nameOrNumber is a name, null if it is a number
386 void notifyMissedCall(String name, String number, String label, long date) {
389 // the text in the notification's line 1 and 2.
390 String expandedText, callName;
392 // increment number of missed calls.
393 mNumberMissedCalls++;
395 // get the name for the ticker text
396 // i.e. "Missed call from <caller name or number>"
397 if (name != null && TextUtils.isGraphic(name)) {
399 } else if (!TextUtils.isEmpty(number)){
402 // use "unknown" if the caller is unidentifiable.
403 callName = mContext.getString(R.string.unknown);
406 // display the first line of the notification:
407 // 1 missed call: call name
408 // more than 1 missed call: <number of calls> + "missed calls"
409 if (mNumberMissedCalls == 1) {
410 titleResId = R.string.notification_missedCallTitle;
411 expandedText = callName;
413 titleResId = R.string.notification_missedCallsTitle;
414 expandedText = mContext.getString(R.string.notification_missedCallsMsg,
418 // create the target call log intent
419 final Intent intent = PhoneApp.createCallLogIntent();
421 // make the notification
422 mNotificationMgr.notify(
423 MISSED_CALL_NOTIFICATION,
426 android.R.drawable.stat_notify_missed_call, // icon
428 R.string.notification_missedCallTicker, callName), // tickerText
430 mContext.getText(titleResId), // expandedTitle
431 expandedText, // expandedText
432 intent // contentIntent
436 void cancelMissedCallNotification() {
437 // reset the number of missed calls to 0.
438 mNumberMissedCalls = 0;
439 mNotificationMgr.cancel(MISSED_CALL_NOTIFICATION);
443 * Displays a notification for Emergency Callback Mode.
445 * @param nameOrNumber either the contact name, or the phone number if no contact
446 * @param label the label of the number if nameOrNumber is a name, null if it is a number
449 // The details of our message
450 CharSequence message = "Emergency Callback Mode is active";
452 // look up the notification manager service
453 mContext.getSystemService(Context.NOTIFICATION_SERVICE);
455 // The PendingIntent to launch our activity if the user selects this notification
456 Intent EmcbAlarm = new Intent(Intent.ACTION_MAIN, null);
457 EmcbAlarm.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
458 EmcbAlarm.setClassName("com.android.phone", EmergencyCallbackMode.class.getName());
460 PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0,
463 // The ticker text, this uses a formatted string so our message could be localized
464 String tickerText = mContext.getString(R.string.ecbm_mode_text, message);
466 // construct the Notification object.
467 Notification ecbmNotif = new Notification(com.android.internal.R.drawable.stat_ecb_mode,
468 tickerText, System.currentTimeMillis());
470 // Set the info for the views that show in the notification panel.
471 ecbmNotif.setLatestEventInfo(mContext, null, message, contentIntent);
473 // Note that we use R.layout.incoming_message_panel as the ID for
474 // the notification. It could be any integer you want, but we use
475 // the convention of using a resource id for a string related to
476 // the notification. It will always be a unique number within your
478 mNotificationMgr.notify(ECBM_NOTIFICATION, ecbmNotif);
481 void cancelEcbmNotification() {
482 mNotificationMgr.cancel(ECBM_NOTIFICATION);
485 void notifySpeakerphone() {
486 if (mSpeakerphoneIcon == null) {
487 mSpeakerphoneIcon = mStatusBar.addIcon("speakerphone",
488 android.R.drawable.stat_sys_speakerphone, 0);
492 void cancelSpeakerphone() {
493 if (mSpeakerphoneIcon != null) {
494 mStatusBar.removeIcon(mSpeakerphoneIcon);
495 mSpeakerphoneIcon = null;
500 * Calls either notifySpeakerphone() or cancelSpeakerphone() based on
501 * the actual current state of the speaker.
503 void updateSpeakerNotification() {
504 AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
506 if ((mPhone.getState() == Phone.State.OFFHOOK) && audioManager.isSpeakerphoneOn()) {
507 if (DBG) log("updateSpeakerNotification: speaker ON");
508 notifySpeakerphone();
510 if (DBG) log("updateSpeakerNotification: speaker OFF (or not offhook)");
511 cancelSpeakerphone();
516 if (mMuteIcon == null) {
517 mMuteIcon = mStatusBar.addIcon("mute", android.R.drawable.stat_notify_call_mute, 0);
522 if (mMuteIcon != null) {
523 mStatusBar.removeIcon(mMuteIcon);
529 * Calls either notifyMute() or cancelMute() based on
530 * the actual current mute state of the Phone.
532 void updateMuteNotification() {
533 if ((mPhone.getState() == Phone.State.OFFHOOK) && mPhone.getMute()) {
534 if (DBG) log("updateMuteNotification: MUTED");
537 if (DBG) log("updateMuteNotification: not muted (or not offhook)");
542 private void updateInCallNotification(boolean enhancedVoicePrivacy) {
543 // WINK:TODO: Teleca, what is the correct code here.
545 if (DBG) log("updateInCallNotification()...");
547 if (mPhone.getState() != Phone.State.OFFHOOK) {
551 final boolean hasActiveCall = !mPhone.getForegroundCall().isIdle();
552 final boolean hasHoldingCall = !mPhone.getBackgroundCall().isIdle();
554 // Display the appropriate "in-call" icon in the status bar,
555 // which depends on the current phone and/or bluetooth state.
558 if (!hasActiveCall && hasHoldingCall) {
559 // There's only one call, and it's on hold.
560 if (enhancedVoicePrivacy) {
561 resId = android.R.drawable.stat_sys_vp_phone_call_on_hold;
563 resId = android.R.drawable.stat_sys_phone_call_on_hold;
565 } else if (PhoneApp.getInstance().showBluetoothIndication()) {
566 // Bluetooth is active.
567 resId = com.android.internal.R.drawable.stat_sys_phone_call_bluetooth;
568 } else if (enhancedVoicePrivacy) {
569 resId = android.R.drawable.stat_sys_vp_phone_call;
571 resId = android.R.drawable.stat_sys_phone_call;
574 // Note we can't just bail out now if (resId == mInCallResId),
575 // since even if the status icon hasn't changed, some *other*
576 // notification-related info may be different from the last time
577 // we were here (like the caller-id info of the foreground call,
578 // if the user swapped calls...)
580 if (DBG) log("- Updating status bar icon: " + resId);
581 mInCallResId = resId;
583 // Even if both lines are in use, we only show a single item in
584 // the expanded Notifications UI. It's labeled "Ongoing call"
585 // (or "On hold" if there's only one call, and it's on hold.)
587 // The icon in the expanded view is the same as in the status bar.
588 int expandedViewIcon = mInCallResId;
590 // Also, we don't have room to display caller-id info from two
591 // different calls. So if there's only one call, use that, but if
592 // both lines are in use we display the caller-id info from the
593 // foreground call and totally ignore the background call.
594 Call currentCall = hasActiveCall ? mPhone.getForegroundCall()
595 : mPhone.getBackgroundCall();
596 Connection currentConn = currentCall.getEarliestConnection();
598 // When expanded, the "Ongoing call" notification is (visually)
599 // different from most other Notifications, so we need to use a
600 // custom view hierarchy.
602 Notification notification = new Notification();
603 notification.icon = mInCallResId;
604 notification.contentIntent = PendingIntent.getActivity(mContext, 0,
605 PhoneApp.createInCallIntent(), 0);
606 notification.flags |= Notification.FLAG_ONGOING_EVENT;
608 // Our custom view, which includes an icon (either "ongoing call" or
609 // "on hold") and 2 lines of text: (1) the label (either "ongoing
610 // call" with time counter, or "on hold), and (2) the compact name of
611 // the current Connection.
612 RemoteViews contentView = new RemoteViews(mContext.getPackageName(),
613 R.layout.ongoing_call_notification);
614 contentView.setImageViewResource(R.id.icon, expandedViewIcon);
616 // if the connection is valid, then build what we need for the
617 // first line of notification information, and start the chronometer.
618 // Otherwise, don't bother and just stick with line 2.
619 if (currentConn != null) {
620 // Determine the "start time" of the current connection, in terms
621 // of the SystemClock.elapsedRealtime() timebase (which is what
622 // the Chronometer widget needs.)
623 // We can't use currentConn.getConnectTime(), because (1) that's
624 // in the currentTimeMillis() time base, and (2) it's zero when
625 // the phone first goes off hook, since the getConnectTime counter
626 // doesn't start until the DIALING -> ACTIVE transition.
627 // Instead we start with the current connection's duration,
628 // and translate that into the elapsedRealtime() timebase.
629 long callDurationMsec = currentConn.getDurationMillis();
630 long chronometerBaseTime = SystemClock.elapsedRealtime() - callDurationMsec;
632 // Line 1 of the expanded view (in bold text):
633 String expandedViewLine1;
634 if (hasHoldingCall && !hasActiveCall) {
635 // Only one call, and it's on hold!
636 // Note this isn't a format string! (We want "On hold" here,
637 // not "On hold (1:23)".) That's OK; if you call
638 // String.format() with more arguments than format specifiers,
639 // the extra arguments are ignored.
640 expandedViewLine1 = mContext.getString(R.string.notification_on_hold);
642 // Format string with a "%s" where the current call time should go.
643 expandedViewLine1 = mContext.getString(R.string.notification_ongoing_call_format);
646 if (DBG) log("- Updating expanded view: line 1 '" + expandedViewLine1 + "'");
648 // Text line #1 is actually a Chronometer, not a plain TextView.
649 // We format the elapsed time of the current call into a line like
650 // "Ongoing call (01:23)".
651 contentView.setChronometer(R.id.text1,
656 log("updateInCallNotification: connection is null, call status not updated.");
659 // display conference call string if this call is a conference
660 // call, otherwise display the connection information.
662 // TODO: it may not make sense for every point to make separate
663 // checks for isConferenceCall, so we need to think about
664 // possibly including this in startGetCallerInfo or some other
666 String expandedViewLine2 = "";
667 if (PhoneUtils.isConferenceCall(currentCall)) {
668 // if this is a conference call, just use that as the caller name.
669 expandedViewLine2 = mContext.getString(R.string.card_title_conf_call);
671 // Start asynchronous call to get the compact name.
672 PhoneUtils.CallerInfoToken cit =
673 PhoneUtils.startGetCallerInfo (mContext, currentCall, this, contentView);
674 // Line 2 of the expanded view (smaller text):
675 expandedViewLine2 = PhoneUtils.getCompactNameFromCallerInfo(cit.currentInfo, mContext);
678 if (DBG) log("- Updating expanded view: line 2 '" + expandedViewLine2 + "'");
679 contentView.setTextViewText(R.id.text2, expandedViewLine2);
680 notification.contentView = contentView;
682 // TODO: We also need to *update* this notification in some cases,
683 // like when a call ends on one line but the other is still in use
684 // (ie. make sure the caller info here corresponds to the active
685 // line), and maybe even when the user swaps calls (ie. if we only
686 // show info here for the "current active call".)
688 if (DBG) log("Notifying IN_CALL_NOTIFICATION: " + notification);
689 mNotificationMgr.notify(IN_CALL_NOTIFICATION,
692 // Finally, refresh the mute and speakerphone notifications (since
693 // some phone state changes can indirectly affect the mute and/or
695 updateSpeakerNotification();
696 updateMuteNotification();
699 void updateInCallNotification() {
700 updateInCallNotification(false);
704 * Implemented for CallerInfoAsyncQuery.OnQueryCompleteListener interface.
705 * refreshes the contentView when called.
707 public void onQueryComplete(int token, Object cookie, CallerInfo ci){
708 if (DBG) log("callerinfo query complete, updating ui.");
710 ((RemoteViews) cookie).setTextViewText(R.id.text2,
711 PhoneUtils.getCompactNameFromCallerInfo(ci, mContext));
714 private void cancelInCall() {
715 if (DBG) log("cancelInCall()...");
717 cancelSpeakerphone();
718 mNotificationMgr.cancel(IN_CALL_NOTIFICATION);
722 void cancelCallInProgressNotification() {
723 if (DBG) log("cancelCallInProgressNotification()...");
724 if (mInCallResId == 0) {
728 if (DBG) log("cancelCallInProgressNotification: " + mInCallResId);
733 * Updates the message waiting indicator (voicemail) notification.
735 * @param visible true if there are messages waiting
737 /* package */ void updateMwi(boolean visible) {
738 if (DBG) log("updateMwi(): " + visible);
740 int resId = android.R.drawable.stat_notify_voicemail;
742 // This Notification can get a lot fancier once we have more
743 // information about the current voicemail messages.
744 // (For example, the current voicemail system can't tell
745 // us the caller-id or timestamp of a message, or tell us the
748 // But for now, the UI is ultra-simple: if the MWI indication
749 // is supposed to be visible, just show a single generic
752 String notificationTitle = mContext.getString(R.string.notification_voicemail_title);
753 String vmNumber = mPhone.getVoiceMailNumber();
754 if (DBG) log("- got vm number: '" + vmNumber + "'");
756 // Watch out: vmNumber may be null, for two possible reasons:
758 // (1) This phone really has no voicemail number
760 // (2) This phone *does* have a voicemail number, but
761 // the SIM isn't ready yet.
763 // Case (2) *does* happen in practice if you have voicemail
764 // messages when the device first boots: we get an MWI
765 // notification as soon as we register on the network, but the
766 // SIM hasn't finished loading yet.
768 // So handle case (2) by retrying the lookup after a short
771 if ((vmNumber == null) && !mPhone.getIccRecordsLoaded()) {
772 if (DBG) log("- Null vm number: SIM records not loaded (yet)...");
774 // TODO: rather than retrying after an arbitrary delay, it
775 // would be cleaner to instead just wait for a
776 // SIM_RECORDS_LOADED notification.
777 // (Unfortunately right now there's no convenient way to
778 // get that notification in phone app code. We'd first
779 // want to add a call like registerForSimRecordsLoaded()
780 // to Phone.java and GSMPhone.java, and *then* we could
781 // listen for that in the CallNotifier class.)
783 // Limit the number of retries (in case the SIM is broken
784 // or missing and can *never* load successfully.)
785 if (mVmNumberRetriesRemaining-- > 0) {
786 if (DBG) log(" - Retrying in " + VM_NUMBER_RETRY_DELAY_MILLIS + " msec...");
787 PhoneApp.getInstance().notifier.sendMwiChangedDelayed(
788 VM_NUMBER_RETRY_DELAY_MILLIS);
791 Log.w(LOG_TAG, "NotificationMgr.updateMwi: getVoiceMailNumber() failed after "
792 + MAX_VM_NUMBER_RETRIES + " retries; giving up.");
793 // ...and continue with vmNumber==null, just as if the
794 // phone had no VM number set up in the first place.
798 String notificationText;
799 if (TextUtils.isEmpty(vmNumber)) {
800 notificationText = mContext.getString(
801 R.string.notification_voicemail_no_vm_number);
803 notificationText = String.format(
804 mContext.getString(R.string.notification_voicemail_text_format),
805 PhoneNumberUtils.formatNumber(vmNumber));
808 Intent intent = new Intent(Intent.ACTION_CALL,
809 Uri.fromParts("voicemail", "", null));
810 PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
812 Notification notification = new Notification(
815 System.currentTimeMillis() // Show the time the MWI notification came in,
816 // since we don't know the actual time of the
817 // most recent voicemail message
819 notification.setLatestEventInfo(
821 notificationTitle, // contentTitle
822 notificationText, // contentText
823 pendingIntent // contentIntent
825 notification.defaults |= Notification.DEFAULT_SOUND;
826 notification.flags |= Notification.FLAG_NO_CLEAR;
827 notification.flags |= Notification.FLAG_SHOW_LIGHTS;
828 notification.ledARGB = 0xff00ff00;
829 notification.ledOnMS = 500;
830 notification.ledOffMS = 2000;
832 mNotificationMgr.notify(
833 VOICEMAIL_NOTIFICATION,
836 mNotificationMgr.cancel(VOICEMAIL_NOTIFICATION);
841 * Updates the message call forwarding indicator notification.
843 * @param visible true if there are messages waiting
845 /* package */ void updateCfi(boolean visible) {
846 if (DBG) log("updateCfi(): " + visible);
848 // If Unconditional Call Forwarding (forward all calls) for VOICE
849 // is enabled, just show a notification. We'll default to expanded
850 // view for now, so the there is less confusion about the icon. If
851 // it is deemed too weird to have CF indications as expanded views,
852 // then we'll flip the flag back.
854 // TODO: We may want to take a look to see if the notification can
855 // display the target to forward calls to. This will require some
856 // effort though, since there are multiple layers of messages that
857 // will need to propagate that information.
859 Notification notification;
860 final boolean showExpandedNotification = true;
861 if (showExpandedNotification) {
862 Intent intent = new Intent(Intent.ACTION_MAIN);
863 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
864 intent.setClassName("com.android.phone",
865 "com.android.phone.CallFeaturesSetting");
867 notification = new Notification(
869 android.R.drawable.stat_sys_phone_call_forward, // icon
871 0, // The "timestamp" of this notification is meaningless;
872 // we only care about whether CFI is currently on or not.
873 mContext.getString(R.string.labelCF), // expandedTitle
874 mContext.getString(R.string.sum_cfu_enabled_indicator), // expandedText
875 intent // contentIntent
879 notification = new Notification(
880 android.R.drawable.stat_sys_phone_call_forward, // icon
882 System.currentTimeMillis() // when
886 notification.flags |= Notification.FLAG_ONGOING_EVENT; // also implies FLAG_NO_CLEAR
888 mNotificationMgr.notify(
889 CALL_FORWARD_NOTIFICATION,
892 mNotificationMgr.cancel(CALL_FORWARD_NOTIFICATION);
897 * Shows the "data disconnected due to roaming" notification, which
898 * appears when you lose data connectivity because you're roaming and
899 * you have the "data roaming" feature turned off.
901 /* package */ void showDataDisconnectedRoaming() {
902 if (DBG) log("showDataDisconnectedRoaming()...");
904 Intent intent = new Intent(mContext,
905 Settings.class); // "Mobile network settings" screen
907 Notification notification = new Notification(
909 android.R.drawable.stat_sys_warning, // icon
911 System.currentTimeMillis(),
912 mContext.getString(R.string.roaming), // expandedTitle
913 mContext.getString(R.string.roaming_reenable_message), // expandedText
914 intent // contentIntent
916 mNotificationMgr.notify(
917 DATA_DISCONNECTED_ROAMING_NOTIFICATION,
922 * Turns off the "data disconnected due to roaming" notification.
924 /* package */ void hideDataDisconnectedRoaming() {
925 if (DBG) log("hideDataDisconnectedRoaming()...");
926 mNotificationMgr.cancel(DATA_DISCONNECTED_ROAMING_NOTIFICATION);
929 /* package */ void postTransientNotification(int notifyId, CharSequence msg) {
930 if (mToast != null) {
934 mToast = Toast.makeText(mContext, msg, Toast.LENGTH_LONG);
938 private void log(String msg) {
939 Log.d(LOG_TAG, "[NotificationMgr] " + msg);