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