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