From 2d588b857347fda465a963742c4f61e4014f3441 Mon Sep 17 00:00:00 2001 From: Nancy Chen Date: Tue, 18 Aug 2015 16:39:02 -0700 Subject: Show snackbar to undo last deleted voicemail. Snackbar will appear for 3 seconds during which the user can undo the last deletion. The way it works is the snackbar appears for 3 seconds and a delayed callback is set for 3 seconds after which the voicemail is permanently deleted from the database. If a second (or third or fourth) voicemail is deleted subsequently, the previous voicemails that were waiting for the undo timeout are deleted immediately. Bug: 22460745 Change-Id: I84b70994275975e4e3020321884d382cc87098dc --- src/com/android/dialer/calllog/CallLogAdapter.java | 92 ++++++++++++++++++++-- .../android/dialer/calllog/CallLogFragment.java | 3 +- .../dialer/voicemail/VoicemailPlaybackLayout.java | 49 +++++++++++- .../voicemail/VoicemailPlaybackPresenter.java | 18 ++++- 4 files changed, 151 insertions(+), 11 deletions(-) (limited to 'src/com/android/dialer') diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java index a0ce85bd9..477e449b3 100644 --- a/src/com/android/dialer/calllog/CallLogAdapter.java +++ b/src/com/android/dialer/calllog/CallLogAdapter.java @@ -109,6 +109,9 @@ public class CallLogAdapter extends GroupingListAdapter // 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 class CallLogAdapter extends GroupingListAdapter } } - 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 @@ public class CallLogAdapter extends GroupingListAdapter @Override public int getItemCount() { - return super.getItemCount() + (mShowVoicemailPromoCard ? 1 : 0); + return super.getItemCount() + (mShowVoicemailPromoCard ? 1 : 0) + - (mHiddenPosition != RecyclerView.NO_POSITION ? 1 : 0); } @Override @@ -615,19 +627,81 @@ public class CallLogAdapter extends GroupingListAdapter */ @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 @@ public class CallLogAdapter extends GroupingListAdapter 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 c63b212d1..dfa895981 100644 --- a/src/com/android/dialer/calllog/CallLogFragment.java +++ b/src/com/android/dialer/calllog/CallLogFragment.java @@ -347,7 +347,7 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis if (mVoicemailPlaybackPresenter != null) { mVoicemailPlaybackPresenter.onPause(); } - mAdapter.pauseCache(); + mAdapter.onPause(); super.onPause(); } @@ -360,7 +360,6 @@ public class CallLogFragment extends Fragment implements CallLogQueryHandler.Lis @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 158ed5834..38f6a1773 100644 --- a/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java +++ b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java @@ -22,10 +22,12 @@ import android.content.Context; 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.SeekBar.OnSeekBarChangeListener; 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 @@ import javax.annotation.concurrent.ThreadSafe; */ @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 @@ public class VoicemailPlaybackLayout extends LinearLayout 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 @@ public class VoicemailPlaybackLayout extends LinearLayout mStateText.setText(getString(R.string.voicemail_playback_error)); } - public void onSpeakerphoneOn(boolean on) { if (mPresenter != null) { mPresenter.setSpeakerphoneOn(on); @@ -357,6 +389,17 @@ public class VoicemailPlaybackLayout extends LinearLayout 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 7270af787..540ffb446 100644 --- a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java +++ b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java @@ -94,6 +94,8 @@ public class VoicemailPlaybackPresenter 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 @@ public class VoicemailPlaybackPresenter } /* 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); -- cgit v1.2.3