Merge "Use solid black background for phone search UI"
diff --git a/res/menu/call_log_options.xml b/res/menu/call_log_options.xml
index c62be77..c75c856 100644
--- a/res/menu/call_log_options.xml
+++ b/res/menu/call_log_options.xml
@@ -29,10 +29,4 @@
android:id="@+id/show_all_calls"
android:title="@string/menu_show_all_calls"
android:showAsAction="withText" />
-
- <item
- android:id="@+id/menu_call_settings_call_log"
- android:title="@string/call_settings"
- android:icon="@drawable/ic_menu_settings_holo_light"
- android:showAsAction="withText" />
</menu>
diff --git a/res/menu/dialpad_options.xml b/res/menu/dialpad_options.xml
index 4dc62a8..77da9cb 100644
--- a/res/menu/dialpad_options.xml
+++ b/res/menu/dialpad_options.xml
@@ -30,10 +30,4 @@
android:icon="@drawable/ic_menu_wait"
android:title="@string/add_wait"
android:showAsAction="withText" />
-
- <item
- android:id="@+id/menu_call_settings_dialpad"
- android:title="@string/call_settings"
- android:icon="@drawable/ic_menu_settings_holo_light"
- android:showAsAction="withText" />
</menu>
diff --git a/res/menu/dialtacts_options.xml b/res/menu/dialtacts_options.xml
index 99f87ff..cc9543a 100644
--- a/res/menu/dialtacts_options.xml
+++ b/res/menu/dialtacts_options.xml
@@ -20,6 +20,12 @@
android:showAsAction="always" />
<item
+ android:id="@+id/menu_call_settings"
+ android:title="@string/call_settings"
+ android:icon="@drawable/ic_menu_settings_holo_light"
+ android:showAsAction="withText" />
+
+ <item
android:id="@+id/filter_option"
android:title="@string/menu_contacts_filter"
android:showAsAction="withText" />
diff --git a/src/com/android/contacts/ContactLoader.java b/src/com/android/contacts/ContactLoader.java
index ceaa246..dbfe411 100644
--- a/src/com/android/contacts/ContactLoader.java
+++ b/src/com/android/contacts/ContactLoader.java
@@ -23,6 +23,7 @@
import com.android.contacts.util.StreamItemPhotoEntry;
import com.google.android.collect.Lists;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Sets;
import android.content.ContentResolver;
import android.content.ContentUris;
@@ -62,6 +63,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* Loads a single Contact and all it constituent RawContacts.
@@ -76,7 +78,7 @@
private Result mContact;
private ForceLoadContentObserver mObserver;
private boolean mDestroyed;
-
+ private final Set<Long> mNotifiedRawContactIds = Sets.newHashSet();
public interface Listener {
public void onContactLoaded(Result contact);
@@ -1115,14 +1117,18 @@
Context context = getContext();
for (Entity entity : mContact.getEntities()) {
final ContentValues entityValues = entity.getEntityValues();
+ final long rawContactId = entityValues.getAsLong(RawContacts.Entity._ID);
+ if (mNotifiedRawContactIds.contains(rawContactId)) {
+ continue; // Already notified for this raw contact.
+ }
+ mNotifiedRawContactIds.add(rawContactId);
final String type = entityValues.getAsString(RawContacts.ACCOUNT_TYPE);
final String dataSet = entityValues.getAsString(RawContacts.DATA_SET);
- final AccountType accountType = AccountTypeManager.getInstance(context ).getAccountType(
+ final AccountType accountType = AccountTypeManager.getInstance(context).getAccountType(
type, dataSet);
final String serviceName = accountType.getViewContactNotifyServiceClassName();
final String resPackageName = accountType.resPackageName;
if (!TextUtils.isEmpty(serviceName) && !TextUtils.isEmpty(resPackageName)) {
- final long rawContactId = entityValues.getAsLong(RawContacts.Entity._ID);
final Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
final Intent intent = new Intent();
intent.setClassName(resPackageName, serviceName);
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index 3bb330f..78c4b18 100644
--- a/src/com/android/contacts/ContactSaveService.java
+++ b/src/com/android/contacts/ContactSaveService.java
@@ -53,8 +53,8 @@
import java.util.ArrayList;
import java.util.HashSet;
-import java.util.LinkedList;
import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
/**
* A service responsible for saving changes to the content provider.
@@ -133,7 +133,8 @@
public void onServiceCompleted(Intent callbackIntent);
}
- private static final LinkedList<Listener> sListeners = new LinkedList<Listener>();
+ private static final CopyOnWriteArrayList<Listener> sListeners =
+ new CopyOnWriteArrayList<Listener>();
private Handler mMainHandler;
@@ -148,15 +149,11 @@
throw new ClassCastException("Only activities can be registered to"
+ " receive callback from " + ContactSaveService.class.getName());
}
- synchronized (sListeners) {
- sListeners.addFirst(listener);
- }
+ sListeners.add(0, listener);
}
public static void unregisterListener(Listener listener) {
- synchronized (sListeners) {
- sListeners.remove(listener);
- }
+ sListeners.remove(listener);
}
@Override
@@ -975,13 +972,11 @@
// TODO: this assumes that if there are multiple instances of the same
// activity registered, the last one registered is the one waiting for
// the callback. Validity of this assumption needs to be verified.
- synchronized (sListeners) {
- for (Listener listener : sListeners) {
- if (callbackIntent.getComponent().equals(
- ((Activity) listener).getIntent().getComponent())) {
- listener.onServiceCompleted(callbackIntent);
- return;
- }
+ for (Listener listener : sListeners) {
+ if (callbackIntent.getComponent().equals(
+ ((Activity) listener).getIntent().getComponent())) {
+ listener.onServiceCompleted(callbackIntent);
+ return;
}
}
}
diff --git a/src/com/android/contacts/ContactsUtils.java b/src/com/android/contacts/ContactsUtils.java
index 76cbc7d..45ce4fe 100644
--- a/src/com/android/contacts/ContactsUtils.java
+++ b/src/com/android/contacts/ContactsUtils.java
@@ -16,10 +16,10 @@
package com.android.contacts;
-import com.google.i18n.phonenumbers.NumberParseException;
-import com.google.i18n.phonenumbers.PhoneNumberUtil;
-import com.google.i18n.phonenumbers.PhoneNumberUtil.MatchType;
-import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
+import com.android.i18n.phonenumbers.NumberParseException;
+import com.android.i18n.phonenumbers.PhoneNumberUtil;
+import com.android.i18n.phonenumbers.PhoneNumberUtil.MatchType;
+import com.android.i18n.phonenumbers.Phonenumber.PhoneNumber;
import android.content.Context;
import android.content.Intent;
diff --git a/src/com/android/contacts/PhoneCallDetailsHelper.java b/src/com/android/contacts/PhoneCallDetailsHelper.java
index e970fcc..e79bdce 100644
--- a/src/com/android/contacts/PhoneCallDetailsHelper.java
+++ b/src/com/android/contacts/PhoneCallDetailsHelper.java
@@ -107,7 +107,8 @@
mPhoneNumberHelper.getDisplayNumber(details.number, details.formattedNumber);
if (TextUtils.isEmpty(details.name)) {
nameText = displayNumber;
- if (TextUtils.isEmpty(details.geocode)) {
+ if (TextUtils.isEmpty(details.geocode)
+ || mPhoneNumberHelper.isVoicemailNumber(details.number)) {
numberText = mResources.getString(R.string.call_log_empty_gecode);
} else {
numberText = details.geocode;
diff --git a/src/com/android/contacts/activities/DialtactsActivity.java b/src/com/android/contacts/activities/DialtactsActivity.java
index cfca831..d0acc6b 100644
--- a/src/com/android/contacts/activities/DialtactsActivity.java
+++ b/src/com/android/contacts/activities/DialtactsActivity.java
@@ -644,19 +644,24 @@
public boolean onPrepareOptionsMenu(Menu menu) {
final MenuItem searchMenuItem = menu.findItem(R.id.search_on_action_bar);
final MenuItem filterOptionMenuItem = menu.findItem(R.id.filter_option);
+ final MenuItem callSettingsMenuItem = menu.findItem(R.id.menu_call_settings);
Tab tab = getActionBar().getSelectedTab();
if (mInSearchUi) {
searchMenuItem.setVisible(false);
filterOptionMenuItem.setVisible(true);
filterOptionMenuItem.setOnMenuItemClickListener(
mFilterOptionsMenuItemClickListener);
- } else if (tab == null || tab.getPosition() == TAB_INDEX_DIALER) {
- searchMenuItem.setVisible(false);
- filterOptionMenuItem.setVisible(false);
+ callSettingsMenuItem.setVisible(false);
} else {
+ if (tab != null && tab.getPosition() == TAB_INDEX_DIALER) {
+ searchMenuItem.setVisible(false);
+ } else {
+ searchMenuItem.setVisible(true);
+ searchMenuItem.setOnMenuItemClickListener(mSearchMenuItemClickListener);
+ }
filterOptionMenuItem.setVisible(false);
- searchMenuItem.setVisible(true);
- searchMenuItem.setOnMenuItemClickListener(mSearchMenuItemClickListener);
+ callSettingsMenuItem.setVisible(true);
+ callSettingsMenuItem.setIntent(DialtactsActivity.getCallSettingsIntent());
}
return true;
diff --git a/src/com/android/contacts/calllog/CallLogAdapter.java b/src/com/android/contacts/calllog/CallLogAdapter.java
new file mode 100644
index 0000000..7e934b6
--- /dev/null
+++ b/src/com/android/contacts/calllog/CallLogAdapter.java
@@ -0,0 +1,755 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.calllog;
+
+import com.android.common.widget.GroupingListAdapter;
+import com.android.contacts.ContactPhotoManager;
+import com.android.contacts.PhoneCallDetails;
+import com.android.contacts.PhoneCallDetailsHelper;
+import com.android.contacts.R;
+import com.android.contacts.util.ExpirableCache;
+import com.google.common.annotations.VisibleForTesting;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.PhoneLookup;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+
+import java.util.LinkedList;
+
+/**
+ * Adapter class to fill in data for the Call Log.
+ */
+public final class CallLogAdapter extends GroupingListAdapter
+ implements Runnable, ViewTreeObserver.OnPreDrawListener, CallLogGroupBuilder.GroupCreator {
+ /** Interface used to initiate a refresh of the content. */
+ public interface CallFetcher {
+ public void startCallsQuery();
+ }
+
+ /** 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 String mCurrentCountryIso;
+ private final CallFetcher mCallFetcher;
+
+ /**
+ * A cache of the contact details for the phone numbers in the call log.
+ * <p>
+ * The content of the cache is expired (but not purged) whenever the application comes to
+ * the foreground.
+ */
+ private ExpirableCache<String, ContactInfo> mContactInfoCache;
+
+ /**
+ * List of requests to update contact details.
+ * <p>
+ * The requests are added when displaying the contacts and are processed by a background
+ * thread.
+ */
+ private final LinkedList<String> mRequests;
+
+ private volatile boolean mDone;
+ private boolean mLoading = true;
+ private ViewTreeObserver.OnPreDrawListener mPreDrawListener;
+ private static final int REDRAW = 1;
+ private static final int START_THREAD = 2;
+ private boolean mFirst;
+ private Thread mCallerIdThread;
+
+ /** Instance of helper class for managing views. */
+ private final CallLogListItemHelper mCallLogViewsHelper;
+
+ /** Helper to set up contact photos. */
+ private final ContactPhotoManager mContactPhotoManager;
+ /** Helper to parse and process phone numbers. */
+ private PhoneNumberHelper mPhoneNumberHelper;
+ /** Helper to group call log entries. */
+ private final CallLogGroupBuilder mCallLogGroupBuilder;
+
+ /** Can be set to true by tests to disable processing of requests. */
+ private volatile boolean mRequestProcessingDisabled = false;
+
+ /** Listener for the primary action in the list, opens the call details. */
+ private final View.OnClickListener mPrimaryActionListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ IntentProvider intentProvider = (IntentProvider) view.getTag();
+ if (intentProvider != null) {
+ mContext.startActivity(intentProvider.getIntent(mContext));
+ }
+ }
+ };
+ /** Listener for the secondary action in the list, either call or play. */
+ private final View.OnClickListener mSecondaryActionListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ IntentProvider intentProvider = (IntentProvider) view.getTag();
+ if (intentProvider != null) {
+ mContext.startActivity(intentProvider.getIntent(mContext));
+ }
+ }
+ };
+
+ @Override
+ public boolean onPreDraw() {
+ if (mFirst) {
+ mHandler.sendEmptyMessageDelayed(START_THREAD,
+ START_PROCESSING_REQUESTS_DELAY_MILLIS);
+ mFirst = false;
+ }
+ return true;
+ }
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case REDRAW:
+ notifyDataSetChanged();
+ break;
+ case START_THREAD:
+ startRequestProcessing();
+ break;
+ }
+ }
+ };
+
+ public CallLogAdapter(Context context, CallFetcher callFetcher,
+ String currentCountryIso, String voicemailNumber) {
+ super(context);
+
+ mContext = context;
+ mCurrentCountryIso = currentCountryIso;
+ mCallFetcher = callFetcher;
+
+ mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE);
+ mRequests = new LinkedList<String>();
+ mPreDrawListener = null;
+
+ Resources resources = mContext.getResources();
+ CallTypeHelper callTypeHelper = new CallTypeHelper(resources);
+
+ mContactPhotoManager = ContactPhotoManager.getInstance(mContext);
+ mPhoneNumberHelper = new PhoneNumberHelper(resources, voicemailNumber);
+ PhoneCallDetailsHelper phoneCallDetailsHelper = new PhoneCallDetailsHelper(
+ resources, callTypeHelper, mPhoneNumberHelper);
+ mCallLogViewsHelper =
+ new CallLogListItemHelper(
+ phoneCallDetailsHelper, mPhoneNumberHelper, resources);
+ mCallLogGroupBuilder = new CallLogGroupBuilder(this);
+ }
+
+ /**
+ * Requery on background thread when {@link Cursor} changes.
+ */
+ @Override
+ protected void onContentChanged() {
+ // When the content changes, always fetch all the calls, in case a new missed call came
+ // in and we were filtering over voicemail only, so that we see the missed call.
+ mCallFetcher.startCallsQuery();
+ }
+
+ 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();
+ }
+ }
+
+ public ContactInfo getContactInfo(String number) {
+ return mContactInfoCache.getPossiblyExpired(number);
+ }
+
+ public void startRequestProcessing() {
+ if (mRequestProcessingDisabled) {
+ return;
+ }
+
+ mDone = false;
+ mCallerIdThread = new Thread(this, "CallLogContactLookup");
+ mCallerIdThread.setPriority(Thread.MIN_PRIORITY);
+ mCallerIdThread.start();
+ }
+
+ /**
+ * Stops the background thread that processes updates and cancels any pending requests to
+ * start it.
+ * <p>
+ * Should be called from the main thread to prevent a race condition between the request to
+ * start the thread being processed and stopping the thread.
+ */
+ public void stopRequestProcessing() {
+ // Remove any pending requests to start the processing thread.
+ mHandler.removeMessages(START_THREAD);
+ mDone = true;
+ if (mCallerIdThread != null) mCallerIdThread.interrupt();
+ }
+
+ public void invalidateCache() {
+ mContactInfoCache.expireAll();
+ // Let it restart the thread after next draw
+ mPreDrawListener = null;
+ }
+
+ private void enqueueRequest(String number, boolean immediate) {
+ synchronized (mRequests) {
+ if (!mRequests.contains(number)) {
+ mRequests.add(number);
+ mRequests.notifyAll();
+ }
+ }
+ if (mFirst && immediate) {
+ startRequestProcessing();
+ mFirst = false;
+ }
+ }
+
+ /**
+ * Determines the contact information for the given SIP address.
+ * <p>
+ * It returns the contact info if found.
+ * <p>
+ * If no contact corresponds to the given SIP address, returns {@link ContactInfo#EMPTY}.
+ * <p>
+ * If the lookup fails for some other reason, it returns null.
+ */
+ private ContactInfo queryContactInfoForSipAddress(String sipAddress) {
+ final ContactInfo info;
+
+ // TODO: This code is duplicated from the
+ // CallerInfoAsyncQuery class. To avoid that, could the
+ // code here just use CallerInfoAsyncQuery, rather than
+ // manually running ContentResolver.query() itself?
+
+ // We look up SIP addresses directly in the Data table:
+ Uri contactRef = Data.CONTENT_URI;
+
+ // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent.
+ //
+ // Also note we use "upper(data1)" in the WHERE clause, and
+ // uppercase the incoming SIP address, in order to do a
+ // case-insensitive match.
+ //
+ // TODO: May also need to normalize by adding "sip:" as a
+ // prefix, if we start storing SIP addresses that way in the
+ // database.
+ String selection = "upper(" + Data.DATA1 + ")=?"
+ + " AND "
+ + Data.MIMETYPE + "='" + SipAddress.CONTENT_ITEM_TYPE + "'";
+ String[] selectionArgs = new String[] { sipAddress.toUpperCase() };
+
+ Cursor dataTableCursor =
+ mContext.getContentResolver().query(
+ contactRef,
+ null, // projection
+ selection, // selection
+ selectionArgs, // selectionArgs
+ null); // sortOrder
+
+ if (dataTableCursor != null) {
+ if (dataTableCursor.moveToFirst()) {
+ info = new ContactInfo();
+
+ // TODO: we could slightly speed this up using an
+ // explicit projection (and thus not have to do
+ // those getColumnIndex() calls) but the benefit is
+ // very minimal.
+
+ // Note the Data.CONTACT_ID column here is
+ // equivalent to the PERSON_ID_COLUMN_INDEX column
+ // we use with "phonesCursor" below.
+ info.personId = dataTableCursor.getLong(
+ dataTableCursor.getColumnIndex(Data.CONTACT_ID));
+ info.name = dataTableCursor.getString(
+ dataTableCursor.getColumnIndex(Data.DISPLAY_NAME));
+ // "type" and "label" are currently unused for SIP addresses
+ info.type = SipAddress.TYPE_OTHER;
+ info.label = null;
+
+ // And "number" is the SIP address.
+ // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent.
+ info.number = dataTableCursor.getString(
+ dataTableCursor.getColumnIndex(Data.DATA1));
+ info.normalizedNumber = null; // meaningless for SIP addresses
+ final String thumbnailUriString = dataTableCursor.getString(
+ dataTableCursor.getColumnIndex(Data.PHOTO_THUMBNAIL_URI));
+ info.thumbnailUri = thumbnailUriString == null
+ ? null
+ : Uri.parse(thumbnailUriString);
+ info.lookupKey = dataTableCursor.getString(
+ dataTableCursor.getColumnIndex(Data.LOOKUP_KEY));
+ } else {
+ info = ContactInfo.EMPTY;
+ }
+ dataTableCursor.close();
+ } else {
+ // Failed to fetch the data, ignore this request.
+ info = null;
+ }
+ return info;
+ }
+
+ /**
+ * Determines the contact information for the given phone number.
+ * <p>
+ * It returns the contact info if found.
+ * <p>
+ * If no contact corresponds to the given phone number, returns {@link ContactInfo#EMPTY}.
+ * <p>
+ * If the lookup fails for some other reason, it returns null.
+ */
+ private ContactInfo queryContactInfoForPhoneNumber(String number) {
+ final ContactInfo info;
+
+ // "number" is a regular phone number, so use the
+ // PhoneLookup table:
+ Cursor phonesCursor =
+ mContext.getContentResolver().query(
+ Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI,
+ Uri.encode(number)),
+ PhoneQuery._PROJECTION, null, null, null);
+ if (phonesCursor != null) {
+ if (phonesCursor.moveToFirst()) {
+ info = new ContactInfo();
+ info.personId = phonesCursor.getLong(PhoneQuery.PERSON_ID);
+ info.name = phonesCursor.getString(PhoneQuery.NAME);
+ info.type = phonesCursor.getInt(PhoneQuery.PHONE_TYPE);
+ info.label = phonesCursor.getString(PhoneQuery.LABEL);
+ info.number = phonesCursor
+ .getString(PhoneQuery.MATCHED_NUMBER);
+ info.normalizedNumber = phonesCursor
+ .getString(PhoneQuery.NORMALIZED_NUMBER);
+ final String thumbnailUriString = phonesCursor.getString(
+ PhoneQuery.THUMBNAIL_URI);
+ info.thumbnailUri = thumbnailUriString == null
+ ? null
+ : Uri.parse(thumbnailUriString);
+ info.lookupKey = phonesCursor.getString(PhoneQuery.LOOKUP_KEY);
+ } else {
+ info = ContactInfo.EMPTY;
+ }
+ phonesCursor.close();
+ } else {
+ // Failed to fetch the data, ignore this request.
+ info = null;
+ }
+ return info;
+ }
+
+ /**
+ * Queries the appropriate content provider for the contact associated with the number.
+ * <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) {
+ final ContactInfo info;
+
+ // Determine the contact info.
+ if (PhoneNumberUtils.isUriNumber(number)) {
+ // This "number" is really a SIP address.
+ info = queryContactInfoForSipAddress(number);
+ } else {
+ info = queryContactInfoForPhoneNumber(number);
+ }
+
+ 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.
+ ContactInfo existingInfo = mContactInfoCache.getPossiblyExpired(number);
+ boolean updated = !info.equals(existingInfo);
+ if (updated) {
+ // The formattedNumber is computed by the UI thread when needed. Since we updated
+ // the details of the contact, set this value to null for now.
+ info.formattedNumber = null;
+ }
+ // 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(number, info);
+ return updated;
+ }
+
+ /*
+ * Handles requests for contact name and number type
+ * @see java.lang.Runnable#run()
+ */
+ @Override
+ public void run() {
+ boolean needNotify = false;
+ while (!mDone) {
+ String number = null;
+ synchronized (mRequests) {
+ if (!mRequests.isEmpty()) {
+ number = mRequests.removeFirst();
+ } else {
+ if (needNotify) {
+ needNotify = false;
+ mHandler.sendEmptyMessage(REDRAW);
+ }
+ try {
+ mRequests.wait(1000);
+ } catch (InterruptedException ie) {
+ // Ignore and continue processing requests
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ if (!mDone && number != null && queryContactInfo(number)) {
+ needNotify = true;
+ }
+ }
+ }
+
+ @Override
+ protected void addGroups(Cursor cursor) {
+ mCallLogGroupBuilder.addGroups(cursor);
+ }
+
+ @VisibleForTesting
+ @Override
+ public 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;
+ }
+
+ @VisibleForTesting
+ @Override
+ public void bindStandAloneView(View view, Context context, Cursor cursor) {
+ bindView(view, cursor, 1);
+ }
+
+ @VisibleForTesting
+ @Override
+ public 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;
+ }
+
+ @VisibleForTesting
+ @Override
+ public void bindChildView(View view, Context context, Cursor cursor) {
+ bindView(view, cursor, 1);
+ }
+
+ @VisibleForTesting
+ @Override
+ public 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;
+ }
+
+ @VisibleForTesting
+ @Override
+ public void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
+ boolean expanded) {
+ bindView(view, cursor, groupSize);
+ }
+
+ private void findAndCacheViews(View view) {
+ // Get the views to bind to.
+ CallLogListItemViews views = CallLogListItemViews.fromView(view);
+ views.primaryActionView.setOnClickListener(mPrimaryActionListener);
+ views.secondaryActionView.setOnClickListener(mSecondaryActionListener);
+ view.setTag(views);
+ }
+
+ /**
+ * Binds the views in the entry to the data in the call log.
+ *
+ * @param view the view corresponding to this entry
+ * @param c the cursor pointing to the entry in the call log
+ * @param count the number of entries in the current item, greater than 1 if it is a group
+ */
+ private void bindView(View view, Cursor c, int count) {
+ final CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+ final int section = c.getInt(CallLogQuery.SECTION);
+
+ // This might be a header: check the value of the section column in the cursor.
+ if (section == CallLogQuery.SECTION_NEW_HEADER
+ || section == CallLogQuery.SECTION_OLD_HEADER) {
+ views.listItemView.setVisibility(View.GONE);
+ views.listHeaderView.setVisibility(View.VISIBLE);
+ views.listHeaderTextView.setText(
+ section == CallLogQuery.SECTION_NEW_HEADER
+ ? R.string.call_log_new_header
+ : R.string.call_log_old_header);
+ // Nothing else to set up for a header.
+ return;
+ }
+ // Default case: an item in the call log.
+ views.listItemView.setVisibility(View.VISIBLE);
+ views.listHeaderView.setVisibility(View.GONE);
+
+ final String number = c.getString(CallLogQuery.NUMBER);
+ final long date = c.getLong(CallLogQuery.DATE);
+ final long duration = c.getLong(CallLogQuery.DURATION);
+ final int callType = c.getInt(CallLogQuery.CALL_TYPE);
+ final String formattedNumber;
+ final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO);
+
+ final ContactInfo cachedContactInfo = getContactInfoFromCallLog(c);
+
+ views.primaryActionView.setTag(
+ IntentProvider.getCallDetailIntentProvider(
+ this, c.getPosition(), c.getLong(CallLogQuery.ID), count));
+ // Store away the voicemail information so we can play it directly.
+ if (callType == Calls.VOICEMAIL_TYPE) {
+ String voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI);
+ final long rowId = c.getLong(CallLogQuery.ID);
+ views.secondaryActionView.setTag(
+ IntentProvider.getPlayVoicemailIntentProvider(rowId, voicemailUri));
+ } else if (!TextUtils.isEmpty(number)) {
+ // Store away the number so we can call it directly if you click on the call icon.
+ views.secondaryActionView.setTag(
+ IntentProvider.getReturnCallIntentProvider(number));
+ } else {
+ // No action enabled.
+ views.secondaryActionView.setTag(null);
+ }
+
+ // Lookup contacts with this number
+ ExpirableCache.CachedValue<ContactInfo> cachedInfo =
+ mContactInfoCache.getCachedValue(number);
+ ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue();
+ if (cachedInfo == null) {
+ // Mark it as empty and queue up a request to find the name.
+ // The db request should happen on a non-UI thread.
+ info = ContactInfo.EMPTY;
+ mContactInfoCache.put(number, info);
+ // Request the contact details immediately since they are currently missing.
+ enqueueRequest(number, true);
+ // Format the phone number in the call log as best as we can.
+ formattedNumber = formatPhoneNumber(number, null, countryIso);
+ } 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, false);
+ }
+
+ if (info != ContactInfo.EMPTY) {
+ // Format and cache phone number for found contact.
+ if (info.formattedNumber == null) {
+ info.formattedNumber =
+ formatPhoneNumber(info.number, info.normalizedNumber, countryIso);
+ }
+ formattedNumber = info.formattedNumber;
+ } else {
+ // Format the phone number in the call log as best as we can.
+ formattedNumber = formatPhoneNumber(number, null, countryIso);
+ }
+ }
+
+ if (info == null || info == ContactInfo.EMPTY) {
+ info = cachedContactInfo;
+ }
+
+ final long personId = info.personId;
+ final String name = info.name;
+ final int ntype = info.type;
+ final String label = info.label;
+ final Uri thumbnailUri = info.thumbnailUri;
+ final String lookupKey = info.lookupKey;
+ final int[] callTypes = getCallTypes(c, count);
+ final String geocode = c.getString(CallLogQuery.GEOCODED_LOCATION);
+ final PhoneCallDetails details;
+ if (TextUtils.isEmpty(name)) {
+ details = new PhoneCallDetails(number, formattedNumber, countryIso, geocode,
+ callTypes, date, duration);
+ } else {
+ details = new PhoneCallDetails(number, formattedNumber, countryIso, geocode,
+ callTypes, date, duration, name, ntype, label, personId, thumbnailUri);
+ }
+
+ final boolean isNew = CallLogQuery.isNewSection(c);
+ // New items also use the highlighted version of the text.
+ final boolean isHighlighted = isNew;
+ mCallLogViewsHelper.setPhoneCallDetails(views, details, isHighlighted);
+ setPhoto(views, thumbnailUri, personId, lookupKey);
+
+ // Listen for the first draw
+ if (mPreDrawListener == null) {
+ mFirst = true;
+ mPreDrawListener = this;
+ view.getViewTreeObserver().addOnPreDrawListener(this);
+ }
+ }
+
+ /** Returns the contact information as stored in the call log. */
+ private ContactInfo getContactInfoFromCallLog(Cursor c) {
+ ContactInfo info = new ContactInfo();
+ info.personId = -1;
+ info.name = c.getString(CallLogQuery.CACHED_NAME);
+ info.type = c.getInt(CallLogQuery.CACHED_NUMBER_TYPE);
+ info.label = c.getString(CallLogQuery.CACHED_NUMBER_LABEL);
+ // TODO: This should be added to the call log cached values.
+ info.number = c.getString(CallLogQuery.NUMBER);
+ info.formattedNumber = info.number;
+ info.normalizedNumber = info.number;
+ info.thumbnailUri = null;
+ info.lookupKey = null;
+ return info;
+ }
+
+ /**
+ * Returns the call types for the given number of items in the cursor.
+ * <p>
+ * It uses the next {@code count} rows in the cursor to extract the types.
+ * <p>
+ * It position in the cursor is unchanged by this function.
+ */
+ private int[] getCallTypes(Cursor cursor, int count) {
+ int position = cursor.getPosition();
+ int[] callTypes = new int[count];
+ for (int index = 0; index < count; ++index) {
+ callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE);
+ cursor.moveToNext();
+ }
+ cursor.moveToPosition(position);
+ return callTypes;
+ }
+
+ private void setPhoto(CallLogListItemViews views, Uri thumbnailUri, long contactId,
+ String lookupKey) {
+ views.quickContactView.assignContactUri(contactId == -1 ? null :
+ Contacts.getLookupUri(contactId, lookupKey));
+ mContactPhotoManager.loadPhoto(views.quickContactView, thumbnailUri);
+ }
+
+ /**
+ * 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.
+ */
+ public void disableRequestProcessingForTest() {
+ mRequestProcessingDisabled = true;
+ }
+
+ public void injectContactInfoForTest(String number, ContactInfo contactInfo) {
+ mContactInfoCache.put(number, contactInfo);
+ }
+
+ @Override
+ public void addGroup(int cursorPosition, int size, boolean expanded) {
+ super.addGroup(cursorPosition, size, expanded);
+ }
+
+ /**
+ * Format the given phone number
+ *
+ * @param number the number to be formatted.
+ * @param normalizedNumber the normalized number of the given number.
+ * @param countryIso the ISO 3166-1 two letters country code, the country's
+ * convention will be used to format the number if the normalized
+ * phone is null.
+ *
+ * @return the formatted number, or the given number if it was formatted.
+ */
+ private String formatPhoneNumber(String number, String normalizedNumber,
+ String countryIso) {
+ if (TextUtils.isEmpty(number)) {
+ return "";
+ }
+ // If "number" is really a SIP address, don't try to do any formatting at all.
+ if (PhoneNumberUtils.isUriNumber(number)) {
+ return number;
+ }
+ if (TextUtils.isEmpty(countryIso)) {
+ countryIso = mCurrentCountryIso;
+ }
+ return PhoneNumberUtils.formatNumber(number, normalizedNumber, countryIso);
+ }
+
+ /*
+ * 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 matchingNumber = null;
+ // Look in the cache first. If it's not found then query the Phones db
+ ContactInfo ci = mContactInfoCache.getPossiblyExpired(number);
+ 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;
+ }
+}
diff --git a/src/com/android/contacts/calllog/CallLogFragment.java b/src/com/android/contacts/calllog/CallLogFragment.java
index 2d0ddbd..215fd7b 100644
--- a/src/com/android/contacts/calllog/CallLogFragment.java
+++ b/src/com/android/contacts/calllog/CallLogFragment.java
@@ -16,40 +16,27 @@
package com.android.contacts.calllog;
-import com.android.common.widget.GroupingListAdapter;
-import com.android.contacts.ContactPhotoManager;
import com.android.contacts.ContactsUtils;
-import com.android.contacts.PhoneCallDetails;
-import com.android.contacts.PhoneCallDetailsHelper;
import com.android.contacts.R;
import com.android.contacts.activities.DialtactsActivity;
import com.android.contacts.activities.DialtactsActivity.ViewPagerVisibilityListener;
import com.android.contacts.test.NeededForTesting;
-import com.android.contacts.util.ExpirableCache;
import com.android.contacts.voicemail.VoicemailStatusHelper;
import com.android.contacts.voicemail.VoicemailStatusHelper.StatusMessage;
import com.android.contacts.voicemail.VoicemailStatusHelperImpl;
import com.android.internal.telephony.CallerInfo;
import com.android.internal.telephony.ITelephony;
-import com.google.common.annotations.VisibleForTesting;
import android.app.KeyguardManager;
import android.app.ListFragment;
import android.content.Context;
import android.content.Intent;
-import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
-import android.os.Handler;
-import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.provider.CallLog.Calls;
-import android.provider.ContactsContract.CommonDataKinds.SipAddress;
-import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.PhoneLookup;
import android.telephony.PhoneNumberUtils;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
@@ -60,108 +47,18 @@
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
import android.widget.ListView;
import android.widget.TextView;
-import java.util.LinkedList;
import java.util.List;
/**
* Displays a list of call log entries.
*/
public class CallLogFragment extends ListFragment implements ViewPagerVisibilityListener,
- CallLogQueryHandler.Listener {
+ CallLogQueryHandler.Listener, CallLogAdapter.CallFetcher {
private static final String TAG = "CallLogFragment";
- /** The size of the cache of contact info. */
- private static final int CONTACT_INFO_CACHE_SIZE = 100;
-
- /** The query for the call log table. */
- public static final class CallLogQuery {
- // If you alter this, you must also alter the method that inserts a fake row to the headers
- // in the CallLogQueryHandler class called createHeaderCursorFor().
- public static final String[] _PROJECTION = new String[] {
- Calls._ID,
- Calls.NUMBER,
- Calls.DATE,
- Calls.DURATION,
- Calls.TYPE,
- Calls.COUNTRY_ISO,
- Calls.VOICEMAIL_URI,
- Calls.GEOCODED_LOCATION,
- };
-
- public static final int ID = 0;
- public static final int NUMBER = 1;
- public static final int DATE = 2;
- public static final int DURATION = 3;
- public static final int CALL_TYPE = 4;
- public static final int COUNTRY_ISO = 5;
- public static final int VOICEMAIL_URI = 6;
- public static final int GEOCODED_LOCATION = 7;
-
- /**
- * The name of the synthetic "section" column.
- * <p>
- * This column identifies whether a row is a header or an actual item, and whether it is
- * part of the new or old calls.
- */
- public static final String SECTION_NAME = "section";
- /** The index of the "section" column in the projection. */
- public static final int SECTION = 8;
- /** The value of the "section" column for the header of the new section. */
- public static final int SECTION_NEW_HEADER = 0;
- /** The value of the "section" column for the items of the new section. */
- public static final int SECTION_NEW_ITEM = 1;
- /** The value of the "section" column for the header of the old section. */
- public static final int SECTION_OLD_HEADER = 2;
- /** The value of the "section" column for the items of the old section. */
- public static final int SECTION_OLD_ITEM = 3;
-
- /** The call log projection including the section name. */
- public static final String[] EXTENDED_PROJECTION;
- static {
- EXTENDED_PROJECTION = new String[_PROJECTION.length + 1];
- System.arraycopy(_PROJECTION, 0, EXTENDED_PROJECTION, 0, _PROJECTION.length);
- EXTENDED_PROJECTION[_PROJECTION.length] = SECTION_NAME;
- }
-
- public static boolean isSectionHeader(Cursor cursor) {
- int section = cursor.getInt(CallLogQuery.SECTION);
- return section == CallLogQuery.SECTION_NEW_HEADER
- || section == CallLogQuery.SECTION_OLD_HEADER;
- }
-
- public static boolean isNewSection(Cursor cursor) {
- int section = cursor.getInt(CallLogQuery.SECTION);
- return section == CallLogQuery.SECTION_NEW_ITEM
- || section == CallLogQuery.SECTION_NEW_HEADER;
- }
- }
-
- /** The query to use for the phones table */
- private static final class PhoneQuery {
- public static final String[] _PROJECTION = new String[] {
- PhoneLookup._ID,
- PhoneLookup.DISPLAY_NAME,
- PhoneLookup.TYPE,
- PhoneLookup.LABEL,
- PhoneLookup.NUMBER,
- PhoneLookup.NORMALIZED_NUMBER,
- PhoneLookup.PHOTO_THUMBNAIL_URI,
- PhoneLookup.LOOKUP_KEY};
-
- public static final int PERSON_ID = 0;
- public static final int NAME = 1;
- public static final int PHONE_TYPE = 2;
- public static final int LABEL = 3;
- public static final int MATCHED_NUMBER = 4;
- public static final int NORMALIZED_NUMBER = 5;
- public static final int THUMBNAIL_URI = 6;
- public static final int LOOKUP_KEY = 7;
- }
-
private CallLogAdapter mAdapter;
private CallLogQueryHandler mCallLogQueryHandler;
private String mVoiceMailNumber;
@@ -177,702 +74,6 @@
private TextView mStatusMessageAction;
private KeyguardManager mKeyguardManager;
- public static final class ContactInfo {
- public long personId = -1;
- public String name;
- public int type;
- public String label;
- public String number;
- public String formattedNumber;
- public String normalizedNumber;
- public Uri thumbnailUri;
- public String lookupKey;
-
- public static ContactInfo EMPTY = new ContactInfo();
-
- @Override
- public int hashCode() {
- // Uses only name and personId to determine hashcode.
- // This should be sufficient to have a reasonable distribution of hash codes.
- // Moreover, there should be no two people with the same personId.
- final int prime = 31;
- int result = 1;
- result = prime * result + (int) (personId ^ (personId >>> 32));
- result = prime * result + ((name == null) ? 0 : name.hashCode());
- return result;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) return true;
- if (obj == null) return false;
- if (getClass() != obj.getClass()) return false;
- ContactInfo other = (ContactInfo) obj;
- if (personId != other.personId) return false;
- if (!TextUtils.equals(name, other.name)) return false;
- if (type != other.type) return false;
- if (!TextUtils.equals(label, other.label)) return false;
- if (!TextUtils.equals(number, other.number)) return false;
- // Ignore formatted number.
- if (!TextUtils.equals(normalizedNumber, other.normalizedNumber)) return false;
- if (!uriEquals(thumbnailUri, other.thumbnailUri)) return false;
- if (!TextUtils.equals(lookupKey, other.lookupKey)) return false;
- return true;
- }
-
- private static boolean uriEquals(Uri thumbnailUri1, Uri thumbnailUri2) {
- if (thumbnailUri1 == thumbnailUri2) return true;
- if (thumbnailUri1 == null) return false;
- return thumbnailUri1.equals(thumbnailUri2);
- }
- }
-
- public interface GroupCreator {
- public void addGroup(int cursorPosition, int size, boolean expanded);
- }
-
- public interface CallFetcher {
- public void fetchAllCalls();
- }
-
- /** Adapter class to fill in data for the Call Log */
- public static final class CallLogAdapter extends GroupingListAdapter
- implements Runnable, ViewTreeObserver.OnPreDrawListener, GroupCreator {
- /** The time in millis to delay starting the thread processing requests. */
- private static final int START_PROCESSING_REQUESTS_DELAY_MILLIS = 1000;
-
- private final Context mContext;
- private final String mCurrentCountryIso;
- private final CallFetcher mCallFetcher;
-
- /**
- * A cache of the contact details for the phone numbers in the call log.
- * <p>
- * The content of the cache is expired (but not purged) whenever the application comes to
- * the foreground.
- */
- private ExpirableCache<String, ContactInfo> mContactInfoCache;
-
- /**
- * List of requests to update contact details.
- * <p>
- * The requests are added when displaying the contacts and are processed by a background
- * thread.
- */
- private final LinkedList<String> mRequests;
-
- private volatile boolean mDone;
- private boolean mLoading = true;
- private ViewTreeObserver.OnPreDrawListener mPreDrawListener;
- private static final int REDRAW = 1;
- private static final int START_THREAD = 2;
- private boolean mFirst;
- private Thread mCallerIdThread;
-
- /** Instance of helper class for managing views. */
- private final CallLogListItemHelper mCallLogViewsHelper;
-
- /** Helper to set up contact photos. */
- private final ContactPhotoManager mContactPhotoManager;
- /** Helper to parse and process phone numbers. */
- private PhoneNumberHelper mPhoneNumberHelper;
- /** Helper to group call log entries. */
- private final CallLogGroupBuilder mCallLogGroupBuilder;
-
- /** Can be set to true by tests to disable processing of requests. */
- private volatile boolean mRequestProcessingDisabled = false;
-
- /** Listener for the primary action in the list, opens the call details. */
- private final View.OnClickListener mPrimaryActionListener = new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- IntentProvider intentProvider = (IntentProvider) view.getTag();
- if (intentProvider != null) {
- mContext.startActivity(intentProvider.getIntent(mContext));
- }
- }
- };
- /** Listener for the secondary action in the list, either call or play. */
- private final View.OnClickListener mSecondaryActionListener = new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- IntentProvider intentProvider = (IntentProvider) view.getTag();
- if (intentProvider != null) {
- mContext.startActivity(intentProvider.getIntent(mContext));
- }
- }
- };
-
- @Override
- public boolean onPreDraw() {
- if (mFirst) {
- mHandler.sendEmptyMessageDelayed(START_THREAD,
- START_PROCESSING_REQUESTS_DELAY_MILLIS);
- mFirst = false;
- }
- return true;
- }
-
- private Handler mHandler = new Handler() {
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case REDRAW:
- notifyDataSetChanged();
- break;
- case START_THREAD:
- startRequestProcessing();
- break;
- }
- }
- };
-
- public CallLogAdapter(Context context, CallFetcher callFetcher,
- String currentCountryIso, String voicemailNumber) {
- super(context);
-
- mContext = context;
- mCurrentCountryIso = currentCountryIso;
- mCallFetcher = callFetcher;
-
- mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE);
- mRequests = new LinkedList<String>();
- mPreDrawListener = null;
-
- Resources resources = mContext.getResources();
- CallTypeHelper callTypeHelper = new CallTypeHelper(resources);
-
- mContactPhotoManager = ContactPhotoManager.getInstance(mContext);
- mPhoneNumberHelper = new PhoneNumberHelper(resources, voicemailNumber);
- PhoneCallDetailsHelper phoneCallDetailsHelper = new PhoneCallDetailsHelper(
- resources, callTypeHelper, mPhoneNumberHelper);
- mCallLogViewsHelper =
- new CallLogListItemHelper(
- phoneCallDetailsHelper, mPhoneNumberHelper, resources);
- mCallLogGroupBuilder = new CallLogGroupBuilder(this);
- }
-
- /**
- * Requery on background thread when {@link Cursor} changes.
- */
- @Override
- protected void onContentChanged() {
- // When the content changes, always fetch all the calls, in case a new missed call came
- // in and we were filtering over voicemail only, so that we see the missed call.
- mCallFetcher.fetchAllCalls();
- }
-
- 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();
- }
- }
-
- public ContactInfo getContactInfo(String number) {
- return mContactInfoCache.getPossiblyExpired(number);
- }
-
- public void startRequestProcessing() {
- if (mRequestProcessingDisabled) {
- return;
- }
-
- mDone = false;
- mCallerIdThread = new Thread(this, "CallLogContactLookup");
- mCallerIdThread.setPriority(Thread.MIN_PRIORITY);
- mCallerIdThread.start();
- }
-
- /**
- * Stops the background thread that processes updates and cancels any pending requests to
- * start it.
- * <p>
- * Should be called from the main thread to prevent a race condition between the request to
- * start the thread being processed and stopping the thread.
- */
- public void stopRequestProcessing() {
- // Remove any pending requests to start the processing thread.
- mHandler.removeMessages(START_THREAD);
- mDone = true;
- if (mCallerIdThread != null) mCallerIdThread.interrupt();
- }
-
- public void invalidateCache() {
- mContactInfoCache.expireAll();
- }
-
- private void enqueueRequest(String number, boolean immediate) {
- synchronized (mRequests) {
- if (!mRequests.contains(number)) {
- mRequests.add(number);
- mRequests.notifyAll();
- }
- }
- if (mFirst && immediate) {
- startRequestProcessing();
- mFirst = false;
- }
- }
-
- /**
- * Determines the contact information for the given SIP address.
- * <p>
- * It returns the contact info if found.
- * <p>
- * If no contact corresponds to the given SIP address, returns {@link ContactInfo#EMPTY}.
- * <p>
- * If the lookup fails for some other reason, it returns null.
- */
- private ContactInfo queryContactInfoForSipAddress(String sipAddress) {
- final ContactInfo info;
-
- // TODO: This code is duplicated from the
- // CallerInfoAsyncQuery class. To avoid that, could the
- // code here just use CallerInfoAsyncQuery, rather than
- // manually running ContentResolver.query() itself?
-
- // We look up SIP addresses directly in the Data table:
- Uri contactRef = Data.CONTENT_URI;
-
- // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent.
- //
- // Also note we use "upper(data1)" in the WHERE clause, and
- // uppercase the incoming SIP address, in order to do a
- // case-insensitive match.
- //
- // TODO: May also need to normalize by adding "sip:" as a
- // prefix, if we start storing SIP addresses that way in the
- // database.
- String selection = "upper(" + Data.DATA1 + ")=?"
- + " AND "
- + Data.MIMETYPE + "='" + SipAddress.CONTENT_ITEM_TYPE + "'";
- String[] selectionArgs = new String[] { sipAddress.toUpperCase() };
-
- Cursor dataTableCursor =
- mContext.getContentResolver().query(
- contactRef,
- null, // projection
- selection, // selection
- selectionArgs, // selectionArgs
- null); // sortOrder
-
- if (dataTableCursor != null) {
- if (dataTableCursor.moveToFirst()) {
- info = new ContactInfo();
-
- // TODO: we could slightly speed this up using an
- // explicit projection (and thus not have to do
- // those getColumnIndex() calls) but the benefit is
- // very minimal.
-
- // Note the Data.CONTACT_ID column here is
- // equivalent to the PERSON_ID_COLUMN_INDEX column
- // we use with "phonesCursor" below.
- info.personId = dataTableCursor.getLong(
- dataTableCursor.getColumnIndex(Data.CONTACT_ID));
- info.name = dataTableCursor.getString(
- dataTableCursor.getColumnIndex(Data.DISPLAY_NAME));
- // "type" and "label" are currently unused for SIP addresses
- info.type = SipAddress.TYPE_OTHER;
- info.label = null;
-
- // And "number" is the SIP address.
- // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent.
- info.number = dataTableCursor.getString(
- dataTableCursor.getColumnIndex(Data.DATA1));
- info.normalizedNumber = null; // meaningless for SIP addresses
- final String thumbnailUriString = dataTableCursor.getString(
- dataTableCursor.getColumnIndex(Data.PHOTO_THUMBNAIL_URI));
- info.thumbnailUri = thumbnailUriString == null
- ? null
- : Uri.parse(thumbnailUriString);
- info.lookupKey = dataTableCursor.getString(
- dataTableCursor.getColumnIndex(Data.LOOKUP_KEY));
- } else {
- info = ContactInfo.EMPTY;
- }
- dataTableCursor.close();
- } else {
- // Failed to fetch the data, ignore this request.
- info = null;
- }
- return info;
- }
-
- /**
- * Determines the contact information for the given phone number.
- * <p>
- * It returns the contact info if found.
- * <p>
- * If no contact corresponds to the given phone number, returns {@link ContactInfo#EMPTY}.
- * <p>
- * If the lookup fails for some other reason, it returns null.
- */
- private ContactInfo queryContactInfoForPhoneNumber(String number) {
- final ContactInfo info;
-
- // "number" is a regular phone number, so use the
- // PhoneLookup table:
- Cursor phonesCursor =
- mContext.getContentResolver().query(
- Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI,
- Uri.encode(number)),
- PhoneQuery._PROJECTION, null, null, null);
- if (phonesCursor != null) {
- if (phonesCursor.moveToFirst()) {
- info = new ContactInfo();
- info.personId = phonesCursor.getLong(PhoneQuery.PERSON_ID);
- info.name = phonesCursor.getString(PhoneQuery.NAME);
- info.type = phonesCursor.getInt(PhoneQuery.PHONE_TYPE);
- info.label = phonesCursor.getString(PhoneQuery.LABEL);
- info.number = phonesCursor
- .getString(PhoneQuery.MATCHED_NUMBER);
- info.normalizedNumber = phonesCursor
- .getString(PhoneQuery.NORMALIZED_NUMBER);
- final String thumbnailUriString = phonesCursor.getString(
- PhoneQuery.THUMBNAIL_URI);
- info.thumbnailUri = thumbnailUriString == null
- ? null
- : Uri.parse(thumbnailUriString);
- info.lookupKey = phonesCursor.getString(PhoneQuery.LOOKUP_KEY);
- } else {
- info = ContactInfo.EMPTY;
- }
- phonesCursor.close();
- } else {
- // Failed to fetch the data, ignore this request.
- info = null;
- }
- return info;
- }
-
- /**
- * Queries the appropriate content provider for the contact associated with the number.
- * <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) {
- final ContactInfo info;
-
- // Determine the contact info.
- if (PhoneNumberUtils.isUriNumber(number)) {
- // This "number" is really a SIP address.
- info = queryContactInfoForSipAddress(number);
- } else {
- info = queryContactInfoForPhoneNumber(number);
- }
-
- 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.
- ContactInfo existingInfo = mContactInfoCache.getPossiblyExpired(number);
- boolean updated = !info.equals(existingInfo);
- if (updated) {
- // The formattedNumber is computed by the UI thread when needed. Since we updated
- // the details of the contact, set this value to null for now.
- info.formattedNumber = null;
- }
- // 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(number, info);
- return updated;
- }
-
- /*
- * Handles requests for contact name and number type
- * @see java.lang.Runnable#run()
- */
- @Override
- public void run() {
- boolean needNotify = false;
- while (!mDone) {
- String number = null;
- synchronized (mRequests) {
- if (!mRequests.isEmpty()) {
- number = mRequests.removeFirst();
- } else {
- if (needNotify) {
- needNotify = false;
- mHandler.sendEmptyMessage(REDRAW);
- }
- try {
- mRequests.wait(1000);
- } catch (InterruptedException ie) {
- // Ignore and continue processing requests
- Thread.currentThread().interrupt();
- }
- }
- }
- if (!mDone && number != null && queryContactInfo(number)) {
- needNotify = true;
- }
- }
- }
-
- @Override
- protected void addGroups(Cursor cursor) {
- mCallLogGroupBuilder.addGroups(cursor);
- }
-
- @VisibleForTesting
- @Override
- public 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;
- }
-
- @VisibleForTesting
- @Override
- public void bindStandAloneView(View view, Context context, Cursor cursor) {
- bindView(view, cursor, 1);
- }
-
- @VisibleForTesting
- @Override
- public 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;
- }
-
- @VisibleForTesting
- @Override
- public void bindChildView(View view, Context context, Cursor cursor) {
- bindView(view, cursor, 1);
- }
-
- @VisibleForTesting
- @Override
- public 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;
- }
-
- @VisibleForTesting
- @Override
- public void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
- boolean expanded) {
- bindView(view, cursor, groupSize);
- }
-
- private void findAndCacheViews(View view) {
- // Get the views to bind to.
- CallLogListItemViews views = CallLogListItemViews.fromView(view);
- views.primaryActionView.setOnClickListener(mPrimaryActionListener);
- views.secondaryActionView.setOnClickListener(mSecondaryActionListener);
- view.setTag(views);
- }
-
- /**
- * Binds the views in the entry to the data in the call log.
- *
- * @param view the view corresponding to this entry
- * @param c the cursor pointing to the entry in the call log
- * @param count the number of entries in the current item, greater than 1 if it is a group
- */
- private void bindView(View view, Cursor c, int count) {
- final CallLogListItemViews views = (CallLogListItemViews) view.getTag();
- final int section = c.getInt(CallLogQuery.SECTION);
-
- // This might be a header: check the value of the section column in the cursor.
- if (section == CallLogQuery.SECTION_NEW_HEADER
- || section == CallLogQuery.SECTION_OLD_HEADER) {
- views.listItemView.setVisibility(View.GONE);
- views.listHeaderView.setVisibility(View.VISIBLE);
- views.listHeaderTextView.setText(
- section == CallLogQuery.SECTION_NEW_HEADER
- ? R.string.call_log_new_header
- : R.string.call_log_old_header);
- // Nothing else to set up for a header.
- return;
- }
- // Default case: an item in the call log.
- views.listItemView.setVisibility(View.VISIBLE);
- views.listHeaderView.setVisibility(View.GONE);
-
- final String number = c.getString(CallLogQuery.NUMBER);
- final long date = c.getLong(CallLogQuery.DATE);
- final long duration = c.getLong(CallLogQuery.DURATION);
- final int callType = c.getInt(CallLogQuery.CALL_TYPE);
- final String formattedNumber;
- final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO);
-
- views.primaryActionView.setTag(
- IntentProvider.getCallDetailIntentProvider(
- this, c.getPosition(), c.getLong(CallLogQuery.ID), count));
- // Store away the voicemail information so we can play it directly.
- if (callType == Calls.VOICEMAIL_TYPE) {
- String voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI);
- final long rowId = c.getLong(CallLogQuery.ID);
- views.secondaryActionView.setTag(
- IntentProvider.getPlayVoicemailIntentProvider(rowId, voicemailUri));
- } else if (!TextUtils.isEmpty(number)) {
- // Store away the number so we can call it directly if you click on the call icon.
- views.secondaryActionView.setTag(
- IntentProvider.getReturnCallIntentProvider(number));
- } else {
- // No action enabled.
- views.secondaryActionView.setTag(null);
- }
-
- // Lookup contacts with this number
- ExpirableCache.CachedValue<ContactInfo> cachedInfo =
- mContactInfoCache.getCachedValue(number);
- ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue();
- if (cachedInfo == null) {
- // Mark it as empty and queue up a request to find the name.
- // The db request should happen on a non-UI thread.
- info = ContactInfo.EMPTY;
- mContactInfoCache.put(number, info);
- // Request the contact details immediately since they are currently missing.
- enqueueRequest(number, true);
- // Format the phone number in the call log as best as we can.
- formattedNumber = formatPhoneNumber(number, null, countryIso);
- } 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, false);
- }
-
- if (info != ContactInfo.EMPTY) {
- // Format and cache phone number for found contact.
- if (info.formattedNumber == null) {
- info.formattedNumber =
- formatPhoneNumber(info.number, info.normalizedNumber, countryIso);
- }
- formattedNumber = info.formattedNumber;
- } else {
- // Format the phone number in the call log as best as we can.
- formattedNumber = formatPhoneNumber(number, null, countryIso);
- }
- }
-
- final long personId = info.personId;
- final String name = info.name;
- final int ntype = info.type;
- final String label = info.label;
- final Uri thumbnailUri = info.thumbnailUri;
- final String lookupKey = info.lookupKey;
- final int[] callTypes = getCallTypes(c, count);
- final String geocode = c.getString(CallLogQuery.GEOCODED_LOCATION);
- final PhoneCallDetails details;
- if (TextUtils.isEmpty(name)) {
- details = new PhoneCallDetails(number, formattedNumber, countryIso, geocode,
- callTypes, date, duration);
- } else {
- details = new PhoneCallDetails(number, formattedNumber, countryIso, geocode,
- callTypes, date, duration, name, ntype, label, personId, thumbnailUri);
- }
-
- final boolean isNew = CallLogQuery.isNewSection(c);
- // New items also use the highlighted version of the text.
- final boolean isHighlighted = isNew;
- mCallLogViewsHelper.setPhoneCallDetails(views, details, isHighlighted);
- setPhoto(views, thumbnailUri, personId, lookupKey);
-
- // Listen for the first draw
- if (mPreDrawListener == null) {
- mFirst = true;
- mPreDrawListener = this;
- view.getViewTreeObserver().addOnPreDrawListener(this);
- }
- }
-
- /**
- * Returns the call types for the given number of items in the cursor.
- * <p>
- * It uses the next {@code count} rows in the cursor to extract the types.
- * <p>
- * It position in the cursor is unchanged by this function.
- */
- private int[] getCallTypes(Cursor cursor, int count) {
- int position = cursor.getPosition();
- int[] callTypes = new int[count];
- for (int index = 0; index < count; ++index) {
- callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE);
- cursor.moveToNext();
- }
- cursor.moveToPosition(position);
- return callTypes;
- }
-
- private void setPhoto(CallLogListItemViews views, Uri thumbnailUri, long contactId,
- String lookupKey) {
- views.quickContactView.assignContactUri(contactId == -1 ? null :
- Contacts.getLookupUri(contactId, lookupKey));
- mContactPhotoManager.loadPhoto(views.quickContactView, thumbnailUri);
- }
-
- /**
- * 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.
- */
- public void disableRequestProcessingForTest() {
- mRequestProcessingDisabled = true;
- }
-
- public void injectContactInfoForTest(String number, ContactInfo contactInfo) {
- mContactInfoCache.put(number, contactInfo);
- }
-
- @Override
- public void addGroup(int cursorPosition, int size, boolean expanded) {
- super.addGroup(cursorPosition, size, expanded);
- }
-
- /**
- * Format the given phone number
- *
- * @param number the number to be formatted.
- * @param normalizedNumber the normalized number of the given number.
- * @param countryIso the ISO 3166-1 two letters country code, the country's
- * convention will be used to format the number if the normalized
- * phone is null.
- *
- * @return the formatted number, or the given number if it was formatted.
- */
- private String formatPhoneNumber(String number, String normalizedNumber,
- String countryIso) {
- if (TextUtils.isEmpty(number)) {
- return "";
- }
- // If "number" is really a SIP address, don't try to do any formatting at all.
- if (PhoneNumberUtils.isUriNumber(number)) {
- return number;
- }
- if (TextUtils.isEmpty(countryIso)) {
- countryIso = mCurrentCountryIso;
- }
- return PhoneNumberUtils.formatNumber(number, normalizedNumber, countryIso);
- }
- }
-
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
@@ -928,13 +129,7 @@
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
String currentCountryIso = ContactsUtils.getCurrentCountryIso(getActivity());
- mAdapter = new CallLogAdapter(getActivity(),
- new CallFetcher() {
- @Override
- public void fetchAllCalls() {
- startCallsQuery();
- }
- }, currentCountryIso, mVoiceMailNumber);
+ mAdapter = new CallLogAdapter(getActivity(), this, currentCountryIso, mVoiceMailNumber);
setListAdapter(mAdapter);
getListView().setItemsCanFocus(true);
}
@@ -1000,7 +195,8 @@
mAdapter.changeCursor(null);
}
- private void startCallsQuery() {
+ @Override
+ public void startCallsQuery() {
mAdapter.setLoading(true);
mCallLogQueryHandler.fetchAllCalls();
if (mShowingVoicemailOnly) {
@@ -1024,8 +220,6 @@
@Override
public void onPrepareOptionsMenu(Menu menu) {
if (mShowOptionsMenu) {
- menu.findItem(R.id.menu_call_settings_call_log)
- .setIntent(DialtactsActivity.getCallSettingsIntent());
menu.findItem(R.id.show_voicemails_only).setVisible(!mShowingVoicemailOnly);
menu.findItem(R.id.show_all_calls).setVisible(mShowingVoicemailOnly);
}
@@ -1052,45 +246,6 @@
return false;
}
}
-
- /*
- * 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
- */
- private String getBetterNumberFromContacts(String number) {
- String matchingNumber = null;
- // Look in the cache first. If it's not found then query the Phones db
- ContactInfo ci = mAdapter.mContactInfoCache.getPossiblyExpired(number);
- if (ci != null && ci != ContactInfo.EMPTY) {
- matchingNumber = ci.number;
- } else {
- try {
- Cursor phonesCursor = getActivity().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;
- }
-
public void callSelectedEntry() {
int position = getListView().getSelectedItemPosition();
if (position < 0) {
@@ -1122,7 +277,7 @@
(callType == Calls.INCOMING_TYPE
|| callType == Calls.MISSED_TYPE)) {
// If the caller-id matches a contact with a better qualified number, use it
- number = getBetterNumberFromContacts(number);
+ number = mAdapter.getBetterNumberFromContacts(number);
}
intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
Uri.fromParts("tel", number, null));
@@ -1162,7 +317,6 @@
mAdapter.invalidateCache();
startCallsQuery();
startVoicemailStatusQuery();
- mAdapter.mPreDrawListener = null; // Let it restart the thread after next draw
updateOnEntry();
}
diff --git a/src/com/android/contacts/calllog/CallLogGroupBuilder.java b/src/com/android/contacts/calllog/CallLogGroupBuilder.java
index f5aef3f..a7f8fa3 100644
--- a/src/com/android/contacts/calllog/CallLogGroupBuilder.java
+++ b/src/com/android/contacts/calllog/CallLogGroupBuilder.java
@@ -17,7 +17,6 @@
package com.android.contacts.calllog;
import com.android.common.widget.GroupingListAdapter;
-import com.android.contacts.calllog.CallLogFragment.CallLogQuery;
import android.database.CharArrayBuffer;
import android.database.Cursor;
@@ -30,15 +29,19 @@
* This class is meant to be used in conjunction with {@link GroupingListAdapter}.
*/
public class CallLogGroupBuilder {
+ public interface GroupCreator {
+ public void addGroup(int cursorPosition, int size, boolean expanded);
+ }
+
/** Reusable char array buffer. */
private CharArrayBuffer mBuffer1 = new CharArrayBuffer(128);
/** Reusable char array buffer. */
private CharArrayBuffer mBuffer2 = new CharArrayBuffer(128);
/** The object on which the groups are created. */
- private final CallLogFragment.GroupCreator mGroupCreator;
+ private final GroupCreator mGroupCreator;
- public CallLogGroupBuilder(CallLogFragment.GroupCreator groupCreator) {
+ public CallLogGroupBuilder(GroupCreator groupCreator) {
mGroupCreator = groupCreator;
}
@@ -74,7 +77,7 @@
final boolean sameNumber = equalPhoneNumbers(firstNumber, currentNumber);
final boolean shouldGroup;
- if (CallLogFragment.CallLogQuery.isSectionHeader(cursor)) {
+ if (CallLogQuery.isSectionHeader(cursor)) {
// Cannot group headers.
shouldGroup = false;
} else if (!sameNumber) {
@@ -120,7 +123,7 @@
* <p>
* The group is always unexpanded.
*
- * @see CallLogFragment.CallLogAdapter#addGroup(int, int, boolean)
+ * @see CallLogAdapter#addGroup(int, int, boolean)
*/
private void addGroup(int cursorPosition, int size) {
mGroupCreator.addGroup(cursorPosition, size, false);
diff --git a/src/com/android/contacts/calllog/CallLogQuery.java b/src/com/android/contacts/calllog/CallLogQuery.java
new file mode 100644
index 0000000..f596032
--- /dev/null
+++ b/src/com/android/contacts/calllog/CallLogQuery.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.calllog;
+
+import android.database.Cursor;
+import android.provider.CallLog.Calls;
+
+/**
+ * The query for the call log table.
+ */
+public final class CallLogQuery {
+ // If you alter this, you must also alter the method that inserts a fake row to the headers
+ // in the CallLogQueryHandler class called createHeaderCursorFor().
+ public static final String[] _PROJECTION = new String[] {
+ Calls._ID,
+ Calls.NUMBER,
+ Calls.DATE,
+ Calls.DURATION,
+ Calls.TYPE,
+ Calls.COUNTRY_ISO,
+ Calls.VOICEMAIL_URI,
+ Calls.GEOCODED_LOCATION,
+ Calls.CACHED_NAME,
+ Calls.CACHED_NUMBER_TYPE,
+ Calls.CACHED_NUMBER_LABEL,
+ };
+
+ public static final int ID = 0;
+ public static final int NUMBER = 1;
+ public static final int DATE = 2;
+ public static final int DURATION = 3;
+ public static final int CALL_TYPE = 4;
+ public static final int COUNTRY_ISO = 5;
+ public static final int VOICEMAIL_URI = 6;
+ public static final int GEOCODED_LOCATION = 7;
+ public static final int CACHED_NAME = 8;
+ public static final int CACHED_NUMBER_TYPE = 9;
+ public static final int CACHED_NUMBER_LABEL = 10;
+
+ /**
+ * The name of the synthetic "section" column.
+ * <p>
+ * This column identifies whether a row is a header or an actual item, and whether it is
+ * part of the new or old calls.
+ */
+ public static final String SECTION_NAME = "section";
+ /** The index of the "section" column in the projection. */
+ public static final int SECTION = 11;
+ /** The value of the "section" column for the header of the new section. */
+ public static final int SECTION_NEW_HEADER = 0;
+ /** The value of the "section" column for the items of the new section. */
+ public static final int SECTION_NEW_ITEM = 1;
+ /** The value of the "section" column for the header of the old section. */
+ public static final int SECTION_OLD_HEADER = 2;
+ /** The value of the "section" column for the items of the old section. */
+ public static final int SECTION_OLD_ITEM = 3;
+
+ /** The call log projection including the section name. */
+ public static final String[] EXTENDED_PROJECTION;
+ static {
+ EXTENDED_PROJECTION = new String[_PROJECTION.length + 1];
+ System.arraycopy(_PROJECTION, 0, EXTENDED_PROJECTION, 0, _PROJECTION.length);
+ EXTENDED_PROJECTION[_PROJECTION.length] = SECTION_NAME;
+ }
+
+ public static boolean isSectionHeader(Cursor cursor) {
+ int section = cursor.getInt(CallLogQuery.SECTION);
+ return section == CallLogQuery.SECTION_NEW_HEADER
+ || section == CallLogQuery.SECTION_OLD_HEADER;
+ }
+
+ public static boolean isNewSection(Cursor cursor) {
+ int section = cursor.getInt(CallLogQuery.SECTION);
+ return section == CallLogQuery.SECTION_NEW_ITEM
+ || section == CallLogQuery.SECTION_NEW_HEADER;
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/calllog/CallLogQueryHandler.java b/src/com/android/contacts/calllog/CallLogQueryHandler.java
index 979db4b..25beba5 100644
--- a/src/com/android/contacts/calllog/CallLogQueryHandler.java
+++ b/src/com/android/contacts/calllog/CallLogQueryHandler.java
@@ -17,7 +17,6 @@
package com.android.contacts.calllog;
import com.android.common.io.MoreCloseables;
-import com.android.contacts.calllog.CallLogFragment.CallLogQuery;
import com.android.contacts.voicemail.VoicemailStatusHelperImpl;
import android.content.AsyncQueryHandler;
@@ -104,10 +103,10 @@
/** Creates a cursor that contains a single row and maps the section to the given value. */
private Cursor createHeaderCursorFor(int section) {
MatrixCursor matrixCursor =
- new MatrixCursor(CallLogFragment.CallLogQuery.EXTENDED_PROJECTION);
+ new MatrixCursor(CallLogQuery.EXTENDED_PROJECTION);
// The values in this row correspond to default values for _PROJECTION from CallLogQuery
// plus the section value.
- matrixCursor.addRow(new Object[]{ -1L, "", 0L, 0L, 0, "", "", "", section });
+ matrixCursor.addRow(new Object[]{ -1L, "", 0L, 0L, 0, "", "", "", null, 0, null, section });
return matrixCursor;
}
diff --git a/src/com/android/contacts/calllog/ContactInfo.java b/src/com/android/contacts/calllog/ContactInfo.java
new file mode 100644
index 0000000..1f106e4
--- /dev/null
+++ b/src/com/android/contacts/calllog/ContactInfo.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.calllog;
+
+import android.net.Uri;
+import android.text.TextUtils;
+
+/**
+ * Information for a contact as needed by the Call Log.
+ */
+public final class ContactInfo {
+ public long personId = -1;
+ public String name;
+ public int type;
+ public String label;
+ public String number;
+ public String formattedNumber;
+ public String normalizedNumber;
+ public Uri thumbnailUri;
+ public String lookupKey;
+
+ public static ContactInfo EMPTY = new ContactInfo();
+
+ @Override
+ public int hashCode() {
+ // Uses only name and personId to determine hashcode.
+ // This should be sufficient to have a reasonable distribution of hash codes.
+ // Moreover, there should be no two people with the same personId.
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (int) (personId ^ (personId >>> 32));
+ result = prime * result + ((name == null) ? 0 : name.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (obj == null) return false;
+ if (getClass() != obj.getClass()) return false;
+ ContactInfo other = (ContactInfo) obj;
+ if (personId != other.personId) return false;
+ if (!TextUtils.equals(name, other.name)) return false;
+ if (type != other.type) return false;
+ if (!TextUtils.equals(label, other.label)) return false;
+ if (!TextUtils.equals(number, other.number)) return false;
+ // Ignore formatted number.
+ if (!TextUtils.equals(normalizedNumber, other.normalizedNumber)) return false;
+ if (!uriEquals(thumbnailUri, other.thumbnailUri)) return false;
+ if (!TextUtils.equals(lookupKey, other.lookupKey)) return false;
+ return true;
+ }
+
+ private static boolean uriEquals(Uri thumbnailUri1, Uri thumbnailUri2) {
+ if (thumbnailUri1 == thumbnailUri2) return true;
+ if (thumbnailUri1 == null) return false;
+ return thumbnailUri1.equals(thumbnailUri2);
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/calllog/IntentProvider.java b/src/com/android/contacts/calllog/IntentProvider.java
index 9ce3d3d..bfee5ec 100644
--- a/src/com/android/contacts/calllog/IntentProvider.java
+++ b/src/com/android/contacts/calllog/IntentProvider.java
@@ -17,8 +17,6 @@
package com.android.contacts.calllog;
import com.android.contacts.CallDetailActivity;
-import com.android.contacts.calllog.CallLogFragment.CallLogAdapter;
-import com.android.contacts.calllog.CallLogFragment.CallLogQuery;
import android.content.ContentUris;
import android.content.Context;
diff --git a/src/com/android/contacts/calllog/PhoneQuery.java b/src/com/android/contacts/calllog/PhoneQuery.java
new file mode 100644
index 0000000..52faa8b
--- /dev/null
+++ b/src/com/android/contacts/calllog/PhoneQuery.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.calllog;
+
+import android.provider.ContactsContract.PhoneLookup;
+
+/**
+ * The query to look up the {@link ContactInfo} for a given number in the Call Log.
+ */
+final class PhoneQuery {
+ public static final String[] _PROJECTION = new String[] {
+ PhoneLookup._ID,
+ PhoneLookup.DISPLAY_NAME,
+ PhoneLookup.TYPE,
+ PhoneLookup.LABEL,
+ PhoneLookup.NUMBER,
+ PhoneLookup.NORMALIZED_NUMBER,
+ PhoneLookup.PHOTO_THUMBNAIL_URI,
+ PhoneLookup.LOOKUP_KEY};
+
+ public static final int PERSON_ID = 0;
+ public static final int NAME = 1;
+ public static final int PHONE_TYPE = 2;
+ public static final int LABEL = 3;
+ public static final int MATCHED_NUMBER = 4;
+ public static final int NORMALIZED_NUMBER = 5;
+ public static final int THUMBNAIL_URI = 6;
+ public static final int LOOKUP_KEY = 7;
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/dialpad/DialpadFragment.java b/src/com/android/contacts/dialpad/DialpadFragment.java
index a5db5ce..377a595 100644
--- a/src/com/android/contacts/dialpad/DialpadFragment.java
+++ b/src/com/android/contacts/dialpad/DialpadFragment.java
@@ -571,14 +571,10 @@
}
private void setupMenuItems(Menu menu) {
- final MenuItem callSettingsMenuItem = menu.findItem(R.id.menu_call_settings_dialpad);
final MenuItem addToContactMenuItem = menu.findItem(R.id.menu_add_contacts);
final MenuItem twoSecPauseMenuItem = menu.findItem(R.id.menu_2s_pause);
final MenuItem waitMenuItem = menu.findItem(R.id.menu_add_wait);
- callSettingsMenuItem.setVisible(true);
- callSettingsMenuItem.setIntent(DialtactsActivity.getCallSettingsIntent());
-
// We show "add to contacts", "2sec pause", and "add wait" menus only when the user is
// seeing usual dialpads and has typed at least one digit.
// We never show a menu if the "choose dialpad" UI is up.
diff --git a/src/com/android/contacts/interactions/PhoneNumberInteraction.java b/src/com/android/contacts/interactions/PhoneNumberInteraction.java
index 0448bd5..918dac0 100644
--- a/src/com/android/contacts/interactions/PhoneNumberInteraction.java
+++ b/src/com/android/contacts/interactions/PhoneNumberInteraction.java
@@ -24,11 +24,11 @@
import com.android.contacts.model.AccountType.StringInflater;
import com.android.contacts.model.AccountTypeManager;
import com.android.contacts.model.DataKind;
+import com.android.i18n.phonenumbers.NumberParseException;
+import com.android.i18n.phonenumbers.PhoneNumberUtil;
+import com.android.i18n.phonenumbers.PhoneNumberUtil.MatchType;
+import com.android.i18n.phonenumbers.Phonenumber.PhoneNumber;
import com.google.common.annotations.VisibleForTesting;
-import com.google.i18n.phonenumbers.NumberParseException;
-import com.google.i18n.phonenumbers.PhoneNumberUtil;
-import com.google.i18n.phonenumbers.PhoneNumberUtil.MatchType;
-import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
import android.app.Activity;
import android.app.AlertDialog;
diff --git a/src/com/android/contacts/model/AccountTypeManager.java b/src/com/android/contacts/model/AccountTypeManager.java
index b517c2c..04b3fa8 100644
--- a/src/com/android/contacts/model/AccountTypeManager.java
+++ b/src/com/android/contacts/model/AccountTypeManager.java
@@ -16,12 +16,12 @@
package com.android.contacts.model;
+import com.android.i18n.phonenumbers.PhoneNumberUtil;
import com.android.internal.util.Objects;
import com.google.android.collect.Lists;
import com.google.android.collect.Maps;
import com.google.android.collect.Sets;
import com.google.common.annotations.VisibleForTesting;
-import com.google.i18n.phonenumbers.PhoneNumberUtil;
import android.accounts.Account;
import android.accounts.AccountManager;
diff --git a/tests/src/com/android/contacts/ContactLoaderTest.java b/tests/src/com/android/contacts/ContactLoaderTest.java
index a731f24..3560ce1 100644
--- a/tests/src/com/android/contacts/ContactLoaderTest.java
+++ b/tests/src/com/android/contacts/ContactLoaderTest.java
@@ -16,7 +16,12 @@
package com.android.contacts;
+import com.android.contacts.model.AccountType;
+import com.android.contacts.model.AccountWithDataSet;
+import com.android.contacts.model.BaseAccountType;
+import com.android.contacts.test.InjectedServices;
import com.android.contacts.tests.mocks.ContactsMockContext;
+import com.android.contacts.tests.mocks.MockAccountTypeManager;
import com.android.contacts.tests.mocks.MockContentProvider;
import android.content.ContentUris;
@@ -36,18 +41,31 @@
*/
@LargeTest
public class ContactLoaderTest extends LoaderTestCase {
- ContactsMockContext mMockContext;
- MockContentProvider mContactsProvider;
+ private ContactsMockContext mMockContext;
+ private MockContentProvider mContactsProvider;
@Override
protected void setUp() throws Exception {
super.setUp();
mMockContext = new ContactsMockContext(getContext());
mContactsProvider = mMockContext.getContactsProvider();
+
+ InjectedServices services = new InjectedServices();
+ AccountType accountType = new BaseAccountType();
+ accountType.accountType = "mockAccountType";
+
+ AccountWithDataSet account =
+ new AccountWithDataSet("mockAccountName", "mockAccountType", null);
+
+ mMockContext.setMockAccountTypeManager(
+ new MockAccountTypeManager(
+ new AccountType[] { accountType }, new AccountWithDataSet[] { account }));
}
@Override
protected void tearDown() throws Exception {
+ mMockContext = null;
+ mContactsProvider = null;
super.tearDown();
}
diff --git a/tests/src/com/android/contacts/activities/CallLogActivityTests.java b/tests/src/com/android/contacts/activities/CallLogActivityTests.java
index 070941c..5926fd9 100644
--- a/tests/src/com/android/contacts/activities/CallLogActivityTests.java
+++ b/tests/src/com/android/contacts/activities/CallLogActivityTests.java
@@ -18,10 +18,12 @@
import com.android.contacts.CallDetailActivity;
import com.android.contacts.R;
+import com.android.contacts.calllog.CallLogAdapter;
import com.android.contacts.calllog.CallLogFragment;
-import com.android.contacts.calllog.CallLogFragment.CallLogQuery;
-import com.android.contacts.calllog.CallLogFragment.ContactInfo;
import com.android.contacts.calllog.CallLogListItemViews;
+import com.android.contacts.calllog.CallLogQuery;
+import com.android.contacts.calllog.CallLogQueryTestUtils;
+import com.android.contacts.calllog.ContactInfo;
import com.android.contacts.calllog.IntentProvider;
import com.android.internal.telephony.CallerInfo;
@@ -61,17 +63,6 @@
@LargeTest
public class CallLogActivityTests
extends ActivityInstrumentationTestCase2<CallLogActivity> {
- private static final String[] EXTENDED_CALL_LOG_PROJECTION = new String[] {
- Calls._ID,
- Calls.NUMBER,
- Calls.DATE,
- Calls.DURATION,
- Calls.TYPE,
- Calls.COUNTRY_ISO,
- Calls.VOICEMAIL_URI,
- Calls.GEOCODED_LOCATION,
- CallLogFragment.CallLogQuery.SECTION_NAME,
- };
private static final int RAND_DURATION = -1;
private static final long NOW = -1L;
@@ -95,7 +86,7 @@
private CallLogActivity mActivity;
private CallLogFragment mFragment;
private FrameLayout mParentView;
- private CallLogFragment.CallLogAdapter mAdapter;
+ private CallLogAdapter mAdapter;
private String mVoicemail;
// In memory array to hold the rows corresponding to the 'calls' table.
@@ -132,7 +123,7 @@
mAdapter.disableRequestProcessingForTest();
mAdapter.stopRequestProcessing();
mParentView = new FrameLayout(mActivity);
- mCursor = new MatrixCursor(EXTENDED_CALL_LOG_PROJECTION);
+ mCursor = new MatrixCursor(CallLogQuery.EXTENDED_PROJECTION);
buildIconMap();
}
@@ -487,27 +478,19 @@
* @param type Either Call.OUTGOING_TYPE or Call.INCOMING_TYPE or Call.MISSED_TYPE.
*/
private void insert(String number, long date, int duration, int type) {
- MatrixCursor.RowBuilder row = mCursor.newRow();
- row.add(mIndex);
- mIndex ++;
- row.add(number);
- if (NOW == date) {
- row.add(new Date().getTime());
- } else {
- row.add(date);
- }
- if (duration < 0) {
- duration = mRnd.nextInt(10 * 60); // 0 - 10 minutes random.
- }
- row.add(duration); // duration
+ Object[] values = CallLogQueryTestUtils.createTestExtendedValues();
+ values[CallLogQuery.ID] = mIndex;
+ values[CallLogQuery.NUMBER] = number;
+ values[CallLogQuery.DATE] = date == NOW ? new Date().getTime() : date;
+ values[CallLogQuery.DURATION] = duration < 0 ? mRnd.nextInt(10 * 60) : duration;
if (mVoicemail != null && mVoicemail.equals(number)) {
assertEquals(Calls.OUTGOING_TYPE, type);
}
- row.add(type); // type
- row.add(TEST_COUNTRY_ISO); // country ISO
- row.add(null); // voicemail_uri
- row.add(null); // geocoded_location
- row.add(CallLogFragment.CallLogQuery.SECTION_OLD_ITEM); // section
+ values[CallLogQuery.CALL_TYPE] = type;
+ values[CallLogQuery.COUNTRY_ISO] = TEST_COUNTRY_ISO;
+ values[CallLogQuery.SECTION] = CallLogQuery.SECTION_OLD_ITEM;
+ mCursor.addRow(values);
+ ++mIndex;
}
/**
@@ -518,27 +501,19 @@
* @param duration In seconds of the call. Use RAND_DURATION to pick a random one.
*/
private void insertVoicemail(String number, long date, int duration) {
- MatrixCursor.RowBuilder row = mCursor.newRow();
+ Object[] values = CallLogQueryTestUtils.createTestExtendedValues();
+ values[CallLogQuery.ID] = mIndex;
+ values[CallLogQuery.NUMBER] = number;
+ values[CallLogQuery.DATE] = date == NOW ? new Date().getTime() : date;
+ values[CallLogQuery.DURATION] = duration < 0 ? mRnd.nextInt(10 * 60) : duration;
+ values[CallLogQuery.CALL_TYPE] = Calls.VOICEMAIL_TYPE;
+ values[CallLogQuery.COUNTRY_ISO] = TEST_COUNTRY_ISO;
// Must have the same index as the row.
- Uri voicemailUri =
+ values[CallLogQuery.VOICEMAIL_URI] =
ContentUris.withAppendedId(VoicemailContract.Voicemails.CONTENT_URI, mIndex);
- row.add(mIndex);
- mIndex ++;
- row.add(number);
- if (NOW == date) {
- row.add(new Date().getTime());
- } else {
- row.add(date);
- }
- if (duration < 0) {
- duration = mRnd.nextInt(10 * 60); // 0 - 10 minutes random.
- }
- row.add(duration); // duration
- row.add(Calls.VOICEMAIL_TYPE); // type
- row.add(TEST_COUNTRY_ISO); // country ISO
- row.add(voicemailUri); // voicemail_uri
- row.add(null); // geocoded_location
- row.add(CallLogFragment.CallLogQuery.SECTION_OLD_ITEM); // section
+ values[CallLogQuery.SECTION] = CallLogQuery.SECTION_OLD_ITEM;
+ mCursor.addRow(values);
+ ++mIndex;
}
/**
diff --git a/tests/src/com/android/contacts/calllog/CallLogGroupBuilderTest.java b/tests/src/com/android/contacts/calllog/CallLogGroupBuilderTest.java
index 8a7e946..31ad548 100644
--- a/tests/src/com/android/contacts/calllog/CallLogGroupBuilderTest.java
+++ b/tests/src/com/android/contacts/calllog/CallLogGroupBuilderTest.java
@@ -18,8 +18,6 @@
import static com.google.android.collect.Lists.newArrayList;
-import com.android.contacts.calllog.CallLogFragment.CallLogQuery;
-
import android.database.MatrixCursor;
import android.provider.CallLog.Calls;
import android.test.AndroidTestCase;
@@ -171,7 +169,7 @@
/** Creates (or recreates) the cursor used to store the call log content for the tests. */
private void createCursor() {
- mCursor = new MatrixCursor(CallLogFragment.CallLogQuery.EXTENDED_PROJECTION);
+ mCursor = new MatrixCursor(CallLogQuery.EXTENDED_PROJECTION);
}
/** Clears the content of the {@link FakeGroupCreator} used in the tests. */
@@ -223,9 +221,12 @@
throw new IllegalArgumentException("not an item section: " + section);
}
mCursor.moveToNext();
- mCursor.addRow(new Object[]{
- mCursor.getPosition(), number, 0L, 0L, type, "", "", "", section
- });
+ Object[] values = CallLogQueryTestUtils.createTestExtendedValues();
+ values[CallLogQuery.ID] = mCursor.getPosition();
+ values[CallLogQuery.NUMBER] = number;
+ values[CallLogQuery.CALL_TYPE] = type;
+ values[CallLogQuery.SECTION] = section;
+ mCursor.addRow(values);
}
/** Adds the old section header to the call log. */
@@ -245,7 +246,10 @@
throw new IllegalArgumentException("not a header section: " + section);
}
mCursor.moveToNext();
- mCursor.addRow(new Object[]{ mCursor.getPosition(), "", 0L, 0L, 0, "", "", "", section });
+ Object[] values = CallLogQueryTestUtils.createTestExtendedValues();
+ values[CallLogQuery.ID] = mCursor.getPosition();
+ values[CallLogQuery.SECTION] = section;
+ mCursor.addRow(values);
}
/** Asserts that the group matches the given values. */
@@ -272,7 +276,7 @@
}
/** Fake implementation of a GroupCreator which stores the created groups in a member field. */
- private static class FakeGroupCreator implements CallLogFragment.GroupCreator {
+ private static class FakeGroupCreator implements CallLogGroupBuilder.GroupCreator {
/** The list of created groups. */
public final List<GroupSpec> groups = newArrayList();
diff --git a/tests/src/com/android/contacts/calllog/CallLogQueryTestUtils.java b/tests/src/com/android/contacts/calllog/CallLogQueryTestUtils.java
new file mode 100644
index 0000000..0e1952a
--- /dev/null
+++ b/tests/src/com/android/contacts/calllog/CallLogQueryTestUtils.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.calllog;
+
+import static junit.framework.Assert.assertEquals;
+import junit.framework.Assert;
+
+/**
+ * Helper class to create test values for {@link CallLogQuery}.
+ */
+public class CallLogQueryTestUtils {
+ public static Object[] createTestValues() {
+ Object[] values = new Object[]{ -1L, "", 0L, 0L, 0, "", "", "", null, 0, null };
+ assertEquals(CallLogQuery._PROJECTION.length, values.length);
+ return values;
+ }
+
+ public static Object[] createTestExtendedValues() {
+ Object[] values = new Object[]{ -1L, "", 0L, 0L, 0, "", "", "", null, 0, null, 0 };
+ Assert.assertEquals(CallLogQuery.EXTENDED_PROJECTION.length, values.length);
+ return values;
+ }
+}
diff --git a/tests/src/com/android/contacts/detail/StreamItemAdapterTest.java b/tests/src/com/android/contacts/detail/StreamItemAdapterTest.java
index 4687f77..d862d6e 100644
--- a/tests/src/com/android/contacts/detail/StreamItemAdapterTest.java
+++ b/tests/src/com/android/contacts/detail/StreamItemAdapterTest.java
@@ -53,13 +53,13 @@
public void testGetCount_Empty() {
mAdapter.setStreamItems(createStreamItemList(0));
// There is actually one view: the header.
- assertEquals(1, mAdapter.getCount());
+ assertEquals(2, mAdapter.getCount());
}
public void testGetCount_NonEmpty() {
mAdapter.setStreamItems(createStreamItemList(3));
// There is one extra view: the header.
- assertEquals(4, mAdapter.getCount());
+ assertEquals(5, mAdapter.getCount());
}
public void testGetView_Header() {
diff --git a/tests/src/com/android/contacts/tests/mocks/ContactsMockContext.java b/tests/src/com/android/contacts/tests/mocks/ContactsMockContext.java
index 93ea4f4..2f959f4 100644
--- a/tests/src/com/android/contacts/tests/mocks/ContactsMockContext.java
+++ b/tests/src/com/android/contacts/tests/mocks/ContactsMockContext.java
@@ -16,6 +16,8 @@
package com.android.contacts.tests.mocks;
+import com.android.contacts.model.AccountTypeManager;
+
import android.content.ContentResolver;
import android.content.Context;
import android.content.ContextWrapper;
@@ -32,13 +34,11 @@
* to mock content providers.
*/
public class ContactsMockContext extends ContextWrapper {
-
- private static final String TAG = "ContactsMockContext";
-
private ContactsMockPackageManager mPackageManager;
private MockContentResolver mContentResolver;
private MockContentProvider mContactsProvider;
private MockContentProvider mSettingsProvider;
+ private MockAccountTypeManager mMockAccountTypeManager;
private Intent mIntentForStartActivity;
public ContactsMockContext(Context base) {
@@ -53,6 +53,10 @@
mContentResolver.addProvider(Settings.AUTHORITY, mSettingsProvider);
}
+ public void setMockAccountTypeManager(MockAccountTypeManager mockAccountTypeManager) {
+ mMockAccountTypeManager = mockAccountTypeManager;
+ }
+
@Override
public ContentResolver getContentResolver() {
return mContentResolver;
@@ -93,4 +97,12 @@
mContactsProvider.verify();
mSettingsProvider.verify();
}
+
+ @Override
+ public Object getSystemService(String name) {
+ if (AccountTypeManager.ACCOUNT_TYPE_SERVICE.equals(name)) {
+ return mMockAccountTypeManager;
+ }
+ return super.getSystemService(name);
+ }
}