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.android.services.telephony.common.CallIdentification; 20import com.google.common.base.Preconditions; 21 22import android.app.Notification; 23import android.app.NotificationManager; 24import android.app.PendingIntent; 25import android.content.Context; 26import android.content.Intent; 27import android.graphics.Bitmap; 28import android.graphics.BitmapFactory; 29import android.graphics.drawable.BitmapDrawable; 30import android.graphics.drawable.Drawable; 31import android.text.TextUtils; 32 33import com.android.incallui.ContactInfoCache.ContactCacheEntry; 34import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback; 35import com.android.incallui.InCallApp.NotificationBroadcastReceiver; 36import com.android.incallui.InCallPresenter.InCallState; 37import com.android.services.telephony.common.Call; 38 39import java.util.HashMap; 40 41/** 42 * This class adds Notifications to the status bar for the in-call experience. 43 */ 44public class StatusBarNotifier implements InCallPresenter.InCallStateListener { 45 // notification types 46 private static final int IN_CALL_NOTIFICATION = 1; 47 48 private final Context mContext; 49 private final ContactInfoCache mContactInfoCache; 50 private final CallList mCallList; 51 private final NotificationManager mNotificationManager; 52 private boolean mIsShowingNotification = false; 53 private int mCallState = Call.State.INVALID; 54 private int mSavedIcon = 0; 55 private int mSavedContent = 0; 56 private Bitmap mSavedLargeIcon; 57 private String mSavedContentTitle; 58 59 public StatusBarNotifier(Context context, ContactInfoCache contactInfoCache, 60 CallList callList) { 61 Preconditions.checkNotNull(context); 62 63 mContext = context; 64 mContactInfoCache = contactInfoCache; 65 mCallList = callList; 66 mNotificationManager = 67 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); 68 } 69 70 /** 71 * Creates notifications according to the state we receive from {@link InCallPresenter}. 72 */ 73 @Override 74 public void onStateChange(InCallState state, CallList callList) { 75 Log.d(this, "onStateChange"); 76 77 updateNotification(state, callList); 78 } 79 80 /** 81 * Updates the phone app's status bar notification based on the 82 * current telephony state, or cancels the notification if the phone 83 * is totally idle. 84 * 85 * This method will never actually launch the incoming-call UI. 86 * (Use updateNotificationAndLaunchIncomingCallUi() for that.) 87 */ 88 public void updateNotification(InCallState state, CallList callList) { 89 Log.d(this, "updateNotification"); 90 // allowFullScreenIntent=false means *don't* allow the incoming 91 // call UI to be launched. 92 updateInCallNotification(false, state, callList); 93 } 94 95 /** 96 * Updates the phone app's status bar notification *and* launches the 97 * incoming call UI in response to a new incoming call. 98 * 99 * This is just like updateInCallNotification(), with one exception: 100 * If an incoming call is ringing (or call-waiting), the notification 101 * will also include a "fullScreenIntent" that will cause the 102 * InCallScreen to be launched immediately, unless the current 103 * foreground activity is marked as "immersive". 104 * 105 * (This is the mechanism that actually brings up the incoming call UI 106 * when we receive a "new ringing connection" event from the telephony 107 * layer.) 108 * 109 * Watch out: this method should ONLY be called directly from the code 110 * path in CallNotifier that handles the "new ringing connection" 111 * event from the telephony layer. All other places that update the 112 * in-call notification (like for phone state changes) should call 113 * updateInCallNotification() instead. (This ensures that we don't 114 * end up launching the InCallScreen multiple times for a single 115 * incoming call, which could cause slow responsiveness and/or visible 116 * glitches.) 117 * 118 * Also note that this method is safe to call even if the phone isn't 119 * actually ringing (or, more likely, if an incoming call *was* 120 * ringing briefly but then disconnected). In that case, we'll simply 121 * update or cancel the in-call notification based on the current 122 * phone state. 123 * 124 * @see #updateInCallNotification(boolean) 125 */ 126 public void updateNotificationAndLaunchIncomingCallUi(InCallState state, CallList callList) { 127 // Set allowFullScreenIntent=true to indicate that we *should* 128 // launch the incoming call UI if necessary. 129 updateInCallNotification(true, state, callList); 130 } 131 132 /** 133 * Take down the in-call notification. 134 * @see updateInCallNotification() 135 */ 136 private void cancelInCall() { 137 Log.d(this, "cancelInCall()..."); 138 mNotificationManager.cancel(IN_CALL_NOTIFICATION); 139 140 mIsShowingNotification = false; 141 } 142 143 /* package */ static void clearInCallNotification(Context backupContext) { 144 Log.i(StatusBarNotifier.class.getSimpleName(), 145 "Something terrible happened. Clear all InCall notifications"); 146 147 NotificationManager notificationManager = 148 (NotificationManager) backupContext.getSystemService(Context.NOTIFICATION_SERVICE); 149 notificationManager.cancel(IN_CALL_NOTIFICATION); 150 } 151 152 /** 153 * Helper method for updateInCallNotification() and 154 * updateNotificationAndLaunchIncomingCallUi(): Update the phone app's 155 * status bar notification based on the current telephony state, or 156 * cancels the notification if the phone is totally idle. 157 * 158 * @param allowFullScreenIntent If true, *and* an incoming call is 159 * ringing, the notification will include a "fullScreenIntent" 160 * pointing at the InCallActivity (which will cause the InCallActivity 161 * to be launched.) 162 * Watch out: This should be set to true *only* when directly 163 * handling a new incoming call for the first time. 164 */ 165 private void updateInCallNotification(final boolean allowFullScreenIntent, 166 final InCallState state, CallList callList) { 167 Log.d(this, "updateInCallNotification(allowFullScreenIntent = " 168 + allowFullScreenIntent + ")..."); 169 170 final Call call = getCallToShow(callList); 171 if (shouldSuppressNotification(state, call)) { 172 Log.d(this, "Suppressing notification"); 173 cancelInCall(); 174 return; 175 } 176 177 // we make a call to the contact info cache to query for supplemental data to what the 178 // call provides. This includes the contact name and photo. 179 // This callback will always get called immediately and synchronously with whatever data 180 // it has available, and may make a subsequent call later (same thread) if it had to 181 // call into the contacts provider for more data. 182 mContactInfoCache.findInfo(call.getIdentification(), call.getState() == Call.State.INCOMING, 183 new ContactInfoCacheCallback() { 184 private boolean mAllowFullScreenIntent = allowFullScreenIntent; 185 186 @Override 187 public void onContactInfoComplete(int callId, ContactCacheEntry entry) { 188 Call call = CallList.getInstance().getCall(callId); 189 if (call != null) { 190 buildAndSendNotification(call, entry, mAllowFullScreenIntent); 191 } 192 193 // Full screen intents are what bring up the in call screen. We only want 194 // to do this the first time we are called back. 195 mAllowFullScreenIntent = false; 196 } 197 198 @Override 199 public void onImageLoadComplete(int callId, ContactCacheEntry entry) { 200 Call call = CallList.getInstance().getCall(callId); 201 if (call != null) { 202 buildAndSendNotification(call, entry, mAllowFullScreenIntent); 203 } 204 } }); 205 } 206 207 /** 208 * Sets up the main Ui for the notification 209 */ 210 private void buildAndSendNotification(Call originalCall, ContactCacheEntry contactInfo, 211 boolean allowFullScreenIntent) { 212 213 // This can get called to update an existing notification after contact information has come 214 // back. However, it can happen much later. Before we continue, we need to make sure that 215 // the call being passed in is still the one we want to show in the notification. 216 final Call call = getCallToShow(CallList.getInstance()); 217 if (call == null || call.getCallId() != originalCall.getCallId()) { 218 return; 219 } 220 221 final int state = call.getState(); 222 final boolean isConference = call.isConferenceCall(); 223 final int iconResId = getIconToDisplay(call); 224 final Bitmap largeIcon = getLargeIconToDisplay(contactInfo, isConference); 225 final int contentResId = getContentString(call); 226 final String contentTitle = getContentTitle(contactInfo, isConference); 227 228 // If we checked and found that nothing is different, dont issue another notification. 229 if (!checkForChangeAndSaveData(iconResId, contentResId, largeIcon, contentTitle, state, 230 allowFullScreenIntent)) { 231 return; 232 } 233 234 /* 235 * Nothing more to check...build and send it. 236 */ 237 final Notification.Builder builder = getNotificationBuilder(); 238 239 // Set up the main intent to send the user to the in-call screen 240 final PendingIntent inCallPendingIntent = createLaunchPendingIntent(); 241 builder.setContentIntent(inCallPendingIntent); 242 243 // Set the intent as a full screen intent as well if requested 244 if (allowFullScreenIntent) { 245 configureFullScreenIntent(builder, inCallPendingIntent, call); 246 } 247 248 // set the content 249 builder.setContentText(mContext.getString(contentResId)); 250 builder.setSmallIcon(iconResId); 251 builder.setContentTitle(contentTitle); 252 builder.setLargeIcon(largeIcon); 253 254 if (state == Call.State.ACTIVE) { 255 builder.setUsesChronometer(true); 256 builder.setWhen(call.getConnectTime()); 257 } else { 258 builder.setUsesChronometer(false); 259 } 260 261 // Add hang up option for any active calls (active | onhold), outgoing calls (dialing). 262 if (state == Call.State.ACTIVE || 263 state == Call.State.ONHOLD || 264 Call.State.isDialing(state)) { 265 addHangupAction(builder); 266 } 267 268 /* 269 * Fire off the notification 270 */ 271 Notification notification = builder.build(); 272 Log.d(this, "Notifying IN_CALL_NOTIFICATION: " + notification); 273 mNotificationManager.notify(IN_CALL_NOTIFICATION, notification); 274 mIsShowingNotification = true; 275 } 276 277 /** 278 * Checks the new notification data and compares it against any notification that we 279 * are already displaying. If the data is exactly the same, we return false so that 280 * we do not issue a new notification for the exact same data. 281 */ 282 private boolean checkForChangeAndSaveData(int icon, int content, Bitmap largeIcon, 283 String contentTitle, int state, boolean showFullScreenIntent) { 284 285 // The two are different: 286 // if new title is not null, it should be different from saved version OR 287 // if new title is null, the saved version should not be null 288 final boolean contentTitleChanged = 289 (contentTitle != null && !contentTitle.equals(mSavedContentTitle)) || 290 (contentTitle == null && mSavedContentTitle != null); 291 292 // any change means we are definitely updating 293 boolean retval = (mSavedIcon != icon) || (mSavedContent != content) || 294 (mCallState != state) || (mSavedLargeIcon != largeIcon) || 295 contentTitleChanged; 296 297 // A full screen intent means that we have been asked to interrupt an activity, 298 // so we definitely want to show it. 299 if (showFullScreenIntent) { 300 Log.d(this, "Forcing full screen intent"); 301 retval = true; 302 } 303 304 // If we aren't showing a notification right now, definitely start showing one. 305 if (!mIsShowingNotification) { 306 Log.d(this, "Showing notification for first time."); 307 retval = true; 308 } 309 310 mSavedIcon = icon; 311 mSavedContent = content; 312 mCallState = state; 313 mSavedLargeIcon = largeIcon; 314 mSavedContentTitle = contentTitle; 315 316 if (retval) { 317 Log.d(this, "Data changed. Showing notification"); 318 } 319 320 return retval; 321 } 322 323 /** 324 * Returns the main string to use in the notification. 325 */ 326 private String getContentTitle(ContactCacheEntry contactInfo, boolean isConference) { 327 if (isConference) { 328 return mContext.getResources().getString(R.string.card_title_conf_call); 329 } 330 if (TextUtils.isEmpty(contactInfo.name)) { 331 return contactInfo.number; 332 } 333 334 return contactInfo.name; 335 } 336 337 /** 338 * Gets a large icon from the contact info object to display in the notification. 339 */ 340 private Bitmap getLargeIconToDisplay(ContactCacheEntry contactInfo, boolean isConference) { 341 Bitmap largeIcon = null; 342 if (isConference) { 343 largeIcon = BitmapFactory.decodeResource(mContext.getResources(), 344 R.drawable.picture_conference); 345 } 346 if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) { 347 largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap(); 348 } 349 350 if (largeIcon != null) { 351 final int height = (int) mContext.getResources().getDimension( 352 android.R.dimen.notification_large_icon_height); 353 final int width = (int) mContext.getResources().getDimension( 354 android.R.dimen.notification_large_icon_width); 355 largeIcon = Bitmap.createScaledBitmap(largeIcon, width, height, false); 356 } 357 358 return largeIcon; 359 } 360 361 /** 362 * Returns the appropriate icon res Id to display based on the call for which 363 * we want to display information. 364 */ 365 private int getIconToDisplay(Call call) { 366 // Even if both lines are in use, we only show a single item in 367 // the expanded Notifications UI. It's labeled "Ongoing call" 368 // (or "On hold" if there's only one call, and it's on hold.) 369 // Also, we don't have room to display caller-id info from two 370 // different calls. So if both lines are in use, display info 371 // from the foreground call. And if there's a ringing call, 372 // display that regardless of the state of the other calls. 373 if (call.getState() == Call.State.ONHOLD) { 374 return R.drawable.stat_sys_phone_call_on_hold; 375 } 376 return R.drawable.stat_sys_phone_call; 377 } 378 379 /** 380 * Returns the message to use with the notificaiton. 381 */ 382 private int getContentString(Call call) { 383 int resId = R.string.notification_ongoing_call; 384 385 if (call.getState() == Call.State.INCOMING) { 386 resId = R.string.notification_incoming_call; 387 388 } else if (call.getState() == Call.State.ONHOLD) { 389 resId = R.string.notification_on_hold; 390 391 } else if (Call.State.isDialing(call.getState())) { 392 resId = R.string.notification_dialing; 393 } 394 395 return resId; 396 } 397 398 /** 399 * Gets the most relevant call to display in the notification. 400 */ 401 private Call getCallToShow(CallList callList) { 402 if (callList == null) { 403 return null; 404 } 405 Call call = callList.getIncomingCall(); 406 if (call == null) { 407 call = callList.getOutgoingCall(); 408 } 409 if (call == null) { 410 call = callList.getActiveOrBackgroundCall(); 411 } 412 return call; 413 } 414 415 private void addHangupAction(Notification.Builder builder) { 416 Log.i(this, "Will show \"hang-up\" action in the ongoing active call Notification"); 417 418 // TODO: use better asset. 419 builder.addAction(R.drawable.stat_sys_phone_call_end, 420 mContext.getText(R.string.notification_action_end_call), 421 createHangUpOngoingCallPendingIntent(mContext)); 422 } 423 424 /** 425 * Adds fullscreen intent to the builder. 426 */ 427 private void configureFullScreenIntent(Notification.Builder builder, PendingIntent intent, 428 Call call) { 429 // Ok, we actually want to launch the incoming call 430 // UI at this point (in addition to simply posting a notification 431 // to the status bar). Setting fullScreenIntent will cause 432 // the InCallScreen to be launched immediately *unless* the 433 // current foreground activity is marked as "immersive". 434 Log.d(this, "- Setting fullScreenIntent: " + intent); 435 builder.setFullScreenIntent(intent, true); 436 437 // Ugly hack alert: 438 // 439 // The NotificationManager has the (undocumented) behavior 440 // that it will *ignore* the fullScreenIntent field if you 441 // post a new Notification that matches the ID of one that's 442 // already active. Unfortunately this is exactly what happens 443 // when you get an incoming call-waiting call: the 444 // "ongoing call" notification is already visible, so the 445 // InCallScreen won't get launched in this case! 446 // (The result: if you bail out of the in-call UI while on a 447 // call and then get a call-waiting call, the incoming call UI 448 // won't come up automatically.) 449 // 450 // The workaround is to just notice this exact case (this is a 451 // call-waiting call *and* the InCallScreen is not in the 452 // foreground) and manually cancel the in-call notification 453 // before (re)posting it. 454 // 455 // TODO: there should be a cleaner way of avoiding this 456 // problem (see discussion in bug 3184149.) 457 458 if (call.getState() == Call.State.CALL_WAITING) { 459 Log.i(this, "updateInCallNotification: call-waiting! force relaunch..."); 460 // Cancel the IN_CALL_NOTIFICATION immediately before 461 // (re)posting it; this seems to force the 462 // NotificationManager to launch the fullScreenIntent. 463 mNotificationManager.cancel(IN_CALL_NOTIFICATION); 464 } 465 } 466 467 private Notification.Builder getNotificationBuilder() { 468 final Notification.Builder builder = new Notification.Builder(mContext); 469 builder.setOngoing(true); 470 471 // Make the notification prioritized over the other normal notifications. 472 builder.setPriority(Notification.PRIORITY_HIGH); 473 474 return builder; 475 } 476 477 /** 478 * Returns true if notification should not be shown in the current state. 479 */ 480 private boolean shouldSuppressNotification(InCallState state, Call call) { 481 482 // We can still be in the INCALL state when a call is disconnected (in order to show 483 // the "Call ended" screen. So check that we have an active connection too. 484 if (call == null) { 485 Log.v(this, "suppressing: no call"); 486 return true; 487 } 488 489 // Suppress the in-call notification if the InCallScreen is the 490 // foreground activity, since it's already obvious that you're on a 491 // call. (The status bar icon is needed only if you navigate *away* 492 // from the in-call UI.) 493 boolean shouldSuppress = InCallPresenter.getInstance().isShowingInCallUi(); 494 495 // Suppress if the call is not active. 496 if (!state.isConnectingOrConnected()) { 497 Log.v(this, "suppressing: not connecting or connected"); 498 shouldSuppress = true; 499 } 500 501 // If there's an incoming ringing call: always show the 502 // notification, since the in-call notification is what actually 503 // launches the incoming call UI in the first place (see 504 // notification.fullScreenIntent below.) This makes sure that we'll 505 // correctly handle the case where a new incoming call comes in but 506 // the InCallScreen is already in the foreground. 507 if (state.isIncoming()) { 508 Log.v(this, "unsuppressing: incoming call"); 509 shouldSuppress = false; 510 } 511 512 // JANK Fix 513 // Do not show the notification for outgoing calls until the UI comes up. 514 // Since we don't normally show a notification while the incall screen is 515 // in the foreground, if we show the outgoing notification before the activity 516 // comes up the user will see it flash on and off on an outgoing call. 517 // This code ensures that we do not show the notification for outgoing calls before 518 // the activity has started. 519 if (state == InCallState.OUTGOING && 520 !InCallPresenter.getInstance().isActivityPreviouslyStarted()) { 521 Log.v(this, "suppressing: activity not started."); 522 shouldSuppress = true; 523 } 524 525 return shouldSuppress; 526 } 527 528 private PendingIntent createLaunchPendingIntent() { 529 530 final Intent intent = InCallPresenter.getInstance().getInCallIntent(/*showdialpad=*/false); 531 532 // PendingIntent that can be used to launch the InCallActivity. The 533 // system fires off this intent if the user pulls down the windowshade 534 // and clicks the notification's expanded view. It's also used to 535 // launch the InCallActivity immediately when when there's an incoming 536 // call (see the "fullScreenIntent" field below). 537 PendingIntent inCallPendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0); 538 539 return inCallPendingIntent; 540 } 541 542 /** 543 * Returns PendingIntent for hanging up ongoing phone call. This will typically be used from 544 * Notification context. 545 */ 546 private static PendingIntent createHangUpOngoingCallPendingIntent(Context context) { 547 final Intent intent = new Intent(InCallApp.ACTION_HANG_UP_ONGOING_CALL, null, 548 context, NotificationBroadcastReceiver.class); 549 return PendingIntent.getBroadcast(context, 0, intent, 0); 550 } 551} 552