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