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