FileDocCategorySizeDatePackage
NotificationMgr.javaAPI DocAndroid 1.5 API35418Wed May 06 22:42:46 BST 2009com.android.phone

NotificationMgr

public class NotificationMgr extends Object implements CallerInfoAsyncQuery.OnQueryCompleteListener
NotificationManager-related utility code for the Phone app.

Fields Summary
private static final String
LOG_TAG
private static final boolean
DBG
private static final String[]
CALL_LOG_PROJECTION
static final int
MISSED_CALL_NOTIFICATION
static final int
IN_CALL_NOTIFICATION
static final int
MMI_NOTIFICATION
static final int
NETWORK_SELECTION_NOTIFICATION
static final int
VOICEMAIL_NOTIFICATION
static final int
CALL_FORWARD_NOTIFICATION
static final int
DATA_DISCONNECTED_ROAMING_NOTIFICATION
private static NotificationMgr
sMe
private com.android.internal.telephony.Phone
mPhone
private android.content.Context
mContext
private android.app.NotificationManager
mNotificationMgr
private android.app.StatusBarManager
mStatusBar
private StatusBarMgr
mStatusBarMgr
private android.widget.Toast
mToast
private android.os.IBinder
mSpeakerphoneIcon
private android.os.IBinder
mMuteIcon
private int
mNumberMissedCalls
private int
mInCallResId
private static final int
MAX_VM_NUMBER_RETRIES
private static final int
VM_NUMBER_RETRY_DELAY_MILLIS
private int
mVmNumberRetriesRemaining
private QueryHandler
mQueryHandler
private static final int
CALL_LOG_TOKEN
private static final int
CONTACT_TOKEN
static final String[]
PHONES_PROJECTION
The projection to use when querying the phones table
Constructors Summary
NotificationMgr(android.content.Context context)


      
        mContext = context;
        mNotificationMgr = (NotificationManager)
            context.getSystemService(Context.NOTIFICATION_SERVICE);

        mStatusBar = (StatusBarManager) context.getSystemService(Context.STATUS_BAR_SERVICE);

        PhoneApp app = PhoneApp.getInstance();
        mPhone = app.phone;
    
Methods Summary
voidcancelCallInProgressNotification()

        if (DBG) log("cancelCallInProgressNotification()...");
        if (mInCallResId == 0) {
            return;
        }

        if (DBG) log("cancelCallInProgressNotification: " + mInCallResId);
        cancelInCall();
    
private voidcancelInCall()

        if (DBG) log("cancelInCall()...");
        cancelMute();
        cancelSpeakerphone();
        mNotificationMgr.cancel(IN_CALL_NOTIFICATION);
        mInCallResId = 0;
    
voidcancelMissedCallNotification()

        // reset the number of missed calls to 0.
        mNumberMissedCalls = 0;
        mNotificationMgr.cancel(MISSED_CALL_NOTIFICATION);
    
voidcancelMute()

        if (mMuteIcon != null) {
            mStatusBar.removeIcon(mMuteIcon);
            mMuteIcon = null;
        }
    
voidcancelSpeakerphone()

        if (mSpeakerphoneIcon != null) {
            mStatusBar.removeIcon(mSpeakerphoneIcon);
            mSpeakerphoneIcon = null;
        }
    
static com.android.phone.NotificationMgrgetDefault()

        return sMe;
    
com.android.phone.NotificationMgr$StatusBarMgrgetStatusBarMgr()
Factory method

        if (mStatusBarMgr == null) {
            mStatusBarMgr = new StatusBarMgr();
        }
        return mStatusBarMgr;
    
voidhideDataDisconnectedRoaming()
Turns off the "data disconnected due to roaming" notification.

        if (DBG) log("hideDataDisconnectedRoaming()...");
        mNotificationMgr.cancel(DATA_DISCONNECTED_ROAMING_NOTIFICATION);
    
static voidinit(android.content.Context context)

        sMe = new NotificationMgr(context);
        
        // update the notifications that need to be touched at startup.
        sMe.updateNotifications();
    
private voidlog(java.lang.String msg)

        Log.d(LOG_TAG, "[NotificationMgr] " + msg);
    
voidnotifyMissedCall(java.lang.String name, java.lang.String number, java.lang.String label, long date)
Displays a notification about a missed call.

param
nameOrNumber either the contact name, or the phone number if no contact
param
label the label of the number if nameOrNumber is a name, null if it is a number

        // title resource id
        int titleResId;
        // the text in the notification's line 1 and 2.
        String expandedText, callName;
        
        // increment number of missed calls.
        mNumberMissedCalls++;

        // get the name for the ticker text
        // i.e. "Missed call from <caller name or number>"
        if (name != null && TextUtils.isGraphic(name)) {
            callName = name;
        } else if (!TextUtils.isEmpty(number)){
            callName = number;
        } else {
            // use "unknown" if the caller is unidentifiable.
            callName = mContext.getString(R.string.unknown);
        }
        
        // display the first line of the notification:
        // 1 missed call: call name
        // more than 1 missed call: <number of calls> + "missed calls"
        if (mNumberMissedCalls == 1) {
            titleResId = R.string.notification_missedCallTitle;
            expandedText = callName;
        } else {
            titleResId = R.string.notification_missedCallsTitle;
            expandedText = mContext.getString(R.string.notification_missedCallsMsg, 
                    mNumberMissedCalls);
        }
        
        // create the target call log intent
        final Intent intent = PhoneApp.createCallLogIntent();
        
        // make the notification
        mNotificationMgr.notify(
                MISSED_CALL_NOTIFICATION,
                new Notification(
                    mContext,  // context
                    android.R.drawable.stat_notify_missed_call,  // icon
                    mContext.getString(
                            R.string.notification_missedCallTicker, callName), // tickerText
                    date, // when
                    mContext.getText(titleResId), // expandedTitle
                    expandedText,  // expandedText
                    intent // contentIntent
                    ));
    
voidnotifyMute()

        if (mMuteIcon == null) {
            mMuteIcon = mStatusBar.addIcon("mute", android.R.drawable.stat_notify_call_mute, 0);
        }
    
voidnotifySpeakerphone()

        if (mSpeakerphoneIcon == null) {
            mSpeakerphoneIcon = mStatusBar.addIcon("speakerphone",
                    android.R.drawable.stat_sys_speakerphone, 0);
        }
    
public voidonQueryComplete(int token, java.lang.Object cookie, com.android.internal.telephony.CallerInfo ci)
Implemented for CallerInfoAsyncQuery.OnQueryCompleteListener interface. refreshes the contentView when called.

        if (DBG) log("callerinfo query complete, updating ui.");

        ((RemoteViews) cookie).setTextViewText(R.id.text2, 
                PhoneUtils.getCompactNameFromCallerInfo(ci, mContext));
    
voidpostTransientNotification(int notifyId, java.lang.CharSequence msg)

        if (mToast != null) {
            mToast.cancel();
        }

        mToast = Toast.makeText(mContext, msg, Toast.LENGTH_LONG);
        mToast.show();
    
voidshowDataDisconnectedRoaming()
Shows the "data disconnected due to roaming" notification, which appears when you lose data connectivity because you're roaming and you have the "data roaming" feature turned off.

        if (DBG) log("showDataDisconnectedRoaming()...");

        Intent intent = new Intent(mContext,
                                   Settings.class);  // "Mobile network settings" screen

        Notification notification = new Notification(
                mContext,  // context
                android.R.drawable.stat_sys_warning,  // icon
                null, // tickerText
                System.currentTimeMillis(),
                mContext.getString(R.string.roaming), // expandedTitle
                mContext.getString(R.string.roaming_reenable_message),  // expandedText
                intent // contentIntent
                );
        mNotificationMgr.notify(
                DATA_DISCONNECTED_ROAMING_NOTIFICATION,
                notification);
    
voidupdateCfi(boolean visible)
Updates the message call forwarding indicator notification.

param
visible true if there are messages waiting

        if (DBG) log("updateCfi(): " + visible);
        if (visible) {
            // If Unconditional Call Forwarding (forward all calls) for VOICE
            // is enabled, just show a notification.  We'll default to expanded
            // view for now, so the there is less confusion about the icon.  If
            // it is deemed too weird to have CF indications as expanded views, 
            // then we'll flip the flag back.

            // TODO: We may want to take a look to see if the notification can 
            // display the target to forward calls to.  This will require some 
            // effort though, since there are multiple layers of messages that 
            // will need to propagate that information.

            Notification notification;
            final boolean showExpandedNotification = true;
            if (showExpandedNotification) {
                Intent intent = new Intent(Intent.ACTION_MAIN);
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                intent.setClassName("com.android.phone",
                        "com.android.phone.CallFeaturesSetting");

                notification = new Notification(
                        mContext,  // context
                        android.R.drawable.stat_sys_phone_call_forward,  // icon
                        null, // tickerText
                        0,  // The "timestamp" of this notification is meaningless;
                            // we only care about whether CFI is currently on or not.
                        mContext.getString(R.string.labelCF), // expandedTitle
                        mContext.getString(R.string.sum_cfu_enabled_indicator),  // expandedText
                        intent // contentIntent
                        );

            } else {
                notification = new Notification(
                        android.R.drawable.stat_sys_phone_call_forward,  // icon
                        null,  // tickerText
                        System.currentTimeMillis()  // when
                        );
            }

            notification.flags |= Notification.FLAG_ONGOING_EVENT;  // also implies FLAG_NO_CLEAR

            mNotificationMgr.notify(
                    CALL_FORWARD_NOTIFICATION,
                    notification);
        } else {
            mNotificationMgr.cancel(CALL_FORWARD_NOTIFICATION);
        }
    
voidupdateInCallNotification()

        if (DBG) log("updateInCallNotification()...");

        if (mPhone.getState() != Phone.State.OFFHOOK) {
            return;
        }

        final boolean hasActiveCall = !mPhone.getForegroundCall().isIdle();
        final boolean hasHoldingCall = !mPhone.getBackgroundCall().isIdle();

        // Display the appropriate "in-call" icon in the status bar,
        // which depends on the current phone and/or bluetooth state.
        int resId = android.R.drawable.stat_sys_phone_call;
        if (!hasActiveCall && hasHoldingCall) {
            // There's only one call, and it's on hold.
            resId = android.R.drawable.stat_sys_phone_call_on_hold;
        } else if (PhoneApp.getInstance().showBluetoothIndication()) {
            // Bluetooth is active.
            resId = com.android.internal.R.drawable.stat_sys_phone_call_bluetooth;
        }

        // Note we can't just bail out now if (resId == mInCallResId),
        // since even if the status icon hasn't changed, some *other*
        // notification-related info may be different from the last time
        // we were here (like the caller-id info of the foreground call,
        // if the user swapped calls...)

        if (DBG) log("- Updating status bar icon: " + resId);
        mInCallResId = resId;

        // Even if both lines are in use, we only show a single item in
        // the expanded Notifications UI.  It's labeled "Ongoing call"
        // (or "On hold" if there's only one call, and it's on hold.)

        // The icon in the expanded view is the same as in the status bar.
        int expandedViewIcon = mInCallResId;

        // Also, we don't have room to display caller-id info from two
        // different calls.  So if there's only one call, use that, but if
        // both lines are in use we display the caller-id info from the
        // foreground call and totally ignore the background call.
        Call currentCall = hasActiveCall ? mPhone.getForegroundCall()
                : mPhone.getBackgroundCall();
        Connection currentConn = currentCall.getEarliestConnection();

        // When expanded, the "Ongoing call" notification is (visually)
        // different from most other Notifications, so we need to use a
        // custom view hierarchy.

        Notification notification = new Notification();
        notification.icon = mInCallResId;
        notification.contentIntent = PendingIntent.getActivity(mContext, 0,
                PhoneApp.createInCallIntent(), 0);
        notification.flags |= Notification.FLAG_ONGOING_EVENT;

        // Our custom view, which includes an icon (either "ongoing call" or
        // "on hold") and 2 lines of text: (1) the label (either "ongoing
        // call" with time counter, or "on hold), and (2) the compact name of
        // the current Connection.
        RemoteViews contentView = new RemoteViews(mContext.getPackageName(),
                                                   R.layout.ongoing_call_notification);
        contentView.setImageViewResource(R.id.icon, expandedViewIcon);
        
        // if the connection is valid, then build what we need for the
        // first line of notification information, and start the chronometer.
        // Otherwise, don't bother and just stick with line 2.
        if (currentConn != null) {
            // Determine the "start time" of the current connection, in terms
            // of the SystemClock.elapsedRealtime() timebase (which is what
            // the Chronometer widget needs.)
            //   We can't use currentConn.getConnectTime(), because (1) that's
            // in the currentTimeMillis() time base, and (2) it's zero when
            // the phone first goes off hook, since the getConnectTime counter
            // doesn't start until the DIALING -> ACTIVE transition.
            //   Instead we start with the current connection's duration,
            // and translate that into the elapsedRealtime() timebase.
            long callDurationMsec = currentConn.getDurationMillis();
            long chronometerBaseTime = SystemClock.elapsedRealtime() - callDurationMsec;

            // Line 1 of the expanded view (in bold text):
            String expandedViewLine1;
            if (hasHoldingCall && !hasActiveCall) {
                // Only one call, and it's on hold!
                // Note this isn't a format string!  (We want "On hold" here,
                // not "On hold (1:23)".)  That's OK; if you call
                // String.format() with more arguments than format specifiers,
                // the extra arguments are ignored.
                expandedViewLine1 = mContext.getString(R.string.notification_on_hold);
            } else {
                // Format string with a "%s" where the current call time should go.
                expandedViewLine1 = mContext.getString(R.string.notification_ongoing_call_format);
            }

            if (DBG) log("- Updating expanded view: line 1 '" + expandedViewLine1 + "'");
    
            // Text line #1 is actually a Chronometer, not a plain TextView.
            // We format the elapsed time of the current call into a line like
            // "Ongoing call (01:23)".
            contentView.setChronometer(R.id.text1,
                                       chronometerBaseTime,
                                       expandedViewLine1,
                                       true);
        } else if (DBG) {
            log("updateInCallNotification: connection is null, call status not updated.");
        }
        
        // display conference call string if this call is a conference 
        // call, otherwise display the connection information.
        
        // TODO: it may not make sense for every point to make separate 
        // checks for isConferenceCall, so we need to think about
        // possibly including this in startGetCallerInfo or some other
        // common point.
        String expandedViewLine2 = ""; 
        if (PhoneUtils.isConferenceCall(currentCall)) {
            // if this is a conference call, just use that as the caller name.
            expandedViewLine2 = mContext.getString(R.string.card_title_conf_call);
        } else {
            // Start asynchronous call to get the compact name.
            PhoneUtils.CallerInfoToken cit = 
                PhoneUtils.startGetCallerInfo (mContext, currentCall, this, contentView);
            // Line 2 of the expanded view (smaller text):
            expandedViewLine2 = PhoneUtils.getCompactNameFromCallerInfo(cit.currentInfo, mContext);
        }
        
        if (DBG) log("- Updating expanded view: line 2 '" + expandedViewLine2 + "'");
        contentView.setTextViewText(R.id.text2, expandedViewLine2);
        notification.contentView = contentView;

        // TODO: We also need to *update* this notification in some cases,
        // like when a call ends on one line but the other is still in use
        // (ie. make sure the caller info here corresponds to the active
        // line), and maybe even when the user swaps calls (ie. if we only
        // show info here for the "current active call".)

        if (DBG) log("Notifying IN_CALL_NOTIFICATION: " + notification);
        mNotificationMgr.notify(IN_CALL_NOTIFICATION,
                                notification);

        // Finally, refresh the mute and speakerphone notifications (since
        // some phone state changes can indirectly affect the mute and/or
        // speaker state).
        updateSpeakerNotification();
        updateMuteNotification();
    
voidupdateMuteNotification()
Calls either notifyMute() or cancelMute() based on the actual current mute state of the Phone.

        if ((mPhone.getState() == Phone.State.OFFHOOK) && mPhone.getMute()) {
            if (DBG) log("updateMuteNotification: MUTED");
            notifyMute();
        } else {
            if (DBG) log("updateMuteNotification: not muted (or not offhook)");
            cancelMute();
        }
    
voidupdateMwi(boolean visible)
Updates the message waiting indicator (voicemail) notification.

param
visible true if there are messages waiting

        if (DBG) log("updateMwi(): " + visible);
        if (visible) {
            int resId = android.R.drawable.stat_notify_voicemail;

            // This Notification can get a lot fancier once we have more
            // information about the current voicemail messages. 
            // (For example, the current voicemail system can't tell
            // us the caller-id or timestamp of a message, or tell us the
            // message count.)

            // But for now, the UI is ultra-simple: if the MWI indication
            // is supposed to be visible, just show a single generic
            // notification.

            String notificationTitle = mContext.getString(R.string.notification_voicemail_title);
            String vmNumber = mPhone.getVoiceMailNumber();
            if (DBG) log("- got vm number: '" + vmNumber + "'");

            // Watch out: vmNumber may be null, for two possible reasons:
            //
            //   (1) This phone really has no voicemail number
            //
            //   (2) This phone *does* have a voicemail number, but
            //       the SIM isn't ready yet.
            //
            // Case (2) *does* happen in practice if you have voicemail
            // messages when the device first boots: we get an MWI
            // notification as soon as we register on the network, but the
            // SIM hasn't finished loading yet.
            //
            // So handle case (2) by retrying the lookup after a short
            // delay.

            if ((vmNumber == null) && !mPhone.getSimRecordsLoaded()) {
                if (DBG) log("- Null vm number: SIM records not loaded (yet)...");

                // TODO: rather than retrying after an arbitrary delay, it
                // would be cleaner to instead just wait for a
                // SIM_RECORDS_LOADED notification.
                // (Unfortunately right now there's no convenient way to
                // get that notification in phone app code.  We'd first
                // want to add a call like registerForSimRecordsLoaded()
                // to Phone.java and GSMPhone.java, and *then* we could
                // listen for that in the CallNotifier class.)

                // Limit the number of retries (in case the SIM is broken
                // or missing and can *never* load successfully.)
                if (mVmNumberRetriesRemaining-- > 0) {
                    if (DBG) log("  - Retrying in " + VM_NUMBER_RETRY_DELAY_MILLIS + " msec...");
                    PhoneApp.getInstance().notifier.sendMwiChangedDelayed(
                            VM_NUMBER_RETRY_DELAY_MILLIS);
                    return;
                } else {
                    Log.w(LOG_TAG, "NotificationMgr.updateMwi: getVoiceMailNumber() failed after "
                          + MAX_VM_NUMBER_RETRIES + " retries; giving up.");
                    // ...and continue with vmNumber==null, just as if the
                    // phone had no VM number set up in the first place.
                }
            }

            String notificationText;
            if (TextUtils.isEmpty(vmNumber)) {
                notificationText = mContext.getString(
                        R.string.notification_voicemail_no_vm_number);
            } else {
                notificationText = String.format(
                        mContext.getString(R.string.notification_voicemail_text_format),
                        PhoneNumberUtils.formatNumber(vmNumber));
            }

            Intent intent = new Intent(Intent.ACTION_CALL,
                    Uri.fromParts("voicemail", "", null));
            PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
            
            Notification notification = new Notification(
                    resId,  // icon
                    null, // tickerText
                    System.currentTimeMillis()  // Show the time the MWI notification came in,
                                                // since we don't know the actual time of the
                                                // most recent voicemail message
                    );
            notification.setLatestEventInfo(
                    mContext,  // context
                    notificationTitle,  // contentTitle
                    notificationText,  // contentText
                    pendingIntent  // contentIntent
                    );
            notification.defaults |= Notification.DEFAULT_SOUND;
            notification.flags |= Notification.FLAG_NO_CLEAR;
            notification.flags |= Notification.FLAG_SHOW_LIGHTS;
            notification.ledARGB = 0xff00ff00;
            notification.ledOnMS = 500;
            notification.ledOffMS = 2000;
            
            mNotificationMgr.notify(
                    VOICEMAIL_NOTIFICATION,
                    notification);
        } else {
            mNotificationMgr.cancel(VOICEMAIL_NOTIFICATION);
        }
    
voidupdateNotifications()
Makes sure notifications are up to date.

        if (DBG) log("begin querying call log");

        // instantiate query handler
        mQueryHandler = new QueryHandler(mContext.getContentResolver());

        // setup query spec, look for all Missed calls that are new.
        StringBuilder where = new StringBuilder("type=");
        where.append(Calls.MISSED_TYPE);
        where.append(" AND new=1");
        
        // start the query
        mQueryHandler.startQuery(CALL_LOG_TOKEN, null, Calls.CONTENT_URI,  CALL_LOG_PROJECTION,
                where.toString(), null, Calls.DEFAULT_SORT_ORDER);
        
        // synchronize the in call notification
        if (mPhone.getState() != Phone.State.OFFHOOK) {
            if (DBG) log("Phone is idle, canceling notification.");
            cancelInCall();
        } else {
            if (DBG) log("Phone is offhook, updating notification.");
            updateInCallNotification();
        }
        
        // Depend on android.app.StatusBarManager to be set to
        // disable(DISABLE_NONE) upon startup.  This will be the
        // case even if the phone app crashes.
    
voidupdateSpeakerNotification()
Calls either notifySpeakerphone() or cancelSpeakerphone() based on the actual current state of the speaker.

        AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);

        if ((mPhone.getState() == Phone.State.OFFHOOK) && audioManager.isSpeakerphoneOn()) {
            if (DBG) log("updateSpeakerNotification: speaker ON");
            notifySpeakerphone();
        } else {
            if (DBG) log("updateSpeakerNotification: speaker OFF (or not offhook)");
            cancelSpeakerphone();
        }