/* * 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 com.android.common.widget.GroupingListAdapter; import com.android.contacts.ContactPhotoManager; import com.android.contacts.PhoneCallDetails; import com.android.contacts.PhoneCallDetailsHelper; import com.android.contacts.R; import com.android.contacts.util.ExpirableCache; import com.android.contacts.util.UriUtils; import com.google.common.annotations.VisibleForTesting; import android.content.ContentValues; import android.content.Context; import android.content.res.Resources; import android.database.Cursor; import android.net.Uri; import android.os.Handler; import android.os.Message; import android.provider.CallLog.Calls; import android.provider.ContactsContract.PhoneLookup; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import java.util.LinkedList; import libcore.util.Objects; /** * Adapter class to fill in data for the Call Log. */ /*package*/ class CallLogAdapter extends GroupingListAdapter implements Runnable, ViewTreeObserver.OnPreDrawListener, CallLogGroupBuilder.GroupCreator { /** Interface used to initiate a refresh of the content. */ public interface CallFetcher { public void fetchCalls(); } /** * Stores a phone number of a call with the country code where it originally occurred. * <p> * Note the country does not necessarily specifies the country of the phone number itself, but * it is the country in which the user was in when the call was placed or received. */ private static final class NumberWithCountryIso { public final String number; public final String countryIso; public NumberWithCountryIso(String number, String countryIso) { this.number = number; this.countryIso = countryIso; } @Override public boolean equals(Object o) { if (o == null) return false; if (!(o instanceof NumberWithCountryIso)) return false; NumberWithCountryIso other = (NumberWithCountryIso) o; return TextUtils.equals(number, other.number) && TextUtils.equals(countryIso, other.countryIso); } @Override public int hashCode() { return (number == null ? 0 : number.hashCode()) ^ (countryIso == null ? 0 : countryIso.hashCode()); } } /** The time in millis to delay starting the thread processing requests. */ private static final int START_PROCESSING_REQUESTS_DELAY_MILLIS = 1000; /** The size of the cache of contact info. */ private static final int CONTACT_INFO_CACHE_SIZE = 100; private final Context mContext; private final ContactInfoHelper mContactInfoHelper; private final CallFetcher mCallFetcher; /** * A cache of the contact details for the phone numbers in the call log. * <p> * The content of the cache is expired (but not purged) whenever the application comes to * the foreground. * <p> * The key is number with the country in which the call was placed or received. */ private ExpirableCache<NumberWithCountryIso, ContactInfo> mContactInfoCache; /** * A request for contact details for the given number. */ private static final class ContactInfoRequest { /** The number to look-up. */ public final String number; /** The country in which a call to or from this number was placed or received. */ public final String countryIso; /** The cached contact information stored in the call log. */ public final ContactInfo callLogInfo; public ContactInfoRequest(String number, String countryIso, ContactInfo callLogInfo) { this.number = number; this.countryIso = countryIso; this.callLogInfo = callLogInfo; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (!(obj instanceof ContactInfoRequest)) return false; ContactInfoRequest other = (ContactInfoRequest) obj; if (!TextUtils.equals(number, other.number)) return false; if (!TextUtils.equals(countryIso, other.countryIso)) return false; if (!Objects.equal(callLogInfo, other.callLogInfo)) return false; return true; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((callLogInfo == null) ? 0 : callLogInfo.hashCode()); result = prime * result + ((countryIso == null) ? 0 : countryIso.hashCode()); result = prime * result + ((number == null) ? 0 : number.hashCode()); return result; } } /** * List of requests to update contact details. * <p> * Each request is made of a phone number to look up, and the contact info currently stored in * the call log for this number. * <p> * The requests are added when displaying the contacts and are processed by a background * thread. */ private final LinkedList<ContactInfoRequest> mRequests; private volatile boolean mDone; private boolean mLoading = true; private ViewTreeObserver.OnPreDrawListener mPreDrawListener; private static final int REDRAW = 1; private static final int START_THREAD = 2; private boolean mFirst; private Thread mCallerIdThread; /** Instance of helper class for managing views. */ private final CallLogListItemHelper mCallLogViewsHelper; /** Helper to set up contact photos. */ private final ContactPhotoManager mContactPhotoManager; /** Helper to parse and process phone numbers. */ private PhoneNumberHelper mPhoneNumberHelper; /** Helper to group call log entries. */ private final CallLogGroupBuilder mCallLogGroupBuilder; /** Can be set to true by tests to disable processing of requests. */ private volatile boolean mRequestProcessingDisabled = false; /** Listener for the primary action in the list, opens the call details. */ private final View.OnClickListener mPrimaryActionListener = new View.OnClickListener() { @Override public void onClick(View view) { IntentProvider intentProvider = (IntentProvider) view.getTag(); if (intentProvider != null) { mContext.startActivity(intentProvider.getIntent(mContext)); } } }; /** Listener for the secondary action in the list, either call or play. */ private final View.OnClickListener mSecondaryActionListener = new View.OnClickListener() { @Override public void onClick(View view) { IntentProvider intentProvider = (IntentProvider) view.getTag(); if (intentProvider != null) { mContext.startActivity(intentProvider.getIntent(mContext)); } } }; @Override public boolean onPreDraw() { if (mFirst) { mHandler.sendEmptyMessageDelayed(START_THREAD, START_PROCESSING_REQUESTS_DELAY_MILLIS); mFirst = false; } return true; } private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case REDRAW: notifyDataSetChanged(); break; case START_THREAD: startRequestProcessing(); break; } } }; CallLogAdapter(Context context, CallFetcher callFetcher, ContactInfoHelper contactInfoHelper) { super(context); mContext = context; mCallFetcher = callFetcher; mContactInfoHelper = contactInfoHelper; mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE); mRequests = new LinkedList<ContactInfoRequest>(); mPreDrawListener = null; Resources resources = mContext.getResources(); CallTypeHelper callTypeHelper = new CallTypeHelper(resources); mContactPhotoManager = ContactPhotoManager.getInstance(mContext); mPhoneNumberHelper = new PhoneNumberHelper(resources); PhoneCallDetailsHelper phoneCallDetailsHelper = new PhoneCallDetailsHelper( resources, callTypeHelper, mPhoneNumberHelper); mCallLogViewsHelper = new CallLogListItemHelper( phoneCallDetailsHelper, mPhoneNumberHelper, resources); mCallLogGroupBuilder = new CallLogGroupBuilder(this); } /** * Requery on background thread when {@link Cursor} changes. */ @Override protected void onContentChanged() { mCallFetcher.fetchCalls(); } void setLoading(boolean loading) { mLoading = loading; } @Override public boolean isEmpty() { if (mLoading) { // We don't want the empty state to show when loading. return false; } else { return super.isEmpty(); } } private void startRequestProcessing() { if (mRequestProcessingDisabled) { return; } mDone = false; mCallerIdThread = new Thread(this, "CallLogContactLookup"); mCallerIdThread.setPriority(Thread.MIN_PRIORITY); mCallerIdThread.start(); } /** * Stops the background thread that processes updates and cancels any pending requests to * start it. * <p> * Should be called from the main thread to prevent a race condition between the request to * start the thread being processed and stopping the thread. */ public void stopRequestProcessing() { // Remove any pending requests to start the processing thread. mHandler.removeMessages(START_THREAD); mDone = true; if (mCallerIdThread != null) mCallerIdThread.interrupt(); } public void invalidateCache() { mContactInfoCache.expireAll(); // Let it restart the thread after next draw mPreDrawListener = null; } /** * Enqueues a request to look up the contact details for the given phone number. * <p> * It also provides the current contact info stored in the call log for this number. * <p> * If the {@code immediate} parameter is true, it will start immediately the thread that looks * up the contact information (if it has not been already started). Otherwise, it will be * started with a delay. See {@link #START_PROCESSING_REQUESTS_DELAY_MILLIS}. */ @VisibleForTesting void enqueueRequest(String number, String countryIso, ContactInfo callLogInfo, boolean immediate) { ContactInfoRequest request = new ContactInfoRequest(number, countryIso, callLogInfo); synchronized (mRequests) { if (!mRequests.contains(request)) { mRequests.add(request); mRequests.notifyAll(); } } if (mFirst && immediate) { startRequestProcessing(); mFirst = false; } } /** * Queries the appropriate content provider for the contact associated with the number. * <p> * Upon completion it also updates the cache in the call log, if it is different from * {@code callLogInfo}. * <p> * The number might be either a SIP address or a phone number. * <p> * It returns true if it updated the content of the cache and we should therefore tell the * view to update its content. */ private boolean queryContactInfo(String number, String countryIso, ContactInfo callLogInfo) { final ContactInfo info = mContactInfoHelper.lookupNumber(number, countryIso); if (info == null) { // The lookup failed, just return without requesting to update the view. return false; } // Check the existing entry in the cache: only if it has changed we should update the // view. NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); ContactInfo existingInfo = mContactInfoCache.getPossiblyExpired(numberCountryIso); boolean updated = !info.equals(existingInfo); // Store the data in the cache so that the UI thread can use to display it. Store it // even if it has not changed so that it is marked as not expired. mContactInfoCache.put(numberCountryIso, info); // Update the call log even if the cache it is up-to-date: it is possible that the cache // contains the value from a different call log entry. updateCallLogContactInfoCache(number, countryIso, info, callLogInfo); return updated; } /* * Handles requests for contact name and number type * @see java.lang.Runnable#run() */ @Override public void run() { boolean needNotify = false; while (!mDone) { ContactInfoRequest request = null; synchronized (mRequests) { if (!mRequests.isEmpty()) { request = mRequests.removeFirst(); } else { if (needNotify) { needNotify = false; mHandler.sendEmptyMessage(REDRAW); } try { mRequests.wait(1000); } catch (InterruptedException ie) { // Ignore and continue processing requests Thread.currentThread().interrupt(); } } } if (!mDone && request != null && queryContactInfo(request.number, request.countryIso, request.callLogInfo)) { needNotify = true; } } } @Override protected void addGroups(Cursor cursor) { mCallLogGroupBuilder.addGroups(cursor); } @Override protected View newStandAloneView(Context context, ViewGroup parent) { LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View view = inflater.inflate(R.layout.call_log_list_item, parent, false); findAndCacheViews(view); return view; } @Override protected void bindStandAloneView(View view, Context context, Cursor cursor) { bindView(view, cursor, 1); } @Override protected View newChildView(Context context, ViewGroup parent) { LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View view = inflater.inflate(R.layout.call_log_list_item, parent, false); findAndCacheViews(view); return view; } @Override protected void bindChildView(View view, Context context, Cursor cursor) { bindView(view, cursor, 1); } @Override protected View newGroupView(Context context, ViewGroup parent) { LayoutInflater inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View view = inflater.inflate(R.layout.call_log_list_item, parent, false); findAndCacheViews(view); return view; } @Override protected void bindGroupView(View view, Context context, Cursor cursor, int groupSize, boolean expanded) { bindView(view, cursor, groupSize); } private void findAndCacheViews(View view) { // Get the views to bind to. CallLogListItemViews views = CallLogListItemViews.fromView(view); views.primaryActionView.setOnClickListener(mPrimaryActionListener); views.secondaryActionView.setOnClickListener(mSecondaryActionListener); view.setTag(views); } /** * Binds the views in the entry to the data in the call log. * * @param view the view corresponding to this entry * @param c the cursor pointing to the entry in the call log * @param count the number of entries in the current item, greater than 1 if it is a group */ private void bindView(View view, Cursor c, int count) { final CallLogListItemViews views = (CallLogListItemViews) view.getTag(); final int section = c.getInt(CallLogQuery.SECTION); // This might be a header: check the value of the section column in the cursor. if (section == CallLogQuery.SECTION_NEW_HEADER || section == CallLogQuery.SECTION_OLD_HEADER) { views.primaryActionView.setVisibility(View.GONE); views.bottomDivider.setVisibility(View.GONE); views.listHeaderTextView.setVisibility(View.VISIBLE); views.listHeaderTextView.setText( section == CallLogQuery.SECTION_NEW_HEADER ? R.string.call_log_new_header : R.string.call_log_old_header); // Nothing else to set up for a header. return; } // Default case: an item in the call log. views.primaryActionView.setVisibility(View.VISIBLE); views.bottomDivider.setVisibility(isLastOfSection(c) ? View.GONE : View.VISIBLE); views.listHeaderTextView.setVisibility(View.GONE); final String number = c.getString(CallLogQuery.NUMBER); final long date = c.getLong(CallLogQuery.DATE); final long duration = c.getLong(CallLogQuery.DURATION); final int callType = c.getInt(CallLogQuery.CALL_TYPE); final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO); final ContactInfo cachedContactInfo = getContactInfoFromCallLog(c); views.primaryActionView.setTag( IntentProvider.getCallDetailIntentProvider( this, c.getPosition(), c.getLong(CallLogQuery.ID), count)); // Store away the voicemail information so we can play it directly. if (callType == Calls.VOICEMAIL_TYPE) { String voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI); final long rowId = c.getLong(CallLogQuery.ID); views.secondaryActionView.setTag( IntentProvider.getPlayVoicemailIntentProvider(rowId, voicemailUri)); } else if (!TextUtils.isEmpty(number)) { // Store away the number so we can call it directly if you click on the call icon. views.secondaryActionView.setTag( IntentProvider.getReturnCallIntentProvider(number)); } else { // No action enabled. views.secondaryActionView.setTag(null); } // Lookup contacts with this number NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); ExpirableCache.CachedValue<ContactInfo> cachedInfo = mContactInfoCache.getCachedValue(numberCountryIso); ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue(); if (!mPhoneNumberHelper.canPlaceCallsTo(number) || mPhoneNumberHelper.isVoicemailNumber(number)) { // If this is a number that cannot be dialed, there is no point in looking up a contact // for it. info = ContactInfo.EMPTY; } else if (cachedInfo == null) { mContactInfoCache.put(numberCountryIso, ContactInfo.EMPTY); // Use the cached contact info from the call log. info = cachedContactInfo; // The db request should happen on a non-UI thread. // Request the contact details immediately since they are currently missing. enqueueRequest(number, countryIso, cachedContactInfo, true); // We will format the phone number when we make the background request. } else { if (cachedInfo.isExpired()) { // The contact info is no longer up to date, we should request it. However, we // do not need to request them immediately. enqueueRequest(number, countryIso, cachedContactInfo, false); } else if (!callLogInfoMatches(cachedContactInfo, info)) { // The call log information does not match the one we have, look it up again. // We could simply update the call log directly, but that needs to be done in a // background thread, so it is easier to simply request a new lookup, which will, as // a side-effect, update the call log. enqueueRequest(number, countryIso, cachedContactInfo, false); } if (info == ContactInfo.EMPTY) { // Use the cached contact info from the call log. info = cachedContactInfo; } } final Uri lookupUri = info.lookupUri; final String name = info.name; final int ntype = info.type; final String label = info.label; final long photoId = info.photoId; CharSequence formattedNumber = info.formattedNumber; final int[] callTypes = getCallTypes(c, count); final String geocode = c.getString(CallLogQuery.GEOCODED_LOCATION); final PhoneCallDetails details; if (TextUtils.isEmpty(name)) { details = new PhoneCallDetails(number, formattedNumber, countryIso, geocode, callTypes, date, duration); } else { // We do not pass a photo id since we do not need the high-res picture. details = new PhoneCallDetails(number, formattedNumber, countryIso, geocode, callTypes, date, duration, name, ntype, label, lookupUri, null); } final boolean isNew = c.getInt(CallLogQuery.IS_READ) == 0; // New items also use the highlighted version of the text. final boolean isHighlighted = isNew; mCallLogViewsHelper.setPhoneCallDetails(views, details, isHighlighted); setPhoto(views, photoId, lookupUri); // Listen for the first draw if (mPreDrawListener == null) { mFirst = true; mPreDrawListener = this; view.getViewTreeObserver().addOnPreDrawListener(this); } } /** Returns true if this is the last item of a section. */ private boolean isLastOfSection(Cursor c) { if (c.isLast()) return true; final int section = c.getInt(CallLogQuery.SECTION); if (!c.moveToNext()) return true; final int nextSection = c.getInt(CallLogQuery.SECTION); c.moveToPrevious(); return section != nextSection; } /** Checks whether the contact info from the call log matches the one from the contacts db. */ private boolean callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info) { // The call log only contains a subset of the fields in the contacts db. // Only check those. return TextUtils.equals(callLogInfo.name, info.name) && callLogInfo.type == info.type && TextUtils.equals(callLogInfo.label, info.label); } /** Stores the updated contact info in the call log if it is different from the current one. */ private void updateCallLogContactInfoCache(String number, String countryIso, ContactInfo updatedInfo, ContactInfo callLogInfo) { final ContentValues values = new ContentValues(); boolean needsUpdate = false; if (callLogInfo != null) { if (!TextUtils.equals(updatedInfo.name, callLogInfo.name)) { values.put(Calls.CACHED_NAME, updatedInfo.name); needsUpdate = true; } if (updatedInfo.type != callLogInfo.type) { values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type); needsUpdate = true; } if (!TextUtils.equals(updatedInfo.label, callLogInfo.label)) { values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label); needsUpdate = true; } if (!UriUtils.areEqual(updatedInfo.lookupUri, callLogInfo.lookupUri)) { values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri)); needsUpdate = true; } if (!TextUtils.equals(updatedInfo.normalizedNumber, callLogInfo.normalizedNumber)) { values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber); needsUpdate = true; } if (!TextUtils.equals(updatedInfo.number, callLogInfo.number)) { values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number); needsUpdate = true; } if (updatedInfo.photoId != callLogInfo.photoId) { values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId); needsUpdate = true; } if (!TextUtils.equals(updatedInfo.formattedNumber, callLogInfo.formattedNumber)) { values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber); needsUpdate = true; } } else { // No previous values, store all of them. values.put(Calls.CACHED_NAME, updatedInfo.name); values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type); values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label); values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri)); values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number); values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber); values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId); values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber); needsUpdate = true; } if (!needsUpdate) { return; } if (countryIso == null) { mContext.getContentResolver().update(Calls.CONTENT_URI_WITH_VOICEMAIL, values, Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " IS NULL", new String[]{ number }); } else { mContext.getContentResolver().update(Calls.CONTENT_URI_WITH_VOICEMAIL, values, Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " = ?", new String[]{ number, countryIso }); } } /** Returns the contact information as stored in the call log. */ private ContactInfo getContactInfoFromCallLog(Cursor c) { ContactInfo info = new ContactInfo(); info.lookupUri = UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_LOOKUP_URI)); info.name = c.getString(CallLogQuery.CACHED_NAME); info.type = c.getInt(CallLogQuery.CACHED_NUMBER_TYPE); info.label = c.getString(CallLogQuery.CACHED_NUMBER_LABEL); String matchedNumber = c.getString(CallLogQuery.CACHED_MATCHED_NUMBER); info.number = matchedNumber == null ? c.getString(CallLogQuery.NUMBER) : matchedNumber; info.normalizedNumber = c.getString(CallLogQuery.CACHED_NORMALIZED_NUMBER); info.photoId = c.getLong(CallLogQuery.CACHED_PHOTO_ID); info.photoUri = null; // We do not cache the photo URI. info.formattedNumber = c.getString(CallLogQuery.CACHED_FORMATTED_NUMBER); return info; } /** * Returns the call types for the given number of items in the cursor. * <p> * It uses the next {@code count} rows in the cursor to extract the types. * <p> * It position in the cursor is unchanged by this function. */ private int[] getCallTypes(Cursor cursor, int count) { int position = cursor.getPosition(); int[] callTypes = new int[count]; for (int index = 0; index < count; ++index) { callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE); cursor.moveToNext(); } cursor.moveToPosition(position); return callTypes; } private void setPhoto(CallLogListItemViews views, long photoId, Uri contactUri) { views.quickContactView.assignContactUri(contactUri); mContactPhotoManager.loadPhoto(views.quickContactView, photoId, false, true); } /** * Sets whether processing of requests for contact details should be enabled. * <p> * This method should be called in tests to disable such processing of requests when not * needed. */ @VisibleForTesting void disableRequestProcessingForTest() { mRequestProcessingDisabled = true; } @VisibleForTesting void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) { NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); mContactInfoCache.put(numberCountryIso, contactInfo); } @Override public void addGroup(int cursorPosition, int size, boolean expanded) { super.addGroup(cursorPosition, size, expanded); } /* * Get the number from the Contacts, if available, since sometimes * the number provided by caller id may not be formatted properly * depending on the carrier (roaming) in use at the time of the * incoming call. * Logic : If the caller-id number starts with a "+", use it * Else if the number in the contacts starts with a "+", use that one * Else if the number in the contacts is longer, use that one */ public String getBetterNumberFromContacts(String number, String countryIso) { String matchingNumber = null; // Look in the cache first. If it's not found then query the Phones db NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); ContactInfo ci = mContactInfoCache.getPossiblyExpired(numberCountryIso); if (ci != null && ci != ContactInfo.EMPTY) { matchingNumber = ci.number; } else { try { Cursor phonesCursor = mContext.getContentResolver().query( Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number), PhoneQuery._PROJECTION, null, null, null); if (phonesCursor != null) { if (phonesCursor.moveToFirst()) { matchingNumber = phonesCursor.getString(PhoneQuery.MATCHED_NUMBER); } phonesCursor.close(); } } catch (Exception e) { // Use the number from the call log } } if (!TextUtils.isEmpty(matchingNumber) && (matchingNumber.startsWith("+") || matchingNumber.length() > number.length())) { number = matchingNumber; } return number; } }