/* * Copyright 2014, The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.telecom; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.TaskStackBuilder; import android.content.AsyncQueryHandler; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; import android.os.UserHandle; import android.provider.CallLog; import android.provider.CallLog.Calls; import android.telecom.CallState; import android.telecom.DisconnectCause; import android.telecom.PhoneAccount; import android.telephony.PhoneNumberUtils; import android.text.BidiFormatter; import android.text.TextDirectionHeuristics; import android.text.TextUtils; // TODO: Needed for move to system service: import com.android.internal.R; /** * Creates a notification for calls that the user missed (neither answered nor rejected). * TODO: Make TelephonyManager.clearMissedCalls call into this class. */ class MissedCallNotifier extends CallsManagerListenerBase { private static final String[] CALL_LOG_PROJECTION = new String[] { Calls._ID, Calls.NUMBER, Calls.NUMBER_PRESENTATION, Calls.DATE, Calls.DURATION, Calls.TYPE, }; private static final int CALL_LOG_COLUMN_ID = 0; private static final int CALL_LOG_COLUMN_NUMBER = 1; private static final int CALL_LOG_COLUMN_NUMBER_PRESENTATION = 2; private static final int CALL_LOG_COLUMN_DATE = 3; private static final int CALL_LOG_COLUMN_DURATION = 4; private static final int CALL_LOG_COLUMN_TYPE = 5; private static final int MISSED_CALL_NOTIFICATION_ID = 1; private final Context mContext; private final NotificationManager mNotificationManager; // Used to track the number of missed calls. private int mMissedCallCount = 0; MissedCallNotifier(Context context) { mContext = context; mNotificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); updateOnStartup(); } /** {@inheritDoc} */ @Override public void onCallStateChanged(Call call, int oldState, int newState) { if (oldState == CallState.RINGING && newState == CallState.DISCONNECTED && call.getDisconnectCause().getCode() == DisconnectCause.MISSED) { showMissedCallNotification(call); } } /** Clears missed call notification and marks the call log's missed calls as read. */ void clearMissedCalls() { AsyncTask.execute(new Runnable() { @Override public void run() { // Clear the list of new missed calls from the call log. ContentValues values = new ContentValues(); values.put(Calls.NEW, 0); values.put(Calls.IS_READ, 1); StringBuilder where = new StringBuilder(); where.append(Calls.NEW); where.append(" = 1 AND "); where.append(Calls.TYPE); where.append(" = ?"); mContext.getContentResolver().update(Calls.CONTENT_URI, values, where.toString(), new String[]{ Integer.toString(Calls.MISSED_TYPE) }); } }); cancelMissedCallNotification(); } /** * Create a system notification for the missed call. * * @param call The missed call. */ void showMissedCallNotification(Call call) { mMissedCallCount++; final int titleResId; final String expandedText; // The text in the notification's line 1 and 2. // Display the first line of the notification: // 1 missed call: // More than 1 missed call: + "missed calls" if (mMissedCallCount == 1) { titleResId = R.string.notification_missedCallTitle; expandedText = getNameForCall(call); } else { titleResId = R.string.notification_missedCallsTitle; expandedText = mContext.getString(R.string.notification_missedCallsMsg, mMissedCallCount); } // Create the notification. Notification.Builder builder = new Notification.Builder(mContext); builder.setSmallIcon(android.R.drawable.stat_notify_missed_call) .setColor(mContext.getResources().getColor(R.color.theme_color)) .setWhen(call.getCreationTimeMillis()) .setContentTitle(mContext.getText(titleResId)) .setContentText(expandedText) .setContentIntent(createCallLogPendingIntent()) .setAutoCancel(true) .setDeleteIntent(createClearMissedCallsPendingIntent()); Uri handleUri = call.getHandle(); String handle = handleUri == null ? null : handleUri.getSchemeSpecificPart(); // Add additional actions when there is only 1 missed call, like call-back and SMS. if (mMissedCallCount == 1) { Log.d(this, "Add actions with number %s.", Log.piiHandle(handle)); if (!TextUtils.isEmpty(handle) && !TextUtils.equals(handle, mContext.getString(R.string.handle_restricted))) { builder.addAction(R.drawable.stat_sys_phone_call, mContext.getString(R.string.notification_missedCall_call_back), createCallBackPendingIntent(handleUri)); builder.addAction(R.drawable.ic_text_holo_dark, mContext.getString(R.string.notification_missedCall_message), createSendSmsFromNotificationPendingIntent(handleUri)); } Bitmap photoIcon = call.getPhotoIcon(); if (photoIcon != null) { builder.setLargeIcon(photoIcon); } else { Drawable photo = call.getPhoto(); if (photo != null && photo instanceof BitmapDrawable) { builder.setLargeIcon(((BitmapDrawable) photo).getBitmap()); } } } else { Log.d(this, "Suppress actions. handle: %s, missedCalls: %d.", Log.piiHandle(handle), mMissedCallCount); } Notification notification = builder.build(); configureLedOnNotification(notification); Log.i(this, "Adding missed call notification for %s.", call); mNotificationManager.notifyAsUser( null /* tag */ , MISSED_CALL_NOTIFICATION_ID, notification, UserHandle.CURRENT); } /** Cancels the "missed call" notification. */ private void cancelMissedCallNotification() { // Reset the number of missed calls to 0. mMissedCallCount = 0; mNotificationManager.cancel(MISSED_CALL_NOTIFICATION_ID); } /** * Returns the name to use in the missed call notification. */ private String getNameForCall(Call call) { String handle = call.getHandle() == null ? null : call.getHandle().getSchemeSpecificPart(); String name = call.getName(); if (!TextUtils.isEmpty(name) && TextUtils.isGraphic(name)) { return name; } else if (!TextUtils.isEmpty(handle)) { // A handle should always be displayed LTR using {@link BidiFormatter} regardless of the // content of the rest of the notification. // TODO: Does this apply to SIP addresses? BidiFormatter bidiFormatter = BidiFormatter.getInstance(); return bidiFormatter.unicodeWrap(handle, TextDirectionHeuristics.LTR); } else { // Use "unknown" if the call is unidentifiable. return mContext.getString(R.string.unknown); } } /** * Creates a new pending intent that sends the user to the call log. * * @return The pending intent. */ private PendingIntent createCallLogPendingIntent() { Intent intent = new Intent(Intent.ACTION_VIEW, null); intent.setType(CallLog.Calls.CONTENT_TYPE); TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(mContext); taskStackBuilder.addNextIntent(intent); return taskStackBuilder.getPendingIntent(0, 0); } /** * Creates an intent to be invoked when the missed call notification is cleared. */ private PendingIntent createClearMissedCallsPendingIntent() { return createTelecomPendingIntent( TelecomBroadcastReceiver.ACTION_CLEAR_MISSED_CALLS, null); } /** * Creates an intent to be invoked when the user opts to "call back" from the missed call * notification. * * @param handle The handle to call back. */ private PendingIntent createCallBackPendingIntent(Uri handle) { return createTelecomPendingIntent( TelecomBroadcastReceiver.ACTION_CALL_BACK_FROM_NOTIFICATION, handle); } /** * Creates an intent to be invoked when the user opts to "send sms" from the missed call * notification. */ private PendingIntent createSendSmsFromNotificationPendingIntent(Uri handle) { return createTelecomPendingIntent( TelecomBroadcastReceiver.ACTION_SEND_SMS_FROM_NOTIFICATION, Uri.fromParts(Constants.SCHEME_SMSTO, handle.getSchemeSpecificPart(), null)); } /** * Creates generic pending intent from the specified parameters to be received by * {@link TelecomBroadcastReceiver}. * * @param action The intent action. * @param data The intent data. */ private PendingIntent createTelecomPendingIntent(String action, Uri data) { Intent intent = new Intent(action, data, mContext, TelecomBroadcastReceiver.class); return PendingIntent.getBroadcast(mContext, 0, intent, 0); } /** * Configures a notification to emit the blinky notification light. */ private void configureLedOnNotification(Notification notification) { notification.flags |= Notification.FLAG_SHOW_LIGHTS; notification.defaults |= Notification.DEFAULT_LIGHTS; } /** * Adds the missed call notification on startup if there are unread missed calls. */ private void updateOnStartup() { Log.d(this, "updateOnStartup()..."); // instantiate query handler AsyncQueryHandler queryHandler = new AsyncQueryHandler(mContext.getContentResolver()) { @Override protected void onQueryComplete(int token, Object cookie, Cursor cursor) { Log.d(MissedCallNotifier.this, "onQueryComplete()..."); if (cursor != null) { try { while (cursor.moveToNext()) { // Get data about the missed call from the cursor final String handleString = cursor.getString(CALL_LOG_COLUMN_NUMBER); final int presentation = cursor.getInt(CALL_LOG_COLUMN_NUMBER_PRESENTATION); final long date = cursor.getLong(CALL_LOG_COLUMN_DATE); final Uri handle; if (presentation != Calls.PRESENTATION_ALLOWED || TextUtils.isEmpty(handleString)) { handle = null; } else { handle = Uri.fromParts(PhoneNumberUtils.isUriNumber(handleString) ? PhoneAccount.SCHEME_SIP : PhoneAccount.SCHEME_TEL, handleString, null); } // Convert the data to a call object Call call = new Call(mContext, null, null, null, null, null, true, false); call.setDisconnectCause(new DisconnectCause(DisconnectCause.MISSED)); call.setState(CallState.DISCONNECTED); call.setCreationTimeMillis(date); // Listen for the update to the caller information before posting the // notification so that we have the contact info and photo. call.addListener(new Call.ListenerBase() { @Override public void onCallerInfoChanged(Call call) { call.removeListener(this); // No longer need to listen to call // changes after the contact info // is retrieved. showMissedCallNotification(call); } }); // Set the handle here because that is what triggers the contact info // query. call.setHandle(handle, presentation); } } finally { cursor.close(); } } } }; // setup query spec, look for all Missed calls that are new. StringBuilder where = new StringBuilder("type="); where.append(Calls.MISSED_TYPE); where.append(" AND new=1"); // start the query queryHandler.startQuery(0, null, Calls.CONTENT_URI, CALL_LOG_PROJECTION, where.toString(), null, Calls.DEFAULT_SORT_ORDER); } }