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 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 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. */ 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 */ 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. */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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