Home | History | Annotate | Download | only in incallui
      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         }
     54         State getState();
     55         void schedule();
     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 
     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
    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      */
    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      */
    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      */
    174     private void cancelInCall() {
    175         Log.d(this, "cancelInCall()...");
    176         mNotificationManager.cancel(IN_CALL_NOTIFICATION);
    177         mIsShowingNotification = false;
    178     }
    179 
    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      */
    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 
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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      */
    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 
    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     }
    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      */
    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