Merge "Display unread count for voicemail and update when vm is read." into ub-contactsdialer-a-dev
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 32d88cc..1faab2a 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -17,8 +17,8 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.dialer"
     coreApp="true"
-    android:versionCode="20210"
-    android:versionName="2.21">
+    android:versionCode="20300"
+    android:versionName="2.3">
 
     <uses-sdk
         android:minSdkVersion="23"
diff --git a/res/menu/call_details_options.xml b/res/menu/call_details_options.xml
index 84cc2c9..0e9e5c9 100644
--- a/res/menu/call_details_options.xml
+++ b/res/menu/call_details_options.xml
@@ -13,7 +13,8 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<menu xmlns:android="http://schemas.android.com/apk/res/android">
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/call_details_menu">
 
     <item android:id="@+id/menu_trash"
         android:icon="@drawable/ic_delete_24dp"
diff --git a/res/values/ids.xml b/res/values/ids.xml
index cb2025a..0034fe3 100644
--- a/res/values/ids.xml
+++ b/res/values/ids.xml
@@ -18,5 +18,6 @@
     <item type="id" name="context_menu_copy_to_clipboard" />
     <item type="id" name="context_menu_copy_transcript_to_clipboard" />
     <item type="id" name="context_menu_edit_before_call" />
+    <item type="id" name="context_menu_block_number" />
     <item type="id" name="settings_header_sounds_and_vibration" />
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 1ce6ab6..7bd88ba 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -52,6 +52,24 @@
     <!-- Option displayed in context menu to copy long pressed voicemail transcription to clipboard [CHAR LIMIT=64] -->
     <string name="copy_transcript_text">Copy transcription to clipboard</string>
 
+    <!-- Menu item used to block a number from the call log [CHAR LIMIT=64] -->
+    <string name="call_log_block_number">Block number</string>
+
+    <!-- Text for snackbar to undo blocking a number. [CHAR LIMIT=64] -->
+    <string name="snackbar_number_blocked">
+        <xliff:g id="number" example="(555) 555-5555">%1$s</xliff:g> added to block list</string>
+
+    <!-- Menu item used to unblock a number from the call log [CHAR LIMIT=64]-->
+    <string name="call_log_unblock_number">Unblock number</string>
+
+    <!-- Text for snackbar to undo unblocking a number. [CHAR LIMIT=64] -->
+    <string name="snackbar_number_unblocked">
+        <xliff:g id="number" example="(555) 555-5555">%1$s</xliff:g>
+        removed from block list</string>
+
+    <!-- Text for undo button in snackbar for blocking/unblocking number. [CHAR LIMIT=10] -->
+    <string name="block_number_undo">UNDO</string>
+
     <!-- Menu item used to copy a number from the call log to the dialer so it can be edited before calling it -->
     <string name="call_log_edit_number_before_call">Edit number before call</string>
 
@@ -490,6 +508,20 @@
          [CHAR LIMIT=30] -->
     <string name="call_log_voicemail_title">Voicemail</string>
 
+    <!-- Confirmation dialog for blocking a number. [CHAR LIMIT=NONE] -->
+    <string name="blockNumberConfirmation">Add
+        <xliff:g id="number" example="(555) 555-5555">%1$s</xliff:g> to your block list?</string>
+
+    <!-- Block number alert dialog button [CHAR LIMIT=32] -->
+    <string name="blockNumberOk">Block number</string>
+
+    <!-- Confirmation dialog for unblocking a number. [CHAR LIMIT=NONE] -->
+    <string name="unblockNumberConfirmation">Remove
+        <xliff:g id="number" example="(555) 555-5555">%1$s</xliff:g> from your block list?</string>
+
+    <!-- Unblock number alert dialog button [CHAR LIMIT=32] -->
+    <string name="unblockNumberOk">Unblock number</string>
+
     <!-- Accessibility text for the tab showing recent and favorite contacts who can be called.
          [CHAR LIMIT=40] -->
     <string name="tab_speed_dial">Speed dial</string>
diff --git a/src/com/android/dialer/DialtactsActivity.java b/src/com/android/dialer/DialtactsActivity.java
index f11f1b1..4ade04a 100644
--- a/src/com/android/dialer/DialtactsActivity.java
+++ b/src/com/android/dialer/DialtactsActivity.java
@@ -915,11 +915,11 @@
             return;
         }
 
-        final boolean phoneIsInUse = phoneIsInUse();
-        if (phoneIsInUse || (intent.getData() !=  null && isDialIntent(intent))) {
+        final boolean showDialpadChooser = phoneIsInUse() && !DialpadFragment.isAddCallMode(intent);
+        if (showDialpadChooser || (intent.getData() != null && isDialIntent(intent))) {
             showDialpadFragment(false);
             mDialpadFragment.setStartedFromNewIntent(true);
-            if (phoneIsInUse && !mDialpadFragment.isVisible()) {
+            if (showDialpadChooser && !mDialpadFragment.isVisible()) {
                 mInCallDialpadUp = true;
             }
         }
diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java
index aa307b6..4d4d454 100644
--- a/src/com/android/dialer/calllog/CallLogAdapter.java
+++ b/src/com/android/dialer/calllog/CallLogAdapter.java
@@ -22,12 +22,10 @@
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.net.Uri;
-import android.provider.ContactsContract;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.support.v7.widget.RecyclerView;
 import android.os.Bundle;
 import android.os.Trace;
-import android.preference.PreferenceActivity;
 import android.preference.PreferenceManager;
 import android.provider.CallLog;
 import android.support.v7.widget.RecyclerView.ViewHolder;
@@ -35,28 +33,18 @@
 import android.telephony.PhoneNumberUtils;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
-import android.util.Log;
-import android.view.ContextMenu;
 import android.view.LayoutInflater;
-import android.view.MenuItem;
-import android.view.MenuItem.OnMenuItemClickListener;
 import android.view.View;
 import android.view.View.AccessibilityDelegate;
 import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
-import android.view.ContextMenu.ContextMenuInfo;
 import android.view.accessibility.AccessibilityEvent;
-import android.widget.TextView;
 
-import com.android.contacts.common.CallUtil;
-import com.android.contacts.common.ClipboardUtils;
 import com.android.contacts.common.util.PermissionsUtil;
-import com.android.dialer.DialtactsActivity;
 import com.android.dialer.PhoneCallDetails;
 import com.android.dialer.R;
 import com.android.dialer.contactinfo.ContactInfoCache;
 import com.android.dialer.contactinfo.ContactInfoCache.OnContactInfoChangedListener;
-import com.android.dialer.util.DialerUtils;
+import com.android.dialer.database.FilteredNumberAsyncQueryHandler;
 import com.android.dialer.util.PhoneNumberUtil;
 import com.android.dialer.voicemail.VoicemailPlaybackPresenter;
 
@@ -96,6 +84,7 @@
     private final ContactInfoHelper mContactInfoHelper;
     protected final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
     private final CallFetcher mCallFetcher;
+    private final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
 
     protected ContactInfoCache mContactInfoCache;
 
@@ -131,7 +120,7 @@
 
     private SharedPreferences mPrefs;
 
-    private boolean mShowVoicemailPromoCard = false;
+    protected boolean mShowVoicemailPromoCard = false;
 
     /** Instance of helper class for managing views. */
     private final CallLogListItemHelper mCallLogListItemHelper;
@@ -195,89 +184,6 @@
         }
     };
 
-    /**
-     * Listener that is triggered to populate the context menu with actions to perform on the call's
-     * number, when the call log entry is long pressed.
-     */
-    private final View.OnCreateContextMenuListener mOnCreateContextMenuListener =
-            new View.OnCreateContextMenuListener() {
-                @Override
-                public void onCreateContextMenu(
-                        ContextMenu menu, View v, ContextMenuInfo menuInfo) {
-                    final CallLogListItemViewHolder vh = (CallLogListItemViewHolder) v.getTag();
-                    if (TextUtils.isEmpty(vh.number)) {
-                        return;
-                    }
-
-                    if (vh.callType == CallLog.Calls.VOICEMAIL_TYPE) {
-                        menu.setHeaderTitle(mContext.getResources().getText(R.string.voicemail));
-                    } else {
-                        menu.setHeaderTitle(vh.number);
-                    }
-
-                    final MenuItem copyItem = menu.add(
-                            ContextMenu.NONE,
-                            R.id.context_menu_copy_to_clipboard,
-                            ContextMenu.NONE,
-                            R.string.copy_number_text);
-
-                    copyItem.setOnMenuItemClickListener(new OnMenuItemClickListener() {
-                        @Override
-                        public boolean onMenuItemClick(MenuItem item) {
-                            ClipboardUtils.copyText(mContext, null, vh.number, true);
-                            return true;
-                        }
-                    });
-
-                    // The edit number before call does not show up if any of the conditions apply:
-                    // 1) Number cannot be called
-                    // 2) Number is the voicemail number
-                    // 3) Number is a SIP address
-
-                    if (PhoneNumberUtil.canPlaceCallsTo(vh.number, vh.numberPresentation)
-                            && !mTelecomCallLogCache.isVoicemailNumber(vh.accountHandle, vh.number)
-                            && !PhoneNumberUtil.isSipNumber(vh.number)) {
-                        final MenuItem editItem = menu.add(
-                                ContextMenu.NONE,
-                                R.id.context_menu_edit_before_call,
-                                ContextMenu.NONE,
-                                R.string.call_log_edit_number_before_call);
-
-                        editItem.setOnMenuItemClickListener(new OnMenuItemClickListener() {
-                            @Override
-                            public boolean onMenuItemClick(MenuItem item) {
-                                final Intent intent = new Intent(
-                                        Intent.ACTION_DIAL, CallUtil.getCallUri(vh.number));
-                                intent.setClass(mContext, DialtactsActivity.class);
-                                DialerUtils.startActivityWithErrorToast(mContext, intent);
-                                return true;
-                            }
-                        });
-                    }
-
-                    final TextView transcriptView =
-                            vh.phoneCallDetailsViews.voicemailTranscriptionView;
-                    if (vh.callType == CallLog.Calls.VOICEMAIL_TYPE
-                            && transcriptView.length() > 0) {
-                        final MenuItem copyTranscriptItem = menu.add(
-                                ContextMenu.NONE,
-                                R.id.context_menu_copy_transcript_to_clipboard,
-                                ContextMenu.NONE,
-                                R.string.copy_transcript_text);
-
-                        copyTranscriptItem.setOnMenuItemClickListener(
-                                new OnMenuItemClickListener() {
-                                    @Override
-                                    public boolean onMenuItemClick(MenuItem item) {
-                                        ClipboardUtils.copyText(
-                                                mContext, null, transcriptView.getText(), true);
-                                        return true;
-                                    }
-                                });
-                    }
-                }
-            };
-
     private void expandViewHolderActions(CallLogListItemViewHolder viewHolder) {
         // If another item is expanded, notify it that it has changed. Its actions will be
         // hidden when it is re-binded because we change mCurrentlyExpandedPosition below.
@@ -351,6 +257,8 @@
         mCallLogGroupBuilder = new CallLogGroupBuilder(this);
         mPrefs = PreferenceManager.getDefaultSharedPreferences(context);
         maybeShowVoicemailPromoCard();
+        mFilteredNumberAsyncQueryHandler =
+                new FilteredNumberAsyncQueryHandler(mContext.getContentResolver());
     }
 
     public void onSaveInstanceState(Bundle outState) {
@@ -439,12 +347,12 @@
                 mExpandCollapseListener,
                 mTelecomCallLogCache,
                 mCallLogListItemHelper,
-                mVoicemailPlaybackPresenter);
+                mVoicemailPlaybackPresenter,
+                mFilteredNumberAsyncQueryHandler);
 
         viewHolder.callLogEntryView.setTag(viewHolder);
         viewHolder.callLogEntryView.setAccessibilityDelegate(mAccessibilityDelegate);
 
-        viewHolder.primaryActionView.setOnCreateContextMenuListener(mOnCreateContextMenuListener);
         viewHolder.primaryActionView.setTag(viewHolder);
 
         return viewHolder;
@@ -628,6 +536,11 @@
                         ? 1 : 0));
     }
 
+    @Override
+    public int getGroupSize(int position) {
+        return super.getGroupSize(position - (mShowVoicemailPromoCard ? 1 : 0));
+    }
+
     protected boolean isCallLogActivity() {
         return mIsCallLogActivity;
     }
@@ -794,11 +707,6 @@
         mContactInfoCache.injectContactInfoForTest(number, countryIso, contactInfo);
     }
 
-    @Override
-    public void addGroup(int cursorPosition, int size, boolean expanded) {
-        super.addGroup(cursorPosition, size, expanded);
-    }
-
     /**
      * Stores the day group associated with a call in the call log.
      *
diff --git a/src/com/android/dialer/calllog/CallLogGroupBuilder.java b/src/com/android/dialer/calllog/CallLogGroupBuilder.java
index 0826aeb..4cf2d07 100644
--- a/src/com/android/dialer/calllog/CallLogGroupBuilder.java
+++ b/src/com/android/dialer/calllog/CallLogGroupBuilder.java
@@ -20,6 +20,7 @@
 import android.provider.CallLog.Calls;
 import android.telephony.PhoneNumberUtils;
 import android.text.format.Time;
+import android.text.TextUtils;
 
 import com.android.contacts.common.util.DateUtils;
 import com.android.contacts.common.util.PhoneNumberHelper;
@@ -46,9 +47,8 @@
          * dialed.
          * @param cursorPosition The starting position of the group in the cursor.
          * @param size The size of the group.
-         * @param expanded Whether the group is expanded; always false for the call log.
          */
-        public void addGroup(int cursorPosition, int size, boolean expanded);
+        public void addGroup(int cursorPosition, int size);
 
         /**
          * Defines the interface for tracking the day group each call belongs to.  Calls in a call
@@ -94,7 +94,7 @@
 
     /**
      * Finds all groups of adjacent entries in the call log which should be grouped together and
-     * calls {@link GroupCreator#addGroup(int, int, boolean)} on {@link #mGroupCreator} for each of
+     * calls {@link GroupCreator#addGroup(int, int)} on {@link #mGroupCreator} for each of
      * them.
      * <p>
      * For entries that are not grouped with others, we do not need to create a group of size one.
@@ -114,98 +114,70 @@
 
         // Get current system time, used for calculating which day group calls belong to.
         long currentTime = System.currentTimeMillis();
-
-        int currentGroupSize = 1;
         cursor.moveToFirst();
-        // The number of the first entry in the group.
-        String firstNumber = cursor.getString(CallLogQuery.NUMBER);
-        // This is the type of the first call in the group.
-        int firstCallType = cursor.getInt(CallLogQuery.CALL_TYPE);
-
-        // The account information of the first entry in the group.
-        String firstAccountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME);
-        String firstAccountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
 
         // Determine the day group for the first call in the cursor.
         final long firstDate = cursor.getLong(CallLogQuery.DATE);
         final long firstRowId = cursor.getLong(CallLogQuery.ID);
-        int currentGroupDayGroup = getDayGroup(firstDate, currentTime);
-        mGroupCreator.setDayGroup(firstRowId, currentGroupDayGroup);
+        int groupDayGroup = getDayGroup(firstDate, currentTime);
+        mGroupCreator.setDayGroup(firstRowId, groupDayGroup);
+
+        // Instantiate the group values to those of the first call in the cursor.
+        String groupNumber = cursor.getString(CallLogQuery.NUMBER);
+        int groupCallType = cursor.getInt(CallLogQuery.CALL_TYPE);
+        String groupAccountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME);
+        String groupAccountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
+        int groupSize = 1;
+
+        String number;
+        int callType;
+        String accountComponentName;
+        String accountId;
 
         while (cursor.moveToNext()) {
-            // The number of the current row in the cursor.
-            final String currentNumber = cursor.getString(CallLogQuery.NUMBER);
-            final int callType = cursor.getInt(CallLogQuery.CALL_TYPE);
-            final String currentAccountComponentName = cursor.getString(
-                    CallLogQuery.ACCOUNT_COMPONENT_NAME);
-            final String currentAccountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
+            // Obtain the values for the current call to group.
+            number = cursor.getString(CallLogQuery.NUMBER);
+            callType = cursor.getInt(CallLogQuery.CALL_TYPE);
+            accountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME);
+            accountId = cursor.getString(CallLogQuery.ACCOUNT_ID);
 
-            final boolean sameNumber = equalNumbers(firstNumber, currentNumber);
-            final boolean sameAccountComponentName = Objects.equals(
-                    firstAccountComponentName,
-                    currentAccountComponentName);
-            final boolean sameAccountId = Objects.equals(
-                    firstAccountId,
-                    currentAccountId);
-            final boolean sameAccount = sameAccountComponentName && sameAccountId;
+            final boolean isSameNumber = equalNumbers(groupNumber, number);
+            final boolean isSameAccount = isSameAccount(
+                    groupAccountComponentName, accountComponentName, groupAccountId, accountId);
 
-            final boolean shouldGroup;
-            final long currentCallId = cursor.getLong(CallLogQuery.ID);
-            final long date = cursor.getLong(CallLogQuery.DATE);
-
-            if (!sameNumber || !sameAccount) {
-                // Should only group with calls from the same number.
-                shouldGroup = false;
-            } else if (firstCallType == Calls.VOICEMAIL_TYPE) {
-                // never group voicemail.
-                shouldGroup = false;
-            } else {
-                // Incoming, outgoing, and missed calls group together.
-                shouldGroup = callType != Calls.VOICEMAIL_TYPE;
-            }
-
-            if (shouldGroup) {
+            // Group with the same number and account which are not voicemail.
+            if (isSameNumber && isSameAccount
+                    && (callType != Calls.VOICEMAIL_TYPE)
+                    && (groupCallType != Calls.VOICEMAIL_TYPE)) {
                 // Increment the size of the group to include the current call, but do not create
-                // the group until we find a call that does not match.
-                currentGroupSize++;
+                // the group until finding a call that does not match.
+                groupSize++;
             } else {
-                // The call group has changed, so determine the day group for the new call group.
-                // This ensures all calls grouped together in the call log are assigned the same
-                // day group.
-                currentGroupDayGroup = getDayGroup(date, currentTime);
+                // The call group has changed. Determine the day group for the new call group.
+                final long date = cursor.getLong(CallLogQuery.DATE);
+                groupDayGroup = getDayGroup(date, currentTime);
 
-                // Create a group for the previous set of calls, excluding the current one, but do
-                // not create a group for a single call.
-                if (currentGroupSize > 1) {
-                    addGroup(cursor.getPosition() - currentGroupSize, currentGroupSize);
-                }
+                // Create a group for the previous group of calls, which does not include the
+                // current call.
+                mGroupCreator.addGroup(cursor.getPosition() - groupSize, groupSize);
+
                 // Start a new group; it will include at least the current call.
-                currentGroupSize = 1;
-                // The current entry is now the first in the group.
-                firstNumber = currentNumber;
-                firstCallType = callType;
-                firstAccountComponentName = currentAccountComponentName;
-                firstAccountId = currentAccountId;
+                groupSize = 1;
+
+                // Update the group values to those of the current call.
+                groupNumber = number;
+                groupCallType = callType;
+                groupAccountComponentName = accountComponentName;
+                groupAccountId = accountId;
             }
 
             // Save the day group associated with the current call.
-            mGroupCreator.setDayGroup(currentCallId, currentGroupDayGroup);
+            final long currentCallId = cursor.getLong(CallLogQuery.ID);
+            mGroupCreator.setDayGroup(currentCallId, groupDayGroup);
         }
-        // If the last set of calls at the end of the call log was itself a group, create it now.
-        if (currentGroupSize > 1) {
-            addGroup(count - currentGroupSize, currentGroupSize);
-        }
-    }
 
-    /**
-     * Creates a group of items in the cursor.
-     * <p>
-     * The group is always unexpanded.
-     *
-     * @see CallLogAdapter#addGroup(int, int, boolean)
-     */
-    private void addGroup(int cursorPosition, int size) {
-        mGroupCreator.addGroup(cursorPosition, size, false);
+        // Create a group for the last set of calls.
+        mGroupCreator.addGroup(count - groupSize, groupSize);
     }
 
     @VisibleForTesting
@@ -217,6 +189,10 @@
         }
     }
 
+    private boolean isSameAccount(String name1, String name2, String id1, String id2) {
+        return TextUtils.equals(name1, name2) && TextUtils.equals(id1, id2);
+    }
+
     @VisibleForTesting
     boolean compareSipAddresses(String number1, String number2) {
         if (number1 == null || number2 == null) return number1 == number2;
diff --git a/src/com/android/dialer/calllog/CallLogListItemHelper.java b/src/com/android/dialer/calllog/CallLogListItemHelper.java
index 1c8e397..d18e274 100644
--- a/src/com/android/dialer/calllog/CallLogListItemHelper.java
+++ b/src/com/android/dialer/calllog/CallLogListItemHelper.java
@@ -74,6 +74,9 @@
         // Cache name or number of caller.  Used when setting the content descriptions of buttons
         // when the actions ViewStub is inflated.
         views.nameOrNumber = getNameOrNumber(details);
+
+        // Cache country iso. Used for number filtering.
+        views.countryIso = details.countryIso;
     }
 
     /**
diff --git a/src/com/android/dialer/calllog/CallLogListItemViewHolder.java b/src/com/android/dialer/calllog/CallLogListItemViewHolder.java
index d85deb3..521b2a4 100644
--- a/src/com/android/dialer/calllog/CallLogListItemViewHolder.java
+++ b/src/com/android/dialer/calllog/CallLogListItemViewHolder.java
@@ -21,12 +21,15 @@
 import android.content.res.Resources;
 import android.content.Intent;
 import android.net.Uri;
+import android.provider.CallLog;
 import android.provider.CallLog.Calls;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.support.v7.widget.CardView;
 import android.support.v7.widget.RecyclerView;
 import android.telecom.PhoneAccountHandle;
 import android.text.TextUtils;
+import android.view.ContextMenu;
+import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewStub;
@@ -34,12 +37,17 @@
 import android.widget.ImageView;
 import android.widget.TextView;
 
+import com.android.contacts.common.CallUtil;
+import com.android.contacts.common.ClipboardUtils;
 import com.android.contacts.common.ContactPhotoManager;
 import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
 import com.android.contacts.common.dialog.CallSubjectDialog;
 import com.android.contacts.common.testing.NeededForTesting;
 import com.android.contacts.common.util.UriUtils;
+import com.android.dialer.DialtactsActivity;
 import com.android.dialer.R;
+import com.android.dialer.database.FilteredNumberAsyncQueryHandler;
+import com.android.dialer.filterednumber.FilterNumberDialogFragment;
 import com.android.dialer.util.DialerUtils;
 import com.android.dialer.util.PhoneNumberUtil;
 import com.android.dialer.voicemail.VoicemailPlaybackPresenter;
@@ -52,7 +60,8 @@
  * This object also contains UI logic pertaining to the view, to isolate it from the CallLogAdapter.
  */
 public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
-        implements View.OnClickListener {
+        implements View.OnClickListener, MenuItem.OnMenuItemClickListener,
+        View.OnCreateContextMenuListener {
 
     /** The root view of the call log list item */
     public final View rootView;
@@ -116,12 +125,24 @@
     public String numberType;
 
     /**
+     * The country iso for the call. Cached here as the call back
+     * intent is set only when the actions ViewStub is inflated.
+     */
+    public String countryIso;
+
+    /**
      * The type of call for the current call log entry.  Cached here as the call back
      * intent is set only when the actions ViewStub is inflated.
      */
     public int callType;
 
     /**
+     * ID for blocked numbers database.
+     * Set when context menu is created, if the number is blocked.
+     */
+    public Integer blockId;
+
+    /**
      * The account for the current call log entry.  Cached here as the call back
      * intent is set only when the actions ViewStub is inflated.
      */
@@ -156,6 +177,7 @@
     private final TelecomCallLogCache mTelecomCallLogCache;
     private final CallLogListItemHelper mCallLogListItemHelper;
     private final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
+    private final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
 
     private final int mPhotoSize;
 
@@ -168,6 +190,7 @@
             TelecomCallLogCache telecomCallLogCache,
             CallLogListItemHelper callLogListItemHelper,
             VoicemailPlaybackPresenter voicemailPlaybackPresenter,
+            FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler,
             View rootView,
             QuickContactBadge quickContactView,
             View primaryActionView,
@@ -182,6 +205,7 @@
         mTelecomCallLogCache = telecomCallLogCache;
         mCallLogListItemHelper = callLogListItemHelper;
         mVoicemailPlaybackPresenter = voicemailPlaybackPresenter;
+        mFilteredNumberAsyncQueryHandler = filteredNumberAsyncQueryHandler;
 
         this.rootView = rootView;
         this.quickContactView = quickContactView;
@@ -202,6 +226,7 @@
 
         primaryActionButtonView.setOnClickListener(this);
         primaryActionView.setOnClickListener(mExpandCollapseListener);
+        primaryActionView.setOnCreateContextMenuListener(this);
     }
 
     public static CallLogListItemViewHolder create(
@@ -210,7 +235,8 @@
             View.OnClickListener expandCollapseListener,
             TelecomCallLogCache telecomCallLogCache,
             CallLogListItemHelper callLogListItemHelper,
-            VoicemailPlaybackPresenter voicemailPlaybackPresenter) {
+            VoicemailPlaybackPresenter voicemailPlaybackPresenter,
+            FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler) {
 
         return new CallLogListItemViewHolder(
                 context,
@@ -218,6 +244,7 @@
                 telecomCallLogCache,
                 callLogListItemHelper,
                 voicemailPlaybackPresenter,
+                filteredNumberAsyncQueryHandler,
                 view,
                 (QuickContactBadge) view.findViewById(R.id.quick_contact_photo),
                 view.findViewById(R.id.primary_action_view),
@@ -227,6 +254,92 @@
                 (ImageView) view.findViewById(R.id.primary_action_button));
     }
 
+    @Override
+    public void onCreateContextMenu(
+            final ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+        if (TextUtils.isEmpty(number)) {
+            return;
+        }
+
+        if (callType == CallLog.Calls.VOICEMAIL_TYPE) {
+            menu.setHeaderTitle(mContext.getResources().getText(R.string.voicemail));
+        } else {
+            menu.setHeaderTitle(number);
+        }
+
+        menu.add(ContextMenu.NONE, R.id.context_menu_copy_to_clipboard, ContextMenu.NONE,
+                R.string.copy_number_text)
+                .setOnMenuItemClickListener(this);
+
+        // The edit number before call does not show up if any of the conditions apply:
+        // 1) Number cannot be called
+        // 2) Number is the voicemail number
+        // 3) Number is a SIP address
+
+        if (PhoneNumberUtil.canPlaceCallsTo(number, numberPresentation)
+                && !mTelecomCallLogCache.isVoicemailNumber(accountHandle, number)
+                && !PhoneNumberUtil.isSipNumber(number)) {
+            menu.add(ContextMenu.NONE, R.id.context_menu_edit_before_call, ContextMenu.NONE,
+                    R.string.call_log_edit_number_before_call)
+                    .setOnMenuItemClickListener(this);
+        }
+
+        if (callType == CallLog.Calls.VOICEMAIL_TYPE
+                && phoneCallDetailsViews.voicemailTranscriptionView.length() > 0) {
+            menu.add(ContextMenu.NONE, R.id.context_menu_copy_transcript_to_clipboard,
+                    ContextMenu.NONE, R.string.copy_transcript_text)
+                    .setOnMenuItemClickListener(this);
+        }
+
+        try {
+            mFilteredNumberAsyncQueryHandler.isBlocked(
+                    new FilteredNumberAsyncQueryHandler.OnCheckBlockedListener() {
+                        @Override
+                        public void onCheckComplete(Integer id) {
+                            blockId = id;
+                            int blockTitleId = blockId == null ? R.string.call_log_block_number
+                                    : R.string.call_log_unblock_number;
+                            final MenuItem blockItem = menu.add(
+                                    ContextMenu.NONE,
+                                    R.id.context_menu_block_number,
+                                    ContextMenu.NONE,
+                                    blockTitleId);
+                            blockItem.setOnMenuItemClickListener(
+                                    CallLogListItemViewHolder.this);
+                        }
+                    }, info.normalizedNumber, number, countryIso);
+        } catch (IllegalArgumentException e) {
+        }
+    }
+
+    @Override
+    public boolean onMenuItemClick(MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.context_menu_block_number:
+                FilterNumberDialogFragment newFragment =
+                        FilterNumberDialogFragment.newInstance(blockId, info.normalizedNumber,
+                                number, countryIso, info.formattedNumber);
+                newFragment.setQueryHandler(mFilteredNumberAsyncQueryHandler);
+                newFragment.show(((Activity) mContext).getFragmentManager(),
+                        FilterNumberDialogFragment.BLOCK_DIALOG_FRAGMENT);
+                return true;
+            case R.id.context_menu_copy_to_clipboard:
+                ClipboardUtils.copyText(mContext, null, number, true);
+                return true;
+            case R.id.context_menu_copy_transcript_to_clipboard:
+                ClipboardUtils.copyText(mContext, null,
+                        phoneCallDetailsViews.voicemailTranscriptionView.getText(), true);
+                return true;
+            case R.id.context_menu_edit_before_call:
+                final Intent intent = new Intent(
+                        Intent.ACTION_DIAL, CallUtil.getCallUri(number));
+                intent.setClass(mContext, DialtactsActivity.class);
+                DialerUtils.startActivityWithErrorToast(mContext, intent);
+                return true;
+        }
+        return false;
+    }
+
     /**
      * Configures the action buttons in the expandable actions ViewStub. The ViewStub is not
      * inflated during initial binding, so click handlers, tags and accessibility text must be set
@@ -488,6 +601,7 @@
                 telecomCallLogCache,
                 new CallLogListItemHelper(phoneCallDetailsHelper, resources, telecomCallLogCache),
                 null /* voicemailPlaybackPresenter */,
+                null /* filteredNumberAsyncQueryHandler */,
                 new View(context),
                 new QuickContactBadge(context),
                 new View(context),
diff --git a/src/com/android/dialer/calllog/GroupingListAdapter.java b/src/com/android/dialer/calllog/GroupingListAdapter.java
index 8d3ab45..54dd5f6 100644
--- a/src/com/android/dialer/calllog/GroupingListAdapter.java
+++ b/src/com/android/dialer/calllog/GroupingListAdapter.java
@@ -22,78 +22,28 @@
 import android.database.DataSetObserver;
 import android.os.Handler;
 import android.support.v7.widget.RecyclerView;
-import android.util.Log;
 import android.util.SparseIntArray;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.BaseAdapter;
-
-import com.android.contacts.common.testing.NeededForTesting;
 
 /**
- * Maintains a list that groups adjacent items sharing the same value of a "group-by" field.
+ * Maintains a list that groups items into groups of consecutive elements which are disjoint,
+ * that is, an item can only belong to one group. This is leveraged for grouping calls in the
+ * call log received from or made to the same phone number.
  *
- * The list has three types of elements: stand-alone, group header and group child. Groups are
- * collapsible and collapsed by default. This is used by the call log to group related entries.
+ * There are two integers stored as metadata for every list item in the adapter.
  */
 abstract class GroupingListAdapter extends RecyclerView.Adapter {
 
-    private static final int GROUP_METADATA_ARRAY_INITIAL_SIZE = 16;
-    private static final int GROUP_METADATA_ARRAY_INCREMENT = 128;
-    private static final long GROUP_OFFSET_MASK    = 0x00000000FFFFFFFFL;
-    private static final long GROUP_SIZE_MASK     = 0x7FFFFFFF00000000L;
-    private static final long EXPANDED_GROUP_MASK = 0x8000000000000000L;
-
-    public static final int ITEM_TYPE_STANDALONE = 0;
-    public static final int ITEM_TYPE_GROUP_HEADER = 1;
-    public static final int ITEM_TYPE_IN_GROUP = 2;
-
-    /**
-     * Information about a specific list item: is it a group, if so is it expanded.
-     * Otherwise, is it a stand-alone item or a group member.
-     */
-    protected static class PositionMetadata {
-        int itemType;
-        boolean isExpanded;
-        int cursorPosition;
-        int childCount;
-        private int groupPosition;
-        private int listPosition = -1;
-    }
-
     private Context mContext;
     private Cursor mCursor;
 
     /**
-     * Count of list items.
+     * SparseIntArray, which maps the cursor position of the first element of a group to the size
+     * of the group. The index of a key in this map corresponds to the list position of that group.
      */
-    private int mCount;
-
-    private int mRowIdColumnIndex;
-
-    /**
-     * Count of groups in the list.
-     */
-    private int mGroupCount;
-
-    /**
-     * Information about where these groups are located in the list, how large they are
-     * and whether they are expanded.
-     */
-    private long[] mGroupMetadata;
-
-    private SparseIntArray mPositionCache = new SparseIntArray();
-    private int mLastCachedListPosition;
-    private int mLastCachedCursorPosition;
-    private int mLastCachedGroup;
-
-    /**
-     * A reusable temporary instance of PositionMetadata
-     */
-    private PositionMetadata mPositionMetadata = new PositionMetadata();
+    private SparseIntArray mGroupMetadata;
+    private int mItemCount;
 
     protected ContentObserver mChangeObserver = new ContentObserver(new Handler()) {
-
         @Override
         public boolean deliverSelfNotifications() {
             return true;
@@ -106,7 +56,6 @@
     };
 
     protected DataSetObserver mDataSetObserver = new DataSetObserver() {
-
         @Override
         public void onChanged() {
             notifyDataSetChanged();
@@ -115,7 +64,7 @@
 
     public GroupingListAdapter(Context context) {
         mContext = context;
-        resetCache();
+        reset();
     }
 
     /**
@@ -126,18 +75,6 @@
 
     protected abstract void onContentChanged();
 
-    /**
-     * Cache should be reset whenever the cursor changes or groups are expanded or collapsed.
-     */
-    private void resetCache() {
-        mCount = -1;
-        mLastCachedListPosition = -1;
-        mLastCachedCursorPosition = -1;
-        mLastCachedGroup = -1;
-        mPositionMetadata.listPosition = -1;
-        mPositionCache.clear();
-    }
-
     public void changeCursor(Cursor cursor) {
         if (cursor == mCursor) {
             return;
@@ -148,288 +85,73 @@
             mCursor.unregisterDataSetObserver(mDataSetObserver);
             mCursor.close();
         }
+
+        // Reset whenever the cursor is changed.
+        reset();
         mCursor = cursor;
-        resetCache();
-        findGroups();
 
         if (cursor != null) {
+            addGroups(mCursor);
+
+            // Calculate the item count by subtracting group child counts from the cursor count.
+            mItemCount = mGroupMetadata.size();
+
             cursor.registerContentObserver(mChangeObserver);
             cursor.registerDataSetObserver(mDataSetObserver);
-            mRowIdColumnIndex = cursor.getColumnIndexOrThrow("_id");
             notifyDataSetChanged();
         }
     }
 
-    @NeededForTesting
-    public Cursor getCursor() {
-        return mCursor;
-    }
-
     /**
-     * Scans over the entire cursor looking for duplicate phone numbers that need
-     * to be collapsed.
+     * Records information about grouping in the list.
+     * Should be called by the overridden {@link #addGroups} method.
      */
-    private void findGroups() {
-        mGroupCount = 0;
-        mGroupMetadata = new long[GROUP_METADATA_ARRAY_INITIAL_SIZE];
-
-        if (mCursor == null) {
-            return;
+    public void addGroup(int cursorPosition, int groupSize) {
+        int lastIndex = mGroupMetadata.size() - 1;
+        if (lastIndex < 0 || cursorPosition <= mGroupMetadata.keyAt(lastIndex)) {
+            mGroupMetadata.put(cursorPosition, groupSize);
+        } else {
+            // Optimization to avoid binary search if adding groups in ascending cursor position.
+            mGroupMetadata.append(cursorPosition, groupSize);
         }
-
-        addGroups(mCursor);
-    }
-
-    /**
-     * Records information about grouping in the list.  Should be called by the overridden
-     * {@link #addGroups} method.
-     */
-    protected void addGroup(int cursorPosition, int size, boolean expanded) {
-        if (mGroupCount >= mGroupMetadata.length) {
-            int newSize = idealLongArraySize(
-                    mGroupMetadata.length + GROUP_METADATA_ARRAY_INCREMENT);
-            long[] array = new long[newSize];
-            System.arraycopy(mGroupMetadata, 0, array, 0, mGroupCount);
-            mGroupMetadata = array;
-        }
-
-        long metadata = ((long)size << 32) | cursorPosition;
-        if (expanded) {
-            metadata |= EXPANDED_GROUP_MASK;
-        }
-        mGroupMetadata[mGroupCount++] = metadata;
-    }
-
-    // Copy/paste from ArrayUtils
-    private int idealLongArraySize(int need) {
-        return idealByteArraySize(need * 8) / 8;
-    }
-
-    // Copy/paste from ArrayUtils
-    private int idealByteArraySize(int need) {
-        for (int i = 4; i < 32; i++)
-            if (need <= (1 << i) - 12)
-                return (1 << i) - 12;
-
-        return need;
     }
 
     @Override
     public int getItemCount() {
-        if (mCursor == null) {
+        return mItemCount;
+    }
+
+    /**
+     * Given the position of a list item, returns the size of the group of items corresponding to
+     * that position.
+     */
+    public int getGroupSize(int listPosition) {
+        if (listPosition >= mGroupMetadata.size()) {
             return 0;
         }
 
-        if (mCount != -1) {
-            return mCount;
-        }
-
-        int cursorPosition = 0;
-        int count = 0;
-        for (int i = 0; i < mGroupCount; i++) {
-            long metadata = mGroupMetadata[i];
-            int offset = (int)(metadata & GROUP_OFFSET_MASK);
-            boolean expanded = (metadata & EXPANDED_GROUP_MASK) != 0;
-            int size = (int)((metadata & GROUP_SIZE_MASK) >> 32);
-
-            count += (offset - cursorPosition);
-
-            if (expanded) {
-                count += size + 1;
-            } else {
-                count++;
-            }
-
-            cursorPosition = offset + size;
-        }
-
-        mCount = count + mCursor.getCount() - cursorPosition;
-        return mCount;
+        return mGroupMetadata.valueAt(listPosition);
     }
 
     /**
-     * Figures out whether the item at the specified position represents a
-     * stand-alone element, a group or a group child. Also computes the
-     * corresponding cursor position.
+     * Given the position of a list item, returns the the first item in the group of items
+     * corresponding to that position.
      */
-    public void obtainPositionMetadata(PositionMetadata metadata, int position) {
-        // If the description object already contains requested information, just return
-        if (metadata.listPosition == position) {
-            return;
-        }
-
-        int listPosition = 0;
-        int cursorPosition = 0;
-        int firstGroupToCheck = 0;
-
-        // Check cache for the supplied position.  What we are looking for is
-        // the group descriptor immediately preceding the supplied position.
-        // Once we have that, we will be able to tell whether the position
-        // is the header of the group, a member of the group or a standalone item.
-        if (mLastCachedListPosition != -1) {
-            if (position <= mLastCachedListPosition) {
-
-                // Have SparceIntArray do a binary search for us.
-                int index = mPositionCache.indexOfKey(position);
-
-                // If we get back a positive number, the position corresponds to
-                // a group header.
-                if (index < 0) {
-
-                    // We had a cache miss, but we did obtain valuable information anyway.
-                    // The negative number will allow us to compute the location of
-                    // the group header immediately preceding the supplied position.
-                    index = ~index - 1;
-
-                    if (index >= mPositionCache.size()) {
-                        index--;
-                    }
-                }
-
-                // A non-negative index gives us the position of the group header
-                // corresponding or preceding the position, so we can
-                // search for the group information at the supplied position
-                // starting with the cached group we just found
-                if (index >= 0) {
-                    listPosition = mPositionCache.keyAt(index);
-                    firstGroupToCheck = mPositionCache.valueAt(index);
-                    long descriptor = mGroupMetadata[firstGroupToCheck];
-                    cursorPosition = (int)(descriptor & GROUP_OFFSET_MASK);
-                }
-            } else {
-
-                // If we haven't examined groups beyond the supplied position,
-                // we will start where we left off previously
-                firstGroupToCheck = mLastCachedGroup;
-                listPosition = mLastCachedListPosition;
-                cursorPosition = mLastCachedCursorPosition;
-            }
-        }
-
-        for (int i = firstGroupToCheck; i < mGroupCount; i++) {
-            long group = mGroupMetadata[i];
-            int offset = (int)(group & GROUP_OFFSET_MASK);
-
-            // Move pointers to the beginning of the group
-            listPosition += (offset - cursorPosition);
-            cursorPosition = offset;
-
-            if (i > mLastCachedGroup) {
-                mPositionCache.append(listPosition, i);
-                mLastCachedListPosition = listPosition;
-                mLastCachedCursorPosition = cursorPosition;
-                mLastCachedGroup = i;
-            }
-
-            // Now we have several possibilities:
-            // A) The requested position precedes the group
-            if (position < listPosition) {
-                metadata.itemType = ITEM_TYPE_STANDALONE;
-                metadata.cursorPosition = cursorPosition - (listPosition - position);
-                metadata.childCount = 1;
-                return;
-            }
-
-            boolean expanded = (group & EXPANDED_GROUP_MASK) != 0;
-            int size = (int) ((group & GROUP_SIZE_MASK) >> 32);
-
-            // B) The requested position is a group header
-            if (position == listPosition) {
-                metadata.itemType = ITEM_TYPE_GROUP_HEADER;
-                metadata.groupPosition = i;
-                metadata.isExpanded = expanded;
-                metadata.childCount = size;
-                metadata.cursorPosition = offset;
-                return;
-            }
-
-            if (expanded) {
-                // C) The requested position is an element in the expanded group
-                if (position < listPosition + size + 1) {
-                    metadata.itemType = ITEM_TYPE_IN_GROUP;
-                    metadata.cursorPosition = cursorPosition + (position - listPosition) - 1;
-                    return;
-                }
-
-                // D) The element is past the expanded group
-                listPosition += size + 1;
-            } else {
-
-                // E) The element is past the collapsed group
-                listPosition++;
-            }
-
-            // Move cursor past the group
-            cursorPosition += size;
-        }
-
-        // The required item is past the last group
-        metadata.itemType = ITEM_TYPE_STANDALONE;
-        metadata.cursorPosition = cursorPosition + (position - listPosition);
-        metadata.childCount = 1;
-    }
-
-    /**
-     * Returns true if the specified position in the list corresponds to a
-     * group header.
-     */
-    public boolean isGroupHeader(int position) {
-        obtainPositionMetadata(mPositionMetadata, position);
-        return mPositionMetadata.itemType == ITEM_TYPE_GROUP_HEADER;
-    }
-
-    /**
-     * Given a position of a groups header in the list, returns the size of
-     * the corresponding group.
-     */
-    public int getGroupSize(int position) {
-        obtainPositionMetadata(mPositionMetadata, position);
-        return mPositionMetadata.childCount;
-    }
-
-    /**
-     * Mark group as expanded if it is collapsed and vice versa.
-     */
-    @NeededForTesting
-    public void toggleGroup(int position) {
-        obtainPositionMetadata(mPositionMetadata, position);
-        if (mPositionMetadata.itemType != ITEM_TYPE_GROUP_HEADER) {
-            throw new IllegalArgumentException("Not a group at position " + position);
-        }
-
-        if (mPositionMetadata.isExpanded) {
-            mGroupMetadata[mPositionMetadata.groupPosition] &= ~EXPANDED_GROUP_MASK;
-        } else {
-            mGroupMetadata[mPositionMetadata.groupPosition] |= EXPANDED_GROUP_MASK;
-        }
-        resetCache();
-        notifyDataSetChanged();
-    }
-
-    public int getItemViewType(int position) {
-        obtainPositionMetadata(mPositionMetadata, position);
-        return mPositionMetadata.itemType;
-    }
-
-    public Object getItem(int position) {
-        if (mCursor == null) {
+    public Object getItem(int listPosition) {
+        if (mCursor == null || listPosition >= mGroupMetadata.size()) {
             return null;
         }
 
-        obtainPositionMetadata(mPositionMetadata, position);
-        if (mCursor.moveToPosition(mPositionMetadata.cursorPosition)) {
+        int cursorPosition = mGroupMetadata.keyAt(listPosition);
+        if (mCursor.moveToPosition(cursorPosition)) {
             return mCursor;
         } else {
             return null;
         }
     }
 
-    public long getItemId(int position) {
-        Object item = getItem(position);
-        if (item != null) {
-            return mCursor.getLong(mRowIdColumnIndex);
-        } else {
-            return -1;
-        }
+    private void reset() {
+        mItemCount = 0;
+        mGroupMetadata = new SparseIntArray();
     }
 }
diff --git a/src/com/android/dialer/calllog/PromoCardViewHolder.java b/src/com/android/dialer/calllog/PromoCardViewHolder.java
index 4c96027..656b669 100644
--- a/src/com/android/dialer/calllog/PromoCardViewHolder.java
+++ b/src/com/android/dialer/calllog/PromoCardViewHolder.java
@@ -15,12 +15,14 @@
  */
 package com.android.dialer.calllog;
 
-import com.android.dialer.R;
-
+import android.content.Context;
 import android.support.v7.widget.CardView;
 import android.support.v7.widget.RecyclerView;
 import android.view.View;
 
+import com.android.contacts.common.testing.NeededForTesting;
+import com.android.dialer.R;
+
 /**
  * View holder class for a promo card which will appear in the voicemail tab.
  */
@@ -68,4 +70,12 @@
     public View getOkTextView() {
         return mOkTextView;
     }
+
+    @NeededForTesting
+    public static PromoCardViewHolder createForTest(Context context) {
+        PromoCardViewHolder viewHolder = new PromoCardViewHolder(new View(context));
+        viewHolder.mSettingsTextView = new View(context);
+        viewHolder.mOkTextView = new View(context);
+        return viewHolder;
+    }
 }
diff --git a/src/com/android/dialer/database/FilteredNumberAsyncQueryHandler.java b/src/com/android/dialer/database/FilteredNumberAsyncQueryHandler.java
index 58a717b..2fdea0d 100644
--- a/src/com/android/dialer/database/FilteredNumberAsyncQueryHandler.java
+++ b/src/com/android/dialer/database/FilteredNumberAsyncQueryHandler.java
@@ -21,6 +21,8 @@
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabaseCorruptException;
 import android.net.Uri;
 import android.telephony.PhoneNumberUtils;
 
@@ -51,15 +53,28 @@
     }
 
     public interface OnCheckBlockedListener {
-        public void onQueryComplete(Integer id);
+        /**
+         * Invoked after querying if a number is blocked.
+         * @param id The ID of the row if blocked, null otherwise.
+         */
+        public void onCheckComplete(Integer id);
     }
 
     public interface OnBlockNumberListener {
-        public void onInsertComplete(Uri uri);
+        /**
+         * Invoked after inserting a blocked number.
+         * @param uri The uri of the newly created row.
+         */
+        public void onBlockComplete(Uri uri);
     }
 
     public interface OnUnblockNumberListener {
-        public void onDeleteComplete(int rows);
+        /**
+         * Invoked after removing a blocked number
+         * @param rows The number of rows affected (expected value 1).
+         * @param values The deleted data (used for restoration).
+         */
+        public void onUnblockComplete(int rows, ContentValues values);
     }
 
     @Override
@@ -93,7 +108,6 @@
     /**
      * Check if the number + country iso given has been blocked.
      * This method normalizes the number for the lookup if normalizedNumber is null.
-     * Returns to the listener the the ID of the row if blocked, null otherwise.
      */
     public final void isBlocked(final OnCheckBlockedListener listener,
                                 String normalizedNumber, String number, String countryIso) {
@@ -108,25 +122,24 @@
 
     /**
      * Check if the normalized number given has been blocked.
-     * Returns to the listener  the ID of the row if blocked, null otherwise.
      */
     public final void isBlocked(final OnCheckBlockedListener listener,
-                                       String normalizedNumber) {
+                                String normalizedNumber) {
         startQuery(NO_TOKEN,
                 new Listener() {
                     @Override
                     protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
                         if (cursor.getCount() != 1) {
-                            listener.onQueryComplete(null);
+                            listener.onCheckComplete(null);
                             return;
                         }
                         cursor.moveToFirst();
                         if (cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns.TYPE))
                                 != FilteredNumberTypes.BLOCKED_NUMBER) {
-                            listener.onQueryComplete(null);
+                            listener.onCheckComplete(null);
                             return;
                         }
-                        listener.onQueryComplete(
+                        listener.onCheckComplete(
                                 cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns._ID)));
                     }
                 },
@@ -139,22 +152,11 @@
 
     /**
      * Add a number manually blocked by the user.
-     * Returns to the listener the URL of the newly created row.
      */
     public final void blockNumber(final OnBlockNumberListener listener,
-                                  String number, String countryIso) {
-        blockNumber(listener,
-                PhoneNumberUtils.formatNumberToE164(number, countryIso), number, countryIso);
-    }
-
-    /**
-     * Add a number manually blocked by the user.
-     * Returns to the listener the URL of the newly created row.
-     */
-    public final void blockNumber(final OnBlockNumberListener listener,
-                                        String normalizedNumber, String number, String countryIso) {
+                                  String normalizedNumber, String number, String countryIso) {
         if (normalizedNumber == null) {
-            blockNumber(listener, number, countryIso);
+            normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso);
         }
         ContentValues v = new ContentValues();
         v.put(FilteredNumberColumns.NORMALIZED_NUMBER, normalizedNumber);
@@ -162,30 +164,61 @@
         v.put(FilteredNumberColumns.COUNTRY_ISO, countryIso);
         v.put(FilteredNumberColumns.TYPE, FilteredNumberTypes.BLOCKED_NUMBER);
         v.put(FilteredNumberColumns.SOURCE, FilteredNumberSources.USER);
+        blockNumber(listener, v);
+    }
+
+    /**
+     * Block a number with specified ContentValues. Can be manually added or a restored row
+     * from performing the 'undo' action after unblocking.
+     */
+    public final void blockNumber(final OnBlockNumberListener listener, ContentValues values) {
         startInsert(NO_TOKEN,
                 new Listener() {
                     @Override
                     public void onInsertComplete(int token, Object cookie, Uri uri) {
-                        listener.onInsertComplete(uri);
+                        listener.onBlockComplete(uri);
                     }
-                }, getContentUri(null), v);
+                }, getContentUri(null), values);
     }
 
     /**
      * Removes row from database.
      * Caller should call {@link FilteredNumberAsyncQueryHandler#isBlocked} first.
-     * @param id the ID of the row to remove, from {@link FilteredNumberAsyncQueryHandler#isBlocked}.
-     * Returns to the listener the number of rows affected. Expected value is 1.
+     * @param id The ID of row to remove, from {@link FilteredNumberAsyncQueryHandler#isBlocked}.
      */
     public final void unblock(final OnUnblockNumberListener listener, Integer id) {
         if (id == null) {
             throw new IllegalArgumentException("Null id passed into unblock");
         }
-        startDelete(NO_TOKEN, new Listener() {
-            @Override
-            public void onDeleteComplete(int token, Object cookie, int result) {
-                listener.onDeleteComplete(result);
-            }
-        }, getContentUri(id), null, null);
+        unblock(listener, getContentUri(id));
     }
-}
+
+    /**
+     * Removes row from database.
+     * @param uri The uri of row to remove, from
+     *         {@link FilteredNumberAsyncQueryHandler#blockNumber}.
+     */
+    public final void unblock(final OnUnblockNumberListener listener, final Uri uri) {
+        startQuery(NO_TOKEN, new Listener() {
+            @Override
+            public void onQueryComplete(int token, Object cookie, Cursor cursor) {
+                if (cursor.getCount() != 1) {
+                    throw new SQLiteDatabaseCorruptException
+                            ("Returned " + cursor.getCount() + " rows for uri "
+                                    + uri + "where 1 expected.");
+                }
+                cursor.moveToFirst();
+                final ContentValues values = new ContentValues();
+                DatabaseUtils.cursorRowToContentValues(cursor, values);
+                values.remove(FilteredNumberColumns._ID);
+
+                startDelete(NO_TOKEN, new Listener() {
+                    @Override
+                    public void onDeleteComplete(int token, Object cookie, int result) {
+                        listener.onUnblockComplete(result, values);
+                    }
+                }, uri, null, null);
+            }
+        }, uri, null, null, null, null);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/dialer/dialpad/DialpadFragment.java b/src/com/android/dialer/dialpad/DialpadFragment.java
index 01dc892..0bbf802 100644
--- a/src/com/android/dialer/dialpad/DialpadFragment.java
+++ b/src/com/android/dialer/dialpad/DialpadFragment.java
@@ -481,7 +481,10 @@
      * @param intent The intent.
      * @return {@literal true} if add call operation was requested.  {@literal false} otherwise.
      */
-    private static boolean isAddCallMode(Intent intent) {
+    public static boolean isAddCallMode(Intent intent) {
+        if (intent == null) {
+            return false;
+        }
         final String action = intent.getAction();
         if (Intent.ACTION_DIAL.equals(action) || Intent.ACTION_VIEW.equals(action)) {
             // see if we are "adding a call" from the InCallScreen; false by default.
diff --git a/src/com/android/dialer/filterednumber/FilterNumberDialogFragment.java b/src/com/android/dialer/filterednumber/FilterNumberDialogFragment.java
new file mode 100644
index 0000000..f94d0f8
--- /dev/null
+++ b/src/com/android/dialer/filterednumber/FilterNumberDialogFragment.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.filterednumber;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.ContentValues;
+import android.content.DialogInterface;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.design.widget.Snackbar;
+import android.view.View;
+
+import com.android.dialer.R;
+import com.android.dialer.database.FilteredNumberAsyncQueryHandler;
+
+public class FilterNumberDialogFragment extends DialogFragment {
+    public static final String BLOCK_DIALOG_FRAGMENT = "blockUnblockNumberDialog";
+
+    private static final String ARG_BLOCK_ID = "argBlockId";
+    private static final String ARG_NORMALIZED_NUMBER = "argNormalizedNumber";
+    private static final String ARG_NUMBER = "argNumber";
+    private static final String ARG_COUNTRY_ISO = "argCountryIso";
+    private static final String ARG_DISPLAY_NUMBER = "argDisplayNumber";
+
+    private FilteredNumberAsyncQueryHandler mHandler;
+
+    public void setQueryHandler (FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler) {
+        mHandler = filteredNumberAsyncQueryHandler;
+    }
+
+    public static FilterNumberDialogFragment newInstance(Integer blockId, String normalizedNumber,
+        String number, String countryIso, String displayNumber) {
+        final FilterNumberDialogFragment fragment = new FilterNumberDialogFragment();
+        final Bundle args = new Bundle();
+        if (blockId != null) {
+            args.putInt(ARG_BLOCK_ID, blockId.intValue());
+        }
+        args.putString(ARG_NORMALIZED_NUMBER, normalizedNumber);
+        args.putString(ARG_NUMBER, number);
+        args.putString(ARG_COUNTRY_ISO, countryIso);
+        args.putString(ARG_DISPLAY_NUMBER, displayNumber);
+        fragment.setArguments(args);
+        return fragment;
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        super.onCreateDialog(savedInstanceState);
+        final boolean isBlocked = getArguments().containsKey(ARG_BLOCK_ID);
+        final String displayNumber = getArguments().getString(ARG_DISPLAY_NUMBER);
+
+        String message;
+        String okText;
+        if (isBlocked) {
+            message = getString(R.string.unblockNumberConfirmation, displayNumber);
+            okText = getString(R.string.unblockNumberOk);
+        } else {
+            message = getString(R.string.blockNumberConfirmation, displayNumber);
+            okText = getString(R.string.blockNumberOk);
+        }
+
+        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
+                .setMessage(message)
+                .setPositiveButton(okText, new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int id) {
+                        if (isBlocked) {
+                            unblockNumber();
+                        } else {
+                            blockNumber();
+                        }
+                    }
+                })
+                .setNegativeButton(android.R.string.cancel, null);
+        return builder.create();
+    }
+
+    public void blockNumber() {
+        final View view = getActivity().findViewById(R.id.floating_action_button_container);
+        final String displayNumber = getArguments().getString(ARG_DISPLAY_NUMBER);
+        final String message = getString(R.string.snackbar_number_blocked, displayNumber);
+        final String undoMessage = getString(R.string.snackbar_number_unblocked, displayNumber);
+        final FilteredNumberAsyncQueryHandler.OnUnblockNumberListener undoListener =
+                new FilteredNumberAsyncQueryHandler.OnUnblockNumberListener() {
+                    @Override
+                    public void onUnblockComplete(int rows, ContentValues values) {
+                        Snackbar.make(view, undoMessage, Snackbar.LENGTH_LONG).show();
+                    }
+                };
+
+        mHandler.blockNumber(
+                new FilteredNumberAsyncQueryHandler.OnBlockNumberListener() {
+                    @Override
+                    public void onBlockComplete(final Uri uri) {
+                        Snackbar.make(view, message, Snackbar.LENGTH_LONG)
+                                .setAction(R.string.block_number_undo,
+                                        // Delete the newly created row on 'undo'.
+                                        new View.OnClickListener() {
+                                            @Override
+                                            public void onClick(View view) {
+                                                mHandler.unblock(undoListener, uri);
+                                            }
+                                        })
+                                .show();
+                    }
+                }, getArguments().getString(ARG_NORMALIZED_NUMBER),
+                getArguments().getString(ARG_NUMBER), getArguments().getString(ARG_COUNTRY_ISO));
+    }
+
+    public void unblockNumber() {
+        final View view = getActivity().findViewById(R.id.floating_action_button_container);
+        final String displayNumber = getArguments().getString(ARG_DISPLAY_NUMBER);
+        final String message = getString(R.string.snackbar_number_unblocked, displayNumber);
+        final String undoMessage = getString(R.string.snackbar_number_blocked, displayNumber);
+        final FilteredNumberAsyncQueryHandler.OnBlockNumberListener undoListener =
+                new FilteredNumberAsyncQueryHandler.OnBlockNumberListener() {
+                    @Override
+                    public void onBlockComplete(final Uri uri) {
+                        Snackbar.make(view, undoMessage, Snackbar.LENGTH_LONG).show();
+                    }
+                };
+        mHandler.unblock(
+                new FilteredNumberAsyncQueryHandler.OnUnblockNumberListener() {
+                    @Override
+                    public void onUnblockComplete(int rows, final ContentValues values) {
+                        Snackbar.make(view, message, Snackbar.LENGTH_LONG)
+                                .setAction(R.string.block_number_undo,
+                                        new View.OnClickListener() {
+                                            // Re-insert the row on 'undo', with a new ID.
+                                            @Override
+                                            public void onClick(View view) {
+                                                mHandler.blockNumber(undoListener, values);
+                                            }
+                                        })
+                                .show();
+                    }
+                }, getArguments().getInt(ARG_BLOCK_ID));
+    }
+}
\ No newline at end of file
diff --git a/tests/src/com/android/dialer/CallDetailActivityTest.java b/tests/src/com/android/dialer/CallDetailActivityTest.java
index c874244..59c2434 100644
--- a/tests/src/com/android/dialer/CallDetailActivityTest.java
+++ b/tests/src/com/android/dialer/CallDetailActivityTest.java
@@ -29,12 +29,12 @@
 import android.test.suitebuilder.annotation.LargeTest;
 import android.test.suitebuilder.annotation.Suppress;
 import android.view.Menu;
+import android.widget.PopupMenu;
 import android.widget.TextView;
 
 import com.android.dialer.calllog.CallLogAsyncTaskUtil;
 import com.android.dialer.util.AsyncTaskExecutors;
 import com.android.dialer.util.FakeAsyncTaskExecutor;
-// import com.android.internal.view.menu.ContextMenuBuilder;
 
 /**
  * Unit tests for the {@link CallDetailActivity}. NOTE: The screen needs to be on for the
@@ -86,33 +86,33 @@
      * Test for bug where voicemails should not have remove-from-call-log entry.
      * <p>
      * See http://b/5054103.
+     */
     public void testVoicemailDoesNotHaveRemoveFromCallLog() throws Throwable {
         setActivityIntentForTestVoicemailEntry();
         startActivityUnderTest();
         mFakeAsyncTaskExecutor.runTask(GET_CALL_DETAILS);
 
-        Menu menu = new ContextMenuBuilder(mActivityUnderTest);
-        mActivityUnderTest.onCreateOptionsMenu(menu);
-        mActivityUnderTest.onPrepareOptionsMenu(menu);
-        assertFalse(menu.findItem(R.id.menu_remove_from_call_log).isVisible());
-        assertTrue(menu.findItem(R.id.menu_trash).isVisible());
+        Menu optionsMenu = (new PopupMenu(mActivityUnderTest, null)).getMenu();
+        mActivityUnderTest.onCreateOptionsMenu(optionsMenu);
+        mActivityUnderTest.onPrepareOptionsMenu(optionsMenu);
+        assertFalse(optionsMenu.findItem(R.id.menu_remove_from_call_log).isVisible());
+        assertTrue(optionsMenu.findItem(R.id.menu_trash).isVisible());
     }
-    */
 
     /**
      * Test to check that I haven't broken the remove-from-call-log entry from regular calls.
+     */
     public void testRegularCallDoesHaveRemoveFromCallLog() throws Throwable {
         setActivityIntentForTestCallEntry();
         startActivityUnderTest();
         mFakeAsyncTaskExecutor.runTask(GET_CALL_DETAILS);
 
-        Menu menu = new ContextMenuBuilder(mActivityUnderTest);
-        mActivityUnderTest.onCreateOptionsMenu(menu);
-        mActivityUnderTest.onPrepareOptionsMenu(menu);
-        assertTrue(menu.findItem(R.id.menu_remove_from_call_log).isVisible());
-        assertFalse(menu.findItem(R.id.menu_trash).isVisible());
+        Menu optionsMenu = (new PopupMenu(mActivityUnderTest, null)).getMenu();
+        mActivityUnderTest.onCreateOptionsMenu(optionsMenu);
+        mActivityUnderTest.onPrepareOptionsMenu(optionsMenu);
+        assertTrue(optionsMenu.findItem(R.id.menu_remove_from_call_log).isVisible());
+        assertFalse(optionsMenu.findItem(R.id.menu_trash).isVisible());
     }
-    */
 
     private void setActivityIntentForTestCallEntry() {
         assertNull(mVoicemailUri);
diff --git a/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java b/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
index b4162e1..5c2588a 100644
--- a/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
+++ b/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
@@ -35,7 +35,9 @@
  */
 @SmallTest
 public class CallLogAdapterTest extends AndroidTestCase {
-    private static final String TEST_NUMBER = "12345678";
+    private static final String TEST_NUMBER_1 = "12345678";
+    private static final String TEST_NUMBER_2 = "87654321";
+    private static final String TEST_NUMBER_3 = "18273645";
     private static final String TEST_NAME = "name";
     private static final String TEST_NUMBER_LABEL = "label";
     private static final int TEST_NUMBER_TYPE = 1;
@@ -46,7 +48,7 @@
 
     private MatrixCursor mCursor;
     private View mView;
-    private ViewHolder mViewHolder;
+    private CallLogListItemViewHolder mViewHolder;
 
     @Override
     protected void setUp() throws Exception {
@@ -98,7 +100,7 @@
 
         TestContactInfoCache.Request request = mAdapter.getContactInfoCache().requests.get(0);
         // It is for the number we need to show.
-        assertEquals(TEST_NUMBER, request.number);
+        assertEquals(TEST_NUMBER_1, request.number);
         // It has the right country.
         assertEquals(TEST_COUNTRY_ISO, request.countryIso);
         // Since there is nothing in the cache, it is an immediate request.
@@ -125,7 +127,7 @@
 
     public void testBindView_NoCallLogButMemoryCache_EnqueueRequest() {
         mCursor.addRow(createCallLogEntry());
-        mAdapter.injectContactInfoForTest(TEST_NUMBER, TEST_COUNTRY_ISO, createContactInfo());
+        mAdapter.injectContactInfoForTest(TEST_NUMBER_1, TEST_COUNTRY_ISO, createContactInfo());
 
         // Bind the views of a single row.
         mAdapter.changeCursor(mCursor);
@@ -141,7 +143,7 @@
 
     public void testBindView_BothCallLogAndMemoryCache_NoEnqueueRequest() {
         mCursor.addRow(createCallLogEntryWithCachedValues());
-        mAdapter.injectContactInfoForTest(TEST_NUMBER, TEST_COUNTRY_ISO, createContactInfo());
+        mAdapter.injectContactInfoForTest(TEST_NUMBER_1, TEST_COUNTRY_ISO, createContactInfo());
 
         // Bind the views of a single row.
         mAdapter.changeCursor(mCursor);
@@ -157,7 +159,7 @@
         // Contact info contains a different name.
         ContactInfo info = createContactInfo();
         info.name = "new name";
-        mAdapter.injectContactInfoForTest(TEST_NUMBER, TEST_COUNTRY_ISO, info);
+        mAdapter.injectContactInfoForTest(TEST_NUMBER_1, TEST_COUNTRY_ISO, info);
 
         // Bind the views of a single row.
         mAdapter.changeCursor(mCursor);
@@ -171,10 +173,37 @@
         assertFalse("should not be immediate", request.immediate);
     }
 
+    public void testBindVoicemailPromoCard() {
+        mCursor.addRow(createCallLogEntry(TEST_NUMBER_1));
+        mCursor.addRow(createCallLogEntry(TEST_NUMBER_1));
+        mCursor.addRow(createCallLogEntry(TEST_NUMBER_2));
+        mCursor.addRow(createCallLogEntry(TEST_NUMBER_2));
+        mCursor.addRow(createCallLogEntry(TEST_NUMBER_2));
+        mCursor.addRow(createCallLogEntry(TEST_NUMBER_3));
+
+        // Bind the voicemail promo card.
+        mAdapter.showVoicemailPromoCard(true);
+        mAdapter.changeCursor(mCursor);
+        mAdapter.onBindViewHolder(PromoCardViewHolder.createForTest(getContext()), 0);
+
+        // Check that displaying the promo card does not affect the grouping or list display.
+        mAdapter.onBindViewHolder(mViewHolder, 1);
+        assertEquals(2, mAdapter.getGroupSize(1));
+        assertEquals(TEST_NUMBER_1, mViewHolder.number);
+
+        mAdapter.onBindViewHolder(mViewHolder, 2);
+        assertEquals(3, mAdapter.getGroupSize(2));
+        assertEquals(TEST_NUMBER_2, mViewHolder.number);
+
+        mAdapter.onBindViewHolder(mViewHolder, 3);
+        assertEquals(1, mAdapter.getGroupSize(3));
+        assertEquals(TEST_NUMBER_3, mViewHolder.number);
+    }
+
     /** Returns a contact info with default values. */
     private ContactInfo createContactInfo() {
         ContactInfo info = new ContactInfo();
-        info.number = TEST_NUMBER;
+        info.number = TEST_NUMBER_1;
         info.name = TEST_NAME;
         info.type = TEST_NUMBER_TYPE;
         info.label = TEST_NUMBER_LABEL;
@@ -183,8 +212,12 @@
 
     /** Returns a call log entry without cached values. */
     private Object[] createCallLogEntry() {
+        return createCallLogEntry(TEST_NUMBER_1);
+    }
+
+    private Object[] createCallLogEntry(String testNumber) {
         Object[] values = CallLogQueryTestUtils.createTestValues();
-        values[CallLogQuery.NUMBER] = TEST_NUMBER;
+        values[CallLogQuery.NUMBER] = testNumber;
         values[CallLogQuery.COUNTRY_ISO] = TEST_COUNTRY_ISO;
         return values;
     }
@@ -212,6 +245,10 @@
         public TestContactInfoCache getContactInfoCache() {
             return (TestContactInfoCache) mContactInfoCache;
         }
+
+        public void showVoicemailPromoCard(boolean show) {
+            mShowVoicemailPromoCard = show;
+        }
     }
 
     private static final class TestContactInfoCache extends ContactInfoCache {
diff --git a/tests/src/com/android/dialer/calllog/CallLogGroupBuilderTest.java b/tests/src/com/android/dialer/calllog/CallLogGroupBuilderTest.java
index 891f068..95558bc 100644
--- a/tests/src/com/android/dialer/calllog/CallLogGroupBuilderTest.java
+++ b/tests/src/com/android/dialer/calllog/CallLogGroupBuilderTest.java
@@ -82,7 +82,7 @@
         addCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
         mBuilder.addGroups(mCursor);
         assertEquals(1, mFakeGroupCreator.groups.size());
-        assertGroupIs(0, 3, false, mFakeGroupCreator.groups.get(0));
+        assertGroupIs(0, 3, mFakeGroupCreator.groups.get(0));
     }
 
     public void testAddGroups_MatchingIncomingAndOutgoing() {
@@ -91,13 +91,12 @@
         addCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
         mBuilder.addGroups(mCursor);
         assertEquals(1, mFakeGroupCreator.groups.size());
-        assertGroupIs(0, 3, false, mFakeGroupCreator.groups.get(0));
+        assertGroupIs(0, 3, mFakeGroupCreator.groups.get(0));
     }
 
     public void testAddGroups_Voicemail() {
         // Does not group with other types of calls, include voicemail themselves.
         assertCallsAreNotGrouped(Calls.VOICEMAIL_TYPE, Calls.MISSED_TYPE);
-        //assertCallsAreNotGrouped(Calls.VOICEMAIL_TYPE, Calls.MISSED_TYPE, Calls.MISSED_TYPE);
         assertCallsAreNotGrouped(Calls.VOICEMAIL_TYPE, Calls.VOICEMAIL_TYPE);
         assertCallsAreNotGrouped(Calls.VOICEMAIL_TYPE, Calls.INCOMING_TYPE);
         assertCallsAreNotGrouped(Calls.VOICEMAIL_TYPE, Calls.OUTGOING_TYPE);
@@ -150,8 +149,8 @@
                 Calls.OUTGOING_TYPE);
         mBuilder.addGroups(mCursor);
         assertEquals(2, mFakeGroupCreator.groups.size());
-        assertGroupIs(1, 4, false, mFakeGroupCreator.groups.get(0));
-        assertGroupIs(8, 3, false, mFakeGroupCreator.groups.get(1));
+        assertGroupIs(1, 4, mFakeGroupCreator.groups.get(0));
+        assertGroupIs(8, 3, mFakeGroupCreator.groups.get(1));
     }
 
     public void testEqualPhoneNumbers() {
@@ -228,7 +227,7 @@
         addMultipleCallLogEntries(TEST_NUMBER1, types);
         mBuilder.addGroups(mCursor);
         assertEquals(1, mFakeGroupCreator.groups.size());
-        assertGroupIs(0, types.length, false, mFakeGroupCreator.groups.get(0));
+        assertGroupIs(0, types.length, mFakeGroupCreator.groups.get(0));
 
     }
 
@@ -266,10 +265,9 @@
     }
 
     /** Asserts that the group matches the given values. */
-    private void assertGroupIs(int cursorPosition, int size, boolean expanded, GroupSpec group) {
+    private void assertGroupIs(int cursorPosition, int size, GroupSpec group) {
         assertEquals(cursorPosition, group.cursorPosition);
         assertEquals(size, group.size);
-        assertEquals(expanded, group.expanded);
     }
 
     /** Defines an added group. Used by the {@link FakeGroupCreator}. */
@@ -278,13 +276,10 @@
         public final int cursorPosition;
         /** The number of elements in the group. */
         public final int size;
-        /** Whether the group should be initially expanded. */
-        public final boolean expanded;
 
-        public GroupSpec(int cursorPosition, int size, boolean expanded) {
+        public GroupSpec(int cursorPosition, int size) {
             this.cursorPosition = cursorPosition;
             this.size = size;
-            this.expanded = expanded;
         }
     }
 
@@ -294,8 +289,8 @@
         public final List<GroupSpec> groups = newArrayList();
 
         @Override
-        public void addGroup(int cursorPosition, int size, boolean expanded) {
-            groups.add(new GroupSpec(cursorPosition, size, expanded));
+        public void addGroup(int cursorPosition, int size) {
+            groups.add(new GroupSpec(cursorPosition, size));
         }
 
         @Override
diff --git a/tests/src/com/android/dialer/calllog/GroupingListAdapterTests.java b/tests/src/com/android/dialer/calllog/GroupingListAdapterTests.java
index 53583e0..45bc598 100644
--- a/tests/src/com/android/dialer/calllog/GroupingListAdapterTests.java
+++ b/tests/src/com/android/dialer/calllog/GroupingListAdapterTests.java
@@ -16,10 +16,6 @@
 
 package com.android.dialer.calllog;
 
-import static com.android.dialer.calllog.GroupingListAdapter.ITEM_TYPE_GROUP_HEADER;
-import static com.android.dialer.calllog.GroupingListAdapter.ITEM_TYPE_IN_GROUP;
-import static com.android.dialer.calllog.GroupingListAdapter.ITEM_TYPE_STANDALONE;
-
 import android.content.Context;
 import android.database.Cursor;
 import android.database.MatrixCursor;
@@ -63,17 +59,12 @@
                 if (TextUtils.equals(value, currentValue)) {
                     groupItemCount++;
                 } else {
-                    if (groupItemCount > 1) {
-                        addGroup(i - groupItemCount, groupItemCount, false);
-                    }
-
+                    addGroup(i - groupItemCount, groupItemCount);
                     groupItemCount = 1;
                     currentValue = value;
                 }
             }
-            if (groupItemCount > 1) {
-                addGroup(count - groupItemCount, groupItemCount, false);
-            }
+            addGroup(count - groupItemCount, groupItemCount);
         }
 
         @Override
@@ -92,7 +83,6 @@
         }
     };
 
-
     private void buildCursor(String... numbers) {
         mCursor = new MatrixCursor(PROJECTION);
         mNextId = 1;
@@ -107,170 +97,51 @@
         mAdapter.changeCursor(mCursor);
 
         assertEquals(3, mAdapter.getItemCount());
-        assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
-        assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 1);
-        assertPositionMetadata(2, ITEM_TYPE_STANDALONE, false, 2);
+        assertMetadata(0, 1, "1");
+        assertMetadata(1, 1, "2");
+        assertMetadata(2, 1, "3");
     }
 
-    public void testGroupingWithCollapsedGroupAtTheBeginning() {
+    public void testGroupingWithGroupAtTheBeginning() {
         buildCursor("1", "1", "2");
         mAdapter.changeCursor(mCursor);
 
         assertEquals(2, mAdapter.getItemCount());
-        assertPositionMetadata(0, ITEM_TYPE_GROUP_HEADER, false, 0);
-        assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 2);
+        assertMetadata(0, 2, "1");
+        assertMetadata(1, 1, "2");
     }
 
-    public void testGroupingWithExpandedGroupAtTheBeginning() {
-        buildCursor("1", "1", "2");
-        mAdapter.changeCursor(mCursor);
-        mAdapter.toggleGroup(0);
-
-        assertEquals(4, mAdapter.getItemCount());
-        assertPositionMetadata(0, ITEM_TYPE_GROUP_HEADER, true, 0);
-        assertPositionMetadata(1, ITEM_TYPE_IN_GROUP, false, 0);
-        assertPositionMetadata(2, ITEM_TYPE_IN_GROUP, false, 1);
-        assertPositionMetadata(3, ITEM_TYPE_STANDALONE, false, 2);
-    }
-
-    public void testGroupingWithExpandCollapseCycleAtTheBeginning() {
-        buildCursor("1", "1", "2");
-        mAdapter.changeCursor(mCursor);
-        mAdapter.toggleGroup(0);
-        mAdapter.toggleGroup(0);
-
-        assertEquals(2, mAdapter.getItemCount());
-        assertPositionMetadata(0, ITEM_TYPE_GROUP_HEADER, false, 0);
-        assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 2);
-    }
-
-    public void testGroupingWithCollapsedGroupInTheMiddle() {
+    public void testGroupingWithGroupInTheMiddle() {
         buildCursor("1", "2", "2", "2", "3");
         mAdapter.changeCursor(mCursor);
 
         assertEquals(3, mAdapter.getItemCount());
-        assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
-        assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, false, 1);
-        assertPositionMetadata(2, ITEM_TYPE_STANDALONE, false, 4);
+        assertMetadata(0, 1, "1");
+        assertMetadata(1, 3, "2");
+        assertMetadata(2, 1, "3");
     }
 
-    public void testGroupingWithExpandedGroupInTheMiddle() {
-        buildCursor("1", "2", "2", "2", "3");
-        mAdapter.changeCursor(mCursor);
-        mAdapter.toggleGroup(1);
-
-        assertEquals(6, mAdapter.getItemCount());
-        assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
-        assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, true, 1);
-        assertPositionMetadata(2, ITEM_TYPE_IN_GROUP, false, 1);
-        assertPositionMetadata(3, ITEM_TYPE_IN_GROUP, false, 2);
-        assertPositionMetadata(4, ITEM_TYPE_IN_GROUP, false, 3);
-        assertPositionMetadata(5, ITEM_TYPE_STANDALONE, false, 4);
-    }
-
-    public void testGroupingWithCollapsedGroupAtTheEnd() {
+    public void testGroupingWithGroupAtTheEnd() {
         buildCursor("1", "2", "3", "3", "3");
         mAdapter.changeCursor(mCursor);
 
         assertEquals(3, mAdapter.getItemCount());
-        assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
-        assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 1);
-        assertPositionMetadata(2, ITEM_TYPE_GROUP_HEADER, false, 2);
+        assertMetadata(0, 1, "1");
+        assertMetadata(1, 1, "2");
+        assertMetadata(2, 3, "3");
     }
 
-    public void testGroupingWithExpandedGroupAtTheEnd() {
-        buildCursor("1", "2", "3", "3", "3");
-        mAdapter.changeCursor(mCursor);
-        mAdapter.toggleGroup(2);
-
-        assertEquals(6, mAdapter.getItemCount());
-        assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
-        assertPositionMetadata(1, ITEM_TYPE_STANDALONE, false, 1);
-        assertPositionMetadata(2, ITEM_TYPE_GROUP_HEADER, true, 2);
-        assertPositionMetadata(3, ITEM_TYPE_IN_GROUP, false, 2);
-        assertPositionMetadata(4, ITEM_TYPE_IN_GROUP, false, 3);
-        assertPositionMetadata(5, ITEM_TYPE_IN_GROUP, false, 4);
-    }
-
-    public void testGroupingWithMultipleCollapsedGroups() {
+    public void testGroupingWithMultipleGroups() {
         buildCursor("1", "2", "2", "3", "4", "4", "5", "5", "6");
         mAdapter.changeCursor(mCursor);
 
         assertEquals(6, mAdapter.getItemCount());
-        assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
-        assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, false, 1);
-        assertPositionMetadata(2, ITEM_TYPE_STANDALONE, false, 3);
-        assertPositionMetadata(3, ITEM_TYPE_GROUP_HEADER, false, 4);
-        assertPositionMetadata(4, ITEM_TYPE_GROUP_HEADER, false, 6);
-        assertPositionMetadata(5, ITEM_TYPE_STANDALONE, false, 8);
-    }
-
-    public void testGroupingWithMultipleExpandedGroups() {
-        buildCursor("1", "2", "2", "3", "4", "4", "5", "5", "6");
-        mAdapter.changeCursor(mCursor);
-        mAdapter.toggleGroup(1);
-
-        // Note that expanding the group of 2's shifted the group of 5's down from the
-        // 4th to the 6th position
-        mAdapter.toggleGroup(6);
-
-        assertEquals(10, mAdapter.getItemCount());
-        assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
-        assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, true, 1);
-        assertPositionMetadata(2, ITEM_TYPE_IN_GROUP, false, 1);
-        assertPositionMetadata(3, ITEM_TYPE_IN_GROUP, false, 2);
-        assertPositionMetadata(4, ITEM_TYPE_STANDALONE, false, 3);
-        assertPositionMetadata(5, ITEM_TYPE_GROUP_HEADER, false, 4);
-        assertPositionMetadata(6, ITEM_TYPE_GROUP_HEADER, true, 6);
-        assertPositionMetadata(7, ITEM_TYPE_IN_GROUP, false, 6);
-        assertPositionMetadata(8, ITEM_TYPE_IN_GROUP, false, 7);
-        assertPositionMetadata(9, ITEM_TYPE_STANDALONE, false, 8);
-    }
-
-    public void testPositionCache() {
-        buildCursor("1", "2", "2", "3", "4", "4", "5", "5", "6");
-        mAdapter.changeCursor(mCursor);
-
-        // First pass - building up cache
-        assertEquals(6, mAdapter.getItemCount());
-        assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
-        assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, false, 1);
-        assertPositionMetadata(2, ITEM_TYPE_STANDALONE, false, 3);
-        assertPositionMetadata(3, ITEM_TYPE_GROUP_HEADER, false, 4);
-        assertPositionMetadata(4, ITEM_TYPE_GROUP_HEADER, false, 6);
-        assertPositionMetadata(5, ITEM_TYPE_STANDALONE, false, 8);
-
-        // Second pass - using cache
-        assertEquals(6, mAdapter.getItemCount());
-        assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
-        assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, false, 1);
-        assertPositionMetadata(2, ITEM_TYPE_STANDALONE, false, 3);
-        assertPositionMetadata(3, ITEM_TYPE_GROUP_HEADER, false, 4);
-        assertPositionMetadata(4, ITEM_TYPE_GROUP_HEADER, false, 6);
-        assertPositionMetadata(5, ITEM_TYPE_STANDALONE, false, 8);
-
-        // Invalidate cache by expanding a group
-        mAdapter.toggleGroup(1);
-
-        // First pass - building up cache
-        assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
-        assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, true, 1);
-        assertPositionMetadata(2, ITEM_TYPE_IN_GROUP, false, 1);
-        assertPositionMetadata(3, ITEM_TYPE_IN_GROUP, false, 2);
-        assertPositionMetadata(4, ITEM_TYPE_STANDALONE, false, 3);
-        assertPositionMetadata(5, ITEM_TYPE_GROUP_HEADER, false, 4);
-        assertPositionMetadata(6, ITEM_TYPE_GROUP_HEADER, false, 6);
-        assertPositionMetadata(7, ITEM_TYPE_STANDALONE, false, 8);
-
-        // Second pass - using cache
-        assertPositionMetadata(0, ITEM_TYPE_STANDALONE, false, 0);
-        assertPositionMetadata(1, ITEM_TYPE_GROUP_HEADER, true, 1);
-        assertPositionMetadata(2, ITEM_TYPE_IN_GROUP, false, 1);
-        assertPositionMetadata(3, ITEM_TYPE_IN_GROUP, false, 2);
-        assertPositionMetadata(4, ITEM_TYPE_STANDALONE, false, 3);
-        assertPositionMetadata(5, ITEM_TYPE_GROUP_HEADER, false, 4);
-        assertPositionMetadata(6, ITEM_TYPE_GROUP_HEADER, false, 6);
-        assertPositionMetadata(7, ITEM_TYPE_STANDALONE, false, 8);
+        assertMetadata(0, 1, "1");
+        assertMetadata(1, 2, "2");
+        assertMetadata(2, 1, "3");
+        assertMetadata(3, 2, "4");
+        assertMetadata(4, 2, "5");
+        assertMetadata(5, 1, "6");
     }
 
     public void testGroupDescriptorArrayGrowth() {
@@ -287,14 +158,9 @@
         assertEquals(250, mAdapter.getItemCount());
     }
 
-    private void assertPositionMetadata(int position, int itemType, boolean isExpanded,
-            int cursorPosition) {
-        GroupingListAdapter.PositionMetadata metadata = new GroupingListAdapter.PositionMetadata();
-        mAdapter.obtainPositionMetadata(metadata, position);
-        assertEquals(itemType, metadata.itemType);
-        if (metadata.itemType == ITEM_TYPE_GROUP_HEADER) {
-            assertEquals(isExpanded, metadata.isExpanded);
-        }
-        assertEquals(cursorPosition, metadata.cursorPosition);
+    private void assertMetadata(int listPosition, int groupSize, String objectValue) {
+        assertEquals(groupSize, mAdapter.getGroupSize(listPosition));
+        MatrixCursor cursor = (MatrixCursor) mAdapter.getItem(listPosition);
+        assertEquals(objectValue, (String) cursor.getString(GROUPING_COLUMN_INDEX));
     }
 }