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