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