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