Home | History | Annotate | Download | only in telecom
      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