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