MissedCallNotifier.java revision 701dc006ac11625b55d872f1639107b028933895
1/*
2 * Copyright 2014, 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.server.telecom;
18
19import android.app.Notification;
20import android.app.NotificationManager;
21import android.app.PendingIntent;
22import android.app.TaskStackBuilder;
23import android.content.AsyncQueryHandler;
24import android.content.ContentValues;
25import android.content.Context;
26import android.content.Intent;
27import android.database.Cursor;
28import android.graphics.Bitmap;
29import android.graphics.drawable.BitmapDrawable;
30import android.graphics.drawable.Drawable;
31import android.net.Uri;
32import android.provider.CallLog;
33import android.provider.CallLog.Calls;
34import android.telecom.CallState;
35import android.telecom.DisconnectCause;
36import android.text.BidiFormatter;
37import android.text.TextDirectionHeuristics;
38import android.text.TextUtils;
39
40/**
41 * Creates a notification for calls that the user missed (neither answered nor rejected).
42 * TODO: Make TelephonyManager.clearMissedCalls call into this class.
43 * STOPSHIP: Resolve b/13769374 about moving this class to InCall.
44 */
45class MissedCallNotifier extends CallsManagerListenerBase {
46
47    private static final String[] CALL_LOG_PROJECTION = new String[] {
48        Calls._ID,
49        Calls.NUMBER,
50        Calls.NUMBER_PRESENTATION,
51        Calls.DATE,
52        Calls.DURATION,
53        Calls.TYPE,
54    };
55    private static final int MISSED_CALL_NOTIFICATION_ID = 1;
56
57    private final Context mContext;
58    private final NotificationManager mNotificationManager;
59
60    // Used to track the number of missed calls.
61    private int mMissedCallCount = 0;
62
63    MissedCallNotifier(Context context) {
64        mContext = context;
65        mNotificationManager =
66                (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
67
68        updateOnStartup();
69    }
70
71    /** {@inheritDoc} */
72    @Override
73    public void onCallStateChanged(Call call, int oldState, int newState) {
74        if (oldState == CallState.RINGING && newState == CallState.DISCONNECTED &&
75                call.getDisconnectCause().getCode() == DisconnectCause.MISSED) {
76            showMissedCallNotification(call);
77        }
78    }
79
80    /** Clears missed call notification and marks the call log's missed calls as read. */
81    void clearMissedCalls() {
82        // Clear the list of new missed calls from the call log.
83        ContentValues values = new ContentValues();
84        values.put(Calls.NEW, 0);
85        values.put(Calls.IS_READ, 1);
86        StringBuilder where = new StringBuilder();
87        where.append(Calls.NEW);
88        where.append(" = 1 AND ");
89        where.append(Calls.TYPE);
90        where.append(" = ?");
91        mContext.getContentResolver().update(Calls.CONTENT_URI, values, where.toString(),
92                new String[]{ Integer.toString(Calls.MISSED_TYPE) });
93
94        cancelMissedCallNotification();
95    }
96
97    /**
98     * Create a system notification for the missed call.
99     *
100     * @param call The missed call.
101     */
102    void showMissedCallNotification(Call call) {
103        mMissedCallCount++;
104
105        final int titleResId;
106        final String expandedText;  // The text in the notification's line 1 and 2.
107
108        // Display the first line of the notification:
109        // 1 missed call: <caller name || handle>
110        // More than 1 missed call: <number of calls> + "missed calls"
111        if (mMissedCallCount == 1) {
112            titleResId = R.string.notification_missedCallTitle;
113            expandedText = getNameForCall(call);
114        } else {
115            titleResId = R.string.notification_missedCallsTitle;
116            expandedText =
117                    mContext.getString(R.string.notification_missedCallsMsg, mMissedCallCount);
118        }
119
120        // Create the notification.
121        Notification.Builder builder = new Notification.Builder(mContext);
122        builder.setSmallIcon(android.R.drawable.stat_notify_missed_call)
123                .setColor(mContext.getResources().getColor(R.color.theme_color))
124                .setWhen(call.getCreationTimeMillis())
125                .setContentTitle(mContext.getText(titleResId))
126                .setContentText(expandedText)
127                .setContentIntent(createCallLogPendingIntent())
128                .setAutoCancel(true)
129                .setDeleteIntent(createClearMissedCallsPendingIntent());
130
131        Uri handleUri = call.getHandle();
132        String handle = handleUri == null ? null : handleUri.getSchemeSpecificPart();
133
134        // Add additional actions when there is only 1 missed call, like call-back and SMS.
135        if (mMissedCallCount == 1) {
136            Log.d(this, "Add actions with number %s.", Log.piiHandle(handle));
137
138            if (!TextUtils.isEmpty(handle)
139                    && !TextUtils.equals(handle, mContext.getString(R.string.handle_restricted))) {
140                builder.addAction(R.drawable.stat_sys_phone_call,
141                        mContext.getString(R.string.notification_missedCall_call_back),
142                        createCallBackPendingIntent(handleUri));
143
144                builder.addAction(R.drawable.ic_text_holo_dark,
145                        mContext.getString(R.string.notification_missedCall_message),
146                        createSendSmsFromNotificationPendingIntent(handleUri));
147            }
148
149            Bitmap photoIcon = call.getPhotoIcon();
150            if (photoIcon != null) {
151                builder.setLargeIcon(photoIcon);
152            } else {
153                Drawable photo = call.getPhoto();
154                if (photo != null && photo instanceof BitmapDrawable) {
155                    builder.setLargeIcon(((BitmapDrawable) photo).getBitmap());
156                }
157            }
158        } else {
159            Log.d(this, "Suppress actions. handle: %s, missedCalls: %d.", Log.piiHandle(handle),
160                    mMissedCallCount);
161        }
162
163        Notification notification = builder.build();
164        configureLedOnNotification(notification);
165
166        Log.i(this, "Adding missed call notification for %s.", call);
167        mNotificationManager.notify(MISSED_CALL_NOTIFICATION_ID, notification);
168    }
169
170    /** Cancels the "missed call" notification. */
171    private void cancelMissedCallNotification() {
172        // Reset the number of missed calls to 0.
173        mMissedCallCount = 0;
174        mNotificationManager.cancel(MISSED_CALL_NOTIFICATION_ID);
175    }
176
177    /**
178     * Returns the name to use in the missed call notification.
179     */
180    private String getNameForCall(Call call) {
181        String handle = call.getHandle() == null ? null : call.getHandle().getSchemeSpecificPart();
182        String name = call.getName();
183
184        if (!TextUtils.isEmpty(name) && TextUtils.isGraphic(name)) {
185            return name;
186        } else if (!TextUtils.isEmpty(handle)) {
187            // A handle should always be displayed LTR using {@link BidiFormatter} regardless of the
188            // content of the rest of the notification.
189            // TODO: Does this apply to SIP addresses?
190            BidiFormatter bidiFormatter = BidiFormatter.getInstance();
191            return bidiFormatter.unicodeWrap(handle, TextDirectionHeuristics.LTR);
192        } else {
193            // Use "unknown" if the call is unidentifiable.
194            return mContext.getString(R.string.unknown);
195        }
196    }
197
198    /**
199     * Creates a new pending intent that sends the user to the call log.
200     *
201     * @return The pending intent.
202     */
203    private PendingIntent createCallLogPendingIntent() {
204        Intent intent = new Intent(Intent.ACTION_VIEW, null);
205        intent.setType(CallLog.Calls.CONTENT_TYPE);
206
207        TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(mContext);
208        taskStackBuilder.addNextIntent(intent);
209
210        return taskStackBuilder.getPendingIntent(0, 0);
211    }
212
213    /**
214     * Creates an intent to be invoked when the missed call notification is cleared.
215     */
216    private PendingIntent createClearMissedCallsPendingIntent() {
217        return createTelecomPendingIntent(
218                TelecomBroadcastReceiver.ACTION_CLEAR_MISSED_CALLS, null);
219    }
220
221    /**
222     * Creates an intent to be invoked when the user opts to "call back" from the missed call
223     * notification.
224     *
225     * @param handle The handle to call back.
226     */
227    private PendingIntent createCallBackPendingIntent(Uri handle) {
228        return createTelecomPendingIntent(
229                TelecomBroadcastReceiver.ACTION_CALL_BACK_FROM_NOTIFICATION, handle);
230    }
231
232    /**
233     * Creates an intent to be invoked when the user opts to "send sms" from the missed call
234     * notification.
235     */
236    private PendingIntent createSendSmsFromNotificationPendingIntent(Uri handle) {
237        return createTelecomPendingIntent(
238                TelecomBroadcastReceiver.ACTION_SEND_SMS_FROM_NOTIFICATION,
239                Uri.fromParts(Constants.SCHEME_SMSTO, handle.getSchemeSpecificPart(), null));
240    }
241
242    /**
243     * Creates generic pending intent from the specified parameters to be received by
244     * {@link TelecomBroadcastReceiver}.
245     *
246     * @param action The intent action.
247     * @param data The intent data.
248     */
249    private PendingIntent createTelecomPendingIntent(String action, Uri data) {
250        Intent intent = new Intent(action, data, mContext, TelecomBroadcastReceiver.class);
251        return PendingIntent.getBroadcast(mContext, 0, intent, 0);
252    }
253
254    /**
255     * Configures a notification to emit the blinky notification light.
256     */
257    private void configureLedOnNotification(Notification notification) {
258        notification.flags |= Notification.FLAG_SHOW_LIGHTS;
259        notification.defaults |= Notification.DEFAULT_LIGHTS;
260    }
261
262    /**
263     * Adds the missed call notification on startup if there are unread missed calls.
264     */
265    private void updateOnStartup() {
266        Log.d(this, "updateOnStartup()...");
267
268        // instantiate query handler
269        AsyncQueryHandler queryHandler = new AsyncQueryHandler(mContext.getContentResolver()) {
270            @Override
271            protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
272                Log.d(MissedCallNotifier.this, "onQueryComplete()...");
273                if (cursor != null) {
274                    try {
275                        while (cursor.moveToNext()) {
276                            // Get data about the missed call from the cursor
277                            Uri handle = Uri.parse(cursor.getString(
278                                    cursor.getColumnIndexOrThrow(Calls.NUMBER)));
279                            int presentation = cursor.getInt(cursor.getColumnIndexOrThrow(
280                                    Calls.NUMBER_PRESENTATION));
281
282                            if (presentation != Calls.PRESENTATION_ALLOWED) {
283                                handle = null;
284                            }
285
286                            // Convert the data to a call object
287                            Call call = new Call(null, null, null, null, null, true, false);
288                            call.setDisconnectCause(new DisconnectCause(DisconnectCause.MISSED));
289                            call.setState(CallState.DISCONNECTED);
290
291                            // Listen for the update to the caller information before posting the
292                            // notification so that we have the contact info and photo.
293                            call.addListener(new Call.ListenerBase() {
294                                @Override
295                                public void onCallerInfoChanged(Call call) {
296                                    call.removeListener(this);  // No longer need to listen to call
297                                                                // changes after the contact info
298                                                                // is retrieved.
299                                    showMissedCallNotification(call);
300                                }
301                            });
302                            // Set the handle here because that is what triggers the contact info
303                            // query.
304                            call.setHandle(handle, presentation);
305                        }
306                    } finally {
307                        cursor.close();
308                    }
309                }
310            }
311        };
312
313        // setup query spec, look for all Missed calls that are new.
314        StringBuilder where = new StringBuilder("type=");
315        where.append(Calls.MISSED_TYPE);
316        where.append(" AND new=1");
317
318        // start the query
319        queryHandler.startQuery(0, null, Calls.CONTENT_URI, CALL_LOG_PROJECTION,
320                where.toString(), null, Calls.DEFAULT_SORT_ORDER);
321    }
322}
323