/* Copyright © 2013-2014, Silent Circle, LLC. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Any redistribution, use, or modification is done solely for personal benefit and not for any commercial purpose or for monetary gain * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name Silent Circle nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SILENT CIRCLE, LLC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* * This implementation is edited version of original Android sources. */ /* * 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.silentcircle.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.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Objects; import com.silentcircle.contacts.ContactPhotoManager; import com.silentcircle.contacts.PhoneCallDetails; import com.silentcircle.contacts.PhoneCallDetailsHelper; import com.silentcircle.contacts.utils.ExpirableCache; import com.silentcircle.contacts.utils.GroupingListAdapter; import com.silentcircle.contacts.utils.UriUtils; import com.silentcircle.contacts.R; import com.silentcircle.silentcontacts.ScCallLog.ScCalls; import java.util.LinkedList; /** * Adapter class to fill in data for the Call Log. */ class ScCallLogAdapter extends GroupingListAdapter implements ViewTreeObserver.OnPreDrawListener, ScCallLogGroupBuilder.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 ScCallLogListItemHelper 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 ScCallLogGroupBuilder 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) { // if (isGroupHeader(intentProvider.listItemPosition)) { // toggleGroup(intentProvider.listItemPosition); // } // else { 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; } } }; ScCallLogAdapter(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 ScCallLogListItemHelper(phoneCallDetailsHelper, mPhoneNumberHelper, resources); mCallLogGroupBuilder = new ScCallLogGroupBuilder(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, int listPos) { bindView(view, cursor, 1, listPos); } @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, int listPos) { bindView(view, cursor, 1, listPos); } @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, int listPos) { bindView(view, cursor, groupSize, listPos); } 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, int listPos) { final CallLogListItemViews views = (CallLogListItemViews) view.getTag(); final int section = c.getInt(ScCallLogQuery.SECTION); // TODO: change here (and in layout, ListItemViews) to handle date sections (new embedded header in item // visible on data change. // This might be a header: check the value of the section column in the cursor. // if (section == ScCallLogQuery.SECTION_NEW_HEADER || section == ScCallLogQuery.SECTION_OLD_HEADER) { // views.primaryActionView.setVisibility(View.GONE); // views.bottomDivider.setVisibility(View.GONE); // views.listHeaderTextView.setVisibility(View.VISIBLE); // views.listHeaderTextView.setText( // section == ScCallLogQuery.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(ScCallLogQuery.NUMBER); final long date = c.getLong(ScCallLogQuery.DATE); final long duration = c.getLong(ScCallLogQuery.DURATION); final String countryIso = c.getString(ScCallLogQuery.COUNTRY_ISO); final String relDate = c.getString(ScCallLogQuery.RELATIVE_DATE); if (relDate != null) { views.itemRelativeDate.setText(relDate); views.itemRelativeDate.setVisibility(View.VISIBLE); } else { views.itemRelativeDate.setVisibility(View.GONE); } final ContactInfo cachedContactInfo = getContactInfoFromCallLog(c); views.primaryActionView.setTag( IntentProvider.getCallDetailIntentProvider(this, c.getPosition(), c.getLong(ScCallLogQuery.ID), count, listPos)); 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)) { // 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(ScCallLogQuery.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(ScCallLogQuery.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(ScCallLogQuery.SECTION); if (!c.moveToNext()) return true; final int nextSection = c.getInt(ScCallLogQuery.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(ScCalls.CACHED_NAME, updatedInfo.name); needsUpdate = true; } if (updatedInfo.type != callLogInfo.type) { values.put(ScCalls.CACHED_NUMBER_TYPE, updatedInfo.type); needsUpdate = true; } if (!TextUtils.equals(updatedInfo.label, callLogInfo.label)) { values.put(ScCalls.CACHED_NUMBER_LABEL, updatedInfo.label); needsUpdate = true; } if (!UriUtils.areEqual(updatedInfo.lookupUri, callLogInfo.lookupUri)) { values.put(ScCalls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri)); needsUpdate = true; } if (!TextUtils.equals(updatedInfo.normalizedNumber, callLogInfo.normalizedNumber)) { values.put(ScCalls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber); needsUpdate = true; } if (!TextUtils.equals(updatedInfo.number, callLogInfo.number)) { values.put(ScCalls.CACHED_MATCHED_NUMBER, updatedInfo.number); needsUpdate = true; } if (updatedInfo.photoId != callLogInfo.photoId) { values.put(ScCalls.CACHED_PHOTO_ID, updatedInfo.photoId); needsUpdate = true; } if (!TextUtils.equals(updatedInfo.formattedNumber, callLogInfo.formattedNumber)) { values.put(ScCalls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber); needsUpdate = true; } } else { // No previous values, store all of them. values.put(ScCalls.CACHED_NAME, updatedInfo.name); values.put(ScCalls.CACHED_NUMBER_TYPE, updatedInfo.type); values.put(ScCalls.CACHED_NUMBER_LABEL, updatedInfo.label); values.put(ScCalls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri)); values.put(ScCalls.CACHED_MATCHED_NUMBER, updatedInfo.number); values.put(ScCalls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber); values.put(ScCalls.CACHED_PHOTO_ID, updatedInfo.photoId); values.put(ScCalls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber); needsUpdate = true; } if (!needsUpdate) return; if (countryIso == null) { mContext.getContentResolver().update(ScCalls.CONTENT_URI, values, ScCalls.NUMBER + " = ? AND " + ScCalls.COUNTRY_ISO + " IS NULL", new String[]{ number }); } else { mContext.getContentResolver().update(ScCalls.CONTENT_URI, values, ScCalls.NUMBER + " = ? AND " + ScCalls.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(ScCallLogQuery.CACHED_LOOKUP_URI)); info.name = c.getString(ScCallLogQuery.CACHED_NAME); info.type = c.getInt(ScCallLogQuery.CACHED_NUMBER_TYPE); info.label = c.getString(ScCallLogQuery.CACHED_NUMBER_LABEL); String matchedNumber = c.getString(ScCallLogQuery.CACHED_MATCHED_NUMBER); info.number = matchedNumber == null ? c.getString(ScCallLogQuery.NUMBER) : matchedNumber; info.normalizedNumber = c.getString(ScCallLogQuery.CACHED_NORMALIZED_NUMBER); info.photoId = c.getLong(ScCallLogQuery.CACHED_PHOTO_ID); // info.photoUri = null; // We do not cache the photo URI. info.formattedNumber = c.getString(ScCallLogQuery.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(ScCallLogQuery.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) { // TODO - if we use SilentContacts for "normal" phone numbers as well // 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; } }