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