import android.database.Cursor;
import android.media.AudioManager;
import android.net.Uri;
-import android.os.IBinder;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.preference.PreferenceManager;
-import android.provider.Settings;
import android.provider.CallLog.Calls;
import android.provider.ContactsContract.PhoneLookup;
import android.telephony.PhoneNumberUtils;
import com.android.internal.telephony.Connection;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.PhoneBase;
-
+import com.android.internal.telephony.CallManager;
/**
* NotificationManager-related utility code for the Phone app.
+ *
+ * This is a singleton object which acts as the interface to the
+ * framework's NotificationManager, and is used to display status bar
+ * icons and control other status bar-related behavior.
+ *
+ * @see PhoneApp.notificationMgr
*/
public class NotificationMgr implements CallerInfoAsyncQuery.OnQueryCompleteListener{
private static final String LOG_TAG = "NotificationMgr";
- private static final boolean DBG = (PhoneApp.DBG_LEVEL >= 2);
+ private static final boolean DBG =
+ (PhoneApp.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
private static final String[] CALL_LOG_PROJECTION = new String[] {
Calls._ID,
static final int DATA_DISCONNECTED_ROAMING_NOTIFICATION = 7;
static final int SELECTED_OPERATOR_FAIL_NOTIFICATION = 8;
- private static NotificationMgr sMe = null;
+ /** The singleton NotificationMgr instance. */
+ private static NotificationMgr sInstance;
+
+ private PhoneApp mApp;
private Phone mPhone;
+ private CallManager mCM;
private Context mContext;
- private NotificationManager mNotificationMgr;
- private StatusBarManager mStatusBar;
- private StatusBarMgr mStatusBarMgr;
+ private NotificationManager mNotificationManager;
+ private StatusBarManager mStatusBarManager;
private Toast mToast;
- private IBinder mSpeakerphoneIcon;
- private IBinder mMuteIcon;
+ private boolean mShowingSpeakerphoneIcon;
+ private boolean mShowingMuteIcon;
+
+ public StatusBarHelper statusBarHelper;
// used to track the missed call counter, default to 0.
private int mNumberMissedCalls = 0;
private static final int CALL_LOG_TOKEN = -1;
private static final int CONTACT_TOKEN = -2;
- NotificationMgr(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;
- }
-
- static void init(Context context) {
- sMe = new NotificationMgr(context);
-
- // update the notifications that need to be touched at startup.
- sMe.updateNotifications();
- }
-
- static NotificationMgr getDefault() {
- return sMe;
- }
-
/**
- * Class that controls the status bar. This class maintains a set
- * of state and acts as an interface between the Phone process and
- * the Status bar. All interaction with the status bar should be
- * though the methods contained herein.
+ * Private constructor (this is a singleton).
+ * @see init()
*/
+ private NotificationMgr(PhoneApp app) {
+ mApp = app;
+ mContext = app;
+ mNotificationManager =
+ (NotificationManager) app.getSystemService(Context.NOTIFICATION_SERVICE);
+ mStatusBarManager =
+ (StatusBarManager) app.getSystemService(Context.STATUS_BAR_SERVICE);
+ mPhone = app.phone; // TODO: better style to use mCM.getDefaultPhone() everywhere instead
+ mCM = app.mCM;
+ statusBarHelper = new StatusBarHelper();
+ }
/**
- * Factory method
+ * Initialize the singleton NotificationMgr instance.
+ *
+ * This is only done once, at startup, from PhoneApp.onCreate().
+ * From then on, the NotificationMgr instance is available via the
+ * PhoneApp's public "notificationMgr" field, which is why there's no
+ * getInstance() method here.
*/
- StatusBarMgr getStatusBarMgr() {
- if (mStatusBarMgr == null) {
- mStatusBarMgr = new StatusBarMgr();
+ /* package */ static NotificationMgr init(PhoneApp app) {
+ synchronized (NotificationMgr.class) {
+ if (sInstance == null) {
+ sInstance = new NotificationMgr(app);
+ // Update the notifications that need to be touched at startup.
+ sInstance.updateNotificationsAtStartup();
+ } else {
+ Log.wtf(LOG_TAG, "init() called multiple times! sInstance = " + sInstance);
+ }
+ return sInstance;
}
- return mStatusBarMgr;
}
/**
- * StatusBarMgr implementation
+ * Helper class that's a wrapper around the framework's
+ * StatusBarManager.disable() API.
+ *
+ * This class is used to control features like:
+ *
+ * - Disabling the status bar "notification windowshade"
+ * while the in-call UI is up
+ *
+ * - Disabling notification alerts (audible or vibrating)
+ * while a phone call is active
+ *
+ * - Disabling navigation via the system bar (the "soft buttons" at
+ * the bottom of the screen on devices with no hard buttons)
+ *
+ * We control these features through a single point of control to make
+ * sure that the various StatusBarManager.disable() calls don't
+ * interfere with each other.
*/
- class StatusBarMgr {
- // current settings
+ public class StatusBarHelper {
+ // Current desired state of status bar / system bar behavior
private boolean mIsNotificationEnabled = true;
private boolean mIsExpandedViewEnabled = true;
+ private boolean mIsSystemBarNavigationEnabled = true;
- private StatusBarMgr () {
+ private StatusBarHelper () {
}
/**
- * Sets the notification state (enable / disable
- * vibrating notifications) for the status bar,
- * updates the status bar service if there is a change.
- * Independent of the remaining Status Bar
- * functionality, including icons and expanded view.
+ * Enables or disables auditory / vibrational alerts.
+ *
+ * (We disable these any time a voice call is active, regardless
+ * of whether or not the in-call UI is visible.)
*/
- void enableNotificationAlerts(boolean enable) {
+ public void enableNotificationAlerts(boolean enable) {
if (mIsNotificationEnabled != enable) {
mIsNotificationEnabled = enable;
updateStatusBar();
}
/**
- * Sets the ability to expand the notifications for the
- * status bar, updates the status bar service if there
- * is a change. Independent of the remaining Status Bar
- * functionality, including icons and notification
- * alerts.
+ * Enables or disables the expanded view of the status bar
+ * (i.e. the ability to pull down the "notification windowshade").
+ *
+ * (This feature is disabled by the InCallScreen while the in-call
+ * UI is active.)
*/
- void enableExpandedView(boolean enable) {
+ public void enableExpandedView(boolean enable) {
if (mIsExpandedViewEnabled != enable) {
mIsExpandedViewEnabled = enable;
updateStatusBar();
}
/**
- * Method to synchronize status bar state with our current
- * state.
+ * Enables or disables the navigation via the system bar (the
+ * "soft buttons" at the bottom of the screen)
+ *
+ * (This feature is disabled while an incoming call is ringing,
+ * because it's easy to accidentally touch the system bar while
+ * pulling the phone out of your pocket.)
+ */
+ public void enableSystemBarNavigation(boolean enable) {
+ if (mIsSystemBarNavigationEnabled != enable) {
+ mIsSystemBarNavigationEnabled = enable;
+ updateStatusBar();
+ }
+ }
+
+ /**
+ * Updates the status bar to reflect the current desired state.
*/
- void updateStatusBar() {
+ private void updateStatusBar() {
int state = StatusBarManager.DISABLE_NONE;
if (!mIsExpandedViewEnabled) {
state |= StatusBarManager.DISABLE_EXPAND;
}
-
if (!mIsNotificationEnabled) {
state |= StatusBarManager.DISABLE_NOTIFICATION_ALERTS;
}
+ if (!mIsSystemBarNavigationEnabled) {
+ // Disable *all* possible navigation via the system bar.
+ state |= StatusBarManager.DISABLE_HOME;
+ state |= StatusBarManager.DISABLE_RECENT;
+ state |= StatusBarManager.DISABLE_BACK;
+ }
- // send the message to the status bar manager.
- if (DBG) log("updating status bar state: " + state);
- mStatusBar.disable(state);
+ if (DBG) log("updateStatusBar: state = 0x" + Integer.toHexString(state));
+ mStatusBarManager.disable(state);
}
}
/**
- * Makes sure notifications are up to date.
+ * Makes sure phone-related notifications are up to date on a
+ * freshly-booted device.
*/
- void updateNotifications() {
- if (DBG) log("begin querying call log");
+ private void updateNotificationsAtStartup() {
+ if (DBG) log("updateNotificationsAtStartup()...");
// instantiate query handler
mQueryHandler = new QueryHandler(mContext.getContentResolver());
where.append(" AND new=1");
// start the query
+ if (DBG) log("- start call log 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();
- }
+ // Update (or cancel) the in-call notification
+ if (DBG) log("- updating in-call notification at startup...");
+ updateInCallNotification();
// Depend on android.app.StatusBarManager to be set to
// disable(DISABLE_NONE) upon startup. This will be the
/**
* Handles the query results. There are really 2 steps to this,
- * similar to what happens in RecentCallsListActivity.
+ * similar to what happens in CallLogActivity.
* 1. Find the list of missed calls
* 2. For each call, run a query to retrieve the caller's name.
*/
// Provider.
// At this point, we will execute subqueries on each row just as
- // RecentCallsListActivity.java does.
+ // CallLogActivity.java does.
switch (token) {
case CALL_LOG_TOKEN:
if (DBG) log("call log query complete.");
}
}
+ /**
+ * Configures a Notification to emit the blinky green message-waiting/
+ * missed-call signal.
+ */
+ private static void configureLedNotification(Notification note) {
+ note.flags |= Notification.FLAG_SHOW_LIGHTS;
+ note.defaults |= Notification.DEFAULT_LIGHTS;
+ }
+
/**
* Displays a notification about a missed call.
*
* @param label the label of the number if nameOrNumber is a name, null if it is a number
*/
void notifyMissedCall(String name, String number, String label, long date) {
+ // When the user clicks this notification, we go to the call log.
+ final Intent callLogIntent = PhoneApp.createCallLogIntent();
+
+ // Never display the missed call notification on non-voice-capable
+ // devices, even if the device does somehow manage to get an
+ // incoming call.
+ if (!PhoneApp.sVoiceCapable) {
+ if (DBG) log("notifyMissedCall: non-voice-capable device, not posting notification");
+ return;
+ }
+
// title resource id
int titleResId;
// the text in the notification's line 1 and 2.
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
- ));
+ Notification note = new Notification(
+ android.R.drawable.stat_notify_missed_call, // icon
+ mContext.getString(R.string.notification_missedCallTicker, callName), // tickerText
+ date // when
+ );
+ note.setLatestEventInfo(mContext, mContext.getText(titleResId), expandedText,
+ PendingIntent.getActivity(mContext, 0, callLogIntent, 0));
+ note.flags |= Notification.FLAG_AUTO_CANCEL;
+ // This intent will be called when the notification is dismissed.
+ // It will take care of clearing the list of missed calls.
+ note.deleteIntent = createClearMissedCallsIntent();
+
+ configureLedNotification(note);
+ mNotificationManager.notify(MISSED_CALL_NOTIFICATION, note);
}
+ /** Returns an intent to be invoked when the missed call notification is cleared. */
+ private PendingIntent createClearMissedCallsIntent() {
+ Intent intent = new Intent(mContext, ClearMissedCallsService.class);
+ intent.setAction(ClearMissedCallsService.ACTION_CLEAR_MISSED_CALLS);
+ return PendingIntent.getService(mContext, 0, intent, 0);
+ }
+
+ /**
+ * Cancels the "missed call" notification.
+ *
+ * @see ITelephony.cancelMissedCallsNotification()
+ */
void cancelMissedCallNotification() {
// reset the number of missed calls to 0.
mNumberMissedCalls = 0;
- mNotificationMgr.cancel(MISSED_CALL_NOTIFICATION);
+ mNotificationManager.cancel(MISSED_CALL_NOTIFICATION);
}
- void notifySpeakerphone() {
- if (mSpeakerphoneIcon == null) {
- mSpeakerphoneIcon = mStatusBar.addIcon("speakerphone",
- android.R.drawable.stat_sys_speakerphone, 0);
+ private void notifySpeakerphone() {
+ if (!mShowingSpeakerphoneIcon) {
+ mStatusBarManager.setIcon("speakerphone", android.R.drawable.stat_sys_speakerphone, 0,
+ mContext.getString(R.string.accessibility_speakerphone_enabled));
+ mShowingSpeakerphoneIcon = true;
}
}
- void cancelSpeakerphone() {
- if (mSpeakerphoneIcon != null) {
- mStatusBar.removeIcon(mSpeakerphoneIcon);
- mSpeakerphoneIcon = null;
+ private void cancelSpeakerphone() {
+ if (mShowingSpeakerphoneIcon) {
+ mStatusBarManager.removeIcon("speakerphone");
+ mShowingSpeakerphoneIcon = false;
}
}
/**
- * Calls either notifySpeakerphone() or cancelSpeakerphone() based on
- * the actual current state of the speaker.
+ * Shows or hides the "speakerphone" notification in the status bar,
+ * based on the actual current state of the speaker.
+ *
+ * If you already know the current speaker state (e.g. if you just
+ * called AudioManager.setSpeakerphoneOn() yourself) then you should
+ * directly call {@link updateSpeakerNotification(boolean)} instead.
+ *
+ * (But note that the status bar icon is *never* shown while the in-call UI
+ * is active; it only appears if you bail out to some other activity.)
*/
- void updateSpeakerNotification() {
+ public void updateSpeakerNotification() {
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+ boolean showNotification =
+ (mPhone.getState() == Phone.State.OFFHOOK) && audioManager.isSpeakerphoneOn();
+
+ if (DBG) log(showNotification
+ ? "updateSpeakerNotification: speaker ON"
+ : "updateSpeakerNotification: speaker OFF (or not offhook)");
+
+ updateSpeakerNotification(showNotification);
+ }
+
+ /**
+ * Shows or hides the "speakerphone" notification in the status bar.
+ *
+ * @param showNotification if true, call notifySpeakerphone();
+ * if false, call cancelSpeakerphone().
+ *
+ * Use {@link updateSpeakerNotification()} to update the status bar
+ * based on the actual current state of the speaker.
+ *
+ * (But note that the status bar icon is *never* shown while the in-call UI
+ * is active; it only appears if you bail out to some other activity.)
+ */
+ public void updateSpeakerNotification(boolean showNotification) {
+ if (DBG) log("updateSpeakerNotification(" + showNotification + ")...");
+
+ // Regardless of the value of the showNotification param, suppress
+ // the status bar icon if the the InCallScreen is the foreground
+ // activity, since the in-call UI already provides an onscreen
+ // indication of the speaker state. (This reduces clutter in the
+ // status bar.)
+ if (mApp.isShowingCallScreen()) {
+ cancelSpeakerphone();
+ return;
+ }
- if ((mPhone.getState() == Phone.State.OFFHOOK) && audioManager.isSpeakerphoneOn()) {
- if (DBG) log("updateSpeakerNotification: speaker ON");
+ if (showNotification) {
notifySpeakerphone();
} else {
- if (DBG) log("updateSpeakerNotification: speaker OFF (or not offhook)");
cancelSpeakerphone();
}
}
- void notifyMute() {
- if (mMuteIcon == null) {
- mMuteIcon = mStatusBar.addIcon("mute", android.R.drawable.stat_notify_call_mute, 0);
+ private void notifyMute() {
+ if (!mShowingMuteIcon) {
+ mStatusBarManager.setIcon("mute", android.R.drawable.stat_notify_call_mute, 0,
+ mContext.getString(R.string.accessibility_call_muted));
+ mShowingMuteIcon = true;
}
}
- void cancelMute() {
- if (mMuteIcon != null) {
- mStatusBar.removeIcon(mMuteIcon);
- mMuteIcon = null;
+ private void cancelMute() {
+ if (mShowingMuteIcon) {
+ mStatusBarManager.removeIcon("mute");
+ mShowingMuteIcon = false;
}
}
/**
- * Calls either notifyMute() or cancelMute() based on
- * the actual current mute state of the Phone.
+ * Shows or hides the "mute" notification in the status bar,
+ * based on the current mute state of the Phone.
+ *
+ * (But note that the status bar icon is *never* shown while the in-call UI
+ * is active; it only appears if you bail out to some other activity.)
*/
void updateMuteNotification() {
- if ((mPhone.getState() == Phone.State.OFFHOOK) && mPhone.getMute()) {
+ // Suppress the status bar icon if the the InCallScreen is the
+ // foreground activity, since the in-call UI already provides an
+ // onscreen indication of the mute state. (This reduces clutter
+ // in the status bar.)
+ if (mApp.isShowingCallScreen()) {
+ cancelMute();
+ return;
+ }
+
+ if ((mCM.getState() == Phone.State.OFFHOOK) && PhoneUtils.getMute()) {
if (DBG) log("updateMuteNotification: MUTED");
notifyMute();
} else {
}
}
- void updateInCallNotification() {
+ /**
+ * Updates the phone app's status bar notification based on the
+ * current telephony state, or cancels the notification if the phone
+ * is totally idle.
+ *
+ * This method will never actually launch the incoming-call UI.
+ * (Use updateNotificationAndLaunchIncomingCallUi() for that.)
+ */
+ public void updateInCallNotification() {
+ // allowFullScreenIntent=false means *don't* allow the incoming
+ // call UI to be launched.
+ updateInCallNotification(false);
+ }
+
+ /**
+ * Updates the phone app's status bar notification *and* launches the
+ * incoming call UI in response to a new incoming call.
+ *
+ * This is just like updateInCallNotification(), with one exception:
+ * If an incoming call is ringing (or call-waiting), the notification
+ * will also include a "fullScreenIntent" that will cause the
+ * InCallScreen to be launched immediately, unless the current
+ * foreground activity is marked as "immersive".
+ *
+ * (This is the mechanism that actually brings up the incoming call UI
+ * when we receive a "new ringing connection" event from the telephony
+ * layer.)
+ *
+ * Watch out: this method should ONLY be called directly from the code
+ * path in CallNotifier that handles the "new ringing connection"
+ * event from the telephony layer. All other places that update the
+ * in-call notification (like for phone state changes) should call
+ * updateInCallNotification() instead. (This ensures that we don't
+ * end up launching the InCallScreen multiple times for a single
+ * incoming call, which could cause slow responsiveness and/or visible
+ * glitches.)
+ *
+ * Also note that this method is safe to call even if the phone isn't
+ * actually ringing (or, more likely, if an incoming call *was*
+ * ringing briefly but then disconnected). In that case, we'll simply
+ * update or cancel the in-call notification based on the current
+ * phone state.
+ *
+ * @see updateInCallNotification()
+ */
+ public void updateNotificationAndLaunchIncomingCallUi() {
+ // Set allowFullScreenIntent=true to indicate that we *should*
+ // launch the incoming call UI if necessary.
+ updateInCallNotification(true);
+ }
+
+ /**
+ * Helper method for updateInCallNotification() and
+ * updateNotificationAndLaunchIncomingCallUi(): Update the phone app's
+ * status bar notification based on the current telephony state, or
+ * cancels the notification if the phone is totally idle.
+ *
+ * @param allowLaunchInCallScreen If true, *and* an incoming call is
+ * ringing, the notification will include a "fullScreenIntent"
+ * pointing at the InCallScreen (which will cause the InCallScreen
+ * to be launched.)
+ * Watch out: This should be set to true *only* when directly
+ * handling the "new ringing connection" event from the telephony
+ * layer (see updateNotificationAndLaunchIncomingCallUi().)
+ */
+ private void updateInCallNotification(boolean allowFullScreenIntent) {
int resId;
- if (DBG) log("updateInCallNotification()...");
+ if (DBG) log("updateInCallNotification(allowFullScreenIntent = "
+ + allowFullScreenIntent + ")...");
+
+ // Never display the "ongoing call" notification on
+ // non-voice-capable devices, even if the phone is actually
+ // offhook (like during a non-interactive OTASP call.)
+ if (!PhoneApp.sVoiceCapable) {
+ if (DBG) log("- non-voice-capable device; suppressing notification.");
+ return;
+ }
- if (mPhone.getState() != Phone.State.OFFHOOK) {
+ // If the phone is idle, completely clean up all call-related
+ // notifications.
+ if (mCM.getState() == Phone.State.IDLE) {
+ cancelInCall();
+ cancelMute();
+ cancelSpeakerphone();
return;
}
- final boolean hasActiveCall = !mPhone.getForegroundCall().isIdle();
- final boolean hasHoldingCall = !mPhone.getBackgroundCall().isIdle();
+ final boolean hasRingingCall = mCM.hasActiveRingingCall();
+ final boolean hasActiveCall = mCM.hasActiveFgCall();
+ final boolean hasHoldingCall = mCM.hasActiveBgCall();
+ if (DBG) {
+ log(" - hasRingingCall = " + hasRingingCall);
+ log(" - hasActiveCall = " + hasActiveCall);
+ log(" - hasHoldingCall = " + hasHoldingCall);
+ }
- // Display the appropriate "in-call" icon in the status bar,
- // which depends on the current phone and/or bluetooth state.
+ // Suppress the in-call notification if the InCallScreen is the
+ // foreground activity, since it's already obvious that you're on a
+ // call. (The status bar icon is needed only if you navigate *away*
+ // from the in-call UI.)
+ boolean suppressNotification = mApp.isShowingCallScreen();
+
+ // ...except for a couple of cases where we *never* suppress the
+ // notification:
+ //
+ // - If there's an incoming ringing call: always show the
+ // notification, since the in-call notification is what actually
+ // launches the incoming call UI in the first place (see
+ // notification.fullScreenIntent below.) This makes sure that we'll
+ // correctly handle the case where a new incoming call comes in but
+ // the InCallScreen is already in the foreground.
+ if (hasRingingCall) suppressNotification = false;
+
+ // - If "voice privacy" mode is active: always show the notification,
+ // since that's the only "voice privacy" indication we have.
+ boolean enhancedVoicePrivacy = mApp.notifier.getVoicePrivacyState();
+ if (DBG) log("updateInCallNotification: enhancedVoicePrivacy = " + enhancedVoicePrivacy);
+ if (enhancedVoicePrivacy) suppressNotification = false;
+ if (suppressNotification) {
+ cancelInCall();
+ // Suppress the mute and speaker status bar icons too
+ // (also to reduce clutter in the status bar.)
+ cancelSpeakerphone();
+ cancelMute();
+ return;
+ }
- boolean enhancedVoicePrivacy = PhoneApp.getInstance().notifier.getCdmaVoicePrivacyState();
- if (DBG) log("updateInCallNotification: enhancedVoicePrivacy = " + enhancedVoicePrivacy);
+ // Display the appropriate icon in the status bar,
+ // based on the current phone and/or bluetooth state.
- if (!hasActiveCall && hasHoldingCall) {
+
+ if (hasRingingCall) {
+ // There's an incoming ringing call.
+ resId = R.drawable.stat_sys_phone_call_ringing;
+ } else if (!hasActiveCall && hasHoldingCall) {
// There's only one call, and it's on hold.
if (enhancedVoicePrivacy) {
- resId = android.R.drawable.stat_sys_vp_phone_call_on_hold;
+ resId = R.drawable.stat_sys_vp_phone_call_on_hold;
} else {
- resId = android.R.drawable.stat_sys_phone_call_on_hold;
+ resId = R.drawable.stat_sys_phone_call_on_hold;
}
- } else if (PhoneApp.getInstance().showBluetoothIndication()) {
+ } else if (mApp.showBluetoothIndication()) {
// Bluetooth is active.
if (enhancedVoicePrivacy) {
- resId = com.android.internal.R.drawable.stat_sys_vp_phone_call_bluetooth;
+ resId = R.drawable.stat_sys_vp_phone_call_bluetooth;
} else {
- resId = com.android.internal.R.drawable.stat_sys_phone_call_bluetooth;
+ resId = R.drawable.stat_sys_phone_call_bluetooth;
}
} else {
if (enhancedVoicePrivacy) {
- resId = android.R.drawable.stat_sys_vp_phone_call;
+ resId = R.drawable.stat_sys_vp_phone_call;
} else {
- resId = android.R.drawable.stat_sys_phone_call;
+ resId = R.drawable.stat_sys_phone_call;
}
}
// 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);
+ if (DBG) log("- Updating status bar icon: resId = " + 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;
+ // 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.)
// 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();
+ // different calls. So if both lines are in use, display info
+ // from the foreground call. And if there's a ringing call,
+ // display that regardless of the state of the other calls.
+
+ Call currentCall;
+ if (hasRingingCall) {
+ currentCall = mCM.getFirstActiveRingingCall();
+ } else if (hasActiveCall) {
+ currentCall = mCM.getActiveFgCall();
+ } else {
+ currentCall = mCM.getFirstActiveBgCall();
+ }
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;
+ // PendingIntent that can be used to launch the InCallScreen. The
+ // system fires off this intent if the user pulls down the windowshade
+ // and clicks the notification's expanded view. It's also used to
+ // launch the InCallScreen immediately when when there's an incoming
+ // call (see the "fullScreenIntent" field below).
+ PendingIntent inCallPendingIntent =
+ PendingIntent.getActivity(mContext, 0,
+ PhoneApp.createInCallIntent(), 0);
+ notification.contentIntent = inCallPendingIntent;
+
+ // When expanded, the "Ongoing call" notification is (visually)
+ // different from most other Notifications, so we need to use a
+ // custom view hierarchy.
// 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
// 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.
+ if (hasRingingCall) {
+ // Incoming call is ringing.
+ // Note this isn't a format string! (We want "Incoming call"
+ // here, not "Incoming call (1:23)".) But 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_incoming_call);
+ } else if (hasHoldingCall && !hasActiveCall) {
+ // Only one call, and it's on hold.
+ // Note this isn't a format string either (see comment above.)
expandedViewLine1 = mContext.getString(R.string.notification_on_hold);
} else {
+ // Normal ongoing call.
// 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 + "'");
+ if (DBG) log("- Updating expanded view: line 1 '" + /*expandedViewLine1*/ "xxxxxxx" + "'");
// Text line #1 is actually a Chronometer, not a plain TextView.
// We format the elapsed time of the current call into a line like
expandedViewLine1,
true);
} else if (DBG) {
- log("updateInCallNotification: connection is null, call status not updated.");
+ Log.w(LOG_TAG, "updateInCallNotification: null connection, can't set exp view line 1.");
}
// display conference call string if this call is a conference
// call, otherwise display the connection information.
+ // Line 2 of the expanded view (smaller text). This is usually a
+ // contact name or phone number.
+ String expandedViewLine2 = "";
// 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.
+ // If necessary, start asynchronous query to do the caller-id lookup.
PhoneUtils.CallerInfoToken cit =
- PhoneUtils.startGetCallerInfo (mContext, currentCall, this, contentView);
- // Line 2 of the expanded view (smaller text):
+ PhoneUtils.startGetCallerInfo(mContext, currentCall, this, this);
expandedViewLine2 = PhoneUtils.getCompactNameFromCallerInfo(cit.currentInfo, mContext);
+ // Note: For an incoming call, the very first time we get here we
+ // won't have a contact name yet, since we only just started the
+ // caller-id query. So expandedViewLine2 will start off as a raw
+ // phone number, but we'll update it very quickly when the query
+ // completes (see onQueryComplete() below.)
}
- if (DBG) log("- Updating expanded view: line 2 '" + expandedViewLine2 + "'");
- contentView.setTextViewText(R.id.text2, expandedViewLine2);
+ if (DBG) log("- Updating expanded view: line 2 '" + /*expandedViewLine2*/ "xxxxxxx" + "'");
+ contentView.setTextViewText(R.id.title, expandedViewLine2);
notification.contentView = contentView;
// TODO: We also need to *update* this notification in some cases,
// line), and maybe even when the user swaps calls (ie. if we only
// show info here for the "current active call".)
+ // Activate a couple of special Notification features if an
+ // incoming call is ringing:
+ if (hasRingingCall) {
+ if (DBG) log("- Using hi-pri notification for ringing call!");
+
+ // This is a high-priority event that should be shown even if the
+ // status bar is hidden or if an immersive activity is running.
+ notification.flags |= Notification.FLAG_HIGH_PRIORITY;
+
+ // If an immersive activity is running, we have room for a single
+ // line of text in the small notification popup window.
+ // We use expandedViewLine2 for this (i.e. the name or number of
+ // the incoming caller), since that's more relevant than
+ // expandedViewLine1 (which is something generic like "Incoming
+ // call".)
+ notification.tickerText = expandedViewLine2;
+
+ if (allowFullScreenIntent) {
+ // Ok, we actually want to launch the incoming call
+ // UI at this point (in addition to simply posting a notification
+ // to the status bar). Setting fullScreenIntent will cause
+ // the InCallScreen to be launched immediately *unless* the
+ // current foreground activity is marked as "immersive".
+ if (DBG) log("- Setting fullScreenIntent: " + inCallPendingIntent);
+ notification.fullScreenIntent = inCallPendingIntent;
+
+ // Ugly hack alert:
+ //
+ // The NotificationManager has the (undocumented) behavior
+ // that it will *ignore* the fullScreenIntent field if you
+ // post a new Notification that matches the ID of one that's
+ // already active. Unfortunately this is exactly what happens
+ // when you get an incoming call-waiting call: the
+ // "ongoing call" notification is already visible, so the
+ // InCallScreen won't get launched in this case!
+ // (The result: if you bail out of the in-call UI while on a
+ // call and then get a call-waiting call, the incoming call UI
+ // won't come up automatically.)
+ //
+ // The workaround is to just notice this exact case (this is a
+ // call-waiting call *and* the InCallScreen is not in the
+ // foreground) and manually cancel the in-call notification
+ // before (re)posting it.
+ //
+ // TODO: there should be a cleaner way of avoiding this
+ // problem (see discussion in bug 3184149.)
+ Call ringingCall = mCM.getFirstActiveRingingCall();
+ if ((ringingCall.getState() == Call.State.WAITING) && !mApp.isShowingCallScreen()) {
+ Log.i(LOG_TAG, "updateInCallNotification: call-waiting! force relaunch...");
+ // Cancel the IN_CALL_NOTIFICATION immediately before
+ // (re)posting it; this seems to force the
+ // NotificationManager to launch the fullScreenIntent.
+ mNotificationManager.cancel(IN_CALL_NOTIFICATION);
+ }
+ }
+ }
+
if (DBG) log("Notifying IN_CALL_NOTIFICATION: " + notification);
- mNotificationMgr.notify(IN_CALL_NOTIFICATION,
+ mNotificationManager.notify(IN_CALL_NOTIFICATION,
notification);
// Finally, refresh the mute and speakerphone notifications (since
* refreshes the contentView when called.
*/
public void onQueryComplete(int token, Object cookie, CallerInfo ci){
- if (DBG) log("callerinfo query complete, updating ui.");
-
- ((RemoteViews) cookie).setTextViewText(R.id.text2,
- PhoneUtils.getCompactNameFromCallerInfo(ci, mContext));
+ if (DBG) log("CallerInfo query complete (for NotificationMgr), "
+ + "updating in-call notification..");
+ if (DBG) log("- cookie: " + cookie);
+ if (DBG) log("- ci: " + ci);
+
+ if (cookie == this) {
+ // Ok, this is the caller-id query we fired off in
+ // updateInCallNotification(), presumably when an incoming call
+ // first appeared. If the caller-id info matched any contacts,
+ // compactName should now be a real person name rather than a raw
+ // phone number:
+ if (DBG) log("- compactName is now: "
+ + PhoneUtils.getCompactNameFromCallerInfo(ci, mContext));
+
+ // Now that our CallerInfo object has been fully filled-in,
+ // refresh the in-call notification.
+ if (DBG) log("- updating notification after query complete...");
+ updateInCallNotification();
+ } else {
+ Log.w(LOG_TAG, "onQueryComplete: caller-id query from unknown source! "
+ + "cookie = " + cookie);
+ }
}
+ /**
+ * Take down the in-call notification.
+ * @see updateInCallNotification()
+ */
private void cancelInCall() {
if (DBG) log("cancelInCall()...");
- cancelMute();
- cancelSpeakerphone();
- mNotificationMgr.cancel(IN_CALL_NOTIFICATION);
+ mNotificationManager.cancel(IN_CALL_NOTIFICATION);
mInCallResId = 0;
}
- void cancelCallInProgressNotification() {
- if (DBG) log("cancelCallInProgressNotification()...");
+ /**
+ * Completely take down the in-call notification *and* the mute/speaker
+ * notifications as well, to indicate that the phone is now idle.
+ */
+ /* package */ void cancelCallInProgressNotifications() {
+ if (DBG) log("cancelCallInProgressNotifications()...");
if (mInCallResId == 0) {
return;
}
- if (DBG) log("cancelCallInProgressNotification: " + mInCallResId);
+ if (DBG) log("cancelCallInProgressNotifications: " + mInCallResId);
cancelInCall();
+ cancelMute();
+ cancelSpeakerphone();
}
/**
// 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);
+ mApp.notifier.sendMwiChangedDelayed(VM_NUMBER_RETRY_DELAY_MILLIS);
return;
} else {
Log.w(LOG_TAG, "NotificationMgr.updateMwi: getVoiceMailNumber() failed after "
}
}
- if (mPhone.getPhoneType() == Phone.PHONE_TYPE_CDMA) {
+ if (TelephonyCapabilities.supportsVoiceMessageCount(mPhone)) {
int vmCount = mPhone.getVoiceMessageCount();
String titleFormat = mContext.getString(R.string.notification_voicemail_title_count);
notificationTitle = String.format(titleFormat, vmCount);
}
Intent intent = new Intent(Intent.ACTION_CALL,
- Uri.fromParts("voicemail", "", null));
+ Uri.fromParts(Constants.SCHEME_VOICEMAIL, "", null));
PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
Notification notification = new Notification(
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);
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
+ String vibrateWhen = prefs.getString(
+ CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_VIBRATE_WHEN_KEY, "never");
+ boolean vibrateAlways = vibrateWhen.equals("always");
+ boolean vibrateSilent = vibrateWhen.equals("silent");
+ AudioManager audioManager =
+ (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+ boolean nowSilent = audioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE;
+ if (vibrateAlways || (vibrateSilent && nowSilent)) {
+ notification.defaults |= Notification.DEFAULT_VIBRATE;
+ }
+
+ notification.flags |= Notification.FLAG_NO_CLEAR;
+ configureLedNotification(notification);
+ mNotificationManager.notify(VOICEMAIL_NOTIFICATION, notification);
} else {
- mNotificationMgr.cancel(VOICEMAIL_NOTIFICATION);
+ mNotificationManager.cancel(VOICEMAIL_NOTIFICATION);
}
}
"com.android.phone.CallFeaturesSetting");
notification = new Notification(
- mContext, // context
- android.R.drawable.stat_sys_phone_call_forward, // icon
+ R.drawable.stat_sys_phone_call_forward, // icon
null, // tickerText
- 0, // The "timestamp" of this notification is meaningless;
+ 0); // The "timestamp" of this notification is meaningless;
// we only care about whether CFI is currently on or not.
+ notification.setLatestEventInfo(
+ mContext, // context
mContext.getString(R.string.labelCF), // expandedTitle
- mContext.getString(R.string.sum_cfu_enabled_indicator), // expandedText
- intent // contentIntent
- );
-
+ mContext.getString(R.string.sum_cfu_enabled_indicator), // expandedText
+ PendingIntent.getActivity(mContext, 0, intent, 0)); // contentIntent
} else {
notification = new Notification(
- android.R.drawable.stat_sys_phone_call_forward, // icon
+ 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(
+ mNotificationManager.notify(
CALL_FORWARD_NOTIFICATION,
notification);
} else {
- mNotificationMgr.cancel(CALL_FORWARD_NOTIFICATION);
+ mNotificationManager.cancel(CALL_FORWARD_NOTIFICATION);
}
}
if (DBG) log("showDataDisconnectedRoaming()...");
Intent intent = new Intent(mContext,
- Settings.class); // "Mobile network settings" screen
+ com.android.phone.Settings.class); // "Mobile network settings" screen / dialog
Notification notification = new Notification(
- mContext, // context
- android.R.drawable.stat_sys_warning, // icon
+ android.R.drawable.stat_sys_warning, // icon
null, // tickerText
- System.currentTimeMillis(),
+ System.currentTimeMillis());
+ notification.setLatestEventInfo(
+ mContext, // Context
mContext.getString(R.string.roaming), // expandedTitle
- mContext.getString(R.string.roaming_reenable_message), // expandedText
- intent // contentIntent
- );
- mNotificationMgr.notify(
+ mContext.getString(R.string.roaming_reenable_message), // expandedText
+ PendingIntent.getActivity(mContext, 0, intent, 0)); // contentIntent
+
+ mNotificationManager.notify(
DATA_DISCONNECTED_ROAMING_NOTIFICATION,
notification);
}
*/
/* package */ void hideDataDisconnectedRoaming() {
if (DBG) log("hideDataDisconnectedRoaming()...");
- mNotificationMgr.cancel(DATA_DISCONNECTED_ROAMING_NOTIFICATION);
+ mNotificationManager.cancel(DATA_DISCONNECTED_ROAMING_NOTIFICATION);
}
/**
R.string.notification_network_selection_text, operator);
Notification notification = new Notification();
- notification.icon = com.android.internal.R.drawable.stat_sys_warning;
+ notification.icon = android.R.drawable.stat_sys_warning;
notification.when = 0;
notification.flags = Notification.FLAG_ONGOING_EVENT;
notification.tickerText = null;
notification.setLatestEventInfo(mContext, titleText, expandedText, pi);
- mNotificationMgr.notify(SELECTED_OPERATOR_FAIL_NOTIFICATION, notification);
+ mNotificationManager.notify(SELECTED_OPERATOR_FAIL_NOTIFICATION, notification);
}
/**
*/
private void cancelNetworkSelection() {
if (DBG) log("cancelNetworkSelection()...");
- mNotificationMgr.cancel(SELECTED_OPERATOR_FAIL_NOTIFICATION);
+ mNotificationManager.cancel(SELECTED_OPERATOR_FAIL_NOTIFICATION);
}
/**
* @param serviceState Phone service state
*/
void updateNetworkSelection(int serviceState) {
- if (mPhone.getPhoneType() == Phone.PHONE_TYPE_GSM) {
+ if (TelephonyCapabilities.supportsNetworkSelection(mPhone)) {
// get the shared preference of network_selection.
// empty is auto mode, otherwise it is the operator alpha name
// in case there is no operator name, check the operator numeric