summaryrefslogtreecommitdiffstats
path: root/src/com/android/dialer/calllog
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/dialer/calllog')
-rwxr-xr-xsrc/com/android/dialer/calllog/CallLogActivity.java30
-rwxr-xr-xsrc/com/android/dialer/calllog/CallLogAdapter.java434
-rw-r--r--src/com/android/dialer/calllog/CallLogAdapterHelper.java474
-rw-r--r--src/com/android/dialer/calllog/ContactInfoHelper.java32
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();
+ }
+ }
}