StatusBarNotifier.java revision 73e8dc0225c601f7203dd4d12e4f7653a1f9a9b0
1/* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.incallui; 18 19import com.google.common.base.Preconditions; 20 21import android.app.Notification; 22import android.app.NotificationManager; 23import android.app.PendingIntent; 24import android.content.Context; 25import android.content.Intent; 26 27import com.android.incallui.InCallApp.NotificationBroadcastReceiver; 28import com.android.incallui.InCallPresenter.InCallState; 29import com.android.services.telephony.common.Call; 30 31/** 32 * This class adds Notifications to the status bar for the in-call experience. 33 */ 34public class StatusBarNotifier implements InCallPresenter.InCallStateListener { 35 // notification types 36 private static final int IN_CALL_NOTIFICATION = 1; 37 38 private final Context mContext; 39 private final NotificationManager mNotificationManager; 40 private boolean mIsShowingNotification = false; 41 private InCallState mInCallState = InCallState.HIDDEN; 42 private int mSavedIcon = 0; 43 private int mSavedContent = 0; 44 45 public StatusBarNotifier(Context context) { 46 Preconditions.checkNotNull(context); 47 48 mContext = context; 49 mNotificationManager = 50 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); 51 } 52 53 /** 54 * Creates notifications according to the state we receive from {@link InCallPresenter}. 55 */ 56 @Override 57 public void onStateChange(InCallState state, CallList callList) { 58 updateNotification(state, callList); 59 } 60 61 /** 62 * Updates the phone app's status bar notification based on the 63 * current telephony state, or cancels the notification if the phone 64 * is totally idle. 65 * 66 * This method will never actually launch the incoming-call UI. 67 * (Use updateNotificationAndLaunchIncomingCallUi() for that.) 68 */ 69 public void updateNotification(InCallState state, CallList callList) { 70 // allowFullScreenIntent=false means *don't* allow the incoming 71 // call UI to be launched. 72 updateInCallNotification(false, state, callList); 73 } 74 75 /** 76 * Updates the phone app's status bar notification *and* launches the 77 * incoming call UI in response to a new incoming call. 78 * 79 * This is just like updateInCallNotification(), with one exception: 80 * If an incoming call is ringing (or call-waiting), the notification 81 * will also include a "fullScreenIntent" that will cause the 82 * InCallScreen to be launched immediately, unless the current 83 * foreground activity is marked as "immersive". 84 * 85 * (This is the mechanism that actually brings up the incoming call UI 86 * when we receive a "new ringing connection" event from the telephony 87 * layer.) 88 * 89 * Watch out: this method should ONLY be called directly from the code 90 * path in CallNotifier that handles the "new ringing connection" 91 * event from the telephony layer. All other places that update the 92 * in-call notification (like for phone state changes) should call 93 * updateInCallNotification() instead. (This ensures that we don't 94 * end up launching the InCallScreen multiple times for a single 95 * incoming call, which could cause slow responsiveness and/or visible 96 * glitches.) 97 * 98 * Also note that this method is safe to call even if the phone isn't 99 * actually ringing (or, more likely, if an incoming call *was* 100 * ringing briefly but then disconnected). In that case, we'll simply 101 * update or cancel the in-call notification based on the current 102 * phone state. 103 * 104 * @see #updateInCallNotification(boolean) 105 */ 106 public void updateNotificationAndLaunchIncomingCallUi(InCallState state, CallList callList) { 107 // Set allowFullScreenIntent=true to indicate that we *should* 108 // launch the incoming call UI if necessary. 109 updateInCallNotification(true, state, callList); 110 } 111 112 113 /** 114 * Take down the in-call notification. 115 * @see updateInCallNotification() 116 */ 117 private void cancelInCall() { 118 Logger.d(this, "cancelInCall()..."); 119 mNotificationManager.cancel(IN_CALL_NOTIFICATION); 120 121 mIsShowingNotification = false; 122 } 123 124 /** 125 * Helper method for updateInCallNotification() and 126 * updateNotificationAndLaunchIncomingCallUi(): Update the phone app's 127 * status bar notification based on the current telephony state, or 128 * cancels the notification if the phone is totally idle. 129 * 130 * @param allowFullScreenIntent If true, *and* an incoming call is 131 * ringing, the notification will include a "fullScreenIntent" 132 * pointing at the InCallActivity (which will cause the InCallActivity 133 * to be launched.) 134 * Watch out: This should be set to true *only* when directly 135 * handling a new incoming call for the first time. 136 */ 137 private void updateInCallNotification(boolean allowFullScreenIntent, InCallState state, 138 CallList callList) { 139 Logger.d(this, "updateInCallNotification(allowFullScreenIntent = " 140 + allowFullScreenIntent + ")..."); 141 142 if (shouldSuppressNotification(state, callList)) { 143 cancelInCall(); 144 return; 145 } 146 147 buildAndSendNotification(state, callList, allowFullScreenIntent); 148 149 } 150 151 /** 152 * Sets up the main Ui for the notification 153 */ 154 private void buildAndSendNotification(InCallState state, CallList callList, 155 boolean allowFullScreenIntent) { 156 157 final Call call = getCallToShow(callList); 158 if (call == null) { 159 Logger.wtf(this, "No call for the notification!"); 160 } 161 162 final int iconResId = getIconToDisplay(call); 163 final int contentResId = getContentString(call); 164 165 // If we checked and found that nothing is different, dont issue another notification. 166 if (!checkForChangeAndSaveData(iconResId, contentResId, state, allowFullScreenIntent)) { 167 return; 168 } 169 170 171 /* 172 * Nothing more to check...build and send it. 173 */ 174 final Notification.Builder builder = getNotificationBuilder(); 175 176 // Set up the main intent to send the user to the in-call screen 177 final PendingIntent inCallPendingIntent = createLaunchPendingIntent(); 178 builder.setContentIntent(inCallPendingIntent); 179 180 // Set the intent as a full screen intent as well if requested 181 if (allowFullScreenIntent) { 182 configureFullScreenIntent(builder, inCallPendingIntent); 183 } 184 185 // set the content 186 builder.setContentText(mContext.getString(contentResId)); 187 builder.setSmallIcon(iconResId); 188 189 // Add special Content for calls that are ongoing 190 if (InCallState.INCALL == state || InCallState.OUTGOING == state) { 191 addHangupAction(builder); 192 } 193 194 /* 195 * Fire off the notification 196 */ 197 Notification notification = builder.build(); 198 Logger.d(this, "Notifying IN_CALL_NOTIFICATION: " + notification); 199 mNotificationManager.notify(IN_CALL_NOTIFICATION, notification); 200 mIsShowingNotification = true; 201 } 202 203 /** 204 * Checks the new notification data and compares it against any notification that we 205 * are already displaying. If the data is exactly the same, we return false so that 206 * we do not issue a new notification for the exact same data. 207 */ 208 private boolean checkForChangeAndSaveData(int icon, int content, InCallState state, 209 boolean showFullScreenIntent) { 210 boolean retval = (mSavedIcon != icon) || (mSavedContent != content) || 211 (mInCallState == state); 212 213 // A full screen intent means that we have been asked to interrupt an activity, 214 // so we definitely want to show it. 215 if (showFullScreenIntent) { 216 Logger.d(this, "Forcing full screen intent"); 217 retval = true; 218 } 219 220 // If we aren't showing a notification right now, definitely start showing one. 221 if (!mIsShowingNotification) { 222 Logger.d(this, "Showing notification for first time."); 223 retval = true; 224 } 225 226 mSavedIcon = icon; 227 mSavedContent = content; 228 mInCallState = state; 229 230 if (retval) { 231 Logger.d(this, "Data changed. Showing notification"); 232 } 233 234 return retval; 235 } 236 237 /** 238 * Returns the appropriate icon res Id to display based on the call for which 239 * we want to display information. 240 */ 241 private int getIconToDisplay(Call call) { 242 // Even if both lines are in use, we only show a single item in 243 // the expanded Notifications UI. It's labeled "Ongoing call" 244 // (or "On hold" if there's only one call, and it's on hold.) 245 // Also, we don't have room to display caller-id info from two 246 // different calls. So if both lines are in use, display info 247 // from the foreground call. And if there's a ringing call, 248 // display that regardless of the state of the other calls. 249 if (call.getState() == Call.State.ONHOLD) { 250 return R.drawable.stat_sys_phone_call_on_hold; 251 } 252 return R.drawable.stat_sys_phone_call; 253 } 254 255 /** 256 * Returns the message to use with the notificaiton. 257 */ 258 private int getContentString(Call call) { 259 int resId = R.string.notification_ongoing_call; 260 261 if (call.getState() == Call.State.INCOMING) { 262 resId = R.string.notification_incoming_call; 263 264 } else if (call.getState() == Call.State.ONHOLD) { 265 resId = R.string.notification_on_hold; 266 267 } else if (call.getState() == Call.State.DIALING) { 268 resId = R.string.notification_dialing; 269 } 270 271 return resId; 272 } 273 274 /** 275 * Gets the most relevant call to display in the notification. 276 */ 277 private Call getCallToShow(CallList callList) { 278 Call call = callList.getIncomingCall(); 279 if (call == null) { 280 call = callList.getOutgoingCall(); 281 } 282 if (call == null) { 283 call = callList.getActiveOrBackgroundCall(); 284 } 285 return call; 286 } 287 288 private void addHangupAction(Notification.Builder builder) { 289 Logger.i(this, "Will show \"hang-up\" action in the ongoing active call Notification"); 290 291 // TODO: use better asset. 292 builder.addAction(R.drawable.stat_sys_phone_call_end, 293 mContext.getText(R.string.notification_action_end_call), 294 createHangUpOngoingCallPendingIntent(mContext)); 295 } 296 297 /** 298 * Adds fullscreen intent to the builder. 299 */ 300 private void configureFullScreenIntent(Notification.Builder builder, PendingIntent intent) { 301 // Ok, we actually want to launch the incoming call 302 // UI at this point (in addition to simply posting a notification 303 // to the status bar). Setting fullScreenIntent will cause 304 // the InCallScreen to be launched immediately *unless* the 305 // current foreground activity is marked as "immersive". 306 Logger.d(this, "- Setting fullScreenIntent: " + intent); 307 builder.setFullScreenIntent(intent, true); 308 309 // Ugly hack alert: 310 // 311 // The NotificationManager has the (undocumented) behavior 312 // that it will *ignore* the fullScreenIntent field if you 313 // post a new Notification that matches the ID of one that's 314 // already active. Unfortunately this is exactly what happens 315 // when you get an incoming call-waiting call: the 316 // "ongoing call" notification is already visible, so the 317 // InCallScreen won't get launched in this case! 318 // (The result: if you bail out of the in-call UI while on a 319 // call and then get a call-waiting call, the incoming call UI 320 // won't come up automatically.) 321 // 322 // The workaround is to just notice this exact case (this is a 323 // call-waiting call *and* the InCallScreen is not in the 324 // foreground) and manually cancel the in-call notification 325 // before (re)posting it. 326 // 327 // TODO: there should be a cleaner way of avoiding this 328 // problem (see discussion in bug 3184149.) 329 330 // TODO(klp): reenable this for klp 331 /*if (incomingCall.getState() == Call.State.CALL_WAITING) { 332 Logger.i(this, "updateInCallNotification: call-waiting! force relaunch..."); 333 // Cancel the IN_CALL_NOTIFICATION immediately before 334 // (re)posting it; this seems to force the 335 // NotificationManager to launch the fullScreenIntent. 336 mNotificationManager.cancel(IN_CALL_NOTIFICATION); 337 }*/ 338 } 339 340 private Notification.Builder getNotificationBuilder() { 341 final Notification.Builder builder = new Notification.Builder(mContext); 342 builder.setOngoing(true); 343 344 // Make the notification prioritized over the other normal notifications. 345 builder.setPriority(Notification.PRIORITY_HIGH); 346 347 return builder; 348 } 349 350 /** 351 * Returns true if notification should not be shown in the current state. 352 */ 353 private boolean shouldSuppressNotification(InCallState state, CallList callList) { 354 // Suppress the in-call notification if the InCallScreen is the 355 // foreground activity, since it's already obvious that you're on a 356 // call. (The status bar icon is needed only if you navigate *away* 357 // from the in-call UI.) 358 boolean shouldSuppress = InCallPresenter.getInstance().isShowingInCallUi(); 359 360 // Suppress if the call is not active. 361 if (!state.isConnectingOrConnected()) { 362 shouldSuppress = true; 363 } 364 365 // We can still be in the INCALL state when a call is disconnected (in order to show 366 // the "Call ended" screen. So check that we have an active connection too. 367 final Call call = getCallToShow(callList); 368 if (call == null) { 369 shouldSuppress = true; 370 } 371 372 // If there's an incoming ringing call: always show the 373 // notification, since the in-call notification is what actually 374 // launches the incoming call UI in the first place (see 375 // notification.fullScreenIntent below.) This makes sure that we'll 376 // correctly handle the case where a new incoming call comes in but 377 // the InCallScreen is already in the foreground. 378 if (state.isIncoming()) { 379 shouldSuppress = false; 380 } 381 382 // JANK fix: 383 // This class will issue a notification when user makes an outgoing call. 384 // However, since we suppress the notification when the user is in the in-call screen, 385 // that results is us showing it for a split second, until the in-call screen comes up. 386 // It looks ugly. 387 // 388 // The solution is to ignore the change from HIDDEN to OUTGOING since in that particular 389 // case, we know we'll get called to update again when the UI finally starts. 390 if (InCallState.OUTGOING == state && InCallState.HIDDEN == mInCallState) { 391 shouldSuppress = true; 392 } 393 394 return shouldSuppress; 395 } 396 397 private PendingIntent createLaunchPendingIntent() { 398 399 final Intent intent = new Intent(Intent.ACTION_MAIN, null); 400 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 401 | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS 402 | Intent.FLAG_ACTIVITY_NO_USER_ACTION); 403 intent.setClass(mContext, InCallActivity.class); 404 405 // PendingIntent that can be used to launch the InCallActivity. The 406 // system fires off this intent if the user pulls down the windowshade 407 // and clicks the notification's expanded view. It's also used to 408 // launch the InCallActivity immediately when when there's an incoming 409 // call (see the "fullScreenIntent" field below). 410 PendingIntent inCallPendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0); 411 412 return inCallPendingIntent; 413 } 414 415 /** 416 * Returns PendingIntent for hanging up ongoing phone call. This will typically be used from 417 * Notification context. 418 */ 419 private static PendingIntent createHangUpOngoingCallPendingIntent(Context context) { 420 final Intent intent = new Intent(InCallApp.ACTION_HANG_UP_ONGOING_CALL, null, 421 context, NotificationBroadcastReceiver.class); 422 return PendingIntent.getBroadcast(context, 0, intent, 0); 423 } 424} 425