diff options
Diffstat (limited to 'src/com/android/dialer/calllog')
-rwxr-xr-x | src/com/android/dialer/calllog/CallLogActivity.java | 30 | ||||
-rwxr-xr-x | src/com/android/dialer/calllog/CallLogAdapter.java | 434 | ||||
-rw-r--r-- | src/com/android/dialer/calllog/CallLogAdapterHelper.java | 474 | ||||
-rw-r--r-- | src/com/android/dialer/calllog/ContactInfoHelper.java | 32 |
4 files changed, 559 insertions, 411 deletions
diff --git a/src/com/android/dialer/calllog/CallLogActivity.java b/src/com/android/dialer/calllog/CallLogActivity.java index d6ee030e5..10a77e10d 100755 --- a/src/com/android/dialer/calllog/CallLogActivity.java +++ b/src/com/android/dialer/calllog/CallLogActivity.java @@ -11,7 +11,7 @@ * 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. +m * limitations under the License. */ package com.android.dialer.calllog; @@ -50,14 +50,19 @@ import com.android.dialer.R; import com.android.dialer.voicemail.VoicemailStatusHelper; import com.android.dialer.voicemail.VoicemailStatusHelperImpl; import com.android.dialerbind.analytics.AnalyticsActivity; +import com.android.dialer.calllog.CallLogFragment; +import com.android.dialer.callstats.CallStatsFragment; +import com.android.dialer.widget.DoubleDatePickerDialog; -public class CallLogActivity extends AnalyticsActivity implements CallLogQueryHandler.Listener { +public class CallLogActivity extends AnalyticsActivity implements CallLogQueryHandler.Listener, + DoubleDatePickerDialog.OnDateSetListener { private Handler mHandler; private ViewPager mViewPager; private ViewPagerTabs mViewPagerTabs; private FragmentPagerAdapter mViewPagerAdapter; private CallLogFragment mAllCallsFragment; private CallLogFragment mMissedCallsFragment; + private CallStatsFragment mStatsFragment; private CallLogFragment mVoicemailFragment; private VoicemailStatusHelper mVoicemailStatusHelper; @@ -72,10 +77,11 @@ public class CallLogActivity extends AnalyticsActivity implements CallLogQueryHa private static final int TAB_INDEX_ALL = 0; private static final int TAB_INDEX_MISSED = 1; - private static final int TAB_INDEX_VOICEMAIL = 2; + private static final int TAB_INDEX_STATS = 2; + private static final int TAB_INDEX_VOICEMAIL = 3; - private static final int TAB_INDEX_COUNT_DEFAULT = 2; - private static final int TAB_INDEX_COUNT_WITH_VOICEMAIL = 3; + private static final int TAB_INDEX_COUNT_DEFAULT = 3; + private static final int TAB_INDEX_COUNT_WITH_VOICEMAIL = 4; private boolean mHasActiveVoicemailProvider; @@ -108,6 +114,9 @@ public class CallLogActivity extends AnalyticsActivity implements CallLogQueryHa case TAB_INDEX_VOICEMAIL: mVoicemailFragment = new CallLogFragment(Calls.VOICEMAIL_TYPE); return mVoicemailFragment; + case TAB_INDEX_STATS: + mStatsFragment = new CallStatsFragment(); + return mStatsFragment; } throw new IllegalStateException("No fragment at position " + position); } @@ -188,7 +197,8 @@ public class CallLogActivity extends AnalyticsActivity implements CallLogQueryHa mTabTitles = new String[TAB_INDEX_COUNT_WITH_VOICEMAIL]; mTabTitles[0] = getString(R.string.call_log_all_title); mTabTitles[1] = getString(R.string.call_log_missed_title); - mTabTitles[2] = getString(R.string.call_log_voicemail_title); + mTabTitles[2] = getString(R.string.call_log_stats_title); + mTabTitles[3] = getString(R.string.call_log_voicemail_title); mViewPager = (ViewPager) findViewById(R.id.call_log_pager); @@ -250,7 +260,6 @@ public class CallLogActivity extends AnalyticsActivity implements CallLogQueryHa mViewPagerAdapter = new MSimViewPagerAdapter(getFragmentManager()); mViewPager.setAdapter(mViewPagerAdapter); - mViewPager.setOffscreenPageLimit(1); } @Override @@ -403,6 +412,8 @@ public class CallLogActivity extends AnalyticsActivity implements CallLogQueryHa return mMissedCallsFragment; case TAB_INDEX_VOICEMAIL: return mVoicemailFragment; + case TAB_INDEX_STATS: + return mStatsFragment; default: throw new IllegalStateException("Unknown fragment index: " + position); @@ -550,4 +561,9 @@ public class CallLogActivity extends AnalyticsActivity implements CallLogQueryHa mSearchView.clearFocus(); mInSearchUi = false; } + + @Override + public void onDateSet(long from, long to) { + mStatsFragment.onDateSet(from, to); + } } diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java index fcce7736c..5fcb51638 100755 --- a/src/com/android/dialer/calllog/CallLogAdapter.java +++ b/src/com/android/dialer/calllog/CallLogAdapter.java @@ -24,8 +24,6 @@ import android.content.res.Resources; import android.database.Cursor; import android.graphics.drawable.Drawable; import android.net.Uri; -import android.os.Handler; -import android.os.Message; import android.provider.CallLog.Calls; import android.provider.ContactsContract.PhoneLookup; import android.telecom.PhoneAccountHandle; @@ -53,6 +51,7 @@ import com.android.dialer.R; import com.android.dialer.util.DialerUtils; import com.android.dialer.util.ExpirableCache; +import com.android.dialer.calllog.CallLogAdapterHelper.NumberWithCountryIso; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Objects; @@ -63,7 +62,7 @@ import java.util.LinkedList; * Adapter class to fill in data for the Call Log. */ public class CallLogAdapter extends GroupingListAdapter - implements ViewTreeObserver.OnPreDrawListener, CallLogGroupBuilder.GroupCreator { + implements CallLogAdapterHelper.Callback, CallLogGroupBuilder.GroupCreator { private static final int VOICEMAIL_TRANSCRIPTION_MAX_LINES = 10; @@ -100,43 +99,6 @@ public class CallLogAdapter extends GroupingListAdapter public void onReportButtonClick(String number); } - /** - * 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; - /** Constant used to indicate no row is expanded. */ private static final long NONE_EXPANDED = -1; @@ -145,21 +107,10 @@ public class CallLogAdapter extends GroupingListAdapter private final CallFetcher mCallFetcher; private final Toast mReportedToast; private final OnReportButtonClickListener mOnReportButtonClickListener; - private ViewTreeObserver mViewTreeObserver = null; private String mFilterString; /** - * 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; - - /** * Tracks the call log row which was previously expanded. Used so that the closure of a * previously expanded call log entry can be animated on rebind. */ @@ -184,65 +135,7 @@ public class CallLogAdapter extends GroupingListAdapter */ private HashMap<Long,Integer> mDayGroups = new HashMap<Long, Integer>(); - /** - * 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; @@ -256,8 +149,7 @@ public class CallLogAdapter extends GroupingListAdapter private CallItemExpandedListener mCallItemExpandedListener; - /** Can be set to true by tests to disable processing of requests. */ - private volatile boolean mRequestProcessingDisabled = false; + private final CallLogAdapterHelper mAdapterHelper; private boolean mIsCallLog = true; @@ -315,34 +207,6 @@ public class CallLogAdapter extends GroupingListAdapter } } - @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; - } - } - }; - public CallLogAdapter(Context context, CallFetcher callFetcher, ContactInfoHelper contactInfoHelper, CallItemExpandedListener callItemExpandedListener, OnReportButtonClickListener onReportButtonClickListener, boolean isCallLog) { @@ -358,9 +222,6 @@ public class CallLogAdapter extends GroupingListAdapter mReportedToast = Toast.makeText(mContext, R.string.toast_caller_id_reported, Toast.LENGTH_SHORT); - mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE); - mRequests = new LinkedList<ContactInfoRequest>(); - Resources resources = mContext.getResources(); CallTypeHelper callTypeHelper = new CallTypeHelper(resources); mCallLogBackgroundColor = resources.getColor(R.color.background_dialer_list_items); @@ -369,6 +230,8 @@ public class CallLogAdapter extends GroupingListAdapter mContactPhotoManager = ContactPhotoManager.getInstance(mContext); mPhoneNumberHelper = new PhoneNumberDisplayHelper(resources); + mAdapterHelper = new CallLogAdapterHelper(context, this, + contactInfoHelper, mPhoneNumberHelper); PhoneCallDetailsHelper phoneCallDetailsHelper = new PhoneCallDetailsHelper( resources, callTypeHelper, new PhoneNumberUtilsWrapper()); mCallLogViewsHelper = @@ -399,177 +262,6 @@ public class CallLogAdapter extends GroupingListAdapter } } - /** - * 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}. - */ - protected 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); - - final boolean isRemoteSource = info.sourceType != 0; - - // Don't force redraw if existing info in the cache is equal to {@link ContactInfo#EMPTY} - // to avoid updating the data set for every new row that is scrolled into view. - // see (https://googleplex-android-review.git.corp.google.com/#/c/166680/) - - // Exception: Photo uris for contacts from remote sources are not cached in the call log - // cache, so we have to force a redraw for these contacts regardless. - boolean updated = (existingInfo != ContactInfo.EMPTY || isRemoteSource) && - !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); @@ -705,42 +397,8 @@ public class CallLogAdapter extends GroupingListAdapter } // 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 (!PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation) - || isVoicemailNumber) { - // 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 ContactInfo info = mAdapterHelper.lookupContact( + number, numberPresentation, countryIso, cachedContactInfo); final Uri lookupUri = info.lookupUri; final String name = info.name; final int ntype = info.type; @@ -818,10 +476,7 @@ public class CallLogAdapter extends GroupingListAdapter } // Listen for the first draw - if (mViewTreeObserver == null) { - mViewTreeObserver = view.getViewTreeObserver(); - mViewTreeObserver.addOnPreDrawListener(this); - } + mAdapterHelper.registerOnPreDrawListener(view); bindBadge(view, info, details, callType); } @@ -1095,17 +750,14 @@ public class CallLogAdapter extends GroupingListAdapter } } - /** 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); + @Override + public void dataSetChanged() { + notifyDataSetChanged(); } /** Stores the updated contact info in the call log if it is different from the current one. */ - private void updateCallLogContactInfoCache(String number, String countryIso, + @Override + public void updateContactInfo(String number, String countryIso, ContactInfo updatedInfo, ContactInfo callLogInfo) { final ContentValues values = new ContentValues(); boolean needsUpdate = false; @@ -1268,13 +920,18 @@ public class CallLogAdapter extends GroupingListAdapter */ @VisibleForTesting void disableRequestProcessingForTest() { - mRequestProcessingDisabled = true; + mAdapterHelper.disableRequestProcessingForTest(); } @VisibleForTesting void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) { - NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); - mContactInfoCache.put(numberCountryIso, contactInfo); + mAdapterHelper.injectContactInfoForTest(number, countryIso, contactInfo); + } + + @VisibleForTesting + void enqueueRequest(String number, String countryIso, ContactInfo callLogInfo, + boolean immediate) { + mAdapterHelper.enqueueRequest(number, countryIso, callLogInfo, immediate); } @Override @@ -1303,46 +960,16 @@ public class CallLogAdapter extends GroupingListAdapter mDayGroups.clear(); } - /* - * 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 void stopRequestProcessing() { + mAdapterHelper.stopRequestProcessing(); + } + + public void invalidateCache() { + mAdapterHelper.invalidateCache(); + } + 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) { - try { - if (phonesCursor.moveToFirst()) { - matchingNumber = phonesCursor.getString(PhoneQuery.MATCHED_NUMBER); - } - } finally { - 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; + return mAdapterHelper.getBetterNumberFromContacts(number, countryIso); } /** @@ -1382,7 +1009,6 @@ public class CallLogAdapter extends GroupingListAdapter } public void onBadDataReported(String number) { - mContactInfoCache.expireAll(); mReportedToast.show(); } diff --git a/src/com/android/dialer/calllog/CallLogAdapterHelper.java b/src/com/android/dialer/calllog/CallLogAdapterHelper.java new file mode 100644 index 000000000..a16935cfe --- /dev/null +++ b/src/com/android/dialer/calllog/CallLogAdapterHelper.java @@ -0,0 +1,474 @@ +/* + * 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.dialer.calllog; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Handler; +import android.os.Message; +import android.provider.ContactsContract.PhoneLookup; +import android.text.TextUtils; +import android.view.View; +import android.view.ViewTreeObserver; + +import com.android.dialer.util.ExpirableCache; +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. + */ +public class CallLogAdapterHelper implements ViewTreeObserver.OnPreDrawListener { + public interface Callback { + void dataSetChanged(); + void updateContactInfo(String number, String countryIso, + ContactInfo updatedInfo, ContactInfo callLogInfo); + } + + /** + * 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. + */ + public 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()); + } + } + + /** + * 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; + } + } + + /* + * 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. + } + } + } + } + } + + private static final int REDRAW = 1; + private static final int START_THREAD = 2; + + /** 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 Callback mCb; + private final Context mContext; + private final ContactInfoHelper mContactInfoHelper; + private final PhoneNumberDisplayHelper mPhoneNumberHelper; + + /** + * 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; + + private QueryThread mCallerIdThread; + /** Can be set to true by tests to disable processing of requests. */ + private volatile boolean mRequestProcessingDisabled = false; + + /** + * 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 ViewTreeObserver mViewTreeObserver = null; + + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case REDRAW: + mCb.dataSetChanged(); + break; + case START_THREAD: + startRequestProcessing(); + break; + } + } + }; + + /** + * 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(); + } + + @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; + } + + public void registerOnPreDrawListener(View v) { + // Listen for the first draw + if (mViewTreeObserver == null) { + mViewTreeObserver = v.getViewTreeObserver(); + mViewTreeObserver.addOnPreDrawListener(this); + } + } + + /** + * 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(); + } + + /** + * 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; + } + } + + /** + * 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); + mCb.updateContactInfo(number, countryIso, info, callLogInfo); + return updated; + } + + /* + * 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; + } + + /** 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); + } + + + public ContactInfo lookupContact(String number, int numberPresentation, + String countryIso, ContactInfo cachedContactInfo) { + NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); + ExpirableCache.CachedValue<ContactInfo> cachedInfo = + mContactInfoCache.getCachedValue(numberCountryIso); + ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue(); + if (!PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation) + || new PhoneNumberUtilsWrapper().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; + } + } + + return info; + } + + public CallLogAdapterHelper(Context context, Callback cb, + ContactInfoHelper contactInfoHelper, + PhoneNumberDisplayHelper phoneNumberHelper) { + mContext = context; + mCb = cb; + mContactInfoHelper = contactInfoHelper; + mPhoneNumberHelper = phoneNumberHelper; + + mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE); + mRequests = new LinkedList<ContactInfoRequest>(); + } + + /** + * 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); + } +} diff --git a/src/com/android/dialer/calllog/ContactInfoHelper.java b/src/com/android/dialer/calllog/ContactInfoHelper.java index 68654f28c..3274a9df5 100644 --- a/src/com/android/dialer/calllog/ContactInfoHelper.java +++ b/src/com/android/dialer/calllog/ContactInfoHelper.java @@ -15,6 +15,7 @@ package com.android.dialer.calllog; import android.content.ContentUris; +import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.net.Uri; @@ -24,13 +25,17 @@ import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.DisplayNameSources; import android.provider.ContactsContract.PhoneLookup; import android.provider.ContactsContract.RawContacts; +import android.provider.Settings; +import android.provider.Telephony; import android.telephony.PhoneNumberUtils; import android.text.TextUtils; +import android.widget.Toast; import com.android.contacts.common.util.Constants; import com.android.contacts.common.util.PhoneNumberHelper; import com.android.contacts.common.util.UriUtils; import com.android.dialer.lookup.LookupCache; +import com.android.dialer.R; import com.android.dialer.service.CachedNumberLookupService; import com.android.dialer.service.CachedNumberLookupService.CachedContactInfo; import com.android.dialerbind.ObjectFactory; @@ -324,7 +329,34 @@ public class ContactInfoHelper { public boolean canReportAsInvalid(int sourceType, String objectId) { return mCachedNumberLookupService != null && mCachedNumberLookupService.canReportAsInvalid(sourceType, objectId); + } + + /** + * Checks whether calls can be blacklisted; that is, whether the + * phone blacklist is enabled + */ + public boolean canBlacklistCalls() { + return Settings.System.getInt(mContext.getContentResolver(), + Settings.System.PHONE_BLACKLIST_ENABLED, 1) != 0; } + /** + * Requests the given number to be added to the phone blacklist + * + * @param number the number to be blacklisted + */ + public void addNumberToBlacklist(String number) { + ContentValues cv = new ContentValues(); + cv.put(Telephony.Blacklist.PHONE_MODE, 1); + + Uri uri = Uri.withAppendedPath(Telephony.Blacklist.CONTENT_FILTER_BYNUMBER_URI, number); + int count = mContext.getContentResolver().update(uri, cv, null, null); + + if (count != 0) { + // Give the user some feedback + String message = mContext.getString(R.string.toast_added_to_blacklist, number); + Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show(); + } + } } |