Merge "Check for cursor closed." into ics-mr0
diff --git a/res/drawable-hdpi/ic_play_active_holo_dark.png b/res/drawable-hdpi/ic_play_active_holo_dark.png
new file mode 100644
index 0000000..179b5a1
--- /dev/null
+++ b/res/drawable-hdpi/ic_play_active_holo_dark.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_sound_off_speakerphone_disabled_holo_dark.png b/res/drawable-hdpi/ic_sound_off_speakerphone_disabled_holo_dark.png
index e2cb0e4..f1a9154 100644
--- a/res/drawable-hdpi/ic_sound_off_speakerphone_disabled_holo_dark.png
+++ b/res/drawable-hdpi/ic_sound_off_speakerphone_disabled_holo_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_play_active_holo_dark.png b/res/drawable-mdpi/ic_play_active_holo_dark.png
new file mode 100644
index 0000000..042d8c1
--- /dev/null
+++ b/res/drawable-mdpi/ic_play_active_holo_dark.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_sound_off_speakerphone_disabled_holo_dark.png b/res/drawable-mdpi/ic_sound_off_speakerphone_disabled_holo_dark.png
index 2e4cee7..0a83d81 100644
--- a/res/drawable-mdpi/ic_sound_off_speakerphone_disabled_holo_dark.png
+++ b/res/drawable-mdpi/ic_sound_off_speakerphone_disabled_holo_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_play_active_holo_dark.png b/res/drawable-xhdpi/ic_play_active_holo_dark.png
new file mode 100644
index 0000000..20d0583
--- /dev/null
+++ b/res/drawable-xhdpi/ic_play_active_holo_dark.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_sound_off_speakerphone_disabled_holo_dark.png b/res/drawable-xhdpi/ic_sound_off_speakerphone_disabled_holo_dark.png
index 8381be1..764ff65 100644
--- a/res/drawable-xhdpi/ic_sound_off_speakerphone_disabled_holo_dark.png
+++ b/res/drawable-xhdpi/ic_sound_off_speakerphone_disabled_holo_dark.png
Binary files differ
diff --git a/src/com/android/contacts/ContactLoader.java b/src/com/android/contacts/ContactLoader.java
index 007c1e0..c9fbeae 100644
--- a/src/com/android/contacts/ContactLoader.java
+++ b/src/com/android/contacts/ContactLoader.java
@@ -850,17 +850,15 @@
 
         /**
          * Sets the "invitable" account types to {@link Result#mInvitableAccountTypes}.
-         *
-         * TODO Exclude the ones with no raw contacts in the database.
          */
         private void loadInvitableAccountTypes(Result contactData) {
-            Map<AccountTypeWithDataSet, AccountType> allInvitables =
-                    AccountTypeManager.getInstance(getContext()).getInvitableAccountTypes();
-            if (allInvitables.isEmpty()) {
+            Map<AccountTypeWithDataSet, AccountType> invitables =
+                    AccountTypeManager.getInstance(getContext()).getUsableInvitableAccountTypes();
+            if (invitables.isEmpty()) {
                 return;
             }
 
-            HashMap<AccountTypeWithDataSet, AccountType> result = Maps.newHashMap(allInvitables);
+            HashMap<AccountTypeWithDataSet, AccountType> result = Maps.newHashMap(invitables);
 
             // Remove the ones that already have a raw contact in the current contact
             for (Entity entity : contactData.getEntities()) {
diff --git a/src/com/android/contacts/ContactsUtils.java b/src/com/android/contacts/ContactsUtils.java
index 9a3f2ef..b0c0508 100644
--- a/src/com/android/contacts/ContactsUtils.java
+++ b/src/com/android/contacts/ContactsUtils.java
@@ -25,6 +25,8 @@
 import android.content.Context;
 import android.content.Intent;
 import android.location.CountryDetector;
+import android.net.Uri;
+import android.provider.ContactsContract;
 import android.provider.ContactsContract.CommonDataKinds.Im;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.telephony.PhoneNumberUtils;
@@ -184,4 +186,26 @@
                 AccountTypeManager.getInstance(context).getGroupWritableAccounts();
         return !accounts.isEmpty();
     }
+
+    /**
+     * Returns the intent to launch for the given invitable account type and contact lookup URI.
+     * This will return null if the account type is not invitable (i.e. there is no
+     * {@link AccountType#getInviteContactActivityClassName()} or
+     * {@link AccountType#resPackageName}).
+     */
+    public static Intent getInvitableIntent(AccountType accountType, Uri lookupUri) {
+        String resPackageName = accountType.resPackageName;
+        String className = accountType.getInviteContactActivityClassName();
+        if (TextUtils.isEmpty(resPackageName) || TextUtils.isEmpty(className)) {
+            return null;
+        }
+        Intent intent = new Intent();
+        intent.setClassName(resPackageName, className);
+
+        intent.setAction(ContactsContract.Intents.INVITE_CONTACT);
+
+        // Data is the lookup URI.
+        intent.setData(lookupUri);
+        return intent;
+    }
 }
diff --git a/src/com/android/contacts/activities/ContactDetailActivity.java b/src/com/android/contacts/activities/ContactDetailActivity.java
index 49d6672..1a8e383 100644
--- a/src/com/android/contacts/activities/ContactDetailActivity.java
+++ b/src/com/android/contacts/activities/ContactDetailActivity.java
@@ -244,6 +244,9 @@
             new ContactDetailFragment.Listener() {
         @Override
         public void onItemClicked(Intent intent) {
+            if (intent == null) {
+                return;
+            }
             try {
                 startActivity(intent);
             } catch (ActivityNotFoundException e) {
diff --git a/src/com/android/contacts/activities/DialtactsActivity.java b/src/com/android/contacts/activities/DialtactsActivity.java
index 05951d6..0ce7309 100644
--- a/src/com/android/contacts/activities/DialtactsActivity.java
+++ b/src/com/android/contacts/activities/DialtactsActivity.java
@@ -302,8 +302,11 @@
             new OnPhoneNumberPickerActionListener() {
                 @Override
                 public void onPickPhoneNumberAction(Uri dataUri) {
+                    // Specify call-origin so that users will see the previous tab instead of
+                    // CallLog screen (search UI will be automatically exited).
                     PhoneNumberInteraction.startInteractionForPhoneCall(
-                            DialtactsActivity.this, dataUri);
+                            DialtactsActivity.this, dataUri,
+                            CALL_ORIGIN_DIALTACTS);
                 }
 
                 @Override
@@ -553,7 +556,7 @@
     private void setupFavorites() {
         final Tab tab = getActionBar().newTab();
         tab.setContentDescription(R.string.contactsFavoritesLabel);
-        tab.setIcon(R.drawable.ic_tab_starred);
+        tab.setIcon(R.drawable.ic_tab_all);
         tab.setTabListener(mTabListener);
         getActionBar().addTab(tab);
     }
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index 0c3f448..24992cc 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -1098,6 +1098,9 @@
     public class ContactDetailFragmentListener implements ContactDetailFragment.Listener {
         @Override
         public void onItemClicked(Intent intent) {
+            if (intent == null) {
+                return;
+            }
             try {
                 startActivity(intent);
             } catch (ActivityNotFoundException e) {
diff --git a/src/com/android/contacts/calllog/CallLogAdapter.java b/src/com/android/contacts/calllog/CallLogAdapter.java
index 4f274a9..7e6770b 100644
--- a/src/com/android/contacts/calllog/CallLogAdapter.java
+++ b/src/com/android/contacts/calllog/CallLogAdapter.java
@@ -534,7 +534,7 @@
                     callTypes, date, duration, name, ntype, label, lookupUri, null);
         }
 
-        final boolean isNew = CallLogQuery.isNewSection(c);
+        final boolean isNew = c.getInt(CallLogQuery.IS_READ) == 0;
         // New items also use the highlighted version of the text.
         final boolean isHighlighted = isNew;
         mCallLogViewsHelper.setPhoneCallDetails(views, details, isHighlighted);
diff --git a/src/com/android/contacts/calllog/CallLogListItemHelper.java b/src/com/android/contacts/calllog/CallLogListItemHelper.java
index 6378c5e..bfedba5 100644
--- a/src/com/android/contacts/calllog/CallLogListItemHelper.java
+++ b/src/com/android/contacts/calllog/CallLogListItemHelper.java
@@ -65,7 +65,7 @@
 
         if (canPlay) {
             // Playback action takes preference.
-            configurePlaySecondaryAction(views);
+            configurePlaySecondaryAction(views, isHighlighted);
             views.dividerView.setVisibility(View.VISIBLE);
         } else if (canCall) {
             // Call is the secondary action.
@@ -99,9 +99,10 @@
     }
 
     /** Sets the secondary action to correspond to the play button. */
-    private void configurePlaySecondaryAction(CallLogListItemViews views) {
+    private void configurePlaySecondaryAction(CallLogListItemViews views, boolean isHighlighted) {
         views.secondaryActionView.setVisibility(View.VISIBLE);
-        views.secondaryActionView.setImageResource(R.drawable.ic_play);
+        views.secondaryActionView.setImageResource(
+                isHighlighted ? R.drawable.ic_play_active_holo_dark : R.drawable.ic_play_holo_dark);
         views.secondaryActionView.setContentDescription(
                 mResources.getString(R.string.description_call_log_play_button));
     }
diff --git a/src/com/android/contacts/calllog/CallLogQuery.java b/src/com/android/contacts/calllog/CallLogQuery.java
index e622b3d..90017b7 100644
--- a/src/com/android/contacts/calllog/CallLogQuery.java
+++ b/src/com/android/contacts/calllog/CallLogQuery.java
@@ -42,6 +42,7 @@
             Calls.CACHED_NORMALIZED_NUMBER,  // 13
             Calls.CACHED_PHOTO_ID,           // 14
             Calls.CACHED_FORMATTED_NUMBER,   // 15
+            Calls.IS_READ,                   // 16
     };
 
     public static final int ID = 0;
@@ -60,8 +61,9 @@
     public static final int CACHED_NORMALIZED_NUMBER = 13;
     public static final int CACHED_PHOTO_ID = 14;
     public static final int CACHED_FORMATTED_NUMBER = 15;
+    public static final int IS_READ = 16;
     /** The index of the synthetic "section" column in the extended projection. */
-    public static final int SECTION = 16;
+    public static final int SECTION = 17;
 
     /**
      * The name of the synthetic "section" column.
diff --git a/src/com/android/contacts/calllog/CallLogQueryHandler.java b/src/com/android/contacts/calllog/CallLogQueryHandler.java
index 2a11cc3..affdd1d 100644
--- a/src/com/android/contacts/calllog/CallLogQueryHandler.java
+++ b/src/com/android/contacts/calllog/CallLogQueryHandler.java
@@ -18,6 +18,7 @@
 
 import com.android.common.io.MoreCloseables;
 import com.android.contacts.voicemail.VoicemailStatusHelperImpl;
+import com.google.android.collect.Lists;
 
 import android.content.AsyncQueryHandler;
 import android.content.ContentResolver;
@@ -37,11 +38,15 @@
 import android.util.Log;
 
 import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
 
 import javax.annotation.concurrent.GuardedBy;
 
 /** Handles asynchronous queries to the call log. */
 /*package*/ class CallLogQueryHandler extends AsyncQueryHandler {
+    private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
     private static final String TAG = "CallLogQueryHandler";
 
     /** The token for the query to fetch the new entries from the call log. */
@@ -58,6 +63,12 @@
     /** The token for the query to fetch voicemail status messages. */
     private static final int QUERY_VOICEMAIL_STATUS_TOKEN = 58;
 
+    /**
+     * The time window from the current time within which an unread entry will be added to the new
+     * section.
+     */
+    private static final long NEW_SECTION_TIME_WINDOW = TimeUnit.DAYS.toMillis(7);
+
     private final WeakReference<Listener> mListener;
 
     /** The cursor containing the new calls, or null if they have not yet been fetched. */
@@ -107,7 +118,8 @@
         // The values in this row correspond to default values for _PROJECTION from CallLogQuery
         // plus the section value.
         matrixCursor.addRow(new Object[]{
-                0L, "", 0L, 0L, 0, "", "", "", null, 0, null, null, null, null, 0L, null, section
+                0L, "", 0L, 0L, 0, "", "", "", null, 0, null, null, null, null, 0L, null, 0,
+                section
         });
         return matrixCursor;
     }
@@ -157,8 +169,10 @@
         // We need to check for NULL explicitly otherwise entries with where READ is NULL
         // may not match either the query or its negation.
         // We consider the calls that are not yet consumed (i.e. IS_READ = 0) as "new".
-        String selection = String.format("%s IS NOT NULL AND %s = 0", Calls.IS_READ, Calls.IS_READ);
-        String[] selectionArgs = null;
+        String selection = String.format("%s IS NOT NULL AND %s = 0 AND %s > ?",
+                Calls.IS_READ, Calls.IS_READ, Calls.DATE);
+        List<String> selectionArgs = Lists.newArrayList(
+                Long.toString(System.currentTimeMillis() - NEW_SECTION_TIME_WINDOW));
         if (!isNew) {
             // Negate the query.
             selection = String.format("NOT (%s)", selection);
@@ -166,12 +180,11 @@
         if (voicemailOnly) {
             // Add a clause to fetch only items of type voicemail.
             selection = String.format("(%s) AND (%s = ?)", selection, Calls.TYPE);
-            selectionArgs = new String[]{
-                    Integer.toString(Calls.VOICEMAIL_TYPE),
-            };
+            selectionArgs.add(Integer.toString(Calls.VOICEMAIL_TYPE));
         }
         startQuery(token, null, Calls.CONTENT_URI_WITH_VOICEMAIL,
-                CallLogQuery._PROJECTION, selection, selectionArgs, Calls.DEFAULT_SORT_ORDER);
+                CallLogQuery._PROJECTION, selection, selectionArgs.toArray(EMPTY_STRING_ARRAY),
+                Calls.DEFAULT_SORT_ORDER);
     }
 
     /** Cancel any pending fetch request. */
diff --git a/src/com/android/contacts/calllog/ContactInfoHelper.java b/src/com/android/contacts/calllog/ContactInfoHelper.java
index f4b7daf..c837c9a 100644
--- a/src/com/android/contacts/calllog/ContactInfoHelper.java
+++ b/src/com/android/contacts/calllog/ContactInfoHelper.java
@@ -206,8 +206,7 @@
                 info.photoId = phonesCursor.getLong(PhoneQuery.PHOTO_ID);
                 info.photoUri =
                         UriUtils.parseUriOrNull(phonesCursor.getString(PhoneQuery.PHOTO_URI));
-                info.formattedNumber = formatPhoneNumber(info.number, info.formattedNumber,
-                        countryIso);
+                info.formattedNumber = formatPhoneNumber(number, null, countryIso);
 
             } else {
                 info = ContactInfo.EMPTY;
diff --git a/src/com/android/contacts/detail/ContactDetailFragment.java b/src/com/android/contacts/detail/ContactDetailFragment.java
index aa49481..84dbb83 100644
--- a/src/com/android/contacts/detail/ContactDetailFragment.java
+++ b/src/com/android/contacts/detail/ContactDetailFragment.java
@@ -878,8 +878,10 @@
             @Override
             public void onItemClick(AdapterView<?> parent, View view, int position,
                     long id) {
-                if (mListener != null) {
-                    mListener.onItemClicked(popupAdapter.getIntent(mContext, position));
+                if (mListener != null && mContactData != null) {
+                    mListener.onItemClicked(ContactsUtils.getInvitableIntent(
+                            popupAdapter.getItem(position) /* account type */,
+                            mContactData.getLookupUri()));
                 }
             }
         };
@@ -2102,7 +2104,7 @@
 
     public static interface Listener {
         /**
-         * User clicked a single item (e.g. mail)
+         * User clicked a single item (e.g. mail). The intent passed in could be null.
          */
         public void onItemClicked(Intent intent);
 
@@ -2167,19 +2169,6 @@
             return resultView;
         }
 
-        public Intent getIntent(Context context, int position) {
-            final AccountType accountType = mAccountTypes.get(position);
-            Intent intent = new Intent();
-            intent.setClassName(accountType.resPackageName,
-                    accountType.getInviteContactActivityClassName());
-
-            intent.setAction(ContactsContract.Intents.INVITE_CONTACT);
-
-            // Data is the lookup URI.
-            intent.setData(mContactData.getLookupUri());
-            return intent;
-        }
-
         @Override
         public int getCount() {
             return mAccountTypes.size();
diff --git a/src/com/android/contacts/dialpad/DialpadFragment.java b/src/com/android/contacts/dialpad/DialpadFragment.java
index 1790b9e..064e054 100644
--- a/src/com/android/contacts/dialpad/DialpadFragment.java
+++ b/src/com/android/contacts/dialpad/DialpadFragment.java
@@ -248,6 +248,7 @@
         mDigits.setKeyListener(DialerKeyListener.getInstance());
         mDigits.setOnClickListener(this);
         mDigits.setOnKeyListener(this);
+        mDigits.setOnLongClickListener(this);
         mDigits.addTextChangedListener(this);
 
         PhoneNumberFormatter.setPhoneNumberFormattingTextWatcher(getActivity(), mDigits);
@@ -653,6 +654,12 @@
         mHaptic.vibrate();
         KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
         mDigits.onKeyDown(keyCode, event);
+
+        // If the cursor is at the end of the text we hide it.
+        final int length = mDigits.length();
+        if (length == mDigits.getSelectionStart() && length == mDigits.getSelectionEnd()) {
+            mDigits.setCursorVisible(false);
+        }
     }
 
     public boolean onKey(View view, int keyCode, KeyEvent event) {
@@ -804,6 +811,13 @@
                 keyPressed(KeyEvent.KEYCODE_PLUS);
                 return true;
             }
+            case R.id.digits: {
+                // Right now EditText does not show the "paste" option when cursor is not visible.
+                // To show that, make the cursor visible, and return false, letting the EditText
+                // show the option by itself.
+                mDigits.setCursorVisible(true);
+                return false;
+            }
         }
         return false;
     }
diff --git a/src/com/android/contacts/model/AccountTypeManager.java b/src/com/android/contacts/model/AccountTypeManager.java
index 5443196..6a438c6 100644
--- a/src/com/android/contacts/model/AccountTypeManager.java
+++ b/src/com/android/contacts/model/AccountTypeManager.java
@@ -16,6 +16,7 @@
 
 package com.android.contacts.model;
 
+import com.android.contacts.ContactsUtils;
 import com.android.contacts.util.Constants;
 import com.android.i18n.phonenumbers.PhoneNumberUtil;
 import com.android.internal.util.Objects;
@@ -37,6 +38,9 @@
 import android.content.SyncAdapterType;
 import android.content.SyncStatusObserver;
 import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.AsyncTask;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Message;
@@ -56,6 +60,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * Singleton holder for all parsed {@link AccountType} available on the
@@ -110,8 +115,17 @@
     /**
      * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s
      * which support the "invite" feature and have one or more account.
+     *
+     * This is a filtered down and more "usable" list compared to
+     * {@link #getAllInvitableAccountTypes}, where usable is defined as:
+     * (1) making sure that the app that contributed the account type is not disabled
+     * (in order to avoid presenting the user with an option that does nothing), and
+     * (2) that there is at least one raw contact with that account type in the database
+     * (assuming that the user probably doesn't use that account type).
+     *
+     * Warning: Don't use on the UI thread because this can scan the database.
      */
-    public abstract Map<AccountTypeWithDataSet, AccountType> getInvitableAccountTypes();
+    public abstract Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes();
 
     /**
      * Find the best {@link DataKind} matching the requested
@@ -134,6 +148,19 @@
 class AccountTypeManagerImpl extends AccountTypeManager
         implements OnAccountsUpdateListener, SyncStatusObserver {
 
+    private static final Map<AccountTypeWithDataSet, AccountType>
+            EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP =
+            Collections.unmodifiableMap(new HashMap<AccountTypeWithDataSet, AccountType>());
+
+    /**
+     * A sample contact URI used to test whether any activities will respond to an
+     * invitable intent with the given URI as the intent data. This doesn't need to be
+     * specific to a real contact because an app that intercepts the intent should probably do so
+     * for all types of contact URIs.
+     */
+    private static final Uri SAMPLE_CONTACT_URI = ContactsContract.Contacts.getLookupUri(
+            1, "xxx");
+
     private Context mContext;
     private AccountManager mAccountManager;
 
@@ -144,7 +171,21 @@
     private List<AccountWithDataSet> mGroupWritableAccounts = Lists.newArrayList();
     private Map<AccountTypeWithDataSet, AccountType> mAccountTypesWithDataSets = Maps.newHashMap();
     private Map<AccountTypeWithDataSet, AccountType> mInvitableAccountTypes =
-            Collections.unmodifiableMap(new HashMap<AccountTypeWithDataSet, AccountType>());
+            EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP;
+
+    private final InvitableAccountTypeCache mInvitableAccountTypeCache;
+
+    /**
+     * The boolean value is equal to true if the {@link InvitableAccountTypeCache} has been
+     * initialized. False otherwise.
+     */
+    private final AtomicBoolean mInvitablesCacheIsInitialized = new AtomicBoolean(false);
+
+    /**
+     * The boolean value is equal to true if the {@link FindInvitablesTask} is still executing.
+     * False otherwise.
+     */
+    private final AtomicBoolean mInvitablesTaskIsRunning = new AtomicBoolean(false);
 
     private static final int MESSAGE_LOAD_DATA = 0;
     private static final int MESSAGE_PROCESS_BROADCAST_INTENT = 1;
@@ -229,6 +270,8 @@
             }
         };
 
+        mInvitableAccountTypeCache = new InvitableAccountTypeCache();
+
         // Request updates when packages or accounts change
         IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
         filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
@@ -436,7 +479,7 @@
             mAccounts = allAccounts;
             mContactWritableAccounts = contactWritableAccounts;
             mGroupWritableAccounts = groupWritableAccounts;
-            mInvitableAccountTypes = findInvitableAccountTypes(
+            mInvitableAccountTypes = findAllInvitableAccountTypes(
                     mContext, allAccounts, accountTypesByTypeAndDataSet);
         }
 
@@ -542,17 +585,54 @@
         }
     }
 
-    @Override
-    public Map<AccountTypeWithDataSet, AccountType> getInvitableAccountTypes() {
+    /**
+     * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s
+     * which support the "invite" feature and have one or more account. This is an unfiltered
+     * list. See {@link #getUsableInvitableAccountTypes()}.
+     */
+    private Map<AccountTypeWithDataSet, AccountType> getAllInvitableAccountTypes() {
+        ensureAccountsLoaded();
         return mInvitableAccountTypes;
     }
 
+    @Override
+    public Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes() {
+        ensureAccountsLoaded();
+        // Since this method is not thread-safe, it's possible for multiple threads to encounter
+        // the situation where (1) the cache has not been initialized yet or
+        // (2) an async task to refresh the account type list in the cache has already been
+        // started. Hence we use {@link AtomicBoolean}s and return cached values immediately
+        // while we compute the actual result in the background. We use this approach instead of
+        // using "synchronized" because computing the account type list involves a DB read, and
+        // can potentially cause a deadlock situation if this method is called from code which
+        // holds the DB lock. The trade-off of potentially having an incorrect list of invitable
+        // account types for a short period of time seems more manageable than enforcing the
+        // context in which this method is called.
+
+        // Computing the list of usable invitable account types is done on the fly as requested.
+        // If this method has never been called before, then block until the list has been computed.
+        if (!mInvitablesCacheIsInitialized.get()) {
+            mInvitableAccountTypeCache.setCachedValue(findUsableInvitableAccountTypes(mContext));
+            mInvitablesCacheIsInitialized.set(true);
+        } else {
+            // Otherwise, there is a value in the cache. If the value has expired and
+            // an async task has not already been started by another thread, then kick off a new
+            // async task to compute the list.
+            if (mInvitableAccountTypeCache.isExpired() &&
+                    mInvitablesTaskIsRunning.compareAndSet(false, true)) {
+                new FindInvitablesTask().execute();
+            }
+        }
+
+        return mInvitableAccountTypeCache.getCachedValue();
+    }
+
     /**
      * Return all {@link AccountType}s with at least one account which supports "invite", i.e.
      * its {@link AccountType#getInviteContactActivityClassName()} is not empty.
      */
     @VisibleForTesting
-    static Map<AccountTypeWithDataSet, AccountType> findInvitableAccountTypes(Context context,
+    static Map<AccountTypeWithDataSet, AccountType> findAllInvitableAccountTypes(Context context,
             Collection<AccountWithDataSet> accounts,
             Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet) {
         HashMap<AccountTypeWithDataSet, AccountType> result = Maps.newHashMap();
@@ -573,8 +653,58 @@
         return Collections.unmodifiableMap(result);
     }
 
+    /**
+     * Return all usable {@link AccountType}s that support the "invite" feature from the
+     * list of all potential invitable account types (retrieved from
+     * {@link #getAllInvitableAccountTypes}). A usable invitable account type means:
+     * (1) there is at least 1 raw contact in the database with that account type, and
+     * (2) the app contributing the account type is not disabled.
+     *
+     * Warning: Don't use on the UI thread because this can scan the database.
+     */
+    private Map<AccountTypeWithDataSet, AccountType> findUsableInvitableAccountTypes(
+            Context context) {
+        Map<AccountTypeWithDataSet, AccountType> allInvitables = getAllInvitableAccountTypes();
+        if (allInvitables.isEmpty()) {
+            return EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP;
+        }
+
+        final HashMap<AccountTypeWithDataSet, AccountType> result = Maps.newHashMap();
+        result.putAll(allInvitables);
+
+        final PackageManager packageManager = context.getPackageManager();
+        for (AccountTypeWithDataSet accountTypeWithDataSet : allInvitables.keySet()) {
+            AccountType accountType = allInvitables.get(accountTypeWithDataSet);
+
+            // Make sure that account types don't come from apps that are disabled.
+            Intent invitableIntent = ContactsUtils.getInvitableIntent(accountType,
+                    SAMPLE_CONTACT_URI);
+            if (invitableIntent == null) {
+                result.remove(accountTypeWithDataSet);
+                continue;
+            }
+            ResolveInfo resolveInfo = packageManager.resolveActivity(invitableIntent,
+                    PackageManager.MATCH_DEFAULT_ONLY);
+            if (resolveInfo == null) {
+                // If we can't find an activity to start for this intent, then there's no point in
+                // showing this option to the user.
+                result.remove(accountTypeWithDataSet);
+                continue;
+            }
+
+            // Make sure that there is at least 1 raw contact with this account type. This check
+            // is non-trivial and should not be done on the UI thread.
+            if (!accountTypeWithDataSet.hasData(context)) {
+                result.remove(accountTypeWithDataSet);
+            }
+        }
+
+        return Collections.unmodifiableMap(result);
+    }
+
     @Override
     public List<AccountType> getAccountTypes(boolean contactWritableOnly) {
+        ensureAccountsLoaded();
         final List<AccountType> accountTypes = Lists.newArrayList();
         synchronized (this) {
             for (AccountType type : mAccountTypesWithDataSets.values()) {
@@ -585,4 +715,64 @@
         }
         return accountTypes;
     }
+
+    /**
+     * Background task to find all usable {@link AccountType}s that support the "invite" feature
+     * from the list of all potential invitable account types. Once the work is completed,
+     * the list of account types is stored in the {@link AccountTypeManager}'s
+     * {@link InvitableAccountTypeCache}.
+     */
+    private class FindInvitablesTask extends AsyncTask<Void, Void,
+            Map<AccountTypeWithDataSet, AccountType>> {
+
+        @Override
+        protected Map<AccountTypeWithDataSet, AccountType> doInBackground(Void... params) {
+            return findUsableInvitableAccountTypes(mContext);
+        }
+
+        @Override
+        protected void onPostExecute(Map<AccountTypeWithDataSet, AccountType> accountTypes) {
+            mInvitableAccountTypeCache.setCachedValue(accountTypes);
+            mInvitablesTaskIsRunning.set(false);
+        }
+    }
+
+    /**
+     * This cache holds a list of invitable {@link AccountTypeWithDataSet}s, in the form of a
+     * {@link Map<AccountTypeWithDataSet, AccountType>}. Note that the cached value is valid only
+     * for {@link #TIME_TO_LIVE} milliseconds.
+     */
+    private static final class InvitableAccountTypeCache {
+
+        /**
+         * The cached {@link #mInvitableAccountTypes} list expires after this number of milliseconds
+         * has elapsed.
+         */
+        private static final long TIME_TO_LIVE = 60000;
+
+        private Map<AccountTypeWithDataSet, AccountType> mInvitableAccountTypes;
+
+        private long mTimeLastSet;
+
+        /**
+         * Returns true if the data in this cache is stale and needs to be refreshed. Returns false
+         * otherwise.
+         */
+        public boolean isExpired() {
+             return SystemClock.elapsedRealtime() - mTimeLastSet > TIME_TO_LIVE;
+        }
+
+        /**
+         * Returns the cached value. Note that the caller is responsible for checking
+         * {@link #isExpired()} to ensure that the value is not stale.
+         */
+        public Map<AccountTypeWithDataSet, AccountType> getCachedValue() {
+            return mInvitableAccountTypes;
+        }
+
+        public void setCachedValue(Map<AccountTypeWithDataSet, AccountType> map) {
+            mInvitableAccountTypes = map;
+            mTimeLastSet = SystemClock.elapsedRealtime();
+        }
+    }
 }
diff --git a/src/com/android/contacts/model/AccountTypeWithDataSet.java b/src/com/android/contacts/model/AccountTypeWithDataSet.java
index f1b2344..b103755 100644
--- a/src/com/android/contacts/model/AccountTypeWithDataSet.java
+++ b/src/com/android/contacts/model/AccountTypeWithDataSet.java
@@ -18,6 +18,12 @@
 
 import com.google.common.base.Objects;
 
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.RawContacts;
 import android.text.TextUtils;
 
 
@@ -25,6 +31,11 @@
  * Encapsulates an "account type" string and a "data set" string.
  */
 public class AccountTypeWithDataSet {
+
+    private static final String[] ID_PROJECTION = new String[] {BaseColumns._ID};
+    private static final Uri RAW_CONTACTS_URI_LIMIT_1 = RawContacts.CONTENT_URI.buildUpon()
+            .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, "1").build();
+
     /** account type.  Can be null for fallback type. */
     public final String accountType;
 
@@ -40,6 +51,32 @@
         return new AccountTypeWithDataSet(accountType, dataSet);
     }
 
+    /**
+     * Return true if there are any contacts in the database with this account type and data set.
+     * Touches DB. Don't use in the UI thread.
+     */
+    public boolean hasData(Context context) {
+        final String BASE_SELECTION = RawContacts.ACCOUNT_TYPE + " = ?";
+        final String selection;
+        final String[] args;
+        if (TextUtils.isEmpty(dataSet)) {
+            selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " IS NULL";
+            args = new String[] {accountType};
+        } else {
+            selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " = ?";
+            args = new String[] {accountType, dataSet};
+        }
+
+        final Cursor c = context.getContentResolver().query(RAW_CONTACTS_URI_LIMIT_1,
+                ID_PROJECTION, selection, args, null);
+        if (c == null) return false;
+        try {
+            return c.moveToFirst();
+        } finally {
+            c.close();
+        }
+    }
+
     @Override
     public boolean equals(Object o) {
         if (!(o instanceof AccountTypeWithDataSet)) return false;
diff --git a/tests/src/com/android/contacts/calllog/CallLogQueryTestUtils.java b/tests/src/com/android/contacts/calllog/CallLogQueryTestUtils.java
index 331d388..a88bf4f 100644
--- a/tests/src/com/android/contacts/calllog/CallLogQueryTestUtils.java
+++ b/tests/src/com/android/contacts/calllog/CallLogQueryTestUtils.java
@@ -29,7 +29,7 @@
     public static Object[] createTestValues() {
         Object[] values = new Object[]{
                 0L, "", 0L, 0L, Calls.INCOMING_TYPE, "", "", "", null, 0, null, null, null, null,
-                0L, null
+                0L, null, 0,
         };
         assertEquals(CallLogQuery._PROJECTION.length, values.length);
         return values;
@@ -38,7 +38,7 @@
     public static Object[] createTestExtendedValues() {
         Object[] values = new Object[]{
                 0L, "", 0L, 0L, Calls.INCOMING_TYPE, "", "", "", null, 0, null, null, null, null,
-                0L, null, CallLogQuery.SECTION_OLD_ITEM
+                0L, null, 1, CallLogQuery.SECTION_OLD_ITEM
         };
         Assert.assertEquals(CallLogQuery.EXTENDED_PROJECTION.length, values.length);
         return values;
diff --git a/tests/src/com/android/contacts/model/AccountTypeManagerTest.java b/tests/src/com/android/contacts/model/AccountTypeManagerTest.java
index aadf411..09902a3 100644
--- a/tests/src/com/android/contacts/model/AccountTypeManagerTest.java
+++ b/tests/src/com/android/contacts/model/AccountTypeManagerTest.java
@@ -36,7 +36,7 @@
  */
 @SmallTest
 public class AccountTypeManagerTest extends AndroidTestCase {
-    public void testFindInvitableAccountTypes() {
+    public void testFindAllInvitableAccountTypes() {
         final Context c = getContext();
 
         // Define account types.
@@ -53,7 +53,7 @@
 
         // empty - empty
         Map<AccountTypeWithDataSet, AccountType> types =
-                AccountTypeManagerImpl.findInvitableAccountTypes(c,
+                AccountTypeManagerImpl.findAllInvitableAccountTypes(c,
                         buildAccounts(), buildAccountTypes());
         assertEquals(0, types.size());
         try {
@@ -159,7 +159,7 @@
             AccountType... expectedInvitableTypes
             ) {
         Map<AccountTypeWithDataSet, AccountType> result =
-                AccountTypeManagerImpl.findInvitableAccountTypes(getContext(), accounts, types);
+                AccountTypeManagerImpl.findAllInvitableAccountTypes(getContext(), accounts, types);
         for (AccountType type : expectedInvitableTypes) {
             assertTrue("Result doesn't contain type=" + type.getAccountTypeAndDataSet(),
                     result.containsKey(type.getAccountTypeAndDataSet()));
diff --git a/tests/src/com/android/contacts/tests/mocks/MockAccountTypeManager.java b/tests/src/com/android/contacts/tests/mocks/MockAccountTypeManager.java
index 5ca1ccd..9084ef0 100644
--- a/tests/src/com/android/contacts/tests/mocks/MockAccountTypeManager.java
+++ b/tests/src/com/android/contacts/tests/mocks/MockAccountTypeManager.java
@@ -64,7 +64,7 @@
     }
 
     @Override
-    public Map<AccountTypeWithDataSet, AccountType> getInvitableAccountTypes() {
+    public Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes() {
         return Maps.newHashMap(); // Always returns empty
     }