Merge "Added basic bottom sheet to new call log."
am: 2633778c27

Change-Id: Id6b9f75c0cf3c42b28a0c44d75c10d9c80853936
diff --git a/Android.mk b/Android.mk
index 289c16c..62b6f81 100644
--- a/Android.mk
+++ b/Android.mk
@@ -102,7 +102,9 @@
 	com.android.dialer.calldetails \
 	com.android.dialer.calllog.database \
 	com.android.dialer.calllog.ui \
+        com.android.dialer.calllog.ui.menu \
 	com.android.dialer.calllogutils \
+        com.android.dialer.clipboard \
 	com.android.dialer.common \
 	com.android.dialer.configprovider \
 	com.android.dialer.contactactions \
diff --git a/assets/quantum/res/drawable/quantum_ic_content_copy_vd_theme_24.xml b/assets/quantum/res/drawable/quantum_ic_content_copy_vd_theme_24.xml
new file mode 100644
index 0000000..29704d9
--- /dev/null
+++ b/assets/quantum/res/drawable/quantum_ic_content_copy_vd_theme_24.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0"
+        android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
+</vector>
diff --git a/assets/quantum/res/drawable/quantum_ic_info_outline_vd_theme_24.xml b/assets/quantum/res/drawable/quantum_ic_info_outline_vd_theme_24.xml
new file mode 100644
index 0000000..9ac30d7
--- /dev/null
+++ b/assets/quantum/res/drawable/quantum_ic_info_outline_vd_theme_24.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0"
+        android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M11,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2L13,7h-2v2z"/>
+</vector>
diff --git a/java/com/android/contacts/common/res/values/strings.xml b/java/com/android/contacts/common/res/values/strings.xml
index df8d70f..dbe97ea 100644
--- a/java/com/android/contacts/common/res/values/strings.xml
+++ b/java/com/android/contacts/common/res/values/strings.xml
@@ -16,9 +16,6 @@
   -->
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
 
-  <!-- Toast shown when text is copied to the clipboard [CHAR LIMIT=64] -->
-  <string name="toast_text_copied">Text copied</string>
-
   <!-- Action string for calling a custom phone number -->
     <string name="call_custom">Call
         <xliff:g id="custom">%s</xliff:g>
diff --git a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
index 6067c42..cf86ef6 100644
--- a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
+++ b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
@@ -50,7 +50,6 @@
 import android.widget.ImageView;
 import android.widget.TextView;
 import android.widget.Toast;
-import com.android.contacts.common.ClipboardUtils;
 import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
 import com.android.contacts.common.dialog.CallSubjectDialog;
 import com.android.dialer.app.DialtactsActivity;
@@ -67,6 +66,7 @@
 import com.android.dialer.calldetails.CallDetailsEntries;
 import com.android.dialer.callintent.CallIntentBuilder;
 import com.android.dialer.calllogutils.CallbackActionHelper.CallbackAction;
+import com.android.dialer.clipboard.ClipboardUtils;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.compat.CompatUtils;
diff --git a/java/com/android/dialer/calldetails/CallDetailsActivity.java b/java/com/android/dialer/calldetails/CallDetailsActivity.java
index d7f414b..d871fce 100644
--- a/java/com/android/dialer/calldetails/CallDetailsActivity.java
+++ b/java/com/android/dialer/calldetails/CallDetailsActivity.java
@@ -67,9 +67,9 @@
 
   public static final String EXTRA_PHONE_NUMBER = "phone_number";
   public static final String EXTRA_HAS_ENRICHED_CALL_DATA = "has_enriched_call_data";
-  private static final String EXTRA_CALL_DETAILS_ENTRIES = "call_details_entries";
-  private static final String EXTRA_CONTACT = "contact";
-  private static final String EXTRA_CAN_REPORT_CALLER_ID = "can_report_caller_id";
+  public static final String EXTRA_CALL_DETAILS_ENTRIES = "call_details_entries";
+  public static final String EXTRA_CONTACT = "contact";
+  public static final String EXTRA_CAN_REPORT_CALLER_ID = "can_report_caller_id";
   private static final String EXTRA_CAN_SUPPORT_ASSISTED_DIALING = "can_support_assisted_dialing";
   private static final String TASK_DELETE = "task_delete";
 
diff --git a/java/com/android/dialer/calldetails/CallDetailsFooterViewHolder.java b/java/com/android/dialer/calldetails/CallDetailsFooterViewHolder.java
index 9d3f4bc..6a5188e 100644
--- a/java/com/android/dialer/calldetails/CallDetailsFooterViewHolder.java
+++ b/java/com/android/dialer/calldetails/CallDetailsFooterViewHolder.java
@@ -22,7 +22,7 @@
 import android.text.TextUtils;
 import android.view.View;
 import android.view.View.OnClickListener;
-import com.android.contacts.common.ClipboardUtils;
+import com.android.dialer.clipboard.ClipboardUtils;
 import com.android.dialer.common.Assert;
 import com.android.dialer.logging.DialerImpression;
 import com.android.dialer.logging.Logger;
diff --git a/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java b/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java
index 40d922f..a5f1425 100644
--- a/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java
+++ b/java/com/android/dialer/calllog/database/AnnotatedCallLogDatabaseHelper.java
@@ -39,6 +39,7 @@
           .append(AnnotatedCallLog._ID + " integer primary key, ")
           .append(AnnotatedCallLog.TIMESTAMP + " integer, ")
           .append(AnnotatedCallLog.NAME + " string, ")
+          .append(AnnotatedCallLog.NUMBER + " blob, ")
           .append(AnnotatedCallLog.FORMATTED_NUMBER + " string, ")
           .append(AnnotatedCallLog.PHOTO_URI + " string, ")
           .append(AnnotatedCallLog.PHOTO_ID + " integer, ")
@@ -47,13 +48,13 @@
           .append(AnnotatedCallLog.IS_READ + " integer, ")
           .append(AnnotatedCallLog.NEW + " integer, ")
           .append(AnnotatedCallLog.GEOCODED_LOCATION + " string, ")
+          .append(AnnotatedCallLog.PHONE_ACCOUNT_COMPONENT_NAME + " string, ")
+          .append(AnnotatedCallLog.PHONE_ACCOUNT_ID + " string, ")
           .append(AnnotatedCallLog.PHONE_ACCOUNT_LABEL + " string, ")
           .append(AnnotatedCallLog.PHONE_ACCOUNT_COLOR + " integer, ")
           .append(AnnotatedCallLog.FEATURES + " integer, ")
           .append(AnnotatedCallLog.IS_BUSINESS + " integer, ")
           .append(AnnotatedCallLog.IS_VOICEMAIL + " integer, ")
-          // Columns only in AnnotatedCallLog
-          .append(AnnotatedCallLog.NUMBER + " blob, ")
           .append(AnnotatedCallLog.CALL_TYPE + " integer")
           .append(");")
           .toString();
diff --git a/java/com/android/dialer/calllog/database/Coalescer.java b/java/com/android/dialer/calllog/database/Coalescer.java
index 63fa9f8..a8a8f2f 100644
--- a/java/com/android/dialer/calllog/database/Coalescer.java
+++ b/java/com/android/dialer/calllog/database/Coalescer.java
@@ -20,11 +20,13 @@
 import android.database.MatrixCursor;
 import android.support.annotation.NonNull;
 import android.support.annotation.WorkerThread;
+import android.telecom.PhoneAccountHandle;
 import com.android.dialer.DialerPhoneNumber;
 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.CoalescedAnnotatedCallLog;
 import com.android.dialer.calllog.datasources.CallLogDataSource;
 import com.android.dialer.calllog.datasources.DataSources;
+import com.android.dialer.calllogutils.PhoneAccountUtils;
 import com.android.dialer.common.Assert;
 import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil;
 import com.google.i18n.phonenumbers.PhoneNumberUtil;
@@ -131,11 +133,19 @@
   private static boolean rowsShouldBeCombined(
       DialerPhoneNumberUtil dialerPhoneNumberUtil, ContentValues row1, ContentValues row2) {
     // Don't combine rows which don't use the same phone account.
-    if (!Objects.equals(
-        row1.getAsString(AnnotatedCallLog.PHONE_ACCOUNT_LABEL),
-        row2.getAsString(AnnotatedCallLog.PHONE_ACCOUNT_LABEL))) {
+    PhoneAccountHandle phoneAccount1 =
+        PhoneAccountUtils.getAccount(
+            row1.getAsString(AnnotatedCallLog.PHONE_ACCOUNT_COMPONENT_NAME),
+            row1.getAsString(AnnotatedCallLog.PHONE_ACCOUNT_ID));
+    PhoneAccountHandle phoneAccount2 =
+        PhoneAccountUtils.getAccount(
+            row2.getAsString(AnnotatedCallLog.PHONE_ACCOUNT_COMPONENT_NAME),
+            row2.getAsString(AnnotatedCallLog.PHONE_ACCOUNT_ID));
+
+    if (!Objects.equals(phoneAccount1, phoneAccount2)) {
       return false;
     }
+
     DialerPhoneNumber number1;
     DialerPhoneNumber number2;
     try {
@@ -153,13 +163,8 @@
       throw Assert.createAssertionFailException("error parsing DialerPhoneNumber proto", e);
     }
 
-    if (!number1.hasDialerInternalPhoneNumber() && !number2.hasDialerInternalPhoneNumber()) {
-      // Empty numbers should not be combined.
-      return false;
-    }
-
     if (!number1.hasDialerInternalPhoneNumber() || !number2.hasDialerInternalPhoneNumber()) {
-      // An empty number should not be combined with a non-empty number.
+      // An empty number should not be combined with any other number.
       return false;
     }
     return dialerPhoneNumberUtil.isExactMatch(number1, number2);
diff --git a/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java b/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java
index d466da9..e79ffd0 100644
--- a/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java
+++ b/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java
@@ -52,6 +52,15 @@
     String NAME = "name";
 
     /**
+     * The phone number called or number the call came from, encoded as a {@link
+     * com.android.dialer.DialerPhoneNumber} proto. The number may be empty if it was an incoming
+     * call and the number was unknown.
+     *
+     * <p>Type: BLOB
+     */
+    String NUMBER = "number";
+
+    /**
      * Copied from {@link android.provider.CallLog.Calls#CACHED_FORMATTED_NUMBER}.
      *
      * <p>Type: TEXT
@@ -112,6 +121,20 @@
     String GEOCODED_LOCATION = "geocoded_location";
 
     /**
+     * See {@link android.provider.CallLog.Calls#PHONE_ACCOUNT_COMPONENT_NAME}.
+     *
+     * <p>TYPE: TEXT
+     */
+    String PHONE_ACCOUNT_COMPONENT_NAME = "phone_account_component_name";
+
+    /**
+     * See {@link android.provider.CallLog.Calls#PHONE_ACCOUNT_ID}.
+     *
+     * <p>TYPE: TEXT
+     */
+    String PHONE_ACCOUNT_ID = "phone_account_id";
+
+    /**
      * String suitable for display which indicates the phone account used to make the call.
      *
      * <p>TYPE: TEXT
@@ -160,6 +183,7 @@
           _ID,
           TIMESTAMP,
           NAME,
+          NUMBER,
           FORMATTED_NUMBER,
           PHOTO_URI,
           PHOTO_ID,
@@ -168,6 +192,8 @@
           IS_READ,
           NEW,
           GEOCODED_LOCATION,
+          PHONE_ACCOUNT_COMPONENT_NAME,
+          PHONE_ACCOUNT_ID,
           PHONE_ACCOUNT_LABEL,
           PHONE_ACCOUNT_COLOR,
           FEATURES,
@@ -192,18 +218,6 @@
 
     /** The MIME type of a {@link android.content.ContentProvider#getType(Uri)} single entry. */
     public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/annotated_call_log";
-
-    /**
-     * The phone number called or number the call came from, encoded as a {@link
-     * com.android.dialer.DialerPhoneNumber} proto. The number may be empty if it was an incoming
-     * call and the number was unknown.
-     *
-     * <p>This column is only present in the annotated call log, and not the coalesced annotated
-     * call log. The coalesced version uses a formatted number string rather than proto bytes.
-     *
-     * <p>Type: BLOB
-     */
-    public static final String NUMBER = "number";
   }
 
   /**
diff --git a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java
index d6ad618..0a965f6 100644
--- a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java
+++ b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java
@@ -38,6 +38,7 @@
 import android.telecom.PhoneAccountHandle;
 import android.text.TextUtils;
 import android.util.ArraySet;
+import com.android.dialer.DialerPhoneNumber;
 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
 import com.android.dialer.calllog.datasources.CallLogDataSource;
 import com.android.dialer.calllog.datasources.CallLogMutations;
@@ -156,11 +157,17 @@
         .useMostRecentLong(AnnotatedCallLog.NEW)
         .useMostRecentString(AnnotatedCallLog.NUMBER_TYPE_LABEL)
         .useMostRecentString(AnnotatedCallLog.NAME)
+        // Two different DialerPhoneNumbers could be combined if they are different but considered
+        // to be an "exact match" by libphonenumber; in this case we arbitrarily select the most
+        // recent one.
+        .useMostRecentBlob(AnnotatedCallLog.NUMBER)
         .useMostRecentString(AnnotatedCallLog.FORMATTED_NUMBER)
         .useMostRecentString(AnnotatedCallLog.PHOTO_URI)
         .useMostRecentLong(AnnotatedCallLog.PHOTO_ID)
         .useMostRecentString(AnnotatedCallLog.LOOKUP_URI)
         .useMostRecentString(AnnotatedCallLog.GEOCODED_LOCATION)
+        .useSingleValueString(AnnotatedCallLog.PHONE_ACCOUNT_COMPONENT_NAME)
+        .useSingleValueString(AnnotatedCallLog.PHONE_ACCOUNT_ID)
         .useSingleValueString(AnnotatedCallLog.PHONE_ACCOUNT_LABEL)
         .useSingleValueLong(AnnotatedCallLog.PHONE_ACCOUNT_COLOR)
         .useMostRecentLong(AnnotatedCallLog.CALL_TYPE)
@@ -272,10 +279,14 @@
                 dialerPhoneNumberUtil.parse(numberAsStr, countryIso).toByteArray();
             // TODO(zachh): Need to handle post-dial digits; different on N and M.
             contentValues.put(AnnotatedCallLog.NUMBER, numberAsProtoBytes);
+          } else {
+            contentValues.put(
+                AnnotatedCallLog.NUMBER, DialerPhoneNumber.getDefaultInstance().toByteArray());
           }
 
           contentValues.put(AnnotatedCallLog.CALL_TYPE, type);
           contentValues.put(AnnotatedCallLog.NAME, cachedName);
+          // TODO(zachh): Format the number using DialerPhoneNumberUtil here.
           contentValues.put(AnnotatedCallLog.FORMATTED_NUMBER, formattedNumber);
           contentValues.put(AnnotatedCallLog.PHOTO_URI, cachedPhotoUri);
           contentValues.put(AnnotatedCallLog.PHOTO_ID, cachedPhotoId);
@@ -292,6 +303,9 @@
           contentValues.put(AnnotatedCallLog.IS_READ, isRead);
           contentValues.put(AnnotatedCallLog.NEW, isNew);
           contentValues.put(AnnotatedCallLog.GEOCODED_LOCATION, geocodedLocation);
+          contentValues.put(
+              AnnotatedCallLog.PHONE_ACCOUNT_COMPONENT_NAME, phoneAccountComponentName);
+          contentValues.put(AnnotatedCallLog.PHONE_ACCOUNT_ID, phoneAccountId);
           populatePhoneAccountLabelAndColor(
               appContext, contentValues, phoneAccountComponentName, phoneAccountId);
           contentValues.put(AnnotatedCallLog.FEATURES, features);
diff --git a/java/com/android/dialer/calllog/datasources/util/RowCombiner.java b/java/com/android/dialer/calllog/datasources/util/RowCombiner.java
index adb7a07..8e9e9c6 100644
--- a/java/com/android/dialer/calllog/datasources/util/RowCombiner.java
+++ b/java/com/android/dialer/calllog/datasources/util/RowCombiner.java
@@ -43,6 +43,12 @@
     return this;
   }
 
+  public RowCombiner useMostRecentBlob(String columnName) {
+    combinedRow.put(
+        columnName, individualRowsSortedByTimestampDesc.get(0).getAsByteArray(columnName));
+    return this;
+  }
+
   /** Asserts that all column values for the given column name are the same, and uses it. */
   public RowCombiner useSingleValueString(String columnName) {
     Iterator<ContentValues> iterator = individualRowsSortedByTimestampDesc.iterator();
diff --git a/java/com/android/dialer/calllog/model/CoalescedRow.java b/java/com/android/dialer/calllog/model/CoalescedRow.java
new file mode 100644
index 0000000..0914674
--- /dev/null
+++ b/java/com/android/dialer/calllog/model/CoalescedRow.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.calllog.model;
+
+import android.support.annotation.ColorInt;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.DialerPhoneNumber;
+import com.google.auto.value.AutoValue;
+
+/** Data class containing the contents of a row from the CoalescedAnnotatedCallLog. */
+@AutoValue
+public abstract class CoalescedRow {
+
+  public static Builder builder() {
+    return new AutoValue_CoalescedRow.Builder()
+        .setId(0)
+        .setTimestamp(0)
+        .setNumber(DialerPhoneNumber.getDefaultInstance())
+        .setPhotoId(0)
+        .setIsRead(false)
+        .setIsNew(false)
+        .setPhoneAccountColor(0)
+        .setFeatures(0)
+        .setIsBusiness(false)
+        .setIsVoicemail(false)
+        .setNumberCalls(0)
+        .setCallType(0);
+  }
+
+  public abstract int id();
+
+  public abstract long timestamp();
+
+  @NonNull
+  public abstract DialerPhoneNumber number();
+
+  @Nullable
+  public abstract String name();
+
+  @Nullable
+  public abstract String formattedNumber();
+
+  @Nullable
+  public abstract String photoUri();
+
+  public abstract long photoId();
+
+  @Nullable
+  public abstract String lookupUri();
+
+  @Nullable
+  public abstract String numberTypeLabel();
+
+  public abstract boolean isRead();
+
+  public abstract boolean isNew();
+
+  @Nullable
+  public abstract String geocodedLocation();
+
+  @Nullable
+  public abstract String phoneAccountComponentName();
+
+  @Nullable
+  public abstract String phoneAccountId();
+
+  @Nullable
+  public abstract String phoneAccountLabel();
+
+  @ColorInt
+  public abstract int phoneAccountColor();
+
+  public abstract int features();
+
+  public abstract boolean isBusiness();
+
+  public abstract boolean isVoicemail();
+
+  public abstract int callType();
+
+  public abstract int numberCalls();
+
+  /** Builder for {@link CoalescedRow}. */
+  @AutoValue.Builder
+  public abstract static class Builder {
+
+    public abstract Builder setId(int id);
+
+    public abstract Builder setTimestamp(long timestamp);
+
+    public abstract Builder setNumber(@NonNull DialerPhoneNumber number);
+
+    public abstract Builder setName(@Nullable String name);
+
+    public abstract Builder setFormattedNumber(@Nullable String formattedNumber);
+
+    public abstract Builder setPhotoUri(@Nullable String photoUri);
+
+    public abstract Builder setPhotoId(long photoId);
+
+    public abstract Builder setLookupUri(@Nullable String lookupUri);
+
+    public abstract Builder setNumberTypeLabel(@Nullable String numberTypeLabel);
+
+    public abstract Builder setIsRead(boolean isRead);
+
+    public abstract Builder setIsNew(boolean isNew);
+
+    public abstract Builder setGeocodedLocation(@Nullable String geocodedLocation);
+
+    public abstract Builder setPhoneAccountComponentName(
+        @Nullable String phoneAccountComponentName);
+
+    public abstract Builder setPhoneAccountId(@Nullable String phoneAccountId);
+
+    public abstract Builder setPhoneAccountLabel(@Nullable String phoneAccountLabel);
+
+    public abstract Builder setPhoneAccountColor(@ColorInt int phoneAccountColor);
+
+    public abstract Builder setFeatures(int features);
+
+    public abstract Builder setIsBusiness(boolean isBusiness);
+
+    public abstract Builder setIsVoicemail(boolean isVoicemail);
+
+    public abstract Builder setCallType(int callType);
+
+    public abstract Builder setNumberCalls(int numberCalls);
+
+    public abstract CoalescedRow build();
+  }
+}
diff --git a/java/com/android/dialer/calllog/ui/CoalescedAnnotatedCallLogCursorLoader.java b/java/com/android/dialer/calllog/ui/CoalescedAnnotatedCallLogCursorLoader.java
index 13a801a..9f63543 100644
--- a/java/com/android/dialer/calllog/ui/CoalescedAnnotatedCallLogCursorLoader.java
+++ b/java/com/android/dialer/calllog/ui/CoalescedAnnotatedCallLogCursorLoader.java
@@ -18,115 +18,37 @@
 
 import android.content.Context;
 import android.database.Cursor;
-import android.support.annotation.ColorInt;
 import android.support.v4.content.CursorLoader;
+import com.android.dialer.DialerPhoneNumber;
 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.CoalescedAnnotatedCallLog;
+import com.android.dialer.calllog.model.CoalescedRow;
+import com.google.protobuf.InvalidProtocolBufferException;
 
 /** CursorLoader for the coalesced annotated call log. */
 final class CoalescedAnnotatedCallLogCursorLoader extends CursorLoader {
 
-  /** Indexes for CoalescedAnnotatedCallLog.ALL_COLUMNS */
+  // Indexes for CoalescedAnnotatedCallLog.ALL_COLUMNS
   private static final int ID = 0;
-
   private static final int TIMESTAMP = 1;
   private static final int NAME = 2;
-  private static final int FORMATTED_NUMBER = 3;
-  private static final int PHOTO_URI = 4;
-  private static final int PHOTO_ID = 5;
-  private static final int LOOKUP_URI = 6;
-  private static final int NUMBER_TYPE_LABEL = 7;
-  private static final int IS_READ = 8;
-  private static final int NEW = 9;
-  private static final int GEOCODED_LOCATION = 10;
-  private static final int PHONE_ACCOUNT_LABEL = 11;
-  private static final int PHONE_ACCOUNT_COLOR = 12;
-  private static final int FEATURES = 13;
-  private static final int IS_BUSINESS = 14;
-  private static final int IS_VOICEMAIL = 15;
-  private static final int TYPE = 16;
-  private static final int NUMBER_CALLS = 17;
-
-  /** Convenience class for accessing values using an abbreviated syntax. */
-  static final class Row {
-    private final Cursor cursor;
-
-    Row(Cursor cursor) {
-      this.cursor = cursor;
-    }
-
-    long id() {
-      return cursor.getInt(ID);
-    }
-
-    long timestamp() {
-      return cursor.getLong(TIMESTAMP);
-    }
-
-    String name() {
-      return cursor.getString(NAME);
-    }
-
-    String formattedNumber() {
-      return cursor.getString(FORMATTED_NUMBER);
-    }
-
-    String photoUri() {
-      return cursor.getString(PHOTO_URI);
-    }
-
-    long photoId() {
-      return cursor.getLong(PHOTO_ID);
-    }
-
-    String lookupUri() {
-      return cursor.getString(LOOKUP_URI);
-    }
-
-    String numberTypeLabel() {
-      return cursor.getString(NUMBER_TYPE_LABEL);
-    }
-
-    boolean isRead() {
-      return cursor.getInt(IS_READ) == 1;
-    }
-
-    boolean isNew() {
-      return cursor.getInt(NEW) == 1;
-    }
-
-    String geocodedLocation() {
-      return cursor.getString(GEOCODED_LOCATION);
-    }
-
-    String phoneAccountLabel() {
-      return cursor.getString(PHONE_ACCOUNT_LABEL);
-    }
-
-    @ColorInt
-    int phoneAccountColor() {
-      return cursor.getInt(PHONE_ACCOUNT_COLOR);
-    }
-
-    int features() {
-      return cursor.getInt(FEATURES);
-    }
-
-    boolean isBusiness() {
-      return cursor.getInt(IS_BUSINESS) == 1;
-    }
-
-    boolean isVoicemail() {
-      return cursor.getInt(IS_VOICEMAIL) == 1;
-    }
-
-    int numberCalls() {
-      return cursor.getInt(NUMBER_CALLS);
-    }
-
-    int callType() {
-      return cursor.getInt(TYPE);
-    }
-  }
+  private static final int NUMBER = 3;
+  private static final int FORMATTED_NUMBER = 4;
+  private static final int PHOTO_URI = 5;
+  private static final int PHOTO_ID = 6;
+  private static final int LOOKUP_URI = 7;
+  private static final int NUMBER_TYPE_LABEL = 8;
+  private static final int IS_READ = 9;
+  private static final int NEW = 10;
+  private static final int GEOCODED_LOCATION = 11;
+  private static final int PHONE_ACCOUNT_COMPONENT_NAME = 12;
+  private static final int PHONE_ACCOUNT_ID = 13;
+  private static final int PHONE_ACCOUNT_LABEL = 14;
+  private static final int PHONE_ACCOUNT_COLOR = 15;
+  private static final int FEATURES = 16;
+  private static final int IS_BUSINESS = 17;
+  private static final int IS_VOICEMAIL = 18;
+  private static final int CALL_TYPE = 19;
+  private static final int NUMBER_CALLS = 20;
 
   CoalescedAnnotatedCallLogCursorLoader(Context context) {
     // CoalescedAnnotatedCallLog requires that PROJECTION be ALL_COLUMNS and the following params be
@@ -139,4 +61,42 @@
         null,
         null);
   }
+
+  /** Creates a new {@link CoalescedRow} from the provided cursor using the current position. */
+  static CoalescedRow toRow(Cursor cursor) {
+    DialerPhoneNumber number;
+    try {
+      number = DialerPhoneNumber.parseFrom(cursor.getBlob(NUMBER));
+    } catch (InvalidProtocolBufferException e) {
+      throw new IllegalStateException("Couldn't parse DialerPhoneNumber bytes");
+    }
+
+    return CoalescedRow.builder()
+        .setId(cursor.getInt(ID))
+        .setTimestamp(cursor.getLong(TIMESTAMP))
+        .setName(cursor.getString(NAME))
+        .setNumber(number)
+        .setFormattedNumber(cursor.getString(FORMATTED_NUMBER))
+        .setPhotoUri(cursor.getString(PHOTO_URI))
+        .setPhotoId(cursor.getLong(PHOTO_ID))
+        .setLookupUri(cursor.getString(LOOKUP_URI))
+        .setNumberTypeLabel(cursor.getString(NUMBER_TYPE_LABEL))
+        .setIsRead(cursor.getInt(IS_READ) == 1)
+        .setIsNew(cursor.getInt(NEW) == 1)
+        .setGeocodedLocation(cursor.getString(GEOCODED_LOCATION))
+        .setPhoneAccountComponentName(cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME))
+        .setPhoneAccountId(cursor.getString(PHONE_ACCOUNT_ID))
+        .setPhoneAccountLabel(cursor.getString(PHONE_ACCOUNT_LABEL))
+        .setPhoneAccountColor(cursor.getInt(PHONE_ACCOUNT_COLOR))
+        .setFeatures(cursor.getInt(FEATURES))
+        .setIsBusiness(cursor.getInt(IS_BUSINESS) == 1)
+        .setIsVoicemail(cursor.getInt(IS_VOICEMAIL) == 1)
+        .setCallType(cursor.getInt(CALL_TYPE))
+        .setNumberCalls(cursor.getInt(NUMBER_CALLS))
+        .build();
+  }
+
+  static long getTimestamp(Cursor cursor) {
+    return cursor.getLong(TIMESTAMP);
+  }
 }
diff --git a/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java b/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java
index b922a6e..d5cfb7e 100644
--- a/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java
+++ b/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java
@@ -58,15 +58,14 @@
     // Calculate header adapter positions by reading cursor.
     long currentTimeMillis = clock.currentTimeMillis();
     if (cursor.moveToNext()) {
-      CoalescedAnnotatedCallLogCursorLoader.Row firstRow =
-          new CoalescedAnnotatedCallLogCursorLoader.Row(cursor);
-      if (CallLogDates.isSameDay(currentTimeMillis, firstRow.timestamp())) {
+      long firstTimestamp = CoalescedAnnotatedCallLogCursorLoader.getTimestamp(cursor);
+      if (CallLogDates.isSameDay(currentTimeMillis, firstTimestamp)) {
         this.todayHeaderPosition = 0;
         int adapterPosition = 2; // Accounted for "Today" header and first row.
         while (cursor.moveToNext()) {
-          CoalescedAnnotatedCallLogCursorLoader.Row row =
-              new CoalescedAnnotatedCallLogCursorLoader.Row(cursor);
-          if (CallLogDates.isSameDay(currentTimeMillis, row.timestamp())) {
+          long timestamp = CoalescedAnnotatedCallLogCursorLoader.getTimestamp(cursor);
+
+          if (CallLogDates.isSameDay(currentTimeMillis, timestamp)) {
             adapterPosition++;
           } else {
             this.olderHeaderPosition = adapterPosition;
diff --git a/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java b/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java
index 8ac419e..4e59301 100644
--- a/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java
+++ b/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java
@@ -16,16 +16,19 @@
 package com.android.dialer.calllog.ui;
 
 import android.content.Context;
+import android.content.Intent;
 import android.database.Cursor;
 import android.net.Uri;
 import android.provider.CallLog.Calls;
 import android.support.v7.widget.RecyclerView;
-import android.text.TextUtils;
 import android.view.View;
+import android.widget.ImageView;
 import android.widget.QuickContactBadge;
 import android.widget.TextView;
-import com.android.dialer.calllog.ui.CoalescedAnnotatedCallLogCursorLoader.Row;
-import com.android.dialer.calllogutils.CallLogDates;
+import com.android.dialer.calllog.model.CoalescedRow;
+import com.android.dialer.calllog.ui.menu.NewCallLogMenu;
+import com.android.dialer.calllogutils.CallLogEntryText;
+import com.android.dialer.calllogutils.CallLogIntents;
 import com.android.dialer.calllogutils.CallTypeIconsView;
 import com.android.dialer.contactphoto.ContactPhotoManager;
 import com.android.dialer.lettertile.LetterTileDrawable;
@@ -43,6 +46,8 @@
   private final CallTypeIconsView primaryCallTypeIconsView; // Used for Wifi, HD icons
   private final CallTypeIconsView secondaryCallTypeIconsView; // Used for call types
   private final TextView phoneAccountView;
+  private final ImageView menuButton;
+
   private final Clock clock;
 
   NewCallLogViewHolder(View view, Clock clock) {
@@ -54,17 +59,18 @@
     primaryCallTypeIconsView = view.findViewById(R.id.primary_call_type_icons);
     secondaryCallTypeIconsView = view.findViewById(R.id.secondary_call_type_icons);
     phoneAccountView = view.findViewById(R.id.phone_account);
+    menuButton = view.findViewById(R.id.menu_button);
+
     this.clock = clock;
   }
 
   /** @param cursor a cursor from {@link CoalescedAnnotatedCallLogCursorLoader}. */
   void bind(Cursor cursor) {
-    CoalescedAnnotatedCallLogCursorLoader.Row row =
-        new CoalescedAnnotatedCallLogCursorLoader.Row(cursor);
+    CoalescedRow row = CoalescedAnnotatedCallLogCursorLoader.toRow(cursor);
 
     // TODO(zachh): Handle RTL properly.
-    primaryTextView.setText(buildPrimaryText(row));
-    secondaryTextView.setText(buildSecondaryText(row));
+    primaryTextView.setText(CallLogEntryText.buildPrimaryText(context, row));
+    secondaryTextView.setText(CallLogEntryText.buildSecondaryTextForEntries(context, clock, row));
 
     if (isNewMissedCall(row)) {
       primaryTextView.setTextAppearance(R.style.primary_textview_new_call);
@@ -72,77 +78,29 @@
       secondaryTextView.setTextAppearance(R.style.secondary_textview_new_call);
     }
 
+    setNumberCalls(row);
     setPhoto(row);
     setPrimaryCallTypes(row);
     setSecondaryCallTypes(row);
     setPhoneAccounts(row);
+    setOnClickListenerForRow(row);
+    setOnClickListenerForMenuButon(row);
   }
 
-  private String buildPrimaryText(CoalescedAnnotatedCallLogCursorLoader.Row row) {
-    StringBuilder primaryText = new StringBuilder();
-    if (!TextUtils.isEmpty(row.name())) {
-      primaryText.append(row.name());
-    } else if (!TextUtils.isEmpty(row.formattedNumber())) {
-      primaryText.append(row.formattedNumber());
-    } else {
-      // TODO(zachh): Handle CallLog.Calls.PRESENTATION_*, including Verizon restricted numbers.
-      primaryText.append(context.getText(R.string.new_call_log_unknown));
-    }
+  private void setNumberCalls(CoalescedRow row) {
+    // TODO(zachh): Number of calls shouldn't be text, but a circle with a number inside.
     if (row.numberCalls() > 1) {
-      primaryText.append(String.format(Locale.getDefault(), " (%d)", row.numberCalls()));
+      primaryTextView.append(String.format(Locale.getDefault(), " (%d)", row.numberCalls()));
     }
-    return primaryText.toString();
   }
 
-  private boolean isNewMissedCall(CoalescedAnnotatedCallLogCursorLoader.Row row) {
+  private boolean isNewMissedCall(CoalescedRow row) {
     // Show missed call styling if the most recent call in the group was missed and it is still
     // marked as NEW. It is not clear what IS_READ should be used for and it is currently not used.
     return row.callType() == Calls.MISSED_TYPE && row.isNew();
   }
 
-  private String buildSecondaryText(CoalescedAnnotatedCallLogCursorLoader.Row row) {
-    /*
-     * Rules: (Duo video, )?$Label|$Location • Date
-     *
-     * Examples:
-     *   Duo Video, Mobile • Now
-     *   Duo Video • 11:45pm
-     *   Mobile • 11:45pm
-     *   Mobile • Sunday
-     *   Brooklyn, NJ • Jan 15
-     *
-     * Date rules:
-     *   if < 1 minute ago: "Now"; else if today: HH:MM(am|pm); else if < 3 days: day; else: MON D
-     */
-    StringBuilder secondaryText = new StringBuilder();
-    if ((row.features() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO) {
-      // TODO(zachh): Add "Duo" prefix?
-      secondaryText.append(context.getText(R.string.new_call_log_video));
-    }
-    String numberTypeLabel = row.numberTypeLabel();
-    if (!TextUtils.isEmpty(numberTypeLabel)) {
-      if (secondaryText.length() > 0) {
-        secondaryText.append(", ");
-      }
-      secondaryText.append(numberTypeLabel);
-    } else { // If there's a number type label, don't show the location.
-      String location = row.geocodedLocation();
-      if (!TextUtils.isEmpty(location)) {
-        if (secondaryText.length() > 0) {
-          secondaryText.append(", ");
-        }
-        secondaryText.append(location);
-      }
-    }
-    if (secondaryText.length() > 0) {
-      secondaryText.append(" • ");
-    }
-    secondaryText.append(
-        CallLogDates.newCallLogTimestampLabel(context, clock.currentTimeMillis(), row.timestamp()));
-    return secondaryText.toString();
-  }
-
-  private void setPhoto(Row row) {
+  private void setPhoto(CoalescedRow row) {
     // TODO(zachh): Set the contact type.
     ContactPhotoManager.getInstance(context)
         .loadDialerThumbnailOrPhoto(
@@ -154,7 +112,7 @@
             LetterTileDrawable.TYPE_DEFAULT);
   }
 
-  private void setPrimaryCallTypes(Row row) {
+  private void setPrimaryCallTypes(CoalescedRow row) {
     // Only HD and Wifi icons are shown following the primary text.
     primaryCallTypeIconsView.setShowHd(
         MotorolaUtils.shouldShowHdIconInCallLog(context, row.features()));
@@ -162,18 +120,32 @@
         MotorolaUtils.shouldShowWifiIconInCallLog(context, row.features()));
   }
 
-  private void setSecondaryCallTypes(Row row) {
+  private void setSecondaryCallTypes(CoalescedRow row) {
     // Only call type icon is shown before the secondary text.
     secondaryCallTypeIconsView.add(row.callType());
 
     // TODO(zachh): Per new mocks, may need to add method to CallTypeIconsView to disable coloring.
   }
 
-  private void setPhoneAccounts(Row row) {
+  private void setPhoneAccounts(CoalescedRow row) {
     if (row.phoneAccountLabel() != null) {
       phoneAccountView.setText(row.phoneAccountLabel());
       phoneAccountView.setTextColor(row.phoneAccountColor());
       phoneAccountView.setVisibility(View.VISIBLE);
     }
   }
+
+  private void setOnClickListenerForRow(CoalescedRow row) {
+    itemView.setOnClickListener(
+        (view) -> {
+          Intent callbackIntent = CallLogIntents.getCallBackIntent(row);
+          if (callbackIntent != null) {
+            context.startActivity(callbackIntent);
+          }
+        });
+  }
+
+  private void setOnClickListenerForMenuButon(CoalescedRow row) {
+    menuButton.setOnClickListener(NewCallLogMenu.createOnClickListener(context, row));
+  }
 }
diff --git a/java/com/android/dialer/calllog/ui/menu/AndroidManifest.xml b/java/com/android/dialer/calllog/ui/menu/AndroidManifest.xml
new file mode 100644
index 0000000..0d8274d
--- /dev/null
+++ b/java/com/android/dialer/calllog/ui/menu/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<!--
+ ~ Copyright (C) 2017 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
+ -->
+<manifest package="com.android.dialer.calllog.ui.menu"/>
diff --git a/java/com/android/dialer/calllog/ui/menu/Modules.java b/java/com/android/dialer/calllog/ui/menu/Modules.java
new file mode 100644
index 0000000..8de6351
--- /dev/null
+++ b/java/com/android/dialer/calllog/ui/menu/Modules.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.calllog.ui.menu;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+import com.android.dialer.calldetails.CallDetailsActivity;
+import com.android.dialer.calldetails.CallDetailsEntries;
+import com.android.dialer.callintent.CallInitiationType;
+import com.android.dialer.calllog.model.CoalescedRow;
+import com.android.dialer.calllogutils.PhoneAccountUtils;
+import com.android.dialer.clipboard.ClipboardUtils;
+import com.android.dialer.contactactions.ContactActionModule;
+import com.android.dialer.contactactions.DividerModule;
+import com.android.dialer.contactactions.IntentModule;
+import com.android.dialer.dialercontact.DialerContact;
+import com.android.dialer.lettertile.LetterTileDrawable;
+import com.android.dialer.util.IntentUtil;
+import com.android.dialer.util.UriUtils;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Configures the modules for the bottom sheet; these are the rows below the top row (primary
+ * action) in the bottom sheet.
+ */
+final class Modules {
+
+  static List<ContactActionModule> fromRow(Context context, CoalescedRow row) {
+    // Conditionally add each module, which are items in the bottom sheet's menu.
+    List<ContactActionModule> modules = new ArrayList<>();
+
+    maybeAddModuleForVideoOrAudioCall(context, row, modules);
+    maybeAddModuleForAddingToContacts(context, row, modules);
+
+    String originalNumber = row.number().getRawInput().getNumber();
+    maybeAddModuleForSendingTextMessage(context, originalNumber, modules);
+
+    if (!modules.isEmpty()) {
+      modules.add(new DividerModule());
+    }
+
+    // TODO(zachh): Module for blocking/unblocking spam.
+    // TODO(zachh): Module for CallComposer.
+    maybeAddModuleForCopyingNumber(context, originalNumber, modules);
+
+    // TODO(zachh): Revisit if DialerContact is the best thing to pass to CallDetails; could
+    // it use a ContactPrimaryActionInfo instead?
+    addModuleForAccessingCallDetails(context, createDialerContactFromRow(row), modules);
+
+    return modules;
+  }
+
+  private static void maybeAddModuleForVideoOrAudioCall(
+      Context context, CoalescedRow row, List<ContactActionModule> modules) {
+    String originalNumber = row.number().getRawInput().getNumber();
+    if (TextUtils.isEmpty(originalNumber)) {
+      // Skip adding the menu item if the phone number is unknown.
+      return;
+    }
+
+    PhoneAccountHandle phoneAccountHandle =
+        PhoneAccountUtils.getAccount(row.phoneAccountComponentName(), row.phoneAccountId());
+
+    if ((row.features() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO) {
+      // Add an audio call item for video calls. Clicking the top entry on the bottom sheet will
+      // trigger a video call.
+      modules.add(
+          IntentModule.newCallModule(
+              context, originalNumber, phoneAccountHandle, CallInitiationType.Type.CALL_LOG));
+    } else {
+      // Add a video call item for audio calls. Click the top entry on the bottom sheet will
+      // trigger an audio call.
+      // TODO(zachh): Only show video option if video capabilities present?
+      modules.add(
+          IntentModule.newVideoCallModule(
+              context, originalNumber, phoneAccountHandle, CallInitiationType.Type.CALL_LOG));
+    }
+  }
+
+  private static void maybeAddModuleForAddingToContacts(
+      Context context, CoalescedRow row, List<ContactActionModule> modules) {
+    // TODO(zachh): Only show this for non-spam/blocked numbers.
+
+    // Skip showing the menu item for existing contacts.
+    if (isExistingContact(row)) {
+      return;
+    }
+
+    // Skip showing the menu item if there is no number.
+    String originalNumber = row.number().getRawInput().getNumber();
+    if (TextUtils.isEmpty(originalNumber)) {
+      return;
+    }
+
+    Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
+    intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
+    intent.putExtra(ContactsContract.Intents.Insert.PHONE, originalNumber);
+
+    if (!TextUtils.isEmpty(row.name())) {
+      intent.putExtra(ContactsContract.Intents.Insert.NAME, row.name());
+    }
+    modules.add(
+        new IntentModule(
+            context,
+            intent,
+            R.string.add_to_contacts,
+            R.drawable.quantum_ic_person_add_vd_theme_24));
+  }
+
+  /**
+   * Lookup URIs are currently fetched from the cached column of the system call log. This URI
+   * contains encoded information for non-contacts for the purposes of populating contact cards.
+   *
+   * <p>We infer whether a contact is existing or not by checking if the lookup URI is "encoded" or
+   * not.
+   *
+   * <p>TODO(zachh): We should revisit this once the contact URI is no longer being read from the
+   * cached column in the system database, in case we decide not to overload the column.
+   */
+  private static boolean isExistingContact(CoalescedRow row) {
+    return !TextUtils.isEmpty(row.lookupUri())
+        && !UriUtils.isEncodedContactUri(Uri.parse(row.lookupUri()));
+  }
+
+  private static void maybeAddModuleForSendingTextMessage(
+      Context context, String originalNumber, List<ContactActionModule> modules) {
+    // TODO(zachh): There are some conditions where this module should not be shown; consider
+    // voicemail, business numbers, blocked numbers, spam numbers, etc.
+    if (!TextUtils.isEmpty(originalNumber)) {
+      modules.add(
+          new IntentModule(
+              context,
+              IntentUtil.getSendSmsIntent(originalNumber),
+              R.string.send_a_message,
+              R.drawable.quantum_ic_message_vd_theme_24));
+    }
+  }
+
+  private static void maybeAddModuleForCopyingNumber(
+      Context context, String originalNumber, List<ContactActionModule> modules) {
+    if (TextUtils.isEmpty(originalNumber)) {
+      return;
+    }
+    modules.add(
+        new ContactActionModule() {
+          @Override
+          public int getStringId() {
+            return R.string.copy_number;
+          }
+
+          @Override
+          public int getDrawableId() {
+            return R.drawable.quantum_ic_content_copy_vd_theme_24;
+          }
+
+          @Override
+          public boolean onClick() {
+            ClipboardUtils.copyText(context, null, originalNumber, true);
+            return false;
+          }
+        });
+  }
+
+  private static void addModuleForAccessingCallDetails(
+      Context context, DialerContact dialerContact, List<ContactActionModule> modules) {
+    // TODO(zachh): Load CallDetailsEntries and canReportInaccurateNumber in
+    // CallDetailsActivity (see also isPeopleApiSource(sourceType)).
+    CallDetailsEntries callDetailsEntries = CallDetailsEntries.getDefaultInstance();
+    boolean canReportInaccurateNumber = false;
+    boolean canSupportAssistedDialing = false; // TODO(zachh): Properly set value.
+
+    modules.add(
+        new IntentModule(
+            context,
+            CallDetailsActivity.newInstance(
+                context,
+                callDetailsEntries,
+                dialerContact,
+                canReportInaccurateNumber,
+                canSupportAssistedDialing),
+            R.string.call_details,
+            R.drawable.quantum_ic_info_outline_vd_theme_24));
+  }
+
+  private static DialerContact createDialerContactFromRow(CoalescedRow row) {
+    // TODO(zachh): Do something with parsed values to make more dialable?
+    String originalNumber = row.number().getRawInput().getNumber();
+
+    DialerContact.Builder dialerContactBuilder =
+        DialerContact.newBuilder()
+            .setNumber(originalNumber)
+            .setContactType(LetterTileDrawable.TYPE_DEFAULT) // TODO(zachh): Use proper type.
+            .setPhotoId(row.photoId());
+
+    if (!TextUtils.isEmpty(row.name())) {
+      dialerContactBuilder.setNameOrNumber(row.name());
+    } else if (!TextUtils.isEmpty(originalNumber)) {
+      dialerContactBuilder.setNameOrNumber(originalNumber);
+    }
+    if (row.numberTypeLabel() != null) {
+      dialerContactBuilder.setNumberLabel(row.numberTypeLabel());
+    }
+    if (row.photoUri() != null) {
+      dialerContactBuilder.setPhotoUri(row.photoUri());
+    }
+    if (row.lookupUri() != null) {
+      dialerContactBuilder.setContactUri(row.lookupUri());
+    }
+    if (row.formattedNumber() != null) {
+      dialerContactBuilder.setDisplayNumber(row.formattedNumber());
+    }
+    return dialerContactBuilder.build();
+  }
+}
diff --git a/java/com/android/dialer/calllog/ui/menu/NewCallLogMenu.java b/java/com/android/dialer/calllog/ui/menu/NewCallLogMenu.java
new file mode 100644
index 0000000..2ae823e
--- /dev/null
+++ b/java/com/android/dialer/calllog/ui/menu/NewCallLogMenu.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.calllog.ui.menu;
+
+import android.content.Context;
+import android.view.View;
+import com.android.dialer.calllog.model.CoalescedRow;
+import com.android.dialer.contactactions.ContactActionBottomSheet;
+
+/** Handles configuration of the bottom sheet menus for call log entries. */
+public final class NewCallLogMenu {
+
+  /** Creates and returns the OnClickListener which opens the menu for the provided row. */
+  public static View.OnClickListener createOnClickListener(Context context, CoalescedRow row) {
+    return (view) ->
+        ContactActionBottomSheet.show(
+            context, PrimaryAction.fromRow(context, row), Modules.fromRow(context, row));
+  }
+}
diff --git a/java/com/android/dialer/calllog/ui/menu/PrimaryAction.java b/java/com/android/dialer/calllog/ui/menu/PrimaryAction.java
new file mode 100644
index 0000000..7077d02
--- /dev/null
+++ b/java/com/android/dialer/calllog/ui/menu/PrimaryAction.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.calllog.ui.menu;
+
+import android.content.Context;
+import android.provider.CallLog.Calls;
+import com.android.dialer.calllog.model.CoalescedRow;
+import com.android.dialer.calllogutils.CallLogEntryText;
+import com.android.dialer.calllogutils.CallLogIntents;
+import com.android.dialer.contactactions.ContactPrimaryActionInfo;
+import com.android.dialer.contactactions.ContactPrimaryActionInfo.PhotoInfo;
+import com.android.dialer.lettertile.LetterTileDrawable;
+
+/** Configures the primary action row (top row) for the bottom sheet. */
+final class PrimaryAction {
+
+  static ContactPrimaryActionInfo fromRow(Context context, CoalescedRow row) {
+    return ContactPrimaryActionInfo.builder()
+        .setNumber(row.number())
+        .setPhotoInfo(
+            PhotoInfo.builder()
+                .setPhotoId(row.photoId())
+                .setPhotoUri(row.photoUri())
+                .setLookupUri(row.lookupUri())
+                .setIsVideo((row.features() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO)
+                .setContactType(LetterTileDrawable.TYPE_DEFAULT) // TODO(zachh): Use proper type.
+                .setDisplayName(row.name())
+                .build())
+        .setPrimaryText(CallLogEntryText.buildPrimaryText(context, row))
+        .setSecondaryText(CallLogEntryText.buildSecondaryTextForBottomSheet(context, row))
+        .setIntent(CallLogIntents.getCallBackIntent(row))
+        .build();
+  }
+}
diff --git a/java/com/android/dialer/calllog/ui/menu/res/values/strings.xml b/java/com/android/dialer/calllog/ui/menu/res/values/strings.xml
new file mode 100644
index 0000000..aaa7da0
--- /dev/null
+++ b/java/com/android/dialer/calllog/ui/menu/res/values/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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
+  -->
+
+<resources>
+
+  <!-- Option shown in call log menu to add the phone number from an entry to an existing contact
+       (also provides option to create a new contact from the number). [CHAR LIMIT=30] -->
+  <string name="add_to_contacts">Add to contacts</string>
+
+  <!-- Option displayed in call log menu to copy phone number. [CHAR LIMIT=30] -->
+  <string name="copy_number">Copy number</string>
+
+  <!-- Options shown in call log menu to send a SMS to the number represented by the call log entry.
+       [CHAR LIMIT=30] -->
+  <string name="send_a_message">Send a message</string>
+
+  <!-- Option shown in call log menu to navigate the user to the call details screen where the user
+       can view details for the call log entry. [CHAR LIMIT=30] -->
+  <string name="call_details">Call details</string>
+
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/calllog/ui/res/values/strings.xml b/java/com/android/dialer/calllog/ui/res/values/strings.xml
index 9b044ca..0ef0eaf 100644
--- a/java/com/android/dialer/calllog/ui/res/values/strings.xml
+++ b/java/com/android/dialer/calllog/ui/res/values/strings.xml
@@ -17,12 +17,6 @@
 
 <resources>
 
-  <!-- Text to show in call log for a video call. [CHAR LIMIT=16] -->
-  <string name="new_call_log_video">Video</string>
-
-  <!-- String used to display calls from unknown numbers in the call log.  [CHAR LIMIT=30] -->
-  <string name="new_call_log_unknown">Unknown</string>
-
   <!-- Header in call log to group calls from the current day.  [CHAR LIMIT=30] -->
   <string name="new_call_log_header_today">Today</string>
 
diff --git a/java/com/android/dialer/calllogutils/CallLogEntryText.java b/java/com/android/dialer/calllogutils/CallLogEntryText.java
new file mode 100644
index 0000000..873f9eb
--- /dev/null
+++ b/java/com/android/dialer/calllogutils/CallLogEntryText.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2017 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.calllogutils;
+
+import android.content.Context;
+import android.provider.CallLog.Calls;
+import android.text.TextUtils;
+import com.android.dialer.calllog.model.CoalescedRow;
+import com.android.dialer.time.Clock;
+
+/**
+ * Computes the primary text and secondary text for call log entries.
+ *
+ * <p>These text values are shown in the main call log list or in the top item of the bottom sheet
+ * menu.
+ */
+public final class CallLogEntryText {
+
+  /**
+   * The primary text for bottom sheets is the same as shown in the entry list.
+   *
+   * <p>(In the entry list, the number of calls and additional icons are displayed as images
+   * following the primary text.)
+   */
+  public static CharSequence buildPrimaryText(Context context, CoalescedRow row) {
+    StringBuilder primaryText = new StringBuilder();
+    if (!TextUtils.isEmpty(row.name())) {
+      primaryText.append(row.name());
+    } else if (!TextUtils.isEmpty(row.formattedNumber())) {
+      primaryText.append(row.formattedNumber());
+    } else {
+      // TODO(zachh): Handle CallLog.Calls.PRESENTATION_*, including Verizon restricted numbers.
+      primaryText.append(context.getText(R.string.new_call_log_unknown));
+    }
+    return primaryText.toString();
+  }
+
+  /** The secondary text to show in the main call log entry list. */
+  public static CharSequence buildSecondaryTextForEntries(
+      Context context, Clock clock, CoalescedRow row) {
+    /*
+     * Rules: (Duo video, )?$Label|$Location • Date
+     *
+     * Examples:
+     *   Duo Video, Mobile • Now
+     *   Duo Video • 11:45pm
+     *   Mobile • 11:45pm
+     *   Mobile • Sunday
+     *   Brooklyn, NJ • Jan 15
+     *
+     * Date rules:
+     *   if < 1 minute ago: "Now"; else if today: HH:MM(am|pm); else if < 3 days: day; else: MON D
+     */
+    StringBuilder secondaryText = secondaryTextPrefix(context, row);
+
+    if (secondaryText.length() > 0) {
+      secondaryText.append(" • ");
+    }
+    secondaryText.append(
+        CallLogDates.newCallLogTimestampLabel(context, clock.currentTimeMillis(), row.timestamp()));
+    return secondaryText.toString();
+  }
+
+  /**
+   * The secondary text to show in the top item of the bottom sheet.
+   *
+   * <p>This is basically the same as {@link #buildSecondaryTextForEntries(Context, Clock,
+   * CoalescedRow)} except that instead of suffixing with the time of the call, we suffix with the
+   * formatted number.
+   */
+  public static String buildSecondaryTextForBottomSheet(Context context, CoalescedRow row) {
+    /*
+     * Rules: (Duo video, )?$Label|$Location [• NumberIfNoName]?
+     *
+     * The number is shown at the end if there is no name for the entry. (It is shown in primary
+     * text otherwise.)
+     *
+     * Examples:
+     *   Duo Video, Mobile • 555-1234
+     *   Duo Video • 555-1234
+     *   Mobile • 555-1234
+     *   Mobile • 555-1234
+     *   Brooklyn, NJ
+     */
+    StringBuilder secondaryText = secondaryTextPrefix(context, row);
+
+    if (TextUtils.isEmpty(row.name())) {
+      // If the name is empty the number is shown as the primary text and there's nothing to add.
+      return secondaryText.toString();
+    }
+    if (TextUtils.isEmpty(row.formattedNumber())) {
+      // If there's no number, don't append anything.
+      return secondaryText.toString();
+    }
+    // Otherwise append the number.
+    if (secondaryText.length() > 0) {
+      secondaryText.append(" • ");
+    }
+    secondaryText.append(row.formattedNumber());
+    return secondaryText.toString();
+  }
+
+  /**
+   * Returns a value such as "Duo Video, Mobile" without the time of the call or formatted number
+   * appended.
+   *
+   * <p>When the secondary text is shown in call log entry list, this prefix is suffixed with the
+   * time of the call, and when it is shown in a bottom sheet, it is suffixed with the formatted
+   * number.
+   */
+  private static StringBuilder secondaryTextPrefix(Context context, CoalescedRow row) {
+    StringBuilder secondaryText = new StringBuilder();
+    if ((row.features() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO) {
+      // TODO(zachh): Add "Duo" prefix?
+      secondaryText.append(context.getText(R.string.new_call_log_video));
+    }
+    String numberTypeLabel = row.numberTypeLabel();
+    if (!TextUtils.isEmpty(numberTypeLabel)) {
+      if (secondaryText.length() > 0) {
+        secondaryText.append(", ");
+      }
+      secondaryText.append(numberTypeLabel);
+    } else { // If there's a number type label, don't show the location.
+      String location = row.geocodedLocation();
+      if (!TextUtils.isEmpty(location)) {
+        if (secondaryText.length() > 0) {
+          secondaryText.append(", ");
+        }
+        secondaryText.append(location);
+      }
+    }
+    return secondaryText;
+  }
+}
diff --git a/java/com/android/dialer/calllogutils/CallLogIntents.java b/java/com/android/dialer/calllogutils/CallLogIntents.java
new file mode 100644
index 0000000..11308e6
--- /dev/null
+++ b/java/com/android/dialer/calllogutils/CallLogIntents.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2017 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.calllogutils;
+
+import android.content.Intent;
+import android.provider.CallLog.Calls;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import com.android.dialer.callintent.CallInitiationType;
+import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.calllog.model.CoalescedRow;
+
+/** Provides intents related to call log entries. */
+public final class CallLogIntents {
+
+  /**
+   * Returns an intent which can be used to call back for the provided row.
+   *
+   * <p>If the call was a video call, a video call will be placed, and if the call was an audio
+   * call, an audio call will be placed.
+   *
+   * @return null if the provided {@code row} doesn't have a number
+   */
+  @Nullable
+  public static Intent getCallBackIntent(CoalescedRow row) {
+    // TODO(zachh): Do something with parsed values to make more dialable?
+    String originalNumber = row.number().getRawInput().getNumber();
+
+    // TODO(zachh): Make this more sophisticated, e.g. return null for non-dialable numbers?
+    if (TextUtils.isEmpty(originalNumber)) {
+      return null;
+    }
+
+    // TODO(zachh): More granular logging?
+    // TODO(zachh): Support assisted dialing.
+    return new CallIntentBuilder(originalNumber, CallInitiationType.Type.CALL_LOG)
+        .setPhoneAccountHandle(
+            PhoneAccountUtils.getAccount(row.phoneAccountComponentName(), row.phoneAccountId()))
+        .setIsVideoCall((row.features() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO)
+        .build();
+  }
+}
diff --git a/java/com/android/dialer/calllogutils/PhoneAccountUtils.java b/java/com/android/dialer/calllogutils/PhoneAccountUtils.java
index c639893..153f291 100644
--- a/java/com/android/dialer/calllogutils/PhoneAccountUtils.java
+++ b/java/com/android/dialer/calllogutils/PhoneAccountUtils.java
@@ -31,7 +31,7 @@
 
   /** Return a list of phone accounts that are subscription/SIM accounts. */
   public static List<PhoneAccountHandle> getSubscriptionPhoneAccounts(Context context) {
-    List<PhoneAccountHandle> subscriptionAccountHandles = new ArrayList<PhoneAccountHandle>();
+    List<PhoneAccountHandle> subscriptionAccountHandles = new ArrayList<>();
     final List<PhoneAccountHandle> accountHandles =
         TelecomUtil.getCallCapablePhoneAccounts(context);
     for (PhoneAccountHandle accountHandle : accountHandles) {
diff --git a/java/com/android/dialer/calllogutils/res/values/strings.xml b/java/com/android/dialer/calllogutils/res/values/strings.xml
index 56cd94a..b8ba5b1 100644
--- a/java/com/android/dialer/calllogutils/res/values/strings.xml
+++ b/java/com/android/dialer/calllogutils/res/values/strings.xml
@@ -130,4 +130,10 @@
 
   <!-- String to be displayed to indicate in the call log that a call just now occurred. -->
   <string name="now">Now</string>
+
+  <!-- Text to show in call log for a video call. [CHAR LIMIT=16] -->
+  <string name="new_call_log_video">Video</string>
+
+  <!-- String used to display calls from unknown numbers in the call log.  [CHAR LIMIT=30] -->
+  <string name="new_call_log_unknown">Unknown</string>
 </resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/clipboard/AndroidManifest.xml b/java/com/android/dialer/clipboard/AndroidManifest.xml
new file mode 100644
index 0000000..d6da6ef
--- /dev/null
+++ b/java/com/android/dialer/clipboard/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<!--
+ ~ Copyright (C) 2017 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
+ -->
+<manifest package="com.android.dialer.clipboard"/>
diff --git a/java/com/android/contacts/common/ClipboardUtils.java b/java/com/android/dialer/clipboard/ClipboardUtils.java
similarity index 90%
rename from java/com/android/contacts/common/ClipboardUtils.java
rename to java/com/android/dialer/clipboard/ClipboardUtils.java
index 3d76839..933ac75 100644
--- a/java/com/android/contacts/common/ClipboardUtils.java
+++ b/java/com/android/dialer/clipboard/ClipboardUtils.java
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.contacts.common;
+package com.android.dialer.clipboard;
 
 import android.content.ClipData;
 import android.content.ClipboardManager;
@@ -22,7 +22,8 @@
 import android.text.TextUtils;
 import android.widget.Toast;
 
-public class ClipboardUtils {
+/** Copies provided label and text to the clipboard and optionally shows a "text copied" toast. */
+public final class ClipboardUtils {
 
   private ClipboardUtils() {}
 
diff --git a/java/com/android/dialer/clipboard/res/values/strings.xml b/java/com/android/dialer/clipboard/res/values/strings.xml
new file mode 100644
index 0000000..2edd293
--- /dev/null
+++ b/java/com/android/dialer/clipboard/res/values/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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
+  -->
+
+<resources>
+
+  <!-- Toast shown when text is copied to the clipboard [CHAR LIMIT=64] -->
+  <string name="toast_text_copied">Text copied</string>
+
+</resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/contactactions/ContactActionBottomSheet.java b/java/com/android/dialer/contactactions/ContactActionBottomSheet.java
index 9bf7ca0..f2f1d18 100644
--- a/java/com/android/dialer/contactactions/ContactActionBottomSheet.java
+++ b/java/com/android/dialer/contactactions/ContactActionBottomSheet.java
@@ -29,33 +29,38 @@
 import android.widget.LinearLayout;
 import android.widget.TextView;
 import com.android.dialer.common.Assert;
+import com.android.dialer.contactactions.ContactPrimaryActionInfo.PhotoInfo;
 import com.android.dialer.contactphoto.ContactPhotoManager;
-import com.android.dialer.dialercontact.DialerContact;
 import java.util.List;
 
 /**
  * {@link BottomSheetDialog} used for building a list of contact actions in a bottom sheet menu.
  *
- * <p>{@link #show(Context, DialerContact, List)} should be used to create and display the menu.
- * Modules are built using {@link ContactActionModule} and some defaults are provided by {@link
- * IntentModule} and {@link DividerModule}.
+ * <p>{@link #show(Context, ContactPrimaryActionInfo, List)} should be used to create and display
+ * the menu. Modules are built using {@link ContactActionModule} and some defaults are provided by
+ * {@link IntentModule} and {@link DividerModule}.
  */
 public class ContactActionBottomSheet extends BottomSheetDialog implements OnClickListener {
 
   private final List<ContactActionModule> modules;
-  private final DialerContact contact;
+  private final ContactPrimaryActionInfo contactPrimaryActionInfo;
 
   private ContactActionBottomSheet(
-      Context context, DialerContact contact, List<ContactActionModule> modules) {
+      Context context,
+      ContactPrimaryActionInfo contactPrimaryActionInfo,
+      List<ContactActionModule> modules) {
     super(context);
     this.modules = modules;
-    this.contact = contact;
+    this.contactPrimaryActionInfo = contactPrimaryActionInfo;
     setContentView(LayoutInflater.from(context).inflate(R.layout.sheet_layout, null));
   }
 
   public static ContactActionBottomSheet show(
-      Context context, DialerContact contact, List<ContactActionModule> modules) {
-    ContactActionBottomSheet sheet = new ContactActionBottomSheet(context, contact, modules);
+      Context context,
+      ContactPrimaryActionInfo contactPrimaryActionInfo,
+      List<ContactActionModule> modules) {
+    ContactActionBottomSheet sheet =
+        new ContactActionBottomSheet(context, contactPrimaryActionInfo, modules);
     sheet.show();
     return sheet;
   }
@@ -75,38 +80,37 @@
     }
   }
 
-  // TODO(calderwoodra): add on click action to contact.
   private View getContactView(ViewGroup container) {
     LayoutInflater inflater = LayoutInflater.from(getContext());
     View contactView = inflater.inflate(R.layout.contact_layout, container, false);
 
+    // TODO(zachh): The contact image should be badged with a video icon if it is for a video call.
+    PhotoInfo photoInfo = contactPrimaryActionInfo.photoInfo();
     ContactPhotoManager.getInstance(getContext())
         .loadDialerThumbnailOrPhoto(
             contactView.findViewById(R.id.quick_contact_photo),
-            contact.hasContactUri() ? Uri.parse(contact.getContactUri()) : null,
-            contact.getPhotoId(),
-            contact.hasPhotoUri() ? Uri.parse(contact.getPhotoUri()) : null,
-            contact.getNameOrNumber(),
-            contact.getContactType());
+            photoInfo.lookupUri() != null ? Uri.parse(photoInfo.lookupUri()) : null,
+            photoInfo.photoId(),
+            photoInfo.photoUri() != null ? Uri.parse(photoInfo.photoUri()) : null,
+            photoInfo.displayName(),
+            photoInfo.contactType());
 
-    TextView nameView = contactView.findViewById(R.id.contact_name);
-    TextView numberView = contactView.findViewById(R.id.phone_number);
+    TextView primaryTextView = contactView.findViewById(R.id.primary_text);
+    TextView secondaryTextView = contactView.findViewById(R.id.secondary_text);
 
-    nameView.setText(contact.getNameOrNumber());
-    if (!TextUtils.isEmpty(contact.getDisplayNumber())) {
-      numberView.setVisibility(View.VISIBLE);
-      String secondaryInfo =
-          TextUtils.isEmpty(contact.getNumberLabel())
-              ? contact.getDisplayNumber()
-              : getContext()
-                  .getString(
-                      com.android.contacts.common.R.string.call_subject_type_and_number,
-                      contact.getNumberLabel(),
-                      contact.getDisplayNumber());
-      numberView.setText(secondaryInfo);
+    primaryTextView.setText(contactPrimaryActionInfo.primaryText());
+    if (!TextUtils.isEmpty(contactPrimaryActionInfo.secondaryText())) {
+      secondaryTextView.setText(contactPrimaryActionInfo.secondaryText());
     } else {
-      numberView.setVisibility(View.GONE);
-      numberView.setText(null);
+      secondaryTextView.setVisibility(View.GONE);
+      secondaryTextView.setText(null);
+    }
+    if (contactPrimaryActionInfo.intent() != null) {
+      contactView.setOnClickListener(
+          (view) -> {
+            getContext().startActivity(contactPrimaryActionInfo.intent());
+            dismiss();
+          });
     }
     return contactView;
   }
diff --git a/java/com/android/dialer/contactactions/ContactPrimaryActionInfo.java b/java/com/android/dialer/contactactions/ContactPrimaryActionInfo.java
new file mode 100644
index 0000000..2535f85
--- /dev/null
+++ b/java/com/android/dialer/contactactions/ContactPrimaryActionInfo.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2017 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.contactactions;
+
+import android.content.Intent;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.DialerPhoneNumber;
+import com.android.dialer.lettertile.LetterTileDrawable;
+import com.google.auto.value.AutoValue;
+
+/**
+ * Contains information necessary to construct the primary action for a contact bottom sheet.
+ *
+ * <p>This may include information about the call, for instance when the bottom sheet is shown from
+ * the call log.
+ */
+@AutoValue
+public abstract class ContactPrimaryActionInfo {
+
+  @Nullable
+  public abstract DialerPhoneNumber number();
+
+  /** Information used to construct the photo for the contact. */
+  @AutoValue
+  public abstract static class PhotoInfo {
+    public abstract long photoId();
+
+    @Nullable
+    public abstract String photoUri();
+
+    @Nullable
+    public abstract String lookupUri();
+
+    /** Badges the photo with a video icon if true. */
+    public abstract boolean isVideo();
+
+    @LetterTileDrawable.ContactType
+    public abstract int contactType();
+
+    /** Used to generate letter tile if there is no photo. */
+    @Nullable
+    public abstract String displayName();
+
+    /** Builder for {@link PhotoInfo}. */
+    @AutoValue.Builder
+    public abstract static class Builder {
+      public abstract Builder setPhotoId(long photoId);
+
+      public abstract Builder setPhotoUri(@Nullable String photoUri);
+
+      public abstract Builder setLookupUri(@Nullable String lookupUri);
+
+      public abstract Builder setIsVideo(boolean isVideo);
+
+      public abstract Builder setContactType(@LetterTileDrawable.ContactType int contactType);
+
+      public abstract Builder setDisplayName(@Nullable String displayName);
+
+      public abstract PhotoInfo build();
+    }
+
+    public static Builder builder() {
+      return new AutoValue_ContactPrimaryActionInfo_PhotoInfo.Builder();
+    }
+  }
+
+  @NonNull
+  public abstract PhotoInfo photoInfo();
+
+  @Nullable
+  public abstract CharSequence primaryText();
+
+  @Nullable
+  public abstract CharSequence secondaryText();
+
+  /**
+   * The intent to fire when the user clicks the top row of the bottom sheet. Null if no action
+   * should occur (e.g. if the number is unknown).
+   */
+  @Nullable
+  public abstract Intent intent();
+
+  // TODO(zachh): Add SIM info here if should be shown in bottom sheet.
+
+  /** Builder for {@link ContactPrimaryActionInfo}. */
+  @AutoValue.Builder
+  public abstract static class Builder {
+    public abstract Builder setNumber(@Nullable DialerPhoneNumber dialerPhoneNumber);
+
+    public abstract Builder setPhotoInfo(@NonNull PhotoInfo photoInfo);
+
+    public abstract Builder setPrimaryText(@Nullable CharSequence primaryText);
+
+    public abstract Builder setSecondaryText(@Nullable CharSequence secondaryText);
+
+    public abstract Builder setIntent(@Nullable Intent intent);
+
+    public abstract ContactPrimaryActionInfo build();
+  }
+
+  public static Builder builder() {
+    return new AutoValue_ContactPrimaryActionInfo.Builder();
+  }
+}
diff --git a/java/com/android/dialer/contactactions/IntentModule.java b/java/com/android/dialer/contactactions/IntentModule.java
index 201f521..5a4870c 100644
--- a/java/com/android/dialer/contactactions/IntentModule.java
+++ b/java/com/android/dialer/contactactions/IntentModule.java
@@ -19,7 +19,9 @@
 import android.content.Context;
 import android.content.Intent;
 import android.support.annotation.DrawableRes;
+import android.support.annotation.Nullable;
 import android.support.annotation.StringRes;
+import android.telecom.PhoneAccountHandle;
 import com.android.dialer.callintent.CallInitiationType.Type;
 import com.android.dialer.callintent.CallIntentBuilder;
 
@@ -56,19 +58,33 @@
     return true;
   }
 
-  public static IntentModule newCallModule(Context context, String number, Type initiationType) {
+  public static IntentModule newCallModule(
+      Context context,
+      String number,
+      @Nullable PhoneAccountHandle phoneAccountHandle,
+      Type initiationType) {
+    // TODO(zachh): Support post-dial digits; consider using DialerPhoneNumber.
     return new IntentModule(
         context,
-        new CallIntentBuilder(number, initiationType).build(),
+        new CallIntentBuilder(number, initiationType)
+            .setPhoneAccountHandle(phoneAccountHandle)
+            .build(),
         R.string.call,
         R.drawable.quantum_ic_call_white_24);
   }
 
   public static IntentModule newVideoCallModule(
-      Context context, String number, Type initiationType) {
+      Context context,
+      String number,
+      @Nullable PhoneAccountHandle phoneAccountHandle,
+      Type initiationType) {
+    // TODO(zachh): Support post-dial digits; consider using DialerPhoneNumber.
     return new IntentModule(
         context,
-        new CallIntentBuilder(number, initiationType).setIsVideoCall(true).build(),
+        new CallIntentBuilder(number, initiationType)
+            .setPhoneAccountHandle(phoneAccountHandle)
+            .setIsVideoCall(true)
+            .build(),
         R.string.video_call,
         R.drawable.quantum_ic_videocam_white_24);
   }
diff --git a/java/com/android/dialer/contactactions/res/layout/contact_layout.xml b/java/com/android/dialer/contactactions/res/layout/contact_layout.xml
index bf32971..8ea05d4 100644
--- a/java/com/android/dialer/contactactions/res/layout/contact_layout.xml
+++ b/java/com/android/dialer/contactactions/res/layout/contact_layout.xml
@@ -38,13 +38,13 @@
       android:gravity="center_vertical">
 
     <TextView
-        android:id="@+id/contact_name"
+        android:id="@+id/primary_text"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         style="@style/PrimaryText"/>
 
     <TextView
-        android:id="@+id/phone_number"
+        android:id="@+id/secondary_text"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_marginTop="2dp"