Home | History | Annotate | Download | only in calllog
      1 /*
      2  * Copyright (C) 2011 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.dialer.calllog;
     18 
     19 import static android.Manifest.permission.READ_CALL_LOG;
     20 import static android.Manifest.permission.READ_CONTACTS;
     21 
     22 import android.app.Notification;
     23 import android.app.NotificationManager;
     24 import android.app.PendingIntent;
     25 import android.content.ContentResolver;
     26 import android.content.ContentUris;
     27 import android.content.Context;
     28 import android.content.Intent;
     29 import android.content.res.Resources;
     30 import android.database.Cursor;
     31 import android.net.Uri;
     32 import android.provider.CallLog.Calls;
     33 import android.provider.ContactsContract.PhoneLookup;
     34 import android.text.TextUtils;
     35 import android.util.Log;
     36 
     37 import com.android.common.io.MoreCloseables;
     38 import com.android.contacts.common.util.PermissionsUtil;
     39 import com.android.dialer.DialtactsActivity;
     40 import com.android.dialer.R;
     41 import com.android.dialer.calllog.PhoneAccountUtils;
     42 import com.android.dialer.list.ListsFragment;
     43 import com.google.common.collect.Maps;
     44 
     45 import java.util.Map;
     46 
     47 /**
     48  * VoicemailNotifier that shows a notification in the status bar.
     49  */
     50 public class DefaultVoicemailNotifier {
     51     public static final String TAG = "DefaultVoicemailNotifier";
     52 
     53     /** The tag used to identify notifications from this class. */
     54     private static final String NOTIFICATION_TAG = "DefaultVoicemailNotifier";
     55     /** The identifier of the notification of new voicemails. */
     56     private static final int NOTIFICATION_ID = 1;
     57 
     58     /** The singleton instance of {@link DefaultVoicemailNotifier}. */
     59     private static DefaultVoicemailNotifier sInstance;
     60 
     61     private final Context mContext;
     62     private final NotificationManager mNotificationManager;
     63     private final NewCallsQuery mNewCallsQuery;
     64     private final NameLookupQuery mNameLookupQuery;
     65 
     66     /** Returns the singleton instance of the {@link DefaultVoicemailNotifier}. */
     67     public static synchronized DefaultVoicemailNotifier getInstance(Context context) {
     68         if (sInstance == null) {
     69             NotificationManager notificationManager =
     70                     (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
     71             ContentResolver contentResolver = context.getContentResolver();
     72             sInstance = new DefaultVoicemailNotifier(context, notificationManager,
     73                     createNewCallsQuery(context, contentResolver),
     74                     createNameLookupQuery(context, contentResolver));
     75         }
     76         return sInstance;
     77     }
     78 
     79     private DefaultVoicemailNotifier(Context context,
     80             NotificationManager notificationManager, NewCallsQuery newCallsQuery,
     81             NameLookupQuery nameLookupQuery) {
     82         mContext = context;
     83         mNotificationManager = notificationManager;
     84         mNewCallsQuery = newCallsQuery;
     85         mNameLookupQuery = nameLookupQuery;
     86     }
     87 
     88     /**
     89      * Updates the notification and notifies of the call with the given URI.
     90      *
     91      * Clears the notification if there are no new voicemails, and notifies if the given URI
     92      * corresponds to a new voicemail.
     93      *
     94      * It is not safe to call this method from the main thread.
     95      */
     96     public void updateNotification(Uri newCallUri) {
     97         // Lookup the list of new voicemails to include in the notification.
     98         // TODO: Move this into a service, to avoid holding the receiver up.
     99         final NewCall[] newCalls = mNewCallsQuery.query();
    100 
    101         if (newCalls == null) {
    102             // Query failed, just return.
    103             return;
    104         }
    105 
    106         if (newCalls.length == 0) {
    107             // No voicemails to notify about: clear the notification.
    108             clearNotification();
    109             return;
    110         }
    111 
    112         Resources resources = mContext.getResources();
    113 
    114         // This represents a list of names to include in the notification.
    115         String callers = null;
    116 
    117         // Maps each number into a name: if a number is in the map, it has already left a more
    118         // recent voicemail.
    119         final Map<String, String> names = Maps.newHashMap();
    120 
    121         // Determine the call corresponding to the new voicemail we have to notify about.
    122         NewCall callToNotify = null;
    123 
    124         // Iterate over the new voicemails to determine all the information above.
    125         for (NewCall newCall : newCalls) {
    126             // Check if we already know the name associated with this number.
    127             String name = names.get(newCall.number);
    128             if (name == null) {
    129                 name = PhoneNumberDisplayUtil.getDisplayName(
    130                         mContext,
    131                         newCall.number,
    132                         newCall.numberPresentation,
    133                         /* isVoicemail */ false).toString();
    134                 // If we cannot lookup the contact, use the number instead.
    135                 if (TextUtils.isEmpty(name)) {
    136                     // Look it up in the database.
    137                     name = mNameLookupQuery.query(newCall.number);
    138                     if (TextUtils.isEmpty(name)) {
    139                         name = newCall.number;
    140                     }
    141                 }
    142                 names.put(newCall.number, name);
    143                 // This is a new caller. Add it to the back of the list of callers.
    144                 if (TextUtils.isEmpty(callers)) {
    145                     callers = name;
    146                 } else {
    147                     callers = resources.getString(
    148                             R.string.notification_voicemail_callers_list, callers, name);
    149                 }
    150             }
    151             // Check if this is the new call we need to notify about.
    152             if (newCallUri != null && newCall.voicemailUri != null &&
    153                     ContentUris.parseId(newCallUri) == ContentUris.parseId(newCall.voicemailUri)) {
    154                 callToNotify = newCall;
    155             }
    156         }
    157 
    158         // If there is only one voicemail, set its transcription as the "long text".
    159         String transcription = null;
    160         if (newCalls.length == 1) {
    161             transcription = newCalls[0].transcription;
    162         }
    163 
    164         if (newCallUri != null && callToNotify == null) {
    165             Log.e(TAG, "The new call could not be found in the call log: " + newCallUri);
    166         }
    167 
    168         // Determine the title of the notification and the icon for it.
    169         final String title = resources.getQuantityString(
    170                 R.plurals.notification_voicemail_title, newCalls.length, newCalls.length);
    171         // TODO: Use the photo of contact if all calls are from the same person.
    172         final int icon = android.R.drawable.stat_notify_voicemail;
    173 
    174         Notification.Builder notificationBuilder = new Notification.Builder(mContext)
    175                 .setSmallIcon(icon)
    176                 .setContentTitle(title)
    177                 .setContentText(callers)
    178                 .setStyle(new Notification.BigTextStyle().bigText(transcription))
    179                 .setColor(resources.getColor(R.color.dialer_theme_color))
    180                 .setDefaults(callToNotify != null ? Notification.DEFAULT_ALL : 0)
    181                 .setDeleteIntent(createMarkNewVoicemailsAsOldIntent())
    182                 .setAutoCancel(true);
    183 
    184         // Determine the intent to fire when the notification is clicked on.
    185         final Intent contentIntent;
    186         // Open the call log.
    187         // TODO: Send to recents tab in Dialer instead.
    188         contentIntent = new Intent(mContext, DialtactsActivity.class);
    189         contentIntent.putExtra(DialtactsActivity.EXTRA_SHOW_TAB, ListsFragment.TAB_INDEX_VOICEMAIL);
    190         notificationBuilder.setContentIntent(PendingIntent.getActivity(
    191                 mContext, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT));
    192 
    193         // The text to show in the ticker, describing the new event.
    194         if (callToNotify != null) {
    195             notificationBuilder.setTicker(resources.getString(
    196                     R.string.notification_new_voicemail_ticker, names.get(callToNotify.number)));
    197         }
    198 
    199         mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_ID, notificationBuilder.build());
    200     }
    201 
    202     /** Creates a pending intent that marks all new voicemails as old. */
    203     private PendingIntent createMarkNewVoicemailsAsOldIntent() {
    204         Intent intent = new Intent(mContext, CallLogNotificationsService.class);
    205         intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD);
    206         return PendingIntent.getService(mContext, 0, intent, 0);
    207     }
    208 
    209     public void clearNotification() {
    210         mNotificationManager.cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
    211     }
    212 
    213     /** Information about a new voicemail. */
    214     private static final class NewCall {
    215         public final Uri callsUri;
    216         public final Uri voicemailUri;
    217         public final String number;
    218         public final int numberPresentation;
    219         public final String accountComponentName;
    220         public final String accountId;
    221         public final String transcription;
    222 
    223         public NewCall(
    224                 Uri callsUri,
    225                 Uri voicemailUri,
    226                 String number,
    227                 int numberPresentation,
    228                 String accountComponentName,
    229                 String accountId,
    230                 String transcription) {
    231             this.callsUri = callsUri;
    232             this.voicemailUri = voicemailUri;
    233             this.number = number;
    234             this.numberPresentation = numberPresentation;
    235             this.accountComponentName = accountComponentName;
    236             this.accountId = accountId;
    237             this.transcription = transcription;
    238         }
    239     }
    240 
    241     /** Allows determining the new calls for which a notification should be generated. */
    242     public interface NewCallsQuery {
    243         /**
    244          * Returns the new calls for which a notification should be generated.
    245          */
    246         public NewCall[] query();
    247     }
    248 
    249     /** Create a new instance of {@link NewCallsQuery}. */
    250     public static NewCallsQuery createNewCallsQuery(Context context,
    251             ContentResolver contentResolver) {
    252         return new DefaultNewCallsQuery(context.getApplicationContext(), contentResolver);
    253     }
    254 
    255     /**
    256      * Default implementation of {@link NewCallsQuery} that looks up the list of new calls to
    257      * notify about in the call log.
    258      */
    259     private static final class DefaultNewCallsQuery implements NewCallsQuery {
    260         private static final String[] PROJECTION = {
    261             Calls._ID,
    262             Calls.NUMBER,
    263             Calls.VOICEMAIL_URI,
    264             Calls.NUMBER_PRESENTATION,
    265             Calls.PHONE_ACCOUNT_COMPONENT_NAME,
    266             Calls.PHONE_ACCOUNT_ID,
    267             Calls.TRANSCRIPTION
    268         };
    269         private static final int ID_COLUMN_INDEX = 0;
    270         private static final int NUMBER_COLUMN_INDEX = 1;
    271         private static final int VOICEMAIL_URI_COLUMN_INDEX = 2;
    272         private static final int NUMBER_PRESENTATION_COLUMN_INDEX = 3;
    273         private static final int PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX = 4;
    274         private static final int PHONE_ACCOUNT_ID_COLUMN_INDEX = 5;
    275         private static final int TRANSCRIPTION_COLUMN_INDEX = 6;
    276 
    277         private final ContentResolver mContentResolver;
    278         private final Context mContext;
    279 
    280         private DefaultNewCallsQuery(Context context, ContentResolver contentResolver) {
    281             mContext = context;
    282             mContentResolver = contentResolver;
    283         }
    284 
    285         @Override
    286         public NewCall[] query() {
    287             if (!PermissionsUtil.hasPermission(mContext, READ_CALL_LOG)) {
    288                 Log.w(TAG, "No READ_CALL_LOG permission, returning null for calls lookup.");
    289                 return null;
    290             }
    291             final String selection = String.format("%s = 1 AND %s = ?", Calls.NEW, Calls.TYPE);
    292             final String[] selectionArgs = new String[]{ Integer.toString(Calls.VOICEMAIL_TYPE) };
    293             Cursor cursor = null;
    294             try {
    295                 cursor = mContentResolver.query(Calls.CONTENT_URI_WITH_VOICEMAIL, PROJECTION,
    296                         selection, selectionArgs, Calls.DEFAULT_SORT_ORDER);
    297                 if (cursor == null) {
    298                     return null;
    299                 }
    300                 NewCall[] newCalls = new NewCall[cursor.getCount()];
    301                 while (cursor.moveToNext()) {
    302                     newCalls[cursor.getPosition()] = createNewCallsFromCursor(cursor);
    303                 }
    304                 return newCalls;
    305             } catch (RuntimeException e) {
    306                 Log.w(TAG, "Exception when querying Contacts Provider for calls lookup");
    307                 return null;
    308             } finally {
    309                 MoreCloseables.closeQuietly(cursor);
    310             }
    311         }
    312 
    313         /** Returns an instance of {@link NewCall} created by using the values of the cursor. */
    314         private NewCall createNewCallsFromCursor(Cursor cursor) {
    315             String voicemailUriString = cursor.getString(VOICEMAIL_URI_COLUMN_INDEX);
    316             Uri callsUri = ContentUris.withAppendedId(
    317                     Calls.CONTENT_URI_WITH_VOICEMAIL, cursor.getLong(ID_COLUMN_INDEX));
    318             Uri voicemailUri = voicemailUriString == null ? null : Uri.parse(voicemailUriString);
    319             return new NewCall(
    320                     callsUri,
    321                     voicemailUri,
    322                     cursor.getString(NUMBER_COLUMN_INDEX),
    323                     cursor.getInt(NUMBER_PRESENTATION_COLUMN_INDEX),
    324                     cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX),
    325                     cursor.getString(PHONE_ACCOUNT_ID_COLUMN_INDEX),
    326                     cursor.getString(TRANSCRIPTION_COLUMN_INDEX));
    327         }
    328     }
    329 
    330     /** Allows determining the name associated with a given phone number. */
    331     public interface NameLookupQuery {
    332         /**
    333          * Returns the name associated with the given number in the contacts database, or null if
    334          * the number does not correspond to any of the contacts.
    335          * <p>
    336          * If there are multiple contacts with the same phone number, it will return the name of one
    337          * of the matching contacts.
    338          */
    339         public String query(String number);
    340     }
    341 
    342     /** Create a new instance of {@link NameLookupQuery}. */
    343     public static NameLookupQuery createNameLookupQuery(Context context,
    344             ContentResolver contentResolver) {
    345         return new DefaultNameLookupQuery(context.getApplicationContext(), contentResolver);
    346     }
    347 
    348     /**
    349      * Default implementation of {@link NameLookupQuery} that looks up the name of a contact in the
    350      * contacts database.
    351      */
    352     private static final class DefaultNameLookupQuery implements NameLookupQuery {
    353         private static final String[] PROJECTION = { PhoneLookup.DISPLAY_NAME };
    354         private static final int DISPLAY_NAME_COLUMN_INDEX = 0;
    355 
    356         private final ContentResolver mContentResolver;
    357         private final Context mContext;
    358 
    359         private DefaultNameLookupQuery(Context context, ContentResolver contentResolver) {
    360             mContext = context;
    361             mContentResolver = contentResolver;
    362         }
    363 
    364         @Override
    365         public String query(String number) {
    366             if (!PermissionsUtil.hasPermission(mContext, READ_CONTACTS)) {
    367                 Log.w(TAG, "No READ_CONTACTS permission, returning null for name lookup.");
    368                 return null;
    369             }
    370             Cursor cursor = null;
    371             try {
    372                 cursor = mContentResolver.query(
    373                         Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)),
    374                         PROJECTION, null, null, null);
    375                 if (cursor == null || !cursor.moveToFirst()) return null;
    376                 return cursor.getString(DISPLAY_NAME_COLUMN_INDEX);
    377             } catch (RuntimeException e) {
    378                 Log.w(TAG, "Exception when querying Contacts Provider for name lookup");
    379                 return null;
    380             } finally {
    381                 if (cursor != null) {
    382                     cursor.close();
    383                 }
    384             }
    385         }
    386     }
    387 }
    388