blob: 9e757df7e9349c2efc31c015146fb5b5607b685e [file] [log] [blame]
Santos Cordona0e5f1a2014-03-31 21:43:00 -07001/*
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.telecomm;
18
19import android.app.Notification;
20import android.app.NotificationManager;
21import android.app.PendingIntent;
22import android.app.TaskStackBuilder;
Santos Cordon64c7e962014-07-02 15:15:27 -070023import android.content.AsyncQueryHandler;
Santos Cordona0e5f1a2014-03-31 21:43:00 -070024import android.content.ContentValues;
25import android.content.Context;
26import android.content.Intent;
Santos Cordon64c7e962014-07-02 15:15:27 -070027import android.database.Cursor;
Santos Cordon99c8a6f2014-05-28 18:28:47 -070028import android.graphics.Bitmap;
29import android.graphics.drawable.BitmapDrawable;
30import android.graphics.drawable.Drawable;
Santos Cordona0e5f1a2014-03-31 21:43:00 -070031import android.net.Uri;
32import android.provider.CallLog;
33import android.provider.CallLog.Calls;
34import android.telecomm.CallState;
35import android.telephony.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).
Santos Cordondf399862014-08-06 04:39:15 -070042 * TODO: Make TelephonyManager.clearMissedCalls call into this class.
Santos Cordona0e5f1a2014-03-31 21:43:00 -070043 * STOPSHIP: Resolve b/13769374 about moving this class to InCall.
44 */
45class MissedCallNotifier extends CallsManagerListenerBase {
46
Santos Cordon64c7e962014-07-02 15:15:27 -070047 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 };
Santos Cordona0e5f1a2014-03-31 21:43:00 -070055 private static final int MISSED_CALL_NOTIFICATION_ID = 1;
56 private static final String SCHEME_SMSTO = "smsto";
57
58 private final Context mContext;
59 private final NotificationManager mNotificationManager;
60
61 // Used to track the number of missed calls.
62 private int mMissedCallCount = 0;
63
64 MissedCallNotifier(Context context) {
65 mContext = context;
66 mNotificationManager =
67 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
Santos Cordon64c7e962014-07-02 15:15:27 -070068
69 updateOnStartup();
Santos Cordona0e5f1a2014-03-31 21:43:00 -070070 }
71
72 /** {@inheritDoc} */
73 @Override
74 public void onCallStateChanged(Call call, CallState oldState, CallState newState) {
75 if (oldState == CallState.RINGING && newState == CallState.DISCONNECTED &&
76 call.getDisconnectCause() == DisconnectCause.INCOMING_MISSED) {
77 showMissedCallNotification(call);
78 }
79 }
80
81 /** Clears missed call notification and marks the call log's missed calls as read. */
82 void clearMissedCalls() {
83 // Clear the list of new missed calls from the call log.
84 ContentValues values = new ContentValues();
85 values.put(Calls.NEW, 0);
86 values.put(Calls.IS_READ, 1);
87 StringBuilder where = new StringBuilder();
88 where.append(Calls.NEW);
89 where.append(" = 1 AND ");
90 where.append(Calls.TYPE);
91 where.append(" = ?");
92 mContext.getContentResolver().update(Calls.CONTENT_URI, values, where.toString(),
93 new String[]{ Integer.toString(Calls.MISSED_TYPE) });
94
95 cancelMissedCallNotification();
96 }
97
98 /**
99 * Create a system notification for the missed call.
100 *
101 * @param call The missed call.
102 */
103 private void showMissedCallNotification(Call call) {
104 mMissedCallCount++;
105
106 final int titleResId;
107 final String expandedText; // The text in the notification's line 1 and 2.
108
109 // Display the first line of the notification:
110 // 1 missed call: <caller name || handle>
111 // More than 1 missed call: <number of calls> + "missed calls"
112 if (mMissedCallCount == 1) {
113 titleResId = R.string.notification_missedCallTitle;
114 expandedText = getNameForCall(call);
115 } else {
116 titleResId = R.string.notification_missedCallsTitle;
117 expandedText =
118 mContext.getString(R.string.notification_missedCallsMsg, mMissedCallCount);
119 }
120
121 // Create the notification.
122 Notification.Builder builder = new Notification.Builder(mContext);
123 builder.setSmallIcon(android.R.drawable.stat_notify_missed_call)
Sailesh Nepal8c85dee2014-04-07 22:21:40 -0700124 .setWhen(call.getCreationTimeMillis())
Santos Cordona0e5f1a2014-03-31 21:43:00 -0700125 .setContentTitle(mContext.getText(titleResId))
126 .setContentText(expandedText)
127 .setContentIntent(createCallLogPendingIntent())
128 .setAutoCancel(true)
129 .setDeleteIntent(createClearMissedCallsPendingIntent());
130
131 Uri handleUri = call.getHandle();
Santos Cordon61e11ad2014-08-06 02:04:09 -0700132 String handle = handleUri == null ? null : handleUri.getSchemeSpecificPart();
Santos Cordona0e5f1a2014-03-31 21:43:00 -0700133
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
Santos Cordon61e11ad2014-08-06 02:04:09 -0700138 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));
Santos Cordona0e5f1a2014-03-31 21:43:00 -0700143
Santos Cordon61e11ad2014-08-06 02:04:09 -0700144 builder.addAction(R.drawable.ic_text_holo_dark,
145 mContext.getString(R.string.notification_missedCall_message),
146 createSendSmsFromNotificationPendingIntent(handleUri));
147 }
Santos Cordona0e5f1a2014-03-31 21:43:00 -0700148
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700149 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 }
Santos Cordona0e5f1a2014-03-31 21:43:00 -0700158 } else {
Sailesh Nepal9b616c52014-04-10 20:20:35 -0700159 Log.d(this, "Suppress actions. handle: %s, missedCalls: %d.", Log.piiHandle(handle),
Santos Cordona0e5f1a2014-03-31 21:43:00 -0700160 mMissedCallCount);
161 }
162
163 Notification notification = builder.build();
164 configureLedOnNotification(notification);
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700165
166 Log.i(this, "Adding missed call notification for %s.", call);
Santos Cordona0e5f1a2014-03-31 21:43:00 -0700167 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) {
Santos Cordon61e11ad2014-08-06 02:04:09 -0700181 String handle = call.getHandle() == null ? null : call.getHandle().getSchemeSpecificPart();
Santos Cordon99c8a6f2014-05-28 18:28:47 -0700182 String name = call.getName();
183
184 if (!TextUtils.isEmpty(name) && TextUtils.isGraphic(name)) {
185 return name;
186 } else if (!TextUtils.isEmpty(handle)) {
Santos Cordona0e5f1a2014-03-31 21:43:00 -0700187 // A handle should always be displayed LTR using {@link BidiFormatter} regardless of the
188 // content of the rest of the notification.
Santos Cordondf399862014-08-06 04:39:15 -0700189 // TODO: Does this apply to SIP addresses?
Santos Cordona0e5f1a2014-03-31 21:43:00 -0700190 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 createTelecommPendingIntent(
218 TelecommBroadcastReceiver.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 createTelecommPendingIntent(
229 TelecommBroadcastReceiver.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 createTelecommPendingIntent(
238 TelecommBroadcastReceiver.ACTION_SEND_SMS_FROM_NOTIFICATION,
239 Uri.fromParts(SCHEME_SMSTO, handle.getSchemeSpecificPart(), null));
240 }
241
242 /**
243 * Creates generic pending intent from the specified parameters to be received by
244 * {@link TelecommBroadcastReceiver}.
245 *
246 * @param action The intent action.
247 * @param data The intent data.
248 */
249 private PendingIntent createTelecommPendingIntent(String action, Uri data) {
250 Intent intent = new Intent(action, data, mContext, TelecommBroadcastReceiver.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 }
Santos Cordon64c7e962014-07-02 15:15:27 -0700261
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 if (cursor != null) {
273 while (cursor.moveToNext()) {
274 // Get data about the missed call from the cursor
275 Uri handle = Uri.parse(cursor.getString(
276 cursor.getColumnIndexOrThrow(Calls.NUMBER)));
Sailesh Nepale8ecb982014-07-11 17:19:42 -0700277 int presentation = cursor.getInt(cursor.getColumnIndexOrThrow(
278 Calls.NUMBER_PRESENTATION));
279
280 if (presentation != Calls.PRESENTATION_ALLOWED) {
281 handle = null;
282 }
Santos Cordon64c7e962014-07-02 15:15:27 -0700283
284 // Convert the data to a call object
Ihab Awadb78b2762014-07-25 15:16:23 -0700285 Call call = new Call(null, null, null, null, null, true, false);
Santos Cordon64c7e962014-07-02 15:15:27 -0700286 call.setDisconnectCause(DisconnectCause.INCOMING_MISSED, "");
287 call.setState(CallState.DISCONNECTED);
288
289 // Listen for the update to the caller information before posting the
290 // notification so that we have the contact info and photo.
291 call.addListener(new Call.ListenerBase() {
292 @Override
293 public void onCallerInfoChanged(Call call) {
294 call.removeListener(this); // No longer need to listen to call
295 // changes after the contact info
296 // is retrieved.
297 showMissedCallNotification(call);
298 }
299 });
300 // Set the handle here because that is what triggers the contact info query.
Sailesh Nepale8ecb982014-07-11 17:19:42 -0700301 call.setHandle(handle, presentation);
Santos Cordon64c7e962014-07-02 15:15:27 -0700302 }
303 }
304 }
305 };
306
307 // setup query spec, look for all Missed calls that are new.
308 StringBuilder where = new StringBuilder("type=");
309 where.append(Calls.MISSED_TYPE);
310 where.append(" AND new=1");
311
312 // start the query
313 queryHandler.startQuery(0, null, Calls.CONTENT_URI, CALL_LOG_PROJECTION,
314 where.toString(), null, Calls.DEFAULT_SORT_ORDER);
315 }
Santos Cordona0e5f1a2014-03-31 21:43:00 -0700316}