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