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