Merge "Show snackbar to undo last deleted voicemail."
diff --git a/res/values/strings.xml b/res/values/strings.xml
index a80153c..b062047 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -64,6 +64,12 @@
     <!-- Menu item used to delete a voicemail. [CHAR LIMIT=30] -->
     <string name="call_log_trash_voicemail">Delete voicemail</string>
 
+    <!-- Text for snackbar to undo a voicemail delete. [CHAR LIMIT=20] -->
+    <string name="snackbar_voicemail_deleted">Voicemail deleted</string>
+
+    <!-- Text for undo button in snackbar for voicemail deletion. [CHAR LIMIT=10] -->
+    <string name="snackbar_voicemail_deleted_undo">UNDO</string>
+
     <!-- Title of the confirmation dialog for clearing the call log. [CHAR LIMIT=37]  -->
     <string name="clearCallLogConfirmation_title">Clear call history?</string>
 
diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java
index a0ce85b..477e449 100644
--- a/src/com/android/dialer/calllog/CallLogAdapter.java
+++ b/src/com/android/dialer/calllog/CallLogAdapter.java
@@ -109,6 +109,9 @@
     // Tracks the rowId of the currently expanded list item, so the position can be updated if there
     // are any changes to the call log entries, such as additions or removals.
     private long mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
+    private int mHiddenPosition = RecyclerView.NO_POSITION;
+    private Uri mHiddenItemUri = null;
+    private boolean mPendingHide = false;
 
     /**
      *  Hashmap, keyed by call Id, used to track the day group for a call.  As call log entries are
@@ -399,7 +402,15 @@
         }
     }
 
-    public void pauseCache() {
+    public void onPause() {
+        pauseCache();
+        if (mHiddenItemUri != null) {
+            CallLogAsyncTaskUtil.deleteVoicemail(mContext, mHiddenItemUri, null);
+        }
+    }
+
+    @VisibleForTesting
+    /* package */ void pauseCache() {
         mContactInfoCache.stop();
         mTelecomCallLogCache.reset();
     }
@@ -595,7 +606,8 @@
 
     @Override
     public int getItemCount() {
-        return super.getItemCount() + (mShowVoicemailPromoCard ? 1 : 0);
+        return super.getItemCount() + (mShowVoicemailPromoCard ? 1 : 0)
+                - (mHiddenPosition != RecyclerView.NO_POSITION ? 1 : 0);
     }
 
     @Override
@@ -615,20 +627,82 @@
      */
     @Override
     public Object getItem(int position) {
-        return super.getItem(position - (mShowVoicemailPromoCard ? 1 : 0));
+        return super.getItem(position - (mShowVoicemailPromoCard ? 1 : 0)
+                + ((mHiddenPosition != RecyclerView.NO_POSITION && position >= mHiddenPosition)
+                        ? 1 : 0));
     }
 
     protected boolean isCallLogActivity() {
         return mIsCallLogActivity;
     }
 
+    /**
+     * In order to implement the "undo" function, when a voicemail is "deleted" i.e. when the user
+     * clicks the delete button, the deleted item is temporarily hidden from the list. If a user
+     * clicks delete on a second item before the first item's undo option has expired, the first
+     * item is immediately deleted so that only one item can be "undoed" at a time.
+     */
     @Override
     public void onVoicemailDeleted(Uri uri) {
+        if (mHiddenItemUri == null) {
+            // Immediately hide the currently expanded card.
+            mHiddenPosition = mCurrentlyExpandedPosition;
+            notifyDataSetChanged();
+        } else {
+            // This means that there was a previous item that was hidden in the UI but not
+            // yet deleted from the database (call it a "pending delete"). Delete this previous item
+            // now since it is only possible to do one "undo" at a time.
+            CallLogAsyncTaskUtil.deleteVoicemail(mContext, mHiddenItemUri, null);
+
+            // Set pending hide action so that the current item is hidden only after the previous
+            // item is permanently deleted.
+            mPendingHide = true;
+        }
+
+        collapseExpandedCard();
+
+        // Save the new hidden item uri in case it needs to be deleted from the database when
+        // a user attempts to delete another item.
+        mHiddenItemUri = uri;
+    }
+
+    private void collapseExpandedCard() {
         mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
         mCurrentlyExpandedPosition = RecyclerView.NO_POSITION;
     }
 
     /**
+     * When the user clicks "undo", the hidden item is unhidden.
+     */
+    @Override
+    public void onVoicemailDeleteUndo() {
+        mHiddenPosition = RecyclerView.NO_POSITION;
+        mHiddenItemUri = null;
+
+        mPendingHide = false;
+        notifyDataSetChanged();
+    }
+
+    /**
+     * This callback signifies that a database deletion has completed. This means that if there is
+     * an item pending deletion, it will be hidden because the previous item that was in "undo" mode
+     * has been removed from the database. Otherwise it simply resets the hidden state because there
+     * are no pending deletes and thus no hidden items.
+     */
+    @Override
+    public void onVoicemailDeletedInDatabase() {
+        if (mPendingHide) {
+            mHiddenPosition = mCurrentlyExpandedPosition;
+            mPendingHide = false;
+        } else {
+            // There should no longer be any hidden item because it has been deleted from the
+            // database.
+            mHiddenPosition = RecyclerView.NO_POSITION;
+            mHiddenItemUri = null;
+        }
+    }
+
+    /**
      * Retrieves the day group of the previous call in the call log.  Used to determine if the day
      * group has changed and to trigger display of the day group text.
      *
@@ -640,8 +714,16 @@
         int startingPosition = cursor.getPosition();
         int dayGroup = CallLogGroupBuilder.DAY_GROUP_NONE;
         if (cursor.moveToPrevious()) {
-            long previousRowId = cursor.getLong(CallLogQuery.ID);
-            dayGroup = getDayGroupForCall(previousRowId);
+            // If the previous entry is hidden (deleted in the UI but not in the database), skip it
+            // and check the card above it. A list with the voicemail promo card at the top will be
+            // 1-indexed because the 0th index is the promo card iteself.
+            int previousViewPosition = mShowVoicemailPromoCard ? startingPosition :
+                startingPosition - 1;
+            if (previousViewPosition != mHiddenPosition ||
+                    (previousViewPosition == mHiddenPosition && cursor.moveToPrevious())) {
+                long previousRowId = cursor.getLong(CallLogQuery.ID);
+                dayGroup = getDayGroupForCall(previousRowId);
+            }
         }
         cursor.moveToPosition(startingPosition);
         return dayGroup;
diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java
index c63b212..dfa8959 100644
--- a/src/com/android/dialer/calllog/CallLogFragment.java
+++ b/src/com/android/dialer/calllog/CallLogFragment.java
@@ -347,7 +347,7 @@
         if (mVoicemailPlaybackPresenter != null) {
             mVoicemailPlaybackPresenter.onPause();
         }
-        mAdapter.pauseCache();
+        mAdapter.onPause();
         super.onPause();
     }
 
@@ -360,7 +360,6 @@
 
     @Override
     public void onDestroy() {
-        mAdapter.pauseCache();
         mAdapter.changeCursor(null);
 
         if (mVoicemailPlaybackPresenter != null) {
diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java
index 158ed58..38f6a17 100644
--- a/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java
@@ -22,10 +22,12 @@
 import android.media.MediaPlayer;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Handler;
 import android.os.PowerManager;
 import android.provider.VoicemailContract;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.support.design.widget.Snackbar;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -36,6 +38,7 @@
 import android.widget.TextView;
 
 import com.android.common.io.MoreCloseables;
+import com.android.dialer.PhoneCallDetails;
 import com.android.dialer.R;
 import com.android.dialer.calllog.CallLogAsyncTaskUtil;
 
@@ -58,8 +61,10 @@
  */
 @NotThreadSafe
 public class VoicemailPlaybackLayout extends LinearLayout
-        implements VoicemailPlaybackPresenter.PlaybackView {
+        implements VoicemailPlaybackPresenter.PlaybackView,
+        CallLogAsyncTaskUtil.CallLogAsyncTaskListener {
     private static final String TAG = VoicemailPlaybackLayout.class.getSimpleName();
+    private static final int VOICEMAIL_DELETE_DELAY_MS = 3000;
 
     /**
      * Controls the animation of the playback slider.
@@ -184,8 +189,36 @@
                 return;
             }
             mPresenter.pausePlayback();
-            CallLogAsyncTaskUtil.deleteVoicemail(mContext, mVoicemailUri, null);
             mPresenter.onVoicemailDeleted();
+
+            final Uri deleteUri = mVoicemailUri;
+            final Runnable deleteCallback = new Runnable() {
+                @Override
+                public void run() {
+                    if (mVoicemailUri == deleteUri) {
+                        CallLogAsyncTaskUtil.deleteVoicemail(mContext, deleteUri,
+                                VoicemailPlaybackLayout.this);
+                    }
+                }
+            };
+
+            final Handler handler = new Handler();
+            // Add a little buffer time in case the user clicked "undo" at the end of the delay
+            // window.
+            handler.postDelayed(deleteCallback, VOICEMAIL_DELETE_DELAY_MS + 50);
+
+            Snackbar.make(VoicemailPlaybackLayout.this, R.string.snackbar_voicemail_deleted,
+                            Snackbar.LENGTH_LONG)
+                    .setDuration(VOICEMAIL_DELETE_DELAY_MS)
+                    .setAction(R.string.snackbar_voicemail_deleted_undo,
+                            new View.OnClickListener() {
+                                @Override
+                                public void onClick(View view) {
+                                    mPresenter.onVoicemailDeleteUndo();
+                                        handler.removeCallbacks(deleteCallback);
+                                }
+                            })
+                    .show();
         }
     };
 
@@ -282,7 +315,6 @@
         mStateText.setText(getString(R.string.voicemail_playback_error));
     }
 
-
     public void onSpeakerphoneOn(boolean on) {
         if (mPresenter != null) {
             mPresenter.setSpeakerphoneOn(on);
@@ -357,6 +389,17 @@
         mPlaybackSeek.setEnabled(true);
     }
 
+    @Override
+    public void onDeleteCall() {}
+
+    @Override
+    public void onDeleteVoicemail() {
+        mPresenter.onVoicemailDeletedInDatabase();
+    }
+
+    @Override
+    public void onGetCallDetails(PhoneCallDetails[] details) {}
+
     private String getString(int resId) {
         return mContext.getString(resId);
     }
diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
index 7270af7..540ffb4 100644
--- a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java
@@ -94,6 +94,8 @@
 
     public interface OnVoicemailDeletedListener {
         void onVoicemailDeleted(Uri uri);
+        void onVoicemailDeleteUndo();
+        void onVoicemailDeletedInDatabase();
     }
 
     /** The enumeration of {@link AsyncTask} objects we use in this class. */
@@ -730,12 +732,26 @@
     }
 
     /* package */ void onVoicemailDeleted() {
-        // Trampoline the event notification to the interested listener
+        // Trampoline the event notification to the interested listener.
         if (mOnVoicemailDeletedListener != null) {
             mOnVoicemailDeletedListener.onVoicemailDeleted(mVoicemailUri);
         }
     }
 
+    /* package */ void onVoicemailDeleteUndo() {
+        // Trampoline the event notification to the interested listener.
+        if (mOnVoicemailDeletedListener != null) {
+            mOnVoicemailDeletedListener.onVoicemailDeleteUndo();
+        }
+    }
+
+    /* package */ void onVoicemailDeletedInDatabase() {
+        // Trampoline the event notification to the interested listener.
+        if (mOnVoicemailDeletedListener != null) {
+            mOnVoicemailDeletedListener.onVoicemailDeletedInDatabase();
+        }
+    }
+
     private static synchronized ScheduledExecutorService getScheduledExecutorServiceInstance() {
         if (mScheduledExecutorService == null) {
             mScheduledExecutorService = Executors.newScheduledThreadPool(NUMBER_OF_THREADS_IN_POOL);