NotificationMgr.java revision abc47110c17fa8e8cb6161bc045e87f31eeb7a1c
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 17package com.android.phone; 18 19import android.app.Notification; 20import android.app.NotificationManager; 21import android.app.PendingIntent; 22import android.app.StatusBarManager; 23import android.content.AsyncQueryHandler; 24import android.content.ContentResolver; 25import android.content.Context; 26import android.content.Intent; 27import android.database.Cursor; 28import android.media.AudioManager; 29import android.net.Uri; 30import android.os.IBinder; 31import android.os.SystemClock; 32import android.provider.CallLog.Calls; 33import android.provider.Contacts.Phones; 34import com.android.internal.telephony.Call; 35import com.android.internal.telephony.CallerInfo; 36import com.android.internal.telephony.CallerInfoAsyncQuery; 37import com.android.internal.telephony.Connection; 38import com.android.internal.telephony.Phone; 39import android.telephony.PhoneNumberUtils; 40import android.text.TextUtils; 41import android.util.Log; 42import android.widget.RemoteViews; 43import android.widget.Toast; 44 45/** 46 * NotificationManager-related utility code for the Phone app. 47 */ 48public class NotificationMgr implements CallerInfoAsyncQuery.OnQueryCompleteListener{ 49 private static final String LOG_TAG = PhoneApp.LOG_TAG; 50 private static final boolean DBG = false; 51 52 private static final String[] CALL_LOG_PROJECTION = new String[] { 53 Calls._ID, 54 Calls.NUMBER, 55 Calls.DATE, 56 Calls.DURATION, 57 Calls.TYPE, 58 }; 59 60 // notification types 61 static final int MISSED_CALL_NOTIFICATION = 1; 62 static final int IN_CALL_NOTIFICATION = 2; 63 static final int MMI_NOTIFICATION = 3; 64 static final int NETWORK_SELECTION_NOTIFICATION = 4; 65 static final int VOICEMAIL_NOTIFICATION = 5; 66 static final int CALL_FORWARD_NOTIFICATION = 6; 67 68 private static NotificationMgr sMe = null; 69 private Phone mPhone; 70 71 private Context mContext; 72 private NotificationManager mNotificationMgr; 73 private StatusBarManager mStatusBar; 74 private StatusBarMgr mStatusBarMgr; 75 private Toast mToast; 76 private IBinder mSpeakerphoneIcon; 77 private IBinder mMuteIcon; 78 79 // used to track the missed call counter, default to 0. 80 private int mNumberMissedCalls = 0; 81 82 // Currently-displayed resource IDs for some status bar icons (or zero 83 // if no notification is active): 84 private int mInCallResId; 85 86 // Retry params for the getVoiceMailNumber() call; see updateMwi(). 87 private static final int MAX_VM_NUMBER_RETRIES = 5; 88 private static final int VM_NUMBER_RETRY_DELAY_MILLIS = 10000; 89 private int mVmNumberRetriesRemaining = MAX_VM_NUMBER_RETRIES; 90 91 //Used to find the information to populate the caller notification with. 92 private QueryHandler mQueryHandler = null; 93 private static final int CALL_LOG_TOKEN = -1; 94 private static final int CONTACT_TOKEN = -2; 95 96 NotificationMgr(Context context) { 97 mContext = context; 98 mNotificationMgr = (NotificationManager) 99 context.getSystemService(Context.NOTIFICATION_SERVICE); 100 101 mStatusBar = (StatusBarManager) context.getSystemService(Context.STATUS_BAR_SERVICE); 102 103 PhoneApp app = PhoneApp.getInstance(); 104 mPhone = app.phone; 105 } 106 107 static void init(Context context) { 108 sMe = new NotificationMgr(context); 109 110 // update the notifications that need to be touched at startup. 111 sMe.updateNotifications(); 112 } 113 114 static NotificationMgr getDefault() { 115 return sMe; 116 } 117 118 /** 119 * Class that controls the status bar. This class maintains a set 120 * of state and acts as an interface between the Phone process and 121 * the Status bar. All interaction with the status bar should be 122 * though the methods contained herein. 123 */ 124 125 /** 126 * Factory method 127 */ 128 StatusBarMgr getStatusBarMgr() { 129 if (mStatusBarMgr == null) { 130 mStatusBarMgr = new StatusBarMgr(); 131 } 132 return mStatusBarMgr; 133 } 134 135 /** 136 * StatusBarMgr implementation 137 */ 138 class StatusBarMgr { 139 // current settings 140 private boolean mIsNotificationEnabled = true; 141 private boolean mIsExpandedViewEnabled = true; 142 143 private StatusBarMgr () { 144 } 145 146 /** 147 * Sets the notification state (enable / disable 148 * vibrating notifications) for the status bar, 149 * updates the status bar service if there is a change. 150 * Independent of the remaining Status Bar 151 * functionality, including icons and expanded view. 152 */ 153 void enableNotificationAlerts(boolean enable) { 154 if (mIsNotificationEnabled != enable) { 155 mIsNotificationEnabled = enable; 156 updateStatusBar(); 157 } 158 } 159 160 /** 161 * Sets the ability to expand the notifications for the 162 * status bar, updates the status bar service if there 163 * is a change. Independent of the remaining Status Bar 164 * functionality, including icons and notification 165 * alerts. 166 */ 167 void enableExpandedView(boolean enable) { 168 if (mIsExpandedViewEnabled != enable) { 169 mIsExpandedViewEnabled = enable; 170 updateStatusBar(); 171 } 172 } 173 174 /** 175 * Method to synchronize status bar state with our current 176 * state. 177 */ 178 void updateStatusBar() { 179 int state = StatusBarManager.DISABLE_NONE; 180 181 if (!mIsExpandedViewEnabled) { 182 state |= StatusBarManager.DISABLE_EXPAND; 183 } 184 185 if (!mIsNotificationEnabled) { 186 state |= StatusBarManager.DISABLE_NOTIFICATION_ALERTS; 187 } 188 189 // send the message to the status bar manager. 190 if (DBG) log("updating status bar state: " + state); 191 mStatusBar.disable(state); 192 } 193 } 194 195 /** 196 * Makes sure notifications are up to date. 197 */ 198 void updateNotifications() { 199 if (DBG) log("begin querying call log"); 200 201 // instantiate query handler 202 mQueryHandler = new QueryHandler(mContext.getContentResolver()); 203 204 // setup query spec, look for all Missed calls that are new. 205 StringBuilder where = new StringBuilder("type="); 206 where.append(Calls.MISSED_TYPE); 207 where.append(" AND new=1"); 208 209 // start the query 210 mQueryHandler.startQuery(CALL_LOG_TOKEN, null, Calls.CONTENT_URI, CALL_LOG_PROJECTION, 211 where.toString(), null, Calls.DEFAULT_SORT_ORDER); 212 213 // synchronize the in call notification 214 if (mPhone.getState() != Phone.State.OFFHOOK) { 215 if (DBG) log("Phone is idle, canceling notification."); 216 cancelInCall(); 217 } else { 218 if (DBG) log("Phone is offhook, updating notification."); 219 updateInCallNotification(); 220 } 221 222 // Depend on android.app.StatusBarManager to be set to 223 // disable(DISABLE_NONE) upon startup. This will be the 224 // case even if the phone app crashes. 225 } 226 227 /** The projection to use when querying the phones table */ 228 static final String[] PHONES_PROJECTION = new String[] { 229 Phones.NUMBER, 230 Phones.NAME 231 }; 232 233 /** 234 * Class used to run asynchronous queries to re-populate 235 * the notifications we care about. 236 */ 237 private class QueryHandler extends AsyncQueryHandler { 238 239 /** 240 * Used to store relevant fields for the Missed Call 241 * notifications. 242 */ 243 private class NotificationInfo { 244 public String name; 245 public String number; 246 public String label; 247 public long date; 248 } 249 250 public QueryHandler(ContentResolver cr) { 251 super(cr); 252 } 253 254 /** 255 * Handles the query results. There are really 2 steps to this, 256 * similar to what happens in RecentCallsListActivity. 257 * 1. Find the list of missed calls 258 * 2. For each call, run a query to retrieve the caller's name. 259 */ 260 @Override 261 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 262 // TODO: it would be faster to use a join here, but for the purposes 263 // of this small record set, it should be ok. 264 265 // Note that CursorJoiner is not useable here because the number 266 // comparisons are not strictly equals; the comparisons happen in 267 // the SQL function PHONE_NUMBERS_EQUAL, which is not available for 268 // the CursorJoiner. 269 270 // Executing our own query is also feasible (with a join), but that 271 // will require some work (possibly destabilizing) in Contacts 272 // Provider. 273 274 // At this point, we will execute subqueries on each row just as 275 // RecentCallsListActivity.java does. 276 switch (token) { 277 case CALL_LOG_TOKEN: 278 if (DBG) log("call log query complete."); 279 280 // initial call to retrieve the call list. 281 if (cursor != null) { 282 while (cursor.moveToNext()) { 283 // for each call in the call log list, create 284 // the notification object and query contacts 285 NotificationInfo n = getNotificationInfo (cursor); 286 287 if (DBG) log("query contacts for number: " + n.number); 288 289 mQueryHandler.startQuery(CONTACT_TOKEN, n, 290 Uri.withAppendedPath(Phones.CONTENT_FILTER_URL, n.number), 291 PHONES_PROJECTION, null, null, Phones.DEFAULT_SORT_ORDER); 292 } 293 294 if (DBG) log("closing call log cursor."); 295 cursor.close(); 296 } 297 break; 298 case CONTACT_TOKEN: 299 if (DBG) log("contact query complete."); 300 301 // subqueries to get the caller name. 302 if ((cursor != null) && (cookie != null)){ 303 NotificationInfo n = (NotificationInfo) cookie; 304 305 if (cursor.moveToFirst()) { 306 // we have contacts data, get the name. 307 if (DBG) log("contact :" + n.name + " found for phone: " + n.number); 308 n.name = cursor.getString(cursor.getColumnIndexOrThrow(Phones.NAME)); 309 } 310 311 // send the notification 312 if (DBG) log("sending notification."); 313 notifyMissedCall(n.name, n.number, n.label, n.date); 314 315 if (DBG) log("closing contact cursor."); 316 cursor.close(); 317 } 318 break; 319 default: 320 } 321 } 322 323 /** 324 * Factory method to generate a NotificationInfo object given a 325 * cursor from the call log table. 326 */ 327 private final NotificationInfo getNotificationInfo(Cursor cursor) { 328 NotificationInfo n = new NotificationInfo(); 329 n.name = null; 330 n.number = cursor.getString(cursor.getColumnIndexOrThrow(Calls.NUMBER)); 331 n.label = cursor.getString(cursor.getColumnIndexOrThrow(Calls.TYPE)); 332 n.date = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.DATE)); 333 334 // make sure we update the number depending upon saved values in 335 // CallLog.addCall(). If either special values for unknown or 336 // private number are detected, we need to hand off the message 337 // to the missed call notification. 338 if ((n.number.equals(CallerInfo.UNKNOWN_NUMBER)) || 339 (n.number.equals(CallerInfo.PRIVATE_NUMBER))) { 340 n.number = null; 341 } 342 343 if (DBG) log("NotificationInfo constructed for number: " + n.number); 344 345 return n; 346 } 347 } 348 349 /** 350 * Displays a notification about a missed call. 351 * 352 * @param nameOrNumber either the contact name, or the phone number if no contact 353 * @param label the label of the number if nameOrNumber is a name, null if it is a number 354 */ 355 void notifyMissedCall(String name, String number, String label, long date) { 356 // title resource id 357 int titleResId; 358 // the text in the notification's line 1 and 2. 359 String expandedText, callName; 360 361 // increment number of missed calls. 362 mNumberMissedCalls++; 363 364 // get the name for the ticker text 365 // i.e. "Missed call from <caller name or number>" 366 if (name != null && TextUtils.isGraphic(name)) { 367 callName = name; 368 } else if (!TextUtils.isEmpty(number)){ 369 callName = number; 370 } else { 371 // use "unknown" if the caller is unidentifiable. 372 callName = mContext.getString(R.string.unknown); 373 } 374 375 // display the first line of the notification: 376 // 1 missed call: call name 377 // more than 1 missed call: <number of calls> + "missed calls" 378 if (mNumberMissedCalls == 1) { 379 titleResId = R.string.notification_missedCallTitle; 380 expandedText = callName; 381 } else { 382 titleResId = R.string.notification_missedCallsTitle; 383 expandedText = mContext.getString(R.string.notification_missedCallsMsg, 384 mNumberMissedCalls); 385 } 386 387 // create the target call log intent 388 final Intent intent = PhoneApp.createCallLogIntent(); 389 390 // make the notification 391 mNotificationMgr.notify( 392 MISSED_CALL_NOTIFICATION, 393 new Notification( 394 mContext, // context 395 android.R.drawable.stat_notify_missed_call, // icon 396 mContext.getString( 397 R.string.notification_missedCallTicker, callName), // tickerText 398 date, // when 399 mContext.getText(titleResId), // expandedTitle 400 expandedText, // expandedText 401 intent // contentIntent 402 )); 403 } 404 405 void cancelMissedCallNotification() { 406 // reset the number of missed calls to 0. 407 mNumberMissedCalls = 0; 408 mNotificationMgr.cancel(MISSED_CALL_NOTIFICATION); 409 } 410 411 void notifySpeakerphone() { 412 if (mSpeakerphoneIcon == null) { 413 mSpeakerphoneIcon = mStatusBar.addIcon("speakerphone", 414 android.R.drawable.stat_sys_speakerphone, 0); 415 } 416 } 417 418 void cancelSpeakerphone() { 419 if (mSpeakerphoneIcon != null) { 420 mStatusBar.removeIcon(mSpeakerphoneIcon); 421 mSpeakerphoneIcon = null; 422 } 423 } 424 425 /** 426 * Calls either notifySpeakerphone() or cancelSpeakerphone() based on 427 * the actual current state of the speaker. 428 */ 429 void updateSpeakerNotification() { 430 AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); 431 432 if ((mPhone.getState() == Phone.State.OFFHOOK) && audioManager.isSpeakerphoneOn()) { 433 if (DBG) log("updateSpeakerNotification: speaker ON"); 434 notifySpeakerphone(); 435 } else { 436 if (DBG) log("updateSpeakerNotification: speaker OFF (or not offhook)"); 437 cancelSpeakerphone(); 438 } 439 } 440 441 void notifyMute() { 442 if (mMuteIcon == null) { 443 mMuteIcon = mStatusBar.addIcon("mute", android.R.drawable.stat_notify_call_mute, 0); 444 } 445 } 446 447 void cancelMute() { 448 if (mMuteIcon != null) { 449 mStatusBar.removeIcon(mMuteIcon); 450 mMuteIcon = null; 451 } 452 } 453 454 /** 455 * Calls either notifyMute() or cancelMute() based on 456 * the actual current mute state of the Phone. 457 */ 458 void updateMuteNotification() { 459 if ((mPhone.getState() == Phone.State.OFFHOOK) && mPhone.getMute()) { 460 if (DBG) log("updateMuteNotification: MUTED"); 461 notifyMute(); 462 } else { 463 if (DBG) log("updateMuteNotification: not muted (or not offhook)"); 464 cancelMute(); 465 } 466 } 467 468 void updateInCallNotification() { 469 if (DBG) log("updateInCallNotification()..."); 470 471 if (mPhone.getState() != Phone.State.OFFHOOK) { 472 return; 473 } 474 475 final boolean hasActiveCall = !mPhone.getForegroundCall().isIdle(); 476 final boolean hasHoldingCall = !mPhone.getBackgroundCall().isIdle(); 477 478 // Display the regular "in-call" icon in the status bar, except if 479 // there's only one call, and it's on hold (in which case we use the 480 // "on hold" icon.) 481 int resId = (!hasActiveCall && hasHoldingCall) 482 ? android.R.drawable.stat_sys_phone_call_on_hold 483 : android.R.drawable.stat_sys_phone_call; 484 485 // Note we can't just bail out now if (resId == mInCallResId), 486 // since even if the status icon hasn't changed, some *other* 487 // notification-related info may be different from the last time 488 // we were here (like the caller-id info of the foreground call, 489 // if the user swapped calls...) 490 491 if (DBG) log("- Updating status bar icon: " + resId); 492 mInCallResId = resId; 493 494 // Even if both lines are in use, we only show a single item in 495 // the expanded Notifications UI. It's labeled "Ongoing call" 496 // (or "On hold" if there's only one call, and it's on hold.) 497 498 // The icon in the expanded view is the same as in the status bar. 499 int expandedViewIcon = mInCallResId; 500 501 // Also, we don't have room to display caller-id info from two 502 // different calls. So if there's only one call, use that, but if 503 // both lines are in use we display the caller-id info from the 504 // foreground call and totally ignore the background call. 505 Call currentCall = hasActiveCall ? mPhone.getForegroundCall() 506 : mPhone.getBackgroundCall(); 507 Connection currentConn = currentCall.getEarliestConnection(); 508 509 // When expanded, the "Ongoing call" notification is (visually) 510 // different from most other Notifications, so we need to use a 511 // custom view hierarchy. 512 513 Notification notification = new Notification(); 514 notification.icon = mInCallResId; 515 notification.contentIntent = PendingIntent.getActivity(mContext, 0, 516 PhoneApp.createInCallIntent(), 0); 517 notification.flags |= Notification.FLAG_ONGOING_EVENT; 518 519 // Our custom view, which includes an icon (either "ongoing call" or 520 // "on hold") and 2 lines of text: (1) the label (either "ongoing 521 // call" with time counter, or "on hold), and (2) the compact name of 522 // the current Connection. 523 RemoteViews contentView = new RemoteViews(mContext.getPackageName(), 524 R.layout.ongoing_call_notification); 525 contentView.setImageViewResource(R.id.icon, expandedViewIcon); 526 527 // if the connection is valid, then build what we need for the 528 // first line of notification information, and start the chronometer. 529 // Otherwise, don't bother and just stick with line 2. 530 if (currentConn != null) { 531 // Determine the "start time" of the current connection, in terms 532 // of the SystemClock.elapsedRealtime() timebase (which is what 533 // the Chronometer widget needs.) 534 // We can't use currentConn.getConnectTime(), because (1) that's 535 // in the currentTimeMillis() time base, and (2) it's zero when 536 // the phone first goes off hook, since the getConnectTime counter 537 // doesn't start until the DIALING -> ACTIVE transition. 538 // Instead we start with the current connection's duration, 539 // and translate that into the elapsedRealtime() timebase. 540 long callDurationMsec = currentConn.getDurationMillis(); 541 long chronometerBaseTime = SystemClock.elapsedRealtime() - callDurationMsec; 542 543 // Line 1 of the expanded view (in bold text): 544 String expandedViewLine1; 545 if (hasHoldingCall && !hasActiveCall) { 546 // Only one call, and it's on hold! 547 // Note this isn't a format string! (We want "On hold" here, 548 // not "On hold (1:23)".) That's OK; if you call 549 // String.format() with more arguments than format specifiers, 550 // the extra arguments are ignored. 551 expandedViewLine1 = mContext.getString(R.string.notification_on_hold); 552 } else { 553 // Format string with a "%s" where the current call time should go. 554 expandedViewLine1 = mContext.getString(R.string.notification_ongoing_call_format); 555 } 556 557 if (DBG) log("- Updating expanded view: line 1 '" + expandedViewLine1 + "'"); 558 559 // Text line #1 is actually a Chronometer, not a plain TextView. 560 // We format the elapsed time of the current call into a line like 561 // "Ongoing call (01:23)". 562 contentView.setChronometer(R.id.text1, 563 chronometerBaseTime, 564 expandedViewLine1, 565 true); 566 } else if (DBG) { 567 log("updateInCallNotification: connection is null, call status not updated."); 568 } 569 570 // display conference call string if this call is a conference 571 // call, otherwise display the connection information. 572 573 // TODO: it may not make sense for every point to make separate 574 // checks for isConferenceCall, so we need to think about 575 // possibly including this in startGetCallerInfo or some other 576 // common point. 577 String expandedViewLine2 = ""; 578 if (PhoneUtils.isConferenceCall(currentCall)) { 579 // if this is a conference call, just use that as the caller name. 580 expandedViewLine2 = mContext.getString(R.string.card_title_conf_call); 581 } else { 582 // Start asynchronous call to get the compact name. 583 PhoneUtils.CallerInfoToken cit = 584 PhoneUtils.startGetCallerInfo (mContext, currentCall, this, contentView); 585 // Line 2 of the expanded view (smaller text): 586 expandedViewLine2 = PhoneUtils.getCompactNameFromCallerInfo(cit.currentInfo, mContext); 587 } 588 589 if (DBG) log("- Updating expanded view: line 2 '" + expandedViewLine2 + "'"); 590 contentView.setTextViewText(R.id.text2, expandedViewLine2); 591 notification.contentView = contentView; 592 593 // TODO: We also need to *update* this notification in some cases, 594 // like when a call ends on one line but the other is still in use 595 // (ie. make sure the caller info here corresponds to the active 596 // line), and maybe even when the user swaps calls (ie. if we only 597 // show info here for the "current active call".) 598 599 if (DBG) log("Notifying IN_CALL_NOTIFICATION: " + notification); 600 mNotificationMgr.notify(IN_CALL_NOTIFICATION, 601 notification); 602 603 // Finally, refresh the mute and speakerphone notifications (since 604 // some phone state changes can indirectly affect the mute and/or 605 // speaker state). 606 updateSpeakerNotification(); 607 updateMuteNotification(); 608 } 609 610 /** 611 * Implemented for CallerInfoAsyncQuery.OnQueryCompleteListener interface. 612 * refreshes the contentView when called. 613 */ 614 public void onQueryComplete(int token, Object cookie, CallerInfo ci){ 615 if (DBG) log("callerinfo query complete, updating ui."); 616 617 ((RemoteViews) cookie).setTextViewText(R.id.text2, 618 PhoneUtils.getCompactNameFromCallerInfo(ci, mContext)); 619 } 620 621 private void cancelInCall() { 622 if (DBG) log("cancelInCall()..."); 623 cancelMute(); 624 cancelSpeakerphone(); 625 mNotificationMgr.cancel(IN_CALL_NOTIFICATION); 626 mInCallResId = 0; 627 } 628 629 void cancelCallInProgressNotification() { 630 if (DBG) log("cancelCallInProgressNotification()..."); 631 if (mInCallResId == 0) { 632 return; 633 } 634 635 if (DBG) log("cancelCallInProgressNotification: " + mInCallResId); 636 cancelInCall(); 637 } 638 639 /** 640 * Updates the message waiting indicator (voicemail) notification. 641 * 642 * @param visible true if there are messages waiting 643 */ 644 /* package */ void updateMwi(boolean visible) { 645 if (DBG) log("updateMwi(): " + visible); 646 if (visible) { 647 int resId = android.R.drawable.stat_notify_voicemail; 648 649 // This Notification can get a lot fancier once we have more 650 // information about the current voicemail messages. 651 // (For example, the current voicemail system can't tell 652 // us the caller-id or timestamp of a message, or tell us the 653 // message count.) 654 655 // But for now, the UI is ultra-simple: if the MWI indication 656 // is supposed to be visible, just show a single generic 657 // notification. 658 659 String notificationTitle = mContext.getString(R.string.notification_voicemail_title); 660 String vmNumber = mPhone.getVoiceMailNumber(); 661 if (DBG) log("- got vm number: '" + vmNumber + "'"); 662 663 // Watch out: vmNumber may be null, for two possible reasons: 664 // 665 // (1) This phone really has no voicemail number 666 // 667 // (2) This phone *does* have a voicemail number, but 668 // the SIM isn't ready yet. 669 // 670 // Case (2) *does* happen in practice if you have voicemail 671 // messages when the device first boots: we get an MWI 672 // notification as soon as we register on the network, but the 673 // SIM hasn't finished loading yet. 674 // 675 // So handle case (2) by retrying the lookup after a short 676 // delay. 677 678 if ((vmNumber == null) && !mPhone.getSimRecordsLoaded()) { 679 if (DBG) log("- Null vm number: SIM records not loaded (yet)..."); 680 681 // TODO: rather than retrying after an arbitrary delay, it 682 // would be cleaner to instead just wait for a 683 // SIM_RECORDS_LOADED notification. 684 // (Unfortunately right now there's no convenient way to 685 // get that notification in phone app code. We'd first 686 // want to add a call like registerForSimRecordsLoaded() 687 // to Phone.java and GSMPhone.java, and *then* we could 688 // listen for that in the CallNotifier class.) 689 690 // Limit the number of retries (in case the SIM is broken 691 // or missing and can *never* load successfully.) 692 if (mVmNumberRetriesRemaining-- > 0) { 693 if (DBG) log(" - Retrying in " + VM_NUMBER_RETRY_DELAY_MILLIS + " msec..."); 694 PhoneApp.getInstance().notifier.sendMwiChangedDelayed( 695 VM_NUMBER_RETRY_DELAY_MILLIS); 696 return; 697 } else { 698 Log.w(LOG_TAG, "NotificationMgr.updateMwi: getVoiceMailNumber() failed after " 699 + MAX_VM_NUMBER_RETRIES + " retries; giving up."); 700 // ...and continue with vmNumber==null, just as if the 701 // phone had no VM number set up in the first place. 702 } 703 } 704 705 String notificationText; 706 if (TextUtils.isEmpty(vmNumber)) { 707 notificationText = mContext.getString( 708 R.string.notification_voicemail_no_vm_number); 709 } else { 710 notificationText = String.format( 711 mContext.getString(R.string.notification_voicemail_text_format), 712 PhoneNumberUtils.formatNumber(vmNumber)); 713 } 714 715 Intent intent = new Intent(Intent.ACTION_CALL, 716 Uri.fromParts("voicemail", "", null)); 717 PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0); 718 719 Notification notification = new Notification( 720 resId, // icon 721 null, // tickerText 722 System.currentTimeMillis() // Show the time the MWI notification came in, 723 // since we don't know the actual time of the 724 // most recent voicemail message 725 ); 726 notification.setLatestEventInfo( 727 mContext, // context 728 notificationTitle, // contentTitle 729 notificationText, // contentText 730 pendingIntent // contentIntent 731 ); 732 notification.defaults |= Notification.DEFAULT_SOUND; 733 notification.flags |= Notification.FLAG_NO_CLEAR; 734 notification.flags |= Notification.FLAG_SHOW_LIGHTS; 735 notification.ledARGB = 0xff00ff00; 736 notification.ledOnMS = 500; 737 notification.ledOffMS = 2000; 738 739 mNotificationMgr.notify( 740 VOICEMAIL_NOTIFICATION, 741 notification); 742 } else { 743 mNotificationMgr.cancel(VOICEMAIL_NOTIFICATION); 744 } 745 } 746 747 /** 748 * Updates the message call forwarding indicator notification. 749 * 750 * @param visible true if there are messages waiting 751 */ 752 /* package */ void updateCfi(boolean visible) { 753 if (DBG) log("updateCfi(): " + visible); 754 if (visible) { 755 // If Unconditional Call Forwarding (forward all calls) for VOICE 756 // is enabled, just show a notification. We'll default to expanded 757 // view for now, so the there is less confusion about the icon. If 758 // it is deemed too weird to have CF indications as expanded views, 759 // then we'll flip the flag back. 760 761 // TODO: We may want to take a look to see if the notification can 762 // display the target to forward calls to. This will require some 763 // effort though, since there are multiple layers of messages that 764 // will need to propagate that information. 765 766 Notification notification = null; 767 768 final boolean showExpandedNotification = true; 769 if (showExpandedNotification) { 770 Intent intent = new Intent(Intent.ACTION_MAIN); 771 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 772 intent.setClassName("com.android.phone", 773 "com.android.phone.CallFeaturesSetting"); 774 775 notification = new Notification( 776 mContext, // context 777 android.R.drawable.stat_sys_phone_call_forward, // icon 778 null, // tickerText 779 System.currentTimeMillis(), // Show the time the CFI notification came in, 780 // since we don't know the actual time the CFU 781 // change was made. 782 mContext.getString(R.string.labelCF), // expandedTitle 783 mContext.getString(R.string.sum_cfu_enabled_indicator), // expandedText 784 intent // contentIntent 785 ); 786 787 } else { 788 notification = new Notification( 789 android.R.drawable.stat_sys_phone_call_forward, // icon 790 null, // tickerText 791 System.currentTimeMillis() // when 792 ); 793 } 794 795 mNotificationMgr.notify( 796 CALL_FORWARD_NOTIFICATION, 797 notification); 798 } else { 799 mNotificationMgr.cancel(CALL_FORWARD_NOTIFICATION); 800 } 801 } 802 803 /* package */ void postTransientNotification(int notifyId, CharSequence msg) { 804 if (mToast != null) { 805 mToast.cancel(); 806 } 807 808 mToast = Toast.makeText(mContext, msg, Toast.LENGTH_LONG); 809 mToast.show(); 810 } 811 812 private void log(String msg) { 813 Log.d(LOG_TAG, "[NotificationMgr] " + msg); 814 } 815} 816