/* * Copyright (C) 2013 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.dialer.calllog; import static android.Manifest.permission.READ_CALL_LOG; import static android.Manifest.permission.READ_CONTACTS; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.provider.CallLog.Calls; import android.provider.ContactsContract.PhoneLookup; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.telephony.PhoneNumberUtils; import android.text.TextUtils; import android.util.Log; import com.android.contacts.common.GeoUtil; import com.android.contacts.common.util.PermissionsUtil; import com.android.dialer.R; import com.android.dialer.util.TelecomUtil; import java.util.ArrayList; import java.util.List; /** * Helper class operating on call log notifications. */ public class CallLogNotificationsHelper { private static final String TAG = "CallLogNotifHelper"; private static CallLogNotificationsHelper sInstance; /** Returns the singleton instance of the {@link CallLogNotificationsHelper}. */ public static CallLogNotificationsHelper getInstance(Context context) { if (sInstance == null) { ContentResolver contentResolver = context.getContentResolver(); String countryIso = GeoUtil.getCurrentCountryIso(context); sInstance = new CallLogNotificationsHelper(context, createNewCallsQuery(context, contentResolver), createNameLookupQuery(context, contentResolver), new ContactInfoHelper(context, countryIso), countryIso); } return sInstance; } private final Context mContext; private final NewCallsQuery mNewCallsQuery; private final NameLookupQuery mNameLookupQuery; private final ContactInfoHelper mContactInfoHelper; private final String mCurrentCountryIso; CallLogNotificationsHelper(Context context, NewCallsQuery newCallsQuery, NameLookupQuery nameLookupQuery, ContactInfoHelper contactInfoHelper, String countryIso) { mContext = context; mNewCallsQuery = newCallsQuery; mNameLookupQuery = nameLookupQuery; mContactInfoHelper = contactInfoHelper; mCurrentCountryIso = countryIso; } /** * Get all voicemails with the "new" flag set to 1. * * @return A list of NewCall objects where each object represents a new voicemail. */ @Nullable public List getNewVoicemails() { return mNewCallsQuery.query(Calls.VOICEMAIL_TYPE); } /** * Get all missed calls with the "new" flag set to 1. * * @return A list of NewCall objects where each object represents a new missed call. */ @Nullable public List getNewMissedCalls() { return mNewCallsQuery.query(Calls.MISSED_TYPE); } /** * Given a number and number information (presentation and country ISO), get the best name * for display. If the name is empty but we have a special presentation, display that. * Otherwise attempt to look it up in the database or the cache. * If that fails, fall back to displaying the number. */ public String getName(@Nullable String number, int numberPresentation, @Nullable String countryIso) { return getContactInfo(number, numberPresentation, countryIso).name; } /** * Given a number and number information (presentation and country ISO), get * {@link ContactInfo}. If the name is empty but we have a special presentation, display that. * Otherwise attempt to look it up in the database or the cache. * If that fails, fall back to displaying the number. */ public @NonNull ContactInfo getContactInfo(@Nullable String number, int numberPresentation, @Nullable String countryIso) { if (countryIso == null) { countryIso = mCurrentCountryIso; } ContactInfo contactInfo = new ContactInfo(); contactInfo.number = number; contactInfo.formattedNumber = PhoneNumberUtils.formatNumber(number, countryIso); // contactInfo.normalizedNumber is not PhoneNumberUtils.normalizeNumber. Read ContactInfo. contactInfo.normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso); // 1. Special number representation. contactInfo.name = PhoneNumberDisplayUtil.getDisplayName( mContext, number, numberPresentation, false).toString(); if (!TextUtils.isEmpty(contactInfo.name)) { return contactInfo; } // 2. Personal ContactsProvider phonelookup query. contactInfo.name = mNameLookupQuery.query(number); if (!TextUtils.isEmpty(contactInfo.name)) { return contactInfo; } // 3. Look it up in the cache. ContactInfo cachedContactInfo = mContactInfoHelper.lookupNumber(number, countryIso); if (cachedContactInfo != null && !TextUtils.isEmpty(cachedContactInfo.name)) { return cachedContactInfo; } if (!TextUtils.isEmpty(contactInfo.formattedNumber)) { // 4. If we cannot lookup the contact, use the formatted number instead. contactInfo.name = contactInfo.formattedNumber; } else if (!TextUtils.isEmpty(number)) { // 5. If number can't be formatted, use number. contactInfo.name = number; } else { // 6. Otherwise, it's unknown number. contactInfo.name = mContext.getResources().getString(R.string.unknown); } return contactInfo; } /** Removes the missed call notifications. */ public static void removeMissedCallNotifications(Context context) { TelecomUtil.cancelMissedCallsNotification(context); } /** Update the voice mail notifications. */ public static void updateVoicemailNotifications(Context context) { CallLogNotificationsService.updateVoicemailNotifications(context, null); } /** Information about a new voicemail. */ public static final class NewCall { public final Uri callsUri; public final Uri voicemailUri; public final String number; public final int numberPresentation; public final String accountComponentName; public final String accountId; public final String transcription; public final String countryIso; public final long dateMs; public NewCall( Uri callsUri, Uri voicemailUri, String number, int numberPresentation, String accountComponentName, String accountId, String transcription, String countryIso, long dateMs) { this.callsUri = callsUri; this.voicemailUri = voicemailUri; this.number = number; this.numberPresentation = numberPresentation; this.accountComponentName = accountComponentName; this.accountId = accountId; this.transcription = transcription; this.countryIso = countryIso; this.dateMs = dateMs; } } /** Allows determining the new calls for which a notification should be generated. */ public interface NewCallsQuery { /** * Returns the new calls of a certain type for which a notification should be generated. */ @Nullable public List query(int type); } /** Create a new instance of {@link NewCallsQuery}. */ public static NewCallsQuery createNewCallsQuery(Context context, ContentResolver contentResolver) { return new DefaultNewCallsQuery(context.getApplicationContext(), 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, Calls.NUMBER_PRESENTATION, Calls.PHONE_ACCOUNT_COMPONENT_NAME, Calls.PHONE_ACCOUNT_ID, Calls.TRANSCRIPTION, Calls.COUNTRY_ISO, Calls.DATE }; 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 static final int NUMBER_PRESENTATION_COLUMN_INDEX = 3; private static final int PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX = 4; private static final int PHONE_ACCOUNT_ID_COLUMN_INDEX = 5; private static final int TRANSCRIPTION_COLUMN_INDEX = 6; private static final int COUNTRY_ISO_COLUMN_INDEX = 7; private static final int DATE_COLUMN_INDEX = 8; private final ContentResolver mContentResolver; private final Context mContext; private DefaultNewCallsQuery(Context context, ContentResolver contentResolver) { mContext = context; mContentResolver = contentResolver; } @Override @Nullable public List query(int type) { if (!PermissionsUtil.hasPermission(mContext, READ_CALL_LOG)) { Log.w(TAG, "No READ_CALL_LOG permission, returning null for calls lookup."); return null; } final String selection = String.format("%s = 1 AND %s = ?", Calls.NEW, Calls.TYPE); final String[] selectionArgs = new String[]{ Integer.toString(type) }; try (Cursor cursor = mContentResolver.query(Calls.CONTENT_URI_WITH_VOICEMAIL, PROJECTION, selection, selectionArgs, Calls.DEFAULT_SORT_ORDER)) { if (cursor == null) { return null; } List newCalls = new ArrayList<>(); while (cursor.moveToNext()) { newCalls.add(createNewCallsFromCursor(cursor)); } return newCalls; } catch (RuntimeException e) { Log.w(TAG, "Exception when querying Contacts Provider for calls lookup"); return null; } } /** 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), cursor.getInt(NUMBER_PRESENTATION_COLUMN_INDEX), cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX), cursor.getString(PHONE_ACCOUNT_ID_COLUMN_INDEX), cursor.getString(TRANSCRIPTION_COLUMN_INDEX), cursor.getString(COUNTRY_ISO_COLUMN_INDEX), cursor.getLong(DATE_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. *

* If there are multiple contacts with the same phone number, it will return the name of one * of the matching contacts. */ @Nullable public String query(@Nullable String number); } /** Create a new instance of {@link NameLookupQuery}. */ public static NameLookupQuery createNameLookupQuery(Context context, ContentResolver contentResolver) { return new DefaultNameLookupQuery(context.getApplicationContext(), 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 final Context mContext; private DefaultNameLookupQuery(Context context, ContentResolver contentResolver) { mContext = context; mContentResolver = contentResolver; } @Override @Nullable public String query(@Nullable String number) { if (!PermissionsUtil.hasPermission(mContext, READ_CONTACTS)) { Log.w(TAG, "No READ_CONTACTS permission, returning null for name lookup."); return null; } try (Cursor 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); } catch (RuntimeException e) { Log.w(TAG, "Exception when querying Contacts Provider for name lookup"); return null; } } } }