Santos Cordon | a0e5f1a | 2014-03-31 21:43:00 -0700 | [diff] [blame] | 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.telecomm; |
| 18 | |
| 19 | import android.app.Notification; |
| 20 | import android.app.NotificationManager; |
| 21 | import android.app.PendingIntent; |
| 22 | import android.app.TaskStackBuilder; |
Santos Cordon | 64c7e96 | 2014-07-02 15:15:27 -0700 | [diff] [blame] | 23 | import android.content.AsyncQueryHandler; |
Santos Cordon | a0e5f1a | 2014-03-31 21:43:00 -0700 | [diff] [blame] | 24 | import android.content.ContentValues; |
| 25 | import android.content.Context; |
| 26 | import android.content.Intent; |
Santos Cordon | 64c7e96 | 2014-07-02 15:15:27 -0700 | [diff] [blame] | 27 | import android.database.Cursor; |
Santos Cordon | 99c8a6f | 2014-05-28 18:28:47 -0700 | [diff] [blame] | 28 | import android.graphics.Bitmap; |
| 29 | import android.graphics.drawable.BitmapDrawable; |
| 30 | import android.graphics.drawable.Drawable; |
Santos Cordon | a0e5f1a | 2014-03-31 21:43:00 -0700 | [diff] [blame] | 31 | import android.net.Uri; |
| 32 | import android.provider.CallLog; |
| 33 | import android.provider.CallLog.Calls; |
| 34 | import android.telecomm.CallState; |
| 35 | import android.telephony.DisconnectCause; |
| 36 | import android.text.BidiFormatter; |
| 37 | import android.text.TextDirectionHeuristics; |
| 38 | import android.text.TextUtils; |
| 39 | |
| 40 | /** |
| 41 | * Creates a notification for calls that the user missed (neither answered nor rejected). |
| 42 | * TODO(santoscordon): Make TelephonyManager.clearMissedCalls call into this class. |
| 43 | * STOPSHIP: Resolve b/13769374 about moving this class to InCall. |
| 44 | */ |
| 45 | class MissedCallNotifier extends CallsManagerListenerBase { |
| 46 | |
Santos Cordon | 64c7e96 | 2014-07-02 15:15:27 -0700 | [diff] [blame] | 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 | }; |
Santos Cordon | a0e5f1a | 2014-03-31 21:43:00 -0700 | [diff] [blame] | 55 | 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 Cordon | 64c7e96 | 2014-07-02 15:15:27 -0700 | [diff] [blame] | 68 | |
| 69 | updateOnStartup(); |
Santos Cordon | a0e5f1a | 2014-03-31 21:43:00 -0700 | [diff] [blame] | 70 | } |
| 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 Nepal | 8c85dee | 2014-04-07 22:21:40 -0700 | [diff] [blame] | 124 | .setWhen(call.getCreationTimeMillis()) |
Santos Cordon | a0e5f1a | 2014-03-31 21:43:00 -0700 | [diff] [blame] | 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.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 | builder.addAction(R.drawable.stat_sys_phone_call, |
| 139 | mContext.getString(R.string.notification_missedCall_call_back), |
| 140 | createCallBackPendingIntent(handleUri)); |
| 141 | |
| 142 | builder.addAction(R.drawable.ic_text_holo_dark, |
| 143 | mContext.getString(R.string.notification_missedCall_message), |
| 144 | createSendSmsFromNotificationPendingIntent(handleUri)); |
| 145 | |
Santos Cordon | 99c8a6f | 2014-05-28 18:28:47 -0700 | [diff] [blame] | 146 | Bitmap photoIcon = call.getPhotoIcon(); |
| 147 | if (photoIcon != null) { |
| 148 | builder.setLargeIcon(photoIcon); |
| 149 | } else { |
| 150 | Drawable photo = call.getPhoto(); |
| 151 | if (photo != null && photo instanceof BitmapDrawable) { |
| 152 | builder.setLargeIcon(((BitmapDrawable) photo).getBitmap()); |
| 153 | } |
| 154 | } |
Santos Cordon | a0e5f1a | 2014-03-31 21:43:00 -0700 | [diff] [blame] | 155 | } else { |
Sailesh Nepal | 9b616c5 | 2014-04-10 20:20:35 -0700 | [diff] [blame] | 156 | Log.d(this, "Suppress actions. handle: %s, missedCalls: %d.", Log.piiHandle(handle), |
Santos Cordon | a0e5f1a | 2014-03-31 21:43:00 -0700 | [diff] [blame] | 157 | mMissedCallCount); |
| 158 | } |
| 159 | |
| 160 | Notification notification = builder.build(); |
| 161 | configureLedOnNotification(notification); |
Santos Cordon | 99c8a6f | 2014-05-28 18:28:47 -0700 | [diff] [blame] | 162 | |
| 163 | Log.i(this, "Adding missed call notification for %s.", call); |
Santos Cordon | a0e5f1a | 2014-03-31 21:43:00 -0700 | [diff] [blame] | 164 | mNotificationManager.notify(MISSED_CALL_NOTIFICATION_ID, notification); |
| 165 | } |
| 166 | |
| 167 | /** Cancels the "missed call" notification. */ |
| 168 | private void cancelMissedCallNotification() { |
| 169 | // Reset the number of missed calls to 0. |
| 170 | mMissedCallCount = 0; |
| 171 | mNotificationManager.cancel(MISSED_CALL_NOTIFICATION_ID); |
| 172 | } |
| 173 | |
| 174 | /** |
| 175 | * Returns the name to use in the missed call notification. |
| 176 | */ |
| 177 | private String getNameForCall(Call call) { |
Santos Cordon | a0e5f1a | 2014-03-31 21:43:00 -0700 | [diff] [blame] | 178 | String handle = call.getHandle().getSchemeSpecificPart(); |
Santos Cordon | 99c8a6f | 2014-05-28 18:28:47 -0700 | [diff] [blame] | 179 | String name = call.getName(); |
| 180 | |
| 181 | if (!TextUtils.isEmpty(name) && TextUtils.isGraphic(name)) { |
| 182 | return name; |
| 183 | } else if (!TextUtils.isEmpty(handle)) { |
Santos Cordon | a0e5f1a | 2014-03-31 21:43:00 -0700 | [diff] [blame] | 184 | // A handle should always be displayed LTR using {@link BidiFormatter} regardless of the |
| 185 | // content of the rest of the notification. |
| 186 | // TODO(santoscordon): Does this apply to SIP addresses? |
| 187 | BidiFormatter bidiFormatter = BidiFormatter.getInstance(); |
| 188 | return bidiFormatter.unicodeWrap(handle, TextDirectionHeuristics.LTR); |
| 189 | } else { |
| 190 | // Use "unknown" if the call is unidentifiable. |
| 191 | return mContext.getString(R.string.unknown); |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | /** |
| 196 | * Creates a new pending intent that sends the user to the call log. |
| 197 | * |
| 198 | * @return The pending intent. |
| 199 | */ |
| 200 | private PendingIntent createCallLogPendingIntent() { |
| 201 | Intent intent = new Intent(Intent.ACTION_VIEW, null); |
| 202 | intent.setType(CallLog.Calls.CONTENT_TYPE); |
| 203 | |
| 204 | TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(mContext); |
| 205 | taskStackBuilder.addNextIntent(intent); |
| 206 | |
| 207 | return taskStackBuilder.getPendingIntent(0, 0); |
| 208 | } |
| 209 | |
| 210 | /** |
| 211 | * Creates an intent to be invoked when the missed call notification is cleared. |
| 212 | */ |
| 213 | private PendingIntent createClearMissedCallsPendingIntent() { |
| 214 | return createTelecommPendingIntent( |
| 215 | TelecommBroadcastReceiver.ACTION_CLEAR_MISSED_CALLS, null); |
| 216 | } |
| 217 | |
| 218 | /** |
| 219 | * Creates an intent to be invoked when the user opts to "call back" from the missed call |
| 220 | * notification. |
| 221 | * |
| 222 | * @param handle The handle to call back. |
| 223 | */ |
| 224 | private PendingIntent createCallBackPendingIntent(Uri handle) { |
| 225 | return createTelecommPendingIntent( |
| 226 | TelecommBroadcastReceiver.ACTION_CALL_BACK_FROM_NOTIFICATION, handle); |
| 227 | } |
| 228 | |
| 229 | /** |
| 230 | * Creates an intent to be invoked when the user opts to "send sms" from the missed call |
| 231 | * notification. |
| 232 | */ |
| 233 | private PendingIntent createSendSmsFromNotificationPendingIntent(Uri handle) { |
| 234 | return createTelecommPendingIntent( |
| 235 | TelecommBroadcastReceiver.ACTION_SEND_SMS_FROM_NOTIFICATION, |
| 236 | Uri.fromParts(SCHEME_SMSTO, handle.getSchemeSpecificPart(), null)); |
| 237 | } |
| 238 | |
| 239 | /** |
| 240 | * Creates generic pending intent from the specified parameters to be received by |
| 241 | * {@link TelecommBroadcastReceiver}. |
| 242 | * |
| 243 | * @param action The intent action. |
| 244 | * @param data The intent data. |
| 245 | */ |
| 246 | private PendingIntent createTelecommPendingIntent(String action, Uri data) { |
| 247 | Intent intent = new Intent(action, data, mContext, TelecommBroadcastReceiver.class); |
| 248 | return PendingIntent.getBroadcast(mContext, 0, intent, 0); |
| 249 | } |
| 250 | |
| 251 | /** |
| 252 | * Configures a notification to emit the blinky notification light. |
| 253 | */ |
| 254 | private void configureLedOnNotification(Notification notification) { |
| 255 | notification.flags |= Notification.FLAG_SHOW_LIGHTS; |
| 256 | notification.defaults |= Notification.DEFAULT_LIGHTS; |
| 257 | } |
Santos Cordon | 64c7e96 | 2014-07-02 15:15:27 -0700 | [diff] [blame] | 258 | |
| 259 | /** |
| 260 | * Adds the missed call notification on startup if there are unread missed calls. |
| 261 | */ |
| 262 | private void updateOnStartup() { |
| 263 | Log.d(this, "updateOnStartup()..."); |
| 264 | |
| 265 | // instantiate query handler |
| 266 | AsyncQueryHandler queryHandler = new AsyncQueryHandler(mContext.getContentResolver()) { |
| 267 | @Override |
| 268 | protected void onQueryComplete(int token, Object cookie, Cursor cursor) { |
| 269 | if (cursor != null) { |
| 270 | while (cursor.moveToNext()) { |
| 271 | // Get data about the missed call from the cursor |
| 272 | Uri handle = Uri.parse(cursor.getString( |
| 273 | cursor.getColumnIndexOrThrow(Calls.NUMBER))); |
| 274 | |
| 275 | // Convert the data to a call object |
| 276 | Call call = new Call(null, null, null, true, false); |
| 277 | call.setDisconnectCause(DisconnectCause.INCOMING_MISSED, ""); |
| 278 | call.setState(CallState.DISCONNECTED); |
| 279 | |
| 280 | // Listen for the update to the caller information before posting the |
| 281 | // notification so that we have the contact info and photo. |
| 282 | call.addListener(new Call.ListenerBase() { |
| 283 | @Override |
| 284 | public void onCallerInfoChanged(Call call) { |
| 285 | call.removeListener(this); // No longer need to listen to call |
| 286 | // changes after the contact info |
| 287 | // is retrieved. |
| 288 | showMissedCallNotification(call); |
| 289 | } |
| 290 | }); |
| 291 | // Set the handle here because that is what triggers the contact info query. |
| 292 | call.setHandle(handle); |
| 293 | } |
| 294 | } |
| 295 | } |
| 296 | }; |
| 297 | |
| 298 | // setup query spec, look for all Missed calls that are new. |
| 299 | StringBuilder where = new StringBuilder("type="); |
| 300 | where.append(Calls.MISSED_TYPE); |
| 301 | where.append(" AND new=1"); |
| 302 | |
| 303 | // start the query |
| 304 | queryHandler.startQuery(0, null, Calls.CONTENT_URI, CALL_LOG_PROJECTION, |
| 305 | where.toString(), null, Calls.DEFAULT_SORT_ORDER); |
| 306 | } |
Santos Cordon | a0e5f1a | 2014-03-31 21:43:00 -0700 | [diff] [blame] | 307 | } |