| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| |
| package com.android.contacts.calllog; |
| |
| import android.app.Notification; |
| import android.app.NotificationManager; |
| import android.app.PendingIntent; |
| import android.content.ContentResolver; |
| import android.content.ContentUris; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.res.Resources; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.provider.CallLog.Calls; |
| import android.provider.ContactsContract.PhoneLookup; |
| import android.text.TextUtils; |
| import android.util.Log; |
| |
| import com.android.common.io.MoreCloseables; |
| import com.android.contacts.CallDetailActivity; |
| import com.android.contacts.R; |
| import com.google.common.collect.Maps; |
| |
| import java.util.Map; |
| |
| /** |
| * Implementation of {@link VoicemailNotifier} that shows a notification in the |
| * status bar. |
| */ |
| public class DefaultVoicemailNotifier implements VoicemailNotifier { |
| public static final String TAG = "DefaultVoicemailNotifier"; |
| |
| /** The tag used to identify notifications from this class. */ |
| private static final String NOTIFICATION_TAG = "DefaultVoicemailNotifier"; |
| /** The identifier of the notification of new voicemails. */ |
| private static final int NOTIFICATION_ID = 1; |
| |
| /** The singleton instance of {@link DefaultVoicemailNotifier}. */ |
| private static DefaultVoicemailNotifier sInstance; |
| |
| private final Context mContext; |
| private final NotificationManager mNotificationManager; |
| private final NewCallsQuery mNewCallsQuery; |
| private final NameLookupQuery mNameLookupQuery; |
| private final PhoneNumberHelper mPhoneNumberHelper; |
| |
| /** Returns the singleton instance of the {@link DefaultVoicemailNotifier}. */ |
| public static synchronized DefaultVoicemailNotifier getInstance(Context context) { |
| if (sInstance == null) { |
| NotificationManager notificationManager = |
| (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); |
| ContentResolver contentResolver = context.getContentResolver(); |
| sInstance = new DefaultVoicemailNotifier(context, notificationManager, |
| createNewCallsQuery(contentResolver), |
| createNameLookupQuery(contentResolver), |
| createPhoneNumberHelper(context)); |
| } |
| return sInstance; |
| } |
| |
| private DefaultVoicemailNotifier(Context context, |
| NotificationManager notificationManager, NewCallsQuery newCallsQuery, |
| NameLookupQuery nameLookupQuery, PhoneNumberHelper phoneNumberHelper) { |
| mContext = context; |
| mNotificationManager = notificationManager; |
| mNewCallsQuery = newCallsQuery; |
| mNameLookupQuery = nameLookupQuery; |
| mPhoneNumberHelper = phoneNumberHelper; |
| } |
| |
| /** Updates the notification and notifies of the call with the given URI. */ |
| @Override |
| public void updateNotification(Uri newCallUri) { |
| // Lookup the list of new voicemails to include in the notification. |
| // TODO: Move this into a service, to avoid holding the receiver up. |
| final NewCall[] newCalls = mNewCallsQuery.query(); |
| |
| if (newCalls == null) { |
| // Query failed, just return. |
| return; |
| } |
| |
| if (newCalls.length == 0) { |
| // No voicemails to notify about: clear the notification. |
| clearNotification(); |
| return; |
| } |
| |
| Resources resources = mContext.getResources(); |
| |
| // This represents a list of names to include in the notification. |
| String callers = null; |
| |
| // Maps each number into a name: if a number is in the map, it has already left a more |
| // recent voicemail. |
| final Map<String, String> names = Maps.newHashMap(); |
| |
| // Determine the call corresponding to the new voicemail we have to notify about. |
| NewCall callToNotify = null; |
| |
| // Iterate over the new voicemails to determine all the information above. |
| for (NewCall newCall : newCalls) { |
| // Check if we already know the name associated with this number. |
| String name = names.get(newCall.number); |
| if (name == null) { |
| // Look it up in the database. |
| name = mNameLookupQuery.query(newCall.number); |
| // If we cannot lookup the contact, use the number instead. |
| if (name == null) { |
| name = mPhoneNumberHelper.getDisplayNumber(newCall.number, "").toString(); |
| if (TextUtils.isEmpty(name)) { |
| name = newCall.number; |
| } |
| } |
| names.put(newCall.number, name); |
| // This is a new caller. Add it to the back of the list of callers. |
| if (TextUtils.isEmpty(callers)) { |
| callers = name; |
| } else { |
| callers = resources.getString( |
| R.string.notification_voicemail_callers_list, callers, name); |
| } |
| } |
| // Check if this is the new call we need to notify about. |
| if (newCallUri != null && newCallUri.equals(newCall.voicemailUri)) { |
| callToNotify = newCall; |
| } |
| } |
| |
| if (newCallUri != null && callToNotify == null) { |
| Log.e(TAG, "The new call could not be found in the call log: " + newCallUri); |
| } |
| |
| // Determine the title of the notification and the icon for it. |
| final String title = resources.getQuantityString( |
| R.plurals.notification_voicemail_title, newCalls.length, newCalls.length); |
| // TODO: Use the photo of contact if all calls are from the same person. |
| final int icon = android.R.drawable.stat_notify_voicemail; |
| |
| Notification.Builder notificationBuilder = new Notification.Builder(mContext) |
| .setSmallIcon(icon) |
| .setContentTitle(title) |
| .setContentText(callers) |
| .setDefaults(callToNotify != null ? Notification.DEFAULT_ALL : 0) |
| .setDeleteIntent(createMarkNewVoicemailsAsOldIntent()) |
| .setAutoCancel(true); |
| |
| // Determine the intent to fire when the notification is clicked on. |
| final Intent contentIntent; |
| if (newCalls.length == 1) { |
| // Open the voicemail directly. |
| contentIntent = new Intent(mContext, CallDetailActivity.class); |
| contentIntent.setData(newCalls[0].callsUri); |
| contentIntent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI, |
| newCalls[0].voicemailUri); |
| Intent playIntent = new Intent(mContext, CallDetailActivity.class); |
| playIntent.setData(newCalls[0].callsUri); |
| playIntent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI, |
| newCalls[0].voicemailUri); |
| playIntent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK, true); |
| playIntent.putExtra(CallDetailActivity.EXTRA_FROM_NOTIFICATION, true); |
| notificationBuilder.addAction(R.drawable.ic_play_holo_dark, |
| resources.getString(R.string.notification_action_voicemail_play), |
| PendingIntent.getActivity(mContext, 0, playIntent, 0)); |
| } else { |
| // Open the call log. |
| contentIntent = new Intent(Intent.ACTION_VIEW, Calls.CONTENT_URI); |
| } |
| notificationBuilder.setContentIntent( |
| PendingIntent.getActivity(mContext, 0, contentIntent, 0)); |
| |
| // The text to show in the ticker, describing the new event. |
| if (callToNotify != null) { |
| notificationBuilder.setTicker(resources.getString( |
| R.string.notification_new_voicemail_ticker, names.get(callToNotify.number))); |
| } |
| |
| mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_ID, notificationBuilder.build()); |
| } |
| |
| /** Creates a pending intent that marks all new voicemails as old. */ |
| private PendingIntent createMarkNewVoicemailsAsOldIntent() { |
| Intent intent = new Intent(mContext, CallLogNotificationsService.class); |
| intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD); |
| return PendingIntent.getService(mContext, 0, intent, 0); |
| } |
| |
| @Override |
| public void clearNotification() { |
| mNotificationManager.cancel(NOTIFICATION_TAG, NOTIFICATION_ID); |
| } |
| |
| /** Information about a new voicemail. */ |
| private static final class NewCall { |
| public final Uri callsUri; |
| public final Uri voicemailUri; |
| public final String number; |
| |
| public NewCall(Uri callsUri, Uri voicemailUri, String number) { |
| this.callsUri = callsUri; |
| this.voicemailUri = voicemailUri; |
| this.number = number; |
| } |
| } |
| |
| /** Allows determining the new calls for which a notification should be generated. */ |
| public interface NewCallsQuery { |
| /** |
| * Returns the new calls for which a notification should be generated. |
| */ |
| public NewCall[] query(); |
| } |
| |
| /** Create a new instance of {@link NewCallsQuery}. */ |
| public static NewCallsQuery createNewCallsQuery(ContentResolver contentResolver) { |
| return new DefaultNewCallsQuery(contentResolver); |
| } |
| |
| /** |
| * Default implementation of {@link NewCallsQuery} that looks up the list of new calls to |
| * notify about in the call log. |
| */ |
| private static final class DefaultNewCallsQuery implements NewCallsQuery { |
| private static final String[] PROJECTION = { |
| Calls._ID, Calls.NUMBER, Calls.VOICEMAIL_URI |
| }; |
| private static final int ID_COLUMN_INDEX = 0; |
| private static final int NUMBER_COLUMN_INDEX = 1; |
| private static final int VOICEMAIL_URI_COLUMN_INDEX = 2; |
| |
| private final ContentResolver mContentResolver; |
| |
| private DefaultNewCallsQuery(ContentResolver contentResolver) { |
| mContentResolver = contentResolver; |
| } |
| |
| @Override |
| public NewCall[] query() { |
| final String selection = String.format("%s = 1 AND %s = ?", Calls.NEW, Calls.TYPE); |
| final String[] selectionArgs = new String[]{ Integer.toString(Calls.VOICEMAIL_TYPE) }; |
| Cursor cursor = null; |
| try { |
| cursor = mContentResolver.query(Calls.CONTENT_URI_WITH_VOICEMAIL, PROJECTION, |
| selection, selectionArgs, Calls.DEFAULT_SORT_ORDER); |
| if (cursor == null) { |
| return null; |
| } |
| NewCall[] newCalls = new NewCall[cursor.getCount()]; |
| while (cursor.moveToNext()) { |
| newCalls[cursor.getPosition()] = createNewCallsFromCursor(cursor); |
| } |
| return newCalls; |
| } finally { |
| MoreCloseables.closeQuietly(cursor); |
| } |
| } |
| |
| /** Returns an instance of {@link NewCall} created by using the values of the cursor. */ |
| private NewCall createNewCallsFromCursor(Cursor cursor) { |
| String voicemailUriString = cursor.getString(VOICEMAIL_URI_COLUMN_INDEX); |
| Uri callsUri = ContentUris.withAppendedId( |
| Calls.CONTENT_URI_WITH_VOICEMAIL, cursor.getLong(ID_COLUMN_INDEX)); |
| Uri voicemailUri = voicemailUriString == null ? null : Uri.parse(voicemailUriString); |
| return new NewCall(callsUri, voicemailUri, cursor.getString(NUMBER_COLUMN_INDEX)); |
| } |
| } |
| |
| /** Allows determining the name associated with a given phone number. */ |
| public interface NameLookupQuery { |
| /** |
| * Returns the name associated with the given number in the contacts database, or null if |
| * the number does not correspond to any of the contacts. |
| * <p> |
| * If there are multiple contacts with the same phone number, it will return the name of one |
| * of the matching contacts. |
| */ |
| public String query(String number); |
| } |
| |
| /** Create a new instance of {@link NameLookupQuery}. */ |
| public static NameLookupQuery createNameLookupQuery(ContentResolver contentResolver) { |
| return new DefaultNameLookupQuery(contentResolver); |
| } |
| |
| /** |
| * Default implementation of {@link NameLookupQuery} that looks up the name of a contact in the |
| * contacts database. |
| */ |
| private static final class DefaultNameLookupQuery implements NameLookupQuery { |
| private static final String[] PROJECTION = { PhoneLookup.DISPLAY_NAME }; |
| private static final int DISPLAY_NAME_COLUMN_INDEX = 0; |
| |
| private final ContentResolver mContentResolver; |
| |
| private DefaultNameLookupQuery(ContentResolver contentResolver) { |
| mContentResolver = contentResolver; |
| } |
| |
| @Override |
| public String query(String number) { |
| Cursor cursor = null; |
| try { |
| cursor = mContentResolver.query( |
| Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)), |
| PROJECTION, null, null, null); |
| if (cursor == null || !cursor.moveToFirst()) return null; |
| return cursor.getString(DISPLAY_NAME_COLUMN_INDEX); |
| } finally { |
| if (cursor != null) { |
| cursor.close(); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Create a new PhoneNumberHelper. |
| * <p> |
| * This will cause some Disk I/O, at least the first time it is created, so it should not be |
| * called from the main thread. |
| */ |
| public static PhoneNumberHelper createPhoneNumberHelper(Context context) { |
| return new PhoneNumberHelper(context.getResources()); |
| } |
| } |