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 PhoneNumberHelper 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, PhoneNumberHelper 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                 // Look it up in the database.
    122                 name = mNameLookupQuery.query(newCall.number);
    123                 // If we cannot lookup the contact, use the number instead.
    124                 if (name == null) {
    125                     name = mPhoneNumberHelper.getDisplayNumber(newCall.number, "").toString();
    126                     if (TextUtils.isEmpty(name)) {
    127                         name = newCall.number;
    128                     }
    129                 }
    130                 names.put(newCall.number, name);
    131                 // This is a new caller. Add it to the back of the list of callers.
    132                 if (TextUtils.isEmpty(callers)) {
    133                     callers = name;
    134                 } else {
    135                     callers = resources.getString(
    136                             R.string.notification_voicemail_callers_list, callers, name);
    137                 }
    138             }
    139             // Check if this is the new call we need to notify about.
    140             if (newCallUri != null && newCallUri.equals(newCall.voicemailUri)) {
    141                 callToNotify = newCall;
    142             }
    143         }
    144 
    145         if (newCallUri != null && callToNotify == null) {
    146             Log.e(TAG, "The new call could not be found in the call log: " + newCallUri);
    147         }
    148 
    149         // Determine the title of the notification and the icon for it.
    150         final String title = resources.getQuantityString(
    151                 R.plurals.notification_voicemail_title, newCalls.length, newCalls.length);
    152         // TODO: Use the photo of contact if all calls are from the same person.
    153         final int icon = android.R.drawable.stat_notify_voicemail;
    154 
    155         Notification.Builder notificationBuilder = new Notification.Builder(mContext)
    156                 .setSmallIcon(icon)
    157                 .setContentTitle(title)
    158                 .setContentText(callers)
    159                 .setDefaults(callToNotify != null ? Notification.DEFAULT_ALL : 0)
    160                 .setDeleteIntent(createMarkNewVoicemailsAsOldIntent())
    161                 .setAutoCancel(true);
    162 
    163         // Determine the intent to fire when the notification is clicked on.
    164         final Intent contentIntent;
    165         if (newCalls.length == 1) {
    166             // Open the voicemail directly.
    167             contentIntent = new Intent(mContext, CallDetailActivity.class);
    168             contentIntent.setData(newCalls[0].callsUri);
    169             contentIntent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI,
    170                     newCalls[0].voicemailUri);
    171             Intent playIntent = new Intent(mContext, CallDetailActivity.class);
    172             playIntent.setData(newCalls[0].callsUri);
    173             playIntent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI,
    174                     newCalls[0].voicemailUri);
    175             playIntent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK, true);
    176             playIntent.putExtra(CallDetailActivity.EXTRA_FROM_NOTIFICATION, true);
    177             notificationBuilder.addAction(R.drawable.ic_play_holo_dark,
    178                     resources.getString(R.string.notification_action_voicemail_play),
    179                     PendingIntent.getActivity(mContext, 0, playIntent, 0));
    180         } else {
    181             // Open the call log.
    182             contentIntent = new Intent(Intent.ACTION_VIEW, Calls.CONTENT_URI);
    183         }
    184         notificationBuilder.setContentIntent(
    185                 PendingIntent.getActivity(mContext, 0, contentIntent, 0));
    186 
    187         // The text to show in the ticker, describing the new event.
    188         if (callToNotify != null) {
    189             notificationBuilder.setTicker(resources.getString(
    190                     R.string.notification_new_voicemail_ticker, names.get(callToNotify.number)));
    191         }
    192 
    193         mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_ID, notificationBuilder.build());
    194     }
    195 
    196     /** Creates a pending intent that marks all new voicemails as old. */
    197     private PendingIntent createMarkNewVoicemailsAsOldIntent() {
    198         Intent intent = new Intent(mContext, CallLogNotificationsService.class);
    199         intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD);
    200         return PendingIntent.getService(mContext, 0, intent, 0);
    201     }
    202 
    203     @Override
    204     public void clearNotification() {
    205         mNotificationManager.cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
    206     }
    207 
    208     /** Information about a new voicemail. */
    209     private static final class NewCall {
    210         public final Uri callsUri;
    211         public final Uri voicemailUri;
    212         public final String number;
    213 
    214         public NewCall(Uri callsUri, Uri voicemailUri, String number) {
    215             this.callsUri = callsUri;
    216             this.voicemailUri = voicemailUri;
    217             this.number = number;
    218         }
    219     }
    220 
    221     /** Allows determining the new calls for which a notification should be generated. */
    222     public interface NewCallsQuery {
    223         /**
    224          * Returns the new calls for which a notification should be generated.
    225          */
    226         public NewCall[] query();
    227     }
    228 
    229     /** Create a new instance of {@link NewCallsQuery}. */
    230     public static NewCallsQuery createNewCallsQuery(ContentResolver contentResolver) {
    231         return new DefaultNewCallsQuery(contentResolver);
    232     }
    233 
    234     /**
    235      * Default implementation of {@link NewCallsQuery} that looks up the list of new calls to
    236      * notify about in the call log.
    237      */
    238     private static final class DefaultNewCallsQuery implements NewCallsQuery {
    239         private static final String[] PROJECTION = {
    240             Calls._ID, Calls.NUMBER, Calls.VOICEMAIL_URI
    241         };
    242         private static final int ID_COLUMN_INDEX = 0;
    243         private static final int NUMBER_COLUMN_INDEX = 1;
    244         private static final int VOICEMAIL_URI_COLUMN_INDEX = 2;
    245 
    246         private final ContentResolver mContentResolver;
    247 
    248         private DefaultNewCallsQuery(ContentResolver contentResolver) {
    249             mContentResolver = contentResolver;
    250         }
    251 
    252         @Override
    253         public NewCall[] query() {
    254             final String selection = String.format("%s = 1 AND %s = ?", Calls.NEW, Calls.TYPE);
    255             final String[] selectionArgs = new String[]{ Integer.toString(Calls.VOICEMAIL_TYPE) };
    256             Cursor cursor = null;
    257             try {
    258                 cursor = mContentResolver.query(Calls.CONTENT_URI_WITH_VOICEMAIL, PROJECTION,
    259                         selection, selectionArgs, Calls.DEFAULT_SORT_ORDER);
    260                 if (cursor == null) {
    261                     return null;
    262                 }
    263                 NewCall[] newCalls = new NewCall[cursor.getCount()];
    264                 while (cursor.moveToNext()) {
    265                     newCalls[cursor.getPosition()] = createNewCallsFromCursor(cursor);
    266                 }
    267                 return newCalls;
    268             } finally {
    269                 MoreCloseables.closeQuietly(cursor);
    270             }
    271         }
    272 
    273         /** Returns an instance of {@link NewCall} created by using the values of the cursor. */
    274         private NewCall createNewCallsFromCursor(Cursor cursor) {
    275             String voicemailUriString = cursor.getString(VOICEMAIL_URI_COLUMN_INDEX);
    276             Uri callsUri = ContentUris.withAppendedId(
    277                     Calls.CONTENT_URI_WITH_VOICEMAIL, cursor.getLong(ID_COLUMN_INDEX));
    278             Uri voicemailUri = voicemailUriString == null ? null : Uri.parse(voicemailUriString);
    279             return new NewCall(callsUri, voicemailUri, cursor.getString(NUMBER_COLUMN_INDEX));
    280         }
    281     }
    282 
    283     /** Allows determining the name associated with a given phone number. */
    284     public interface NameLookupQuery {
    285         /**
    286          * Returns the name associated with the given number in the contacts database, or null if
    287          * the number does not correspond to any of the contacts.
    288          * <p>
    289          * If there are multiple contacts with the same phone number, it will return the name of one
    290          * of the matching contacts.
    291          */
    292         public String query(String number);
    293     }
    294 
    295     /** Create a new instance of {@link NameLookupQuery}. */
    296     public static NameLookupQuery createNameLookupQuery(ContentResolver contentResolver) {
    297         return new DefaultNameLookupQuery(contentResolver);
    298     }
    299 
    300     /**
    301      * Default implementation of {@link NameLookupQuery} that looks up the name of a contact in the
    302      * contacts database.
    303      */
    304     private static final class DefaultNameLookupQuery implements NameLookupQuery {
    305         private static final String[] PROJECTION = { PhoneLookup.DISPLAY_NAME };
    306         private static final int DISPLAY_NAME_COLUMN_INDEX = 0;
    307 
    308         private final ContentResolver mContentResolver;
    309 
    310         private DefaultNameLookupQuery(ContentResolver contentResolver) {
    311             mContentResolver = contentResolver;
    312         }
    313 
    314         @Override
    315         public String query(String number) {
    316             Cursor cursor = null;
    317             try {
    318                 cursor = mContentResolver.query(
    319                         Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)),
    320                         PROJECTION, null, null, null);
    321                 if (cursor == null || !cursor.moveToFirst()) return null;
    322                 return cursor.getString(DISPLAY_NAME_COLUMN_INDEX);
    323             } finally {
    324                 if (cursor != null) {
    325                     cursor.close();
    326                 }
    327             }
    328         }
    329     }
    330 
    331     /**
    332      * Create a new PhoneNumberHelper.
    333      * <p>
    334      * This will cause some Disk I/O, at least the first time it is created, so it should not be
    335      * called from the main thread.
    336      */
    337     public static PhoneNumberHelper createPhoneNumberHelper(Context context) {
    338         return new PhoneNumberHelper(context.getResources());
    339     }
    340 }
    341