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