| /* |
| * 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.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 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 com.google.common.base.Objects; |
| |
| import java.util.LinkedList; |
| |
| /** |
| * Adapter class to fill in data for the Call Log. |
| */ |
| /*package*/ class CallLogAdapter extends GroupingListAdapter |
| implements 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; |
| private ViewTreeObserver mViewTreeObserver = null; |
| |
| /** |
| * 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 boolean mLoading = true; |
| private static final int REDRAW = 1; |
| private static final int START_THREAD = 2; |
| |
| private QueryThread 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() { |
| // We only wanted to listen for the first draw (and this is it). |
| unregisterPreDrawListener(); |
| |
| // Only schedule a thread-creation message if the thread hasn't been |
| // created yet. This is purely an optimization, to queue fewer messages. |
| if (mCallerIdThread == null) { |
| mHandler.sendEmptyMessageDelayed(START_THREAD, START_PROCESSING_REQUESTS_DELAY_MILLIS); |
| } |
| |
| 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>(); |
| |
| 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(); |
| } |
| } |
| |
| /** |
| * Starts a background thread to process contact-lookup requests, unless one |
| * has already been started. |
| */ |
| private synchronized void startRequestProcessing() { |
| // For unit-testing. |
| if (mRequestProcessingDisabled) return; |
| |
| // Idempotence... if a thread is already started, don't start another. |
| if (mCallerIdThread != null) return; |
| |
| mCallerIdThread = new QueryThread(); |
| mCallerIdThread.setPriority(Thread.MIN_PRIORITY); |
| mCallerIdThread.start(); |
| } |
| |
| /** |
| * Stops the background thread that processes updates and cancels any |
| * pending requests to start it. |
| */ |
| public synchronized void stopRequestProcessing() { |
| // Remove any pending requests to start the processing thread. |
| mHandler.removeMessages(START_THREAD); |
| if (mCallerIdThread != null) { |
| // Stop the thread; we are finished with it. |
| mCallerIdThread.stopProcessing(); |
| mCallerIdThread.interrupt(); |
| mCallerIdThread = null; |
| } |
| } |
| |
| /** |
| * Stop receiving onPreDraw() notifications. |
| */ |
| private void unregisterPreDrawListener() { |
| if (mViewTreeObserver != null && mViewTreeObserver.isAlive()) { |
| mViewTreeObserver.removeOnPreDrawListener(this); |
| } |
| mViewTreeObserver = null; |
| } |
| |
| public void invalidateCache() { |
| mContactInfoCache.expireAll(); |
| |
| // Restart the request-processing thread after the next draw. |
| stopRequestProcessing(); |
| unregisterPreDrawListener(); |
| } |
| |
| /** |
| * 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 (immediate) startRequestProcessing(); |
| } |
| |
| /** |
| * 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 = (existingInfo != ContactInfo.EMPTY) && !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. |
| */ |
| private class QueryThread extends Thread { |
| private volatile boolean mDone = false; |
| |
| public QueryThread() { |
| super("CallLogAdapter.QueryThread"); |
| } |
| |
| public void stopProcessing() { |
| mDone = true; |
| } |
| |
| @Override |
| public void run() { |
| boolean needRedraw = false; |
| while (true) { |
| // Check if thread is finished, and if so return immediately. |
| if (mDone) return; |
| |
| // Obtain next request, if any is available. |
| // Keep synchronized section small. |
| ContactInfoRequest req = null; |
| synchronized (mRequests) { |
| if (!mRequests.isEmpty()) { |
| req = mRequests.removeFirst(); |
| } |
| } |
| |
| if (req != null) { |
| // Process the request. If the lookup succeeds, schedule a |
| // redraw. |
| needRedraw |= queryContactInfo(req.number, req.countryIso, req.callLogInfo); |
| } else { |
| // Throttle redraw rate by only sending them when there are |
| // more requests. |
| if (needRedraw) { |
| needRedraw = false; |
| mHandler.sendEmptyMessage(REDRAW); |
| } |
| |
| // Wait until another request is available, or until this |
| // thread is no longer needed (as indicated by being |
| // interrupted). |
| try { |
| synchronized (mRequests) { |
| mRequests.wait(1000); |
| } |
| } catch (InterruptedException ie) { |
| // Ignore, and attempt to continue processing requests. |
| } |
| } |
| } |
| } |
| } |
| |
| @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 (mViewTreeObserver == null) { |
| mViewTreeObserver = view.getViewTreeObserver(); |
| mViewTreeObserver.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.loadThumbnail(views.quickContactView, photoId, 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; |
| } |
| } |