Merge "Respect setting for incoming call to vibrate" into nyc-dev
diff --git a/res/drawable-hdpi/ic_archive_white_24dp.png b/res/drawable-hdpi/ic_archive_white_24dp.png
new file mode 100644
index 0000000..bb72e89
--- /dev/null
+++ b/res/drawable-hdpi/ic_archive_white_24dp.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_archive_white_24dp.png b/res/drawable-mdpi/ic_archive_white_24dp.png
new file mode 100644
index 0000000..f6aa3f9
--- /dev/null
+++ b/res/drawable-mdpi/ic_archive_white_24dp.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_archive_white_24dp.png b/res/drawable-xhdpi/ic_archive_white_24dp.png
new file mode 100644
index 0000000..3513bd9
--- /dev/null
+++ b/res/drawable-xhdpi/ic_archive_white_24dp.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_archive_white_24dp.png b/res/drawable-xxhdpi/ic_archive_white_24dp.png
new file mode 100644
index 0000000..00e04e4
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_archive_white_24dp.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/ic_archive_white_24dp.png b/res/drawable-xxxhdpi/ic_archive_white_24dp.png
new file mode 100644
index 0000000..34cd3fd
--- /dev/null
+++ b/res/drawable-xxxhdpi/ic_archive_white_24dp.png
Binary files differ
diff --git a/res/layout/phone_favorite_tile_view.xml b/res/layout/phone_favorite_tile_view.xml
index 8b00fba..aa82ca0 100644
--- a/res/layout/phone_favorite_tile_view.xml
+++ b/res/layout/phone_favorite_tile_view.xml
@@ -24,7 +24,8 @@
         android:id="@+id/contact_favorite_card"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:focusable="true" >
+        android:focusable="true"
+        android:nextFocusRight="@+id/contact_tile_secondary_button">
 
         <com.android.contacts.common.widget.LayoutSuppressingImageView
             android:id="@+id/contact_tile_image"
@@ -102,8 +103,7 @@
             android:id="@+id/contact_tile_push_state"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
-            android:focusable="true"
-            android:nextFocusRight="@+id/contact_tile_secondary_button"
+            android:importantForAccessibility="no"
             android:background="@drawable/item_background_material_dark" />
 
         <ImageButton
diff --git a/res/layout/voicemail_playback_layout.xml b/res/layout/voicemail_playback_layout.xml
index bb7fe34..64a68bd 100644
--- a/res/layout/voicemail_playback_layout.xml
+++ b/res/layout/voicemail_playback_layout.xml
@@ -97,6 +97,17 @@
                     android:tint="@color/voicemail_icon_tint"
                     android:contentDescription="@string/call_log_trash_voicemail" />
 
+                <Space
+                    android:layout_width="0dp"
+                    android:layout_height="0dp"
+                    android:layout_weight="1" />
+
+                <ImageButton android:id="@+id/archive_voicemail"
+                    style="@style/VoicemailPlaybackLayoutButtonStyle"
+                    android:src="@drawable/ic_archive_white_24dp"
+                    android:tint="@color/voicemail_icon_tint"
+                    android:contentDescription="@string/call_log_archive_voicemail" />
+
             </LinearLayout>
 
         </LinearLayout>
diff --git a/res/menu/dialtacts_options.xml b/res/menu/dialtacts_options.xml
index 8a9e25f..8e31026 100644
--- a/res/menu/dialtacts_options.xml
+++ b/res/menu/dialtacts_options.xml
@@ -20,6 +20,9 @@
         android:icon="@drawable/ic_menu_history_lt"
         android:title="@string/action_menu_call_history_description" />
     <item
+        android:id="@+id/menu_archive"
+        android:title="@string/voicemail_archive_activity_title" />
+    <item
         android:id="@+id/menu_import_export"
         android:title="@string/menu_import_export" />
     <item
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 6d2bd5e..734dde8 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -84,12 +84,21 @@
     <!-- Menu item used to delete a voicemail. [CHAR LIMIT=30] -->
     <string name="call_log_trash_voicemail">Delete voicemail</string>
 
+    <!-- Menu item used to archive a voicemail. [CHAR LIMIT=30] -->
+    <string name="call_log_archive_voicemail">Archive voicemail</string>
+
     <!-- Text for snackbar to undo a voicemail delete. [CHAR LIMIT=30] -->
     <string name="snackbar_voicemail_deleted">Voicemail deleted</string>
 
+    <!-- Text for snackbar to undo a voicemail archive. [CHAR LIMIT=30] -->
+    <string name="snackbar_voicemail_archived">Voicemail archived</string>
+
     <!-- Text for undo button in snackbar for voicemail deletion. [CHAR LIMIT=10] -->
     <string name="snackbar_voicemail_deleted_undo">UNDO</string>
 
+    <!-- Text for going to archive button in snackbar for voicemail archive. [CHAR LIMIT=10] -->
+    <string name="snackbar_voicemail_archived_goto">GOTO ARCHIVE</string>
+
     <!-- Title of the confirmation dialog for clearing the call log. [CHAR LIMIT=37]  -->
     <string name="clearCallLogConfirmation_title">Clear call history?</string>
 
@@ -158,6 +167,9 @@
     <!-- Message to display whilst we are waiting for the content to be fetched. [CHAR LIMIT=40] -->
     <string name="voicemail_fetching_content">Loading voicemail\u2026</string>
 
+    <!-- Message to display whilst we are waiting for the content to be archived. [CHAR LIMIT=40] -->
+    <string name="voicemail_archiving_content">Archiving voicemail\u2026</string>
+
     <!-- Message to display if we fail to get content within a suitable time period. [CHAR LIMIT=40] -->
     <string name="voicemail_fetching_timout">Couldn\'t load voicemail</string>
 
@@ -455,6 +467,12 @@
          The date will be replaced by 'Today' for voicemails created on the current day. For example: Today at 2:49 PM -->
     <string name="voicemailCallLogDateTimeFormat"><xliff:g id="date" example="Jul 25, 2014">%1$s</xliff:g> at <xliff:g id="time" example="2:49 PM">%2$s</xliff:g></string>
 
+    <!-- Format for duration of voicemails which are displayed when viewing voicemail logs. For example "01:22" -->
+    <string name="voicemailDurationFormat"><xliff:g id="minutes" example="10">%1$02d</xliff:g>:<xliff:g id="seconds" example="20">%2$02d</xliff:g></string>
+
+    <!-- A format string used for displaying the date, time and duration for a voicemail call log. For example: Jul 25, 2014 at 2:49 PM • 00:34 -->
+    <string name="voicemailCallLogDateTimeFormatWithDuration"><xliff:g id="dateAndTime" example="Jul 25, 2014 at 2:49PM">%1$s</xliff:g> \u2022 <xliff:g id="duration" example="01:22">%2$s</xliff:g></string>
+
     <!-- Dialog message which is shown when the user tries to make a phone call
          to prohibited phone numbers [CHAR LIMIT=NONE] -->
     <string name="dialog_phone_call_prohibited_message" msgid="4313552620858880999">Can\'t call this number</string>
@@ -525,12 +543,18 @@
     <!-- Text displayed when the list of voicemails is empty -->
     <string name="call_log_voicemail_empty">Your voicemail inbox is empty.</string>
 
+    <!-- Text displayed when the list of voicemail archives is empty -->
+    <string name="voicemail_archive_empty">Your voicemail archive is empty.</string>
+
     <!--  Menu option to show favorite contacts only -->
     <string name="show_favorites_only">Show favorites only</string>
 
     <!--  Title of activity that displays a list of all calls -->
     <string name="call_log_activity_title">Call History</string>
 
+    <!--  Title of activity that displays a list of all archived voicemails -->
+    <string name="voicemail_archive_activity_title">Voicemail Archive</string>
+
     <!-- Title for the call log tab containing the list of all voicemails and calls
          [CHAR LIMIT=30] -->
     <string name="call_log_all_title">All</string>
@@ -911,6 +935,9 @@
     <!-- Error toast message for when send to voicemail import fails. [CHAR LIMIT=40] -->
     <string name="send_to_voicemail_import_failed">Import failed</string>
 
+    <!-- Error toast message for when voicemail archive fails. [CHAR LIMIT=40] -->
+    <string name="voicemail_archive_failed">Failed to archive voicemail.</string>
+
     <!-- String describing the delete icon on a blocked number list item.
         When tapped, it will show a dialog confirming the unblocking of the number.
         [CHAR LIMIT=NONE]-->
diff --git a/src/com/android/dialer/DialtactsActivity.java b/src/com/android/dialer/DialtactsActivity.java
index d507483..d12cf24 100644
--- a/src/com/android/dialer/DialtactsActivity.java
+++ b/src/com/android/dialer/DialtactsActivity.java
@@ -240,6 +240,7 @@
     private FloatingActionButtonController mFloatingActionButtonController;
 
     private int mActionBarHeight;
+    private int mPreviouslySelectedTabIndex;
 
     /**
      * The text returned from a voice search query.  Set in {@link #onActivityResult} and used in
@@ -423,7 +424,7 @@
 
         mIsLandscape = getResources().getConfiguration().orientation
                 == Configuration.ORIENTATION_LANDSCAPE;
-
+        mPreviouslySelectedTabIndex = ListsFragment.TAB_INDEX_SPEED_DIAL;
         final View floatingActionButtonContainer = findViewById(
                 R.id.floating_action_button_container);
         ImageButton floatingActionButton = (ImageButton) findViewById(R.id.floating_action_button);
@@ -567,6 +568,11 @@
 
     @Override
     protected void onPause() {
+        // Only clear missed calls if the pause was not triggered by an orientation change
+        // (or any other confirguration change)
+        if (!isChangingConfigurations()) {
+            updateMissedCalls();
+        }
         if (mClearSearchOnPause) {
             hideDialpadAndSearchUi();
             mClearSearchOnPause = false;
@@ -1328,7 +1334,9 @@
 
     @Override
     public void onPageSelected(int position) {
+        updateMissedCalls();
         int tabIndex = mListsFragment.getCurrentTabIndex();
+        mPreviouslySelectedTabIndex = tabIndex;
         if (tabIndex == ListsFragment.TAB_INDEX_ALL_CONTACTS) {
             mFloatingActionButtonController.changeIcon(
                     getResources().getDrawable(R.drawable.ic_person_add_24dp),
@@ -1389,4 +1397,10 @@
         }
         return FloatingActionButtonController.ALIGN_END;
     }
+
+    private void updateMissedCalls() {
+        if (mPreviouslySelectedTabIndex == ListsFragment.TAB_INDEX_HISTORY) {
+            mListsFragment.markMissedCallsAsReadAndRemoveNotifications();
+        }
+    }
 }
diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java
index 372c7bd..6f96ee5 100644
--- a/src/com/android/dialer/calllog/CallLogAdapter.java
+++ b/src/com/android/dialer/calllog/CallLogAdapter.java
@@ -16,6 +16,7 @@
 
 package com.android.dialer.calllog;
 
+import com.android.dialer.DialtactsActivity;
 import com.google.common.annotations.VisibleForTesting;
 
 import android.content.Context;
@@ -169,6 +170,12 @@
                 mCurrentlyExpandedPosition = RecyclerView.NO_POSITION;
                 mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
             } else {
+                if (viewHolder.callType == CallLog.Calls.MISSED_TYPE) {
+                    CallLogAsyncTaskUtil.markCallAsRead(mContext, viewHolder.callIds);
+                    if (!mIsCallLogActivity) {
+                        ((DialtactsActivity) v.getContext()).updateTabUnreadCounts();
+                    }
+                }
                 expandViewHolderActions(viewHolder);
             }
 
@@ -506,7 +513,8 @@
         details.features = getCallFeatures(c, count);
         details.geocode = c.getString(CallLogQuery.GEOCODED_LOCATION);
         details.transcription = c.getString(CallLogQuery.TRANSCRIPTION);
-        if (details.callTypes[0] == CallLog.Calls.VOICEMAIL_TYPE) {
+        if (details.callTypes[0] == CallLog.Calls.VOICEMAIL_TYPE ||
+                details.callTypes[0] == CallLog.Calls.MISSED_TYPE) {
             details.isRead = c.getInt(CallLogQuery.IS_READ) == 1;
         }
 
diff --git a/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java b/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java
index bb7bdbd..9825918 100644
--- a/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java
+++ b/src/com/android/dialer/calllog/CallLogAsyncTaskUtil.java
@@ -29,6 +29,7 @@
 import android.util.Log;
 
 import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.util.PermissionsUtil;
 import com.android.dialer.DialtactsActivity;
 import com.android.dialer.PhoneCallDetails;
 import com.android.dialer.util.AppCompatConstants;
@@ -51,7 +52,9 @@
         DELETE_CALL,
         DELETE_BLOCKED_CALL,
         MARK_VOICEMAIL_READ,
+        MARK_CALL_READ,
         GET_CALL_DETAILS,
+        UPDATE_DURATION
     }
 
     private static final class CallDetailQuery {
@@ -348,8 +351,6 @@
                 Intent intent = new Intent(context, CallLogNotificationsService.class);
                 intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD);
                 context.startService(intent);
-
-                ((DialtactsActivity) context).updateTabUnreadCounts();
                 return null;
             }
         });
@@ -379,6 +380,64 @@
         });
     }
 
+    public static void markCallAsRead(final Context context, final long[] callIds) {
+        if (!PermissionsUtil.hasPhonePermissions(context)) {
+            return;
+        }
+        if (sAsyncTaskExecutor == null) {
+            initTaskExecutor();
+        }
+
+        sAsyncTaskExecutor.submit(Tasks.MARK_CALL_READ, new AsyncTask<Void, Void, Void>() {
+            @Override
+            public Void doInBackground(Void... params) {
+
+                StringBuilder where = new StringBuilder();
+                where.append(CallLog.Calls.TYPE).append(" = ").append(CallLog.Calls.MISSED_TYPE);
+                where.append(" AND ");
+
+                Long[] callIdLongs = new Long[callIds.length];
+                for (int i = 0; i < callIds.length; i++) {
+                    callIdLongs[i] = callIds[i];
+                }
+                where.append(CallLog.Calls._ID).append(
+                        " IN (" + TextUtils.join(",", callIdLongs) + ")");
+
+                ContentValues values = new ContentValues(1);
+                values.put(CallLog.Calls.IS_READ, "1");
+                context.getContentResolver().update(
+                        CallLog.Calls.CONTENT_URI, values, where.toString(), null);
+                return null;
+            }
+        });
+    }
+
+    /**
+     * Updates the duration of a voicemail call log entry.
+     */
+    public static void updateVoicemailDuration(
+            final Context context,
+            final Uri voicemailUri,
+            final int duration) {
+        if (!PermissionsUtil.hasPhonePermissions(context)) {
+            return;
+        }
+
+        if (sAsyncTaskExecutor == null) {
+            initTaskExecutor();
+        }
+
+        sAsyncTaskExecutor.submit(Tasks.UPDATE_DURATION, new AsyncTask<Void, Void, Void>() {
+            @Override
+            public Void doInBackground(Void... params) {
+                ContentValues values = new ContentValues(1);
+                values.put(CallLog.Calls.DURATION, duration);
+                context.getContentResolver().update(voicemailUri, values, null, null);
+                return null;
+            }
+        });
+    }
+
     @VisibleForTesting
     public static void resetForTest() {
         sAsyncTaskExecutor = null;
diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java
index 41ff7d3..9cd1359 100644
--- a/src/com/android/dialer/calllog/CallLogFragment.java
+++ b/src/com/android/dialer/calllog/CallLogFragment.java
@@ -41,6 +41,7 @@
 import com.android.contacts.common.GeoUtil;
 import com.android.contacts.common.util.PermissionsUtil;
 import com.android.dialer.R;
+import com.android.dialer.list.ListsFragment;
 import com.android.dialer.util.EmptyLoader;
 import com.android.dialer.voicemail.VoicemailPlaybackPresenter;
 import com.android.dialer.widget.EmptyContentView;
@@ -66,6 +67,7 @@
     private static final String KEY_FILTER_TYPE = "filter_type";
     private static final String KEY_LOG_LIMIT = "log_limit";
     private static final String KEY_DATE_LIMIT = "date_limit";
+    private static final String KEY_IS_CALL_LOG_ACTIVITY = "is_call_log_activity";
 
     // No limit specified for the number of logs to show; use the CallLogQueryHandler's default.
     private static final int NO_LOG_LIMIT = -1;
@@ -193,6 +195,7 @@
             mCallTypeFilter = state.getInt(KEY_FILTER_TYPE, mCallTypeFilter);
             mLogLimit = state.getInt(KEY_LOG_LIMIT, mLogLimit);
             mDateLimit = state.getLong(KEY_DATE_LIMIT, mDateLimit);
+            mIsCallLogActivity = state.getBoolean(KEY_IS_CALL_LOG_ACTIVITY, mIsCallLogActivity);
         }
 
         final Activity activity = getActivity();
@@ -278,6 +281,9 @@
     public void onVoicemailUnreadCountFetched(Cursor cursor) {}
 
     @Override
+    public void onMissedCallsUnreadCountFetched(Cursor cursor) {}
+
+    @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
         View view = inflater.inflate(R.layout.call_log_fragment, container, false);
         setupView(view, null);
@@ -351,7 +357,7 @@
 
     @Override
     public void onStop() {
-        updateOnTransition(false /* onEntry */);
+        updateOnTransition();
 
         super.onStop();
     }
@@ -371,6 +377,7 @@
         outState.putInt(KEY_FILTER_TYPE, mCallTypeFilter);
         outState.putInt(KEY_LOG_LIMIT, mLogLimit);
         outState.putLong(KEY_DATE_LIMIT, mDateLimit);
+        outState.putBoolean(KEY_IS_CALL_LOG_ACTIVITY, mIsCallLogActivity);
 
         mAdapter.onSaveInstanceState(outState);
     }
@@ -378,6 +385,9 @@
     @Override
     public void fetchCalls() {
         mCallLogQueryHandler.fetchCalls(mCallTypeFilter, mDateLimit);
+        if (!mIsCallLogActivity) {
+            ((ListsFragment) getParentFragment()).updateTabUnreadCounts();
+        }
     }
 
     private void updateEmptyMessage(int filterType) {
@@ -425,7 +435,7 @@
         if (mMenuVisible != menuVisible) {
             mMenuVisible = menuVisible;
             if (!menuVisible) {
-                updateOnTransition(false /* onEntry */);
+                updateOnTransition();
             } else if (isResumed()) {
                 refreshData();
             }
@@ -443,8 +453,8 @@
 
             fetchCalls();
             mCallLogQueryHandler.fetchVoicemailStatus();
-
-            updateOnTransition(true /* onEntry */);
+            mCallLogQueryHandler.fetchMissedCallsUnreadCount();
+            updateOnTransition();
             mRefreshDataRequired = false;
         } else {
             // Refresh the display of the existing data to update the timestamp text descriptions.
@@ -453,28 +463,17 @@
     }
 
     /**
-     * Updates the call data and notification state on entering or leaving the call log tab.
-     *
-     * If we are leaving the call log tab, mark all the missed calls as read.
+     * Updates the voicemail notification state.
      *
      * TODO: Move to CallLogActivity
      */
-    private void updateOnTransition(boolean onEntry) {
+    private void updateOnTransition() {
         // We don't want to update any call data when keyguard is on because the user has likely not
         // seen the new calls yet.
         // This might be called before onCreate() and thus we need to check null explicitly.
-        if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode()) {
-            // On either of the transitions we update the missed call and voicemail notifications.
-            // While exiting we additionally consume all missed calls (by marking them as read).
-            mCallLogQueryHandler.markNewCallsAsOld();
-            if (!onEntry) {
-                mCallLogQueryHandler.markMissedCallsAsRead();
-            }
-            if (mCallTypeFilter == Calls.VOICEMAIL_TYPE) {
-                CallLogNotificationsHelper.updateVoicemailNotifications(getActivity());
-            } else {
-                CallLogNotificationsHelper.removeMissedCallNotifications(getActivity());
-            }
+        if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode()
+                && mCallTypeFilter == Calls.VOICEMAIL_TYPE) {
+            CallLogNotificationsHelper.updateVoicemailNotifications(getActivity());
         }
     }
 
diff --git a/src/com/android/dialer/calllog/CallLogQueryHandler.java b/src/com/android/dialer/calllog/CallLogQueryHandler.java
index 4cb835b..9ff74cf 100644
--- a/src/com/android/dialer/calllog/CallLogQueryHandler.java
+++ b/src/com/android/dialer/calllog/CallLogQueryHandler.java
@@ -62,6 +62,8 @@
     private static final int QUERY_VOICEMAIL_STATUS_TOKEN = 57;
     /** The token for the query to fetch the number of unread voicemails. */
     private static final int QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN = 58;
+    /** The token for the query to fetch the number of missed calls. */
+    private static final int QUERY_MISSED_CALLS_UNREAD_COUNT_TOKEN = 59;
 
     private final int mLogLimit;
 
@@ -225,19 +227,25 @@
         if (!PermissionsUtil.hasPhonePermissions(mContext)) {
             return;
         }
-        // Mark all "new" calls as not new anymore.
-        StringBuilder where = new StringBuilder();
-        where.append(Calls.IS_READ).append(" = 0");
-        where.append(" AND ");
-        where.append(Calls.TYPE).append(" = ").append(Calls.MISSED_TYPE);
 
         ContentValues values = new ContentValues(1);
         values.put(Calls.IS_READ, "1");
 
         startUpdate(UPDATE_MARK_MISSED_CALL_AS_READ_TOKEN, null, Calls.CONTENT_URI, values,
-                where.toString(), null);
+                getUnreadMissedCallsQuery(), null);
     }
 
+    /** Fetch all missed calls received since last time the tab was opened. */
+    public void fetchMissedCallsUnreadCount() {
+        if (!PermissionsUtil.hasPhonePermissions(mContext)) {
+            return;
+        }
+
+        startQuery(QUERY_MISSED_CALLS_UNREAD_COUNT_TOKEN, null, Calls.CONTENT_URI,
+                new String[]{Calls._ID}, getUnreadMissedCallsQuery(), null, null);
+    }
+
+
     @Override
     protected synchronized void onNotNullableQueryComplete(int token, Object cookie,
             Cursor cursor) {
@@ -253,6 +261,8 @@
                 updateVoicemailStatus(cursor);
             } else if (token == QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN) {
                 updateVoicemailUnreadCount(cursor);
+            } else if (token == QUERY_MISSED_CALLS_UNREAD_COUNT_TOKEN) {
+                updateMissedCallsUnreadCount(cursor);
             } else {
                 Log.w(TAG, "Unknown query completed: ignoring: " + token);
             }
@@ -276,6 +286,17 @@
 
     }
 
+    /**
+     * @return Query string to get all unread missed calls.
+     */
+    private String getUnreadMissedCallsQuery() {
+        StringBuilder where = new StringBuilder();
+        where.append(Calls.IS_READ).append(" = 0");
+        where.append(" AND ");
+        where.append(Calls.TYPE).append(" = ").append(Calls.MISSED_TYPE);
+        return where.toString();
+    }
+
     private void updateVoicemailStatus(Cursor statusCursor) {
         final Listener listener = mListener.get();
         if (listener != null) {
@@ -290,6 +311,13 @@
         }
     }
 
+    private void updateMissedCallsUnreadCount(Cursor statusCursor) {
+        final Listener listener = mListener.get();
+        if (listener != null) {
+            listener.onMissedCallsUnreadCountFetched(statusCursor);
+        }
+    }
+
     /** Listener to completion of various queries. */
     public interface Listener {
         /** Called when {@link CallLogQueryHandler#fetchVoicemailStatus()} completes. */
@@ -298,6 +326,9 @@
         /** Called when {@link CallLogQueryHandler#fetchVoicemailUnreadCount()} completes. */
         void onVoicemailUnreadCountFetched(Cursor cursor);
 
+        /** Called when {@link CallLogQueryHandler#fetchMissedCallsUnreadCount()} completes. */
+        void onMissedCallsUnreadCountFetched(Cursor cursor);
+
         /**
          * Called when {@link CallLogQueryHandler#fetchCalls(int)} complete.
          * Returns true if takes ownership of cursor.
diff --git a/src/com/android/dialer/calllog/PhoneCallDetailsHelper.java b/src/com/android/dialer/calllog/PhoneCallDetailsHelper.java
index e6b8508..7b149e2 100644
--- a/src/com/android/dialer/calllog/PhoneCallDetailsHelper.java
+++ b/src/com/android/dialer/calllog/PhoneCallDetailsHelper.java
@@ -40,6 +40,7 @@
 
 import java.util.ArrayList;
 import java.util.Calendar;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Helper class to fill in the views in {@link PhoneCallDetailsViews}.
@@ -110,10 +111,8 @@
             callCount = null;
         }
 
-        CharSequence callLocationAndDate = getCallLocationAndDate(details);
-
-        // Set the call count, location and date.
-        setCallCountAndDate(views, callCount, callLocationAndDate);
+        // Set the call count, location, date and if voicemail, set the duration.
+        setDetailText(views, callCount, details);
 
         // Set the account label if it exists.
         String accountLabel = mCallLogCache.getAccountLabel(details.accountHandle);
@@ -314,10 +313,11 @@
         }
     }
 
-    /** Sets the call count and date. */
-    private void setCallCountAndDate(PhoneCallDetailsViews views, Integer callCount,
-            CharSequence dateText) {
+    /** Sets the call count, date, and if it is a voicemail, sets the duration. */
+    private void setDetailText(PhoneCallDetailsViews views, Integer callCount,
+                               PhoneCallDetails details) {
         // Combine the count (if present) and the date.
+        CharSequence dateText = getCallLocationAndDate(details);
         final CharSequence text;
         if (callCount != null) {
             text = mResources.getString(
@@ -326,6 +326,22 @@
             text = dateText;
         }
 
-        views.callLocationAndDate.setText(text);
+        if (details.callTypes[0] == Calls.VOICEMAIL_TYPE && details.duration > 0) {
+            views.callLocationAndDate.setText(mResources.getString(
+                    R.string.voicemailCallLogDateTimeFormatWithDuration, text,
+                    getVoicemailDuration(details)));
+        } else {
+            views.callLocationAndDate.setText(text);
+        }
+
+    }
+
+    private String getVoicemailDuration(PhoneCallDetails details) {
+        long minutes = TimeUnit.SECONDS.toMinutes(details.duration);
+        long seconds = details.duration - TimeUnit.MINUTES.toSeconds(minutes);
+        if (minutes > 99) {
+            minutes = 99;
+        }
+        return mResources.getString(R.string.voicemailDurationFormat, minutes, seconds);
     }
 }
diff --git a/src/com/android/dialer/list/ListsFragment.java b/src/com/android/dialer/list/ListsFragment.java
index d5caa1a..5b7c950 100644
--- a/src/com/android/dialer/list/ListsFragment.java
+++ b/src/com/android/dialer/list/ListsFragment.java
@@ -36,6 +36,7 @@
 import com.android.dialer.DialtactsActivity;
 import com.android.dialer.R;
 import com.android.dialer.calllog.CallLogFragment;
+import com.android.dialer.calllog.CallLogNotificationsHelper;
 import com.android.dialer.calllog.CallLogQueryHandler;
 import com.android.dialer.calllog.VisualVoicemailCallLogFragment;
 import com.android.dialer.logging.Logger;
@@ -192,6 +193,7 @@
         mCallLogQueryHandler =
                 new CallLogQueryHandler(getActivity(), getActivity().getContentResolver(), this);
         mCallLogQueryHandler.fetchVoicemailStatus();
+        mCallLogQueryHandler.fetchMissedCallsUnreadCount();
         Trace.endSection();
     }
 
@@ -347,6 +349,23 @@
     }
 
     @Override
+    public void onMissedCallsUnreadCountFetched(Cursor cursor) {
+        if (getActivity() == null || getActivity().isFinishing() || cursor == null) {
+            return;
+        }
+
+        int count = 0;
+        try {
+            count = cursor.getCount();
+        } finally {
+            cursor.close();
+        }
+
+        mViewPagerTabs.setUnreadCount(count, TAB_INDEX_HISTORY);
+        mViewPagerTabs.updateTab(TAB_INDEX_HISTORY);
+    }
+
+    @Override
     public boolean onCallsFetched(Cursor statusCursor) {
         // Return false; did not take ownership of cursor
         return false;
@@ -358,14 +377,29 @@
 
     /**
      * External method to update unread count because the unread count changes when the user
-     * expands a voicemail in the call log.
+     * expands a voicemail in the call log or when the user expands an unread call in the call
+     * history tab.
      */
     public void updateTabUnreadCounts() {
-        if (mHasActiveVoicemailProvider && mCallLogQueryHandler != null) {
-            mCallLogQueryHandler.fetchVoicemailUnreadCount();
+        if (mCallLogQueryHandler != null) {
+            mCallLogQueryHandler.fetchMissedCallsUnreadCount();
+            if (mHasActiveVoicemailProvider) {
+                mCallLogQueryHandler.fetchVoicemailUnreadCount();
+            }
         }
     }
 
+    /**
+     * External method to mark all missed calls as read.
+     */
+    public void markMissedCallsAsReadAndRemoveNotifications() {
+        if (mCallLogQueryHandler != null) {
+            mCallLogQueryHandler.markMissedCallsAsRead();
+            CallLogNotificationsHelper.removeMissedCallNotifications(getActivity());
+        }
+    }
+
+
     public void showRemoveView(boolean show) {
         mRemoveViewContent.setVisibility(show ? View.VISIBLE : View.GONE);
         mRemoveView.setAlpha(show ? 0 : 1);
diff --git a/src/com/android/dialer/voicemail/VisualVoicemailEnabledChecker.java b/src/com/android/dialer/voicemail/VisualVoicemailEnabledChecker.java
index 3134b14..80a0368 100644
--- a/src/com/android/dialer/voicemail/VisualVoicemailEnabledChecker.java
+++ b/src/com/android/dialer/voicemail/VisualVoicemailEnabledChecker.java
@@ -91,6 +91,11 @@
     }
 
     @Override
+    public void onMissedCallsUnreadCountFetched(Cursor cursor) {
+        // Do nothing
+    }
+
+    @Override
     public boolean onCallsFetched(Cursor combinedCursor) {
         // Do nothing
         return false;
diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
index 93ff002..2bb4de9 100644
--- a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
@@ -36,6 +36,7 @@
 import android.widget.SeekBar;
 
 import com.android.dialer.R;
+import com.android.dialer.calllog.CallLogAsyncTaskUtil;
 import com.android.dialer.util.AsyncTaskExecutor;
 import com.android.dialer.util.AsyncTaskExecutors;
 import com.android.common.io.MoreCloseables;
@@ -557,6 +558,12 @@
         Log.d(TAG, "onPrepared");
         mIsPrepared = true;
 
+        // Update the duration in the database if it was not previously retrieved
+        if (mDuration.get() == 0) {
+            CallLogAsyncTaskUtil.updateVoicemailDuration(mContext, mVoicemailUri,
+                    mMediaPlayer.getDuration() / 1000);
+        }
+
         mDuration.set(mMediaPlayer.getDuration());
 
         Log.d(TAG, "onPrepared: mPosition=" + mPosition);
diff --git a/tests/src/com/android/dialer/calllog/PhoneCallDetailsHelperTest.java b/tests/src/com/android/dialer/calllog/PhoneCallDetailsHelperTest.java
index b6202b9..c0d1203 100644
--- a/tests/src/com/android/dialer/calllog/PhoneCallDetailsHelperTest.java
+++ b/tests/src/com/android/dialer/calllog/PhoneCallDetailsHelperTest.java
@@ -34,6 +34,8 @@
 
 import java.util.GregorianCalendar;
 import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * Unit tests for {@link PhoneCallDetailsHelper}.
@@ -141,9 +143,29 @@
 
     public void testVoicemailLocationNotShownWithDate() {
         setVoicemailPhoneCallDetailsWithDate(TEST_DATE);
+        assertLocationAndDateExactEquals("Jun 3 at 1:00 PM • 99:20");
+    }
+
+    public void testVoicemailDuration() {
+        setVoicemailPhoneCallDetailsWithDuration(100);
+        assertDurationExactEquals("01:40");
+    }
+
+    public void testVoicemailDuration_Capped() {
+        setVoicemailPhoneCallDetailsWithDuration(TEST_DURATION);
+        assertDurationExactEquals("99:20");
+    }
+
+    public void testVoicemailDuration_Zero() {
+        setVoicemailPhoneCallDetailsWithDuration(0);
         assertLocationAndDateExactEquals("Jun 3 at 1:00 PM");
     }
 
+    public void testVoicemailDuration_EvenMinute() {
+        setVoicemailPhoneCallDetailsWithDuration(60);
+        assertDurationExactEquals("01:00");
+    }
+
     /** Asserts that a char sequence is actually a Spanned corresponding to the expected HTML. */
     private void assertEqualsHtml(String expectedHtml, CharSequence actualText) {
         // In order to contain HTML, the text should actually be a Spanned.
@@ -346,6 +368,14 @@
         assertEquals(text, mViews.callLocationAndDate.getText());
     }
 
+    /** Asserts that the duration is exactly as included in the location and date text field. */
+    private void assertDurationExactEquals(String text) {
+        Matcher matcher = Pattern.compile("(.*) (\\u2022) (\\d{2}:\\d{2})").matcher(
+                mViews.callLocationAndDate.getText());
+        assertEquals(true, matcher.matches());
+        assertEquals(text, matcher.group(3));
+    }
+
     /** Asserts that the video icon is shown. */
     private void assertIsVideoCall(boolean isVideoCall) {
         assertEquals(isVideoCall, mViews.callTypeIcons.isVideoShown());
@@ -407,6 +437,14 @@
         mHelper.setPhoneCallDetails(mViews, details);
     }
 
+    /** Sets the voice mail details with default values and the given duration. */
+    private void setVoicemailPhoneCallDetailsWithDuration(long duration) {
+        PhoneCallDetails details = getPhoneCallDetails();
+        details.duration = duration;
+        details.callTypes = new int[] {Calls.VOICEMAIL_TYPE};
+        mHelper.setPhoneCallDetails(mViews, details);
+    }
+
     /** Sets the phone call details with default values and the given call types using icons. */
     private void setPhoneCallDetailsWithCallTypeIcons(int... callTypes) {
         PhoneCallDetails details = getPhoneCallDetails();