Created
August 16, 2010 15:21
-
-
Save CedricGatay/527115 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright (C) 2006 The Android Open Source Project | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
package com.android.phone; | |
import android.app.Notification; | |
import android.app.NotificationManager; | |
import android.app.PendingIntent; | |
import android.app.StatusBarManager; | |
import android.content.AsyncQueryHandler; | |
import android.content.ComponentName; | |
import android.content.ContentResolver; | |
import android.content.Context; | |
import android.content.Intent; | |
import android.content.SharedPreferences; | |
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 android.telephony.ServiceState; | |
import android.text.TextUtils; | |
import android.util.Log; | |
import android.widget.RemoteViews; | |
import android.widget.Toast; | |
import com.android.internal.telephony.Call; | |
import com.android.internal.telephony.CallerInfo; | |
import com.android.internal.telephony.CallerInfoAsyncQuery; | |
import com.android.internal.telephony.Connection; | |
import com.android.internal.telephony.Phone; | |
import com.android.internal.telephony.PhoneBase; | |
import android.preference.PreferenceManager; | |
import java.io.FileInputStream; | |
import java.io.FileOutputStream; | |
/** | |
* NotificationManager-related utility code for the Phone app. | |
*/ | |
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 String[] CALL_LOG_PROJECTION = new String[] { | |
Calls._ID, | |
Calls.NUMBER, | |
Calls.DATE, | |
Calls.DURATION, | |
Calls.TYPE, | |
}; | |
// notification types | |
static final int MISSED_CALL_NOTIFICATION = 1; | |
static final int IN_CALL_NOTIFICATION = 2; | |
static final int MMI_NOTIFICATION = 3; | |
static final int NETWORK_SELECTION_NOTIFICATION = 4; | |
static final int VOICEMAIL_NOTIFICATION = 5; | |
static final int CALL_FORWARD_NOTIFICATION = 6; | |
static final int DATA_DISCONNECTED_ROAMING_NOTIFICATION = 7; | |
static final int SELECTED_OPERATOR_FAIL_NOTIFICATION = 8; | |
private static NotificationMgr sMe = null; | |
private Phone mPhone; | |
private Context mContext; | |
private NotificationManager mNotificationMgr; | |
private StatusBarManager mStatusBar; | |
private StatusBarMgr mStatusBarMgr; | |
private Toast mToast; | |
private IBinder mSpeakerphoneIcon; | |
private IBinder mMuteIcon; | |
private CallFeaturesSetting mSettings; | |
// used to track the missed call counter, default to 0. | |
private int mNumberMissedCalls = 0; | |
// Currently-displayed resource IDs for some status bar icons (or zero | |
// if no notification is active): | |
private int mInCallResId; | |
// used to track the notification of selected network unavailable | |
private boolean mSelectedUnavailableNotify = false; | |
// Retry params for the getVoiceMailNumber() call; see updateMwi(). | |
private static final int MAX_VM_NUMBER_RETRIES = 5; | |
private static final int VM_NUMBER_RETRY_DELAY_MILLIS = 10000; | |
private int mVmNumberRetriesRemaining = MAX_VM_NUMBER_RETRIES; | |
// Query used to look up caller-id info for the "call log" notification. | |
private QueryHandler mQueryHandler = null; | |
private static final int CALL_LOG_TOKEN = -1; | |
private static final int CONTACT_TOKEN = -2; | |
NotificationMgr(Context context) { | |
mContext = context; | |
mSettings = CallFeaturesSetting.getInstance(PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext())); | |
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.updateNotificationsAtStartup(); | |
} | |
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. | |
*/ | |
/** | |
* Factory method | |
*/ | |
StatusBarMgr getStatusBarMgr() { | |
if (mStatusBarMgr == null) { | |
mStatusBarMgr = new StatusBarMgr(); | |
} | |
return mStatusBarMgr; | |
} | |
/** | |
* StatusBarMgr implementation | |
*/ | |
class StatusBarMgr { | |
// current settings | |
private boolean mIsNotificationEnabled = true; | |
private boolean mIsExpandedViewEnabled = true; | |
private StatusBarMgr () { | |
} | |
/** | |
* 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. | |
*/ | |
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. | |
*/ | |
void enableExpandedView(boolean enable) { | |
if (mIsExpandedViewEnabled != enable) { | |
mIsExpandedViewEnabled = enable; | |
updateStatusBar(); | |
} | |
} | |
/** | |
* Method to synchronize status bar state with our current | |
* state. | |
*/ | |
void updateStatusBar() { | |
int state = StatusBarManager.DISABLE_NONE; | |
if (!mIsExpandedViewEnabled) { | |
state |= StatusBarManager.DISABLE_EXPAND; | |
} | |
if (!mIsNotificationEnabled) { | |
state |= StatusBarManager.DISABLE_NOTIFICATION_ALERTS; | |
} | |
// send the message to the status bar manager. | |
if (DBG) log("updating status bar state: " + state); | |
mStatusBar.disable(state); | |
} | |
} | |
/** | |
* Makes sure phone-related notifications are up to date on a | |
* freshly-booted device. | |
*/ | |
private void updateNotificationsAtStartup() { | |
if (DBG) log("updateNotificationsAtStartup()..."); | |
// 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. | |
} | |
/** The projection to use when querying the phones table */ | |
static final String[] PHONES_PROJECTION = new String[] { | |
PhoneLookup.NUMBER, | |
PhoneLookup.DISPLAY_NAME | |
}; | |
/** | |
* Class used to run asynchronous queries to re-populate | |
* the notifications we care about. | |
*/ | |
private class QueryHandler extends AsyncQueryHandler { | |
/** | |
* Used to store relevant fields for the Missed Call | |
* notifications. | |
*/ | |
private class NotificationInfo { | |
public String name; | |
public String number; | |
public String label; | |
public long date; | |
} | |
public QueryHandler(ContentResolver cr) { | |
super(cr); | |
} | |
/** | |
* Handles the query results. There are really 2 steps to this, | |
* similar to what happens in RecentCallsListActivity. | |
* 1. Find the list of missed calls | |
* 2. For each call, run a query to retrieve the caller's name. | |
*/ | |
@Override | |
protected void onQueryComplete(int token, Object cookie, Cursor cursor) { | |
// TODO: it would be faster to use a join here, but for the purposes | |
// of this small record set, it should be ok. | |
// Note that CursorJoiner is not useable here because the number | |
// comparisons are not strictly equals; the comparisons happen in | |
// the SQL function PHONE_NUMBERS_EQUAL, which is not available for | |
// the CursorJoiner. | |
// Executing our own query is also feasible (with a join), but that | |
// will require some work (possibly destabilizing) in Contacts | |
// Provider. | |
// At this point, we will execute subqueries on each row just as | |
// RecentCallsListActivity.java does. | |
switch (token) { | |
case CALL_LOG_TOKEN: | |
if (DBG) log("call log query complete."); | |
// initial call to retrieve the call list. | |
if (cursor != null) { | |
while (cursor.moveToNext()) { | |
// for each call in the call log list, create | |
// the notification object and query contacts | |
NotificationInfo n = getNotificationInfo (cursor); | |
if (DBG) log("query contacts for number: " + n.number); | |
mQueryHandler.startQuery(CONTACT_TOKEN, n, | |
Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, n.number), | |
PHONES_PROJECTION, null, null, PhoneLookup.NUMBER); | |
} | |
if (DBG) log("closing call log cursor."); | |
cursor.close(); | |
} | |
break; | |
case CONTACT_TOKEN: | |
if (DBG) log("contact query complete."); | |
// subqueries to get the caller name. | |
if ((cursor != null) && (cookie != null)){ | |
NotificationInfo n = (NotificationInfo) cookie; | |
if (cursor.moveToFirst()) { | |
// we have contacts data, get the name. | |
if (DBG) log("contact :" + n.name + " found for phone: " + n.number); | |
n.name = cursor.getString( | |
cursor.getColumnIndexOrThrow(PhoneLookup.DISPLAY_NAME)); | |
} | |
// send the notification | |
if (DBG) log("sending notification."); | |
notifyMissedCall(n.name, n.number, n.label, n.date); | |
if (DBG) log("closing contact cursor."); | |
cursor.close(); | |
} | |
break; | |
default: | |
} | |
} | |
/** | |
* Factory method to generate a NotificationInfo object given a | |
* cursor from the call log table. | |
*/ | |
private final NotificationInfo getNotificationInfo(Cursor cursor) { | |
NotificationInfo n = new NotificationInfo(); | |
n.name = null; | |
n.number = cursor.getString(cursor.getColumnIndexOrThrow(Calls.NUMBER)); | |
n.label = cursor.getString(cursor.getColumnIndexOrThrow(Calls.TYPE)); | |
n.date = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.DATE)); | |
// make sure we update the number depending upon saved values in | |
// CallLog.addCall(). If either special values for unknown or | |
// private number are detected, we need to hand off the message | |
// to the missed call notification. | |
if ( (n.number.equals(CallerInfo.UNKNOWN_NUMBER)) || | |
(n.number.equals(CallerInfo.PRIVATE_NUMBER)) || | |
(n.number.equals(CallerInfo.PAYPHONE_NUMBER)) ) { | |
n.number = null; | |
} | |
if (DBG) log("NotificationInfo constructed for number: " + n.number); | |
return n; | |
} | |
} | |
/** | |
* Configures a Notification to emit the blinky green message-waiting/ | |
* missed-call signal. | |
*/ | |
private void configureLedNotification(Notification note) { | |
// note.flags |= Notification.FLAG_SHOW_LIGHTS; | |
// note.ledARGB = 0xff00ffff; | |
// note.ledOnMS = 500; | |
// note.ledOffMS = 2000; | |
mLiquidLEDHack.setLEDEnabled(true); | |
} | |
/** | |
* 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 | |
*/ | |
void notifyMissedCall(String name, String number, String label, long date) { | |
// 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 | |
Notification note = 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 | |
); | |
if (mSettings.mLedNotify) configureLedNotification(note); | |
mNotificationMgr.notify(MISSED_CALL_NOTIFICATION, note); | |
} | |
void cancelMissedCallNotification() { | |
// reset the number of missed calls to 0. | |
mNumberMissedCalls = 0; | |
mNotificationMgr.cancel(MISSED_CALL_NOTIFICATION); | |
mLiquidLEDHack.setLEDEnabled(false); | |
} | |
void notifySpeakerphone() { | |
if (mSpeakerphoneIcon == null) { | |
mSpeakerphoneIcon = mStatusBar.addIcon("speakerphone", | |
android.R.drawable.stat_sys_speakerphone, 0); | |
} | |
} | |
void cancelSpeakerphone() { | |
if (mSpeakerphoneIcon != null) { | |
mStatusBar.removeIcon(mSpeakerphoneIcon); | |
mSpeakerphoneIcon = null; | |
} | |
} | |
/** | |
* Calls either notifySpeakerphone() or cancelSpeakerphone() based on | |
* the actual current state of the speaker. | |
*/ | |
void updateSpeakerNotification() { | |
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(); | |
} | |
} | |
void notifyMute() { | |
if (mMuteIcon == null) { | |
mMuteIcon = mStatusBar.addIcon("mute", android.R.drawable.stat_notify_call_mute, 0); | |
} | |
} | |
void cancelMute() { | |
if (mMuteIcon != null) { | |
mStatusBar.removeIcon(mMuteIcon); | |
mMuteIcon = null; | |
} | |
} | |
/** | |
* Calls either notifyMute() or cancelMute() based on | |
* the actual current mute state of the Phone. | |
*/ | |
void updateMuteNotification() { | |
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(); | |
} | |
} | |
void updateInCallNotification() { | |
int resId; | |
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. | |
boolean enhancedVoicePrivacy = PhoneApp.getInstance().notifier.getCdmaVoicePrivacyState(); | |
if (DBG) log("updateInCallNotification: enhancedVoicePrivacy = " + enhancedVoicePrivacy); | |
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; | |
} else { | |
resId = android.R.drawable.stat_sys_phone_call_on_hold; | |
} | |
} else if (PhoneApp.getInstance().showBluetoothIndication()) { | |
// Bluetooth is active. | |
if (enhancedVoicePrivacy) { | |
resId = com.android.internal.R.drawable.stat_sys_vp_phone_call_bluetooth; | |
} else { | |
resId = com.android.internal.R.drawable.stat_sys_phone_call_bluetooth; | |
} | |
} else { | |
if (enhancedVoicePrivacy) { | |
resId = android.R.drawable.stat_sys_vp_phone_call; | |
} else { | |
resId = android.R.drawable.stat_sys_phone_call; | |
} | |
} | |
// 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. | |
if (callDurationMsec > 0) { | |
expandedViewLine1 = mContext.getString(R.string.notification_ongoing_call_format); | |
} else { | |
expandedViewLine1 = mContext.getString(R.string.notification_ongoing_calling_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(); | |
} | |
/** | |
* Implemented for CallerInfoAsyncQuery.OnQueryCompleteListener interface. | |
* 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)); | |
} | |
private void cancelInCall() { | |
if (DBG) log("cancelInCall()..."); | |
cancelMute(); | |
cancelSpeakerphone(); | |
mNotificationMgr.cancel(IN_CALL_NOTIFICATION); | |
mInCallResId = 0; | |
} | |
void cancelCallInProgressNotification() { | |
if (DBG) log("cancelCallInProgressNotification()..."); | |
if (mInCallResId == 0) { | |
return; | |
} | |
if (DBG) log("cancelCallInProgressNotification: " + mInCallResId); | |
cancelInCall(); | |
} | |
/** | |
* Updates the message waiting indicator (voicemail) notification. | |
* | |
* @param visible true if there are messages waiting | |
*/ | |
/* package */ void updateMwi(boolean visible) { | |
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.getIccRecordsLoaded()) { | |
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. | |
} | |
} | |
if (mPhone.getPhoneType() == Phone.PHONE_TYPE_CDMA) { | |
int vmCount = mPhone.getVoiceMessageCount(); | |
String titleFormat = mContext.getString(R.string.notification_voicemail_title_count); | |
notificationTitle = String.format(titleFormat, vmCount); | |
} | |
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; | |
if (mSettings.mLedNotify) configureLedNotification(notification); | |
mNotificationMgr.notify(VOICEMAIL_NOTIFICATION, notification); | |
} else { | |
mNotificationMgr.cancel(VOICEMAIL_NOTIFICATION); | |
} | |
} | |
/** | |
* Updates the message call forwarding indicator notification. | |
* | |
* @param visible true if there are messages waiting | |
*/ | |
/* package */ void updateCfi(boolean visible) { | |
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); | |
} | |
} | |
/** | |
* 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. | |
*/ | |
/* package */ void showDataDisconnectedRoaming() { | |
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); | |
} | |
/** | |
* Turns off the "data disconnected due to roaming" notification. | |
*/ | |
/* package */ void hideDataDisconnectedRoaming() { | |
if (DBG) log("hideDataDisconnectedRoaming()..."); | |
mNotificationMgr.cancel(DATA_DISCONNECTED_ROAMING_NOTIFICATION); | |
} | |
/** | |
* Display the network selection "no service" notification | |
* @param operator is the numeric operator number | |
*/ | |
private void showNetworkSelection(String operator) { | |
if (DBG) log("showNetworkSelection(" + operator + ")..."); | |
String titleText = mContext.getString( | |
R.string.notification_network_selection_title); | |
String expandedText = mContext.getString( | |
R.string.notification_network_selection_text, operator); | |
Notification notification = new Notification(); | |
notification.icon = com.android.internal.R.drawable.stat_sys_warning; | |
notification.when = 0; | |
notification.flags = Notification.FLAG_ONGOING_EVENT; | |
notification.tickerText = null; | |
// create the target network operators settings intent | |
Intent intent = new Intent(Intent.ACTION_MAIN); | |
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | | |
Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); | |
// Use NetworkSetting to handle the selection intent | |
intent.setComponent(new ComponentName("com.android.phone", | |
"com.android.phone.NetworkSetting")); | |
PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0); | |
notification.setLatestEventInfo(mContext, titleText, expandedText, pi); | |
mNotificationMgr.notify(SELECTED_OPERATOR_FAIL_NOTIFICATION, notification); | |
} | |
/** | |
* Turn off the network selection "no service" notification | |
*/ | |
private void cancelNetworkSelection() { | |
if (DBG) log("cancelNetworkSelection()..."); | |
mNotificationMgr.cancel(SELECTED_OPERATOR_FAIL_NOTIFICATION); | |
} | |
/** | |
* Update notification about no service of user selected operator | |
* | |
* @param serviceState Phone service state | |
*/ | |
void updateNetworkSelection(int serviceState) { | |
if (mPhone.getPhoneType() == Phone.PHONE_TYPE_GSM) { | |
// 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 | |
SharedPreferences sp = | |
PreferenceManager.getDefaultSharedPreferences(mContext); | |
String networkSelection = | |
sp.getString(PhoneBase.NETWORK_SELECTION_NAME_KEY, ""); | |
if (TextUtils.isEmpty(networkSelection)) { | |
networkSelection = | |
sp.getString(PhoneBase.NETWORK_SELECTION_KEY, ""); | |
} | |
if (DBG) log("updateNetworkSelection()..." + "state = " + | |
serviceState + " new network " + networkSelection); | |
if (serviceState == ServiceState.STATE_OUT_OF_SERVICE | |
&& !TextUtils.isEmpty(networkSelection)) { | |
if (!mSelectedUnavailableNotify) { | |
showNetworkSelection(networkSelection); | |
mSelectedUnavailableNotify = true; | |
} | |
} else { | |
if (mSelectedUnavailableNotify) { | |
cancelNetworkSelection(); | |
mSelectedUnavailableNotify = false; | |
} | |
} | |
} | |
} | |
/* package */ void postTransientNotification(int notifyId, CharSequence msg) { | |
if (mToast != null) { | |
mToast.cancel(); | |
} | |
mToast = Toast.makeText(mContext, msg, Toast.LENGTH_LONG); | |
mToast.show(); | |
} | |
private void log(String msg) { | |
Log.d(LOG_TAG, msg); | |
} | |
private final class LiquidCallLEDHack{ | |
private static final String TAG = "LiquidCallLEDHack"; | |
private static final String LED_FILE = "/sys/class/leds2/call"; | |
public boolean getLEDEnabled() { | |
try { | |
FileInputStream fis = new FileInputStream(LED_FILE); | |
int result = fis.read(); | |
fis.close(); | |
return (result != '0'); | |
} catch (Exception e) { | |
Log.e(TAG, "setLEDEnabled failed", e); | |
return false; | |
} | |
} | |
public void setLEDEnabled(boolean on) { | |
try { | |
FileOutputStream fos = new FileOutputStream(LED_FILE); | |
byte[] bytes = new byte[2]; | |
bytes[0] = (byte)(on ? '3' : '0'); | |
bytes[1] = '\n'; | |
fos.write(bytes); | |
fos.close(); | |
} catch (Exception e) { | |
Log.e(TAG, "setLEDEnabled failed", e); | |
} | |
} | |
} | |
private LiquidCallLEDHack mLiquidLEDHack = new LiquidCallLEDHack(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment