diff options
6 files changed, 1257 insertions, 114 deletions
diff --git a/assets/quantum/res/drawable/quantum_ic_pause_vd_theme_24.xml b/assets/quantum/res/drawable/quantum_ic_pause_vd_theme_24.xml new file mode 100644 index 000000000..b683a3b33 --- /dev/null +++ b/assets/quantum/res/drawable/quantum_ic_pause_vd_theme_24.xml @@ -0,0 +1,25 @@ +<!-- + ~ 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 + --> +<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="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/> +</vector>
\ No newline at end of file diff --git a/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java b/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java index 1c53e3801..61fed52e6 100644 --- a/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java +++ b/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java @@ -17,15 +17,24 @@ package com.android.dialer.voicemail.listui; import android.app.FragmentManager; import android.database.Cursor; +import android.media.MediaPlayer; +import android.media.MediaPlayer.OnCompletionListener; +import android.media.MediaPlayer.OnErrorListener; +import android.media.MediaPlayer.OnPreparedListener; +import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; +import android.util.ArrayMap; import android.util.ArraySet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; +import com.android.dialer.common.concurrent.ThreadUtil; import com.android.dialer.time.Clock; import com.android.dialer.voicemail.listui.NewVoicemailViewHolder.NewVoicemailViewHolderListener; import com.android.dialer.voicemail.model.VoicemailEntry; +import java.util.Objects; import java.util.Set; /** {@link RecyclerView.Adapter} for the new voicemail call log fragment. */ @@ -38,8 +47,24 @@ final class NewVoicemailAdapter extends RecyclerView.Adapter<NewVoicemailViewHol /** A valid id for {@link VoicemailEntry} is greater than 0 */ private int currentlyExpandedViewHolderId = -1; - // A set of (re-usable) view holders being used by the recycler view to display voicemails + /** + * A set of (re-usable) view holders being used by the recycler view to display voicemails. This + * set may include multiple view holder with the same ID and shouldn't be used to lookup a + * specific viewholder based on this value, instead use newVoicemailViewHolderArrayMap for that + * purpose. + */ private final Set<NewVoicemailViewHolder> newVoicemailViewHolderSet = new ArraySet<>(); + /** + * This allows us to retrieve the view holder corresponding to a particular view holder id, and + * will always ensure there is only (up-to-date) view holder corresponding to a view holder id, + * unlike the newVoicemailViewHolderSet. + */ + private final ArrayMap<Integer, NewVoicemailViewHolder> newVoicemailViewHolderArrayMap = + new ArrayMap<>(); + + // A single instance of a media player re-used across the expanded view holders. + private final NewVoicemailMediaPlayer mediaPlayer = + new NewVoicemailMediaPlayer(new MediaPlayer()); /** @param cursor whose projection is {@link VoicemailCursorLoader.VOICEMAIL_COLUMNS} */ NewVoicemailAdapter(Cursor cursor, Clock clock, FragmentManager fragmentManager) { @@ -47,11 +72,17 @@ final class NewVoicemailAdapter extends RecyclerView.Adapter<NewVoicemailViewHol this.cursor = cursor; this.clock = clock; this.fragmentManager = fragmentManager; + initializeMediaPlayerListeners(); + } + + private void initializeMediaPlayerListeners() { + mediaPlayer.setOnCompletionListener(onCompletionListener); + mediaPlayer.setOnPreparedListener(onPreparedListener); + mediaPlayer.setOnErrorListener(onErrorListener); } @Override public NewVoicemailViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { - LogUtil.enterBlock("NewVoicemailAdapter.onCreateViewHolder"); LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext()); View view = inflater.inflate(R.layout.new_voicemail_entry, viewGroup, false); NewVoicemailViewHolder newVoicemailViewHolder = new NewVoicemailViewHolder(view, clock, this); @@ -61,58 +92,464 @@ final class NewVoicemailAdapter extends RecyclerView.Adapter<NewVoicemailViewHol @Override public void onBindViewHolder(NewVoicemailViewHolder viewHolder, int position) { + // Remove if the viewholder is being recycled. + if (newVoicemailViewHolderArrayMap.containsKey(viewHolder.getViewHolderId())) { + // TODO(uabdullah): Remove the logging, only here for debugging during development. + LogUtil.i( + "NewVoicemailAdapter.onBindViewHolder", + "Removing from hashset:%d, hashsetSize:%d", + viewHolder.getViewHolderId(), + newVoicemailViewHolderArrayMap.size()); + + newVoicemailViewHolderArrayMap.remove(viewHolder.getViewHolderId()); + } + + viewHolder.reset(); cursor.moveToPosition(position); - viewHolder.bind(cursor, fragmentManager); - expandOrCollapseViewHolder(viewHolder); + viewHolder.bindViewHolderValuesFromAdapter( + cursor, fragmentManager, mediaPlayer, position, currentlyExpandedViewHolderId); + + // Need this to ensure correct getCurrentlyExpandedViewHolder() value + newVoicemailViewHolderArrayMap.put(viewHolder.getViewHolderId(), viewHolder); + + // If the viewholder is playing the voicemail, keep updating its media player view (seekbar, + // duration etc.) + if (viewHolder.isViewHolderExpanded() && mediaPlayer.isPlaying()) { + Assert.checkArgument( + viewHolder + .getViewHolderVoicemailUri() + .equals(mediaPlayer.getLastPlayedOrPlayingVoicemailUri()), + "only the expanded view holder can be playing."); + Assert.isNotNull(getCurrentlyExpandedViewHolder()); + Assert.checkArgument( + getCurrentlyExpandedViewHolder() + .getViewHolderVoicemailUri() + .equals(mediaPlayer.getLastPlayedOrPlayingVoicemailUri())); + + recursivelyUpdateMediaPlayerViewOfExpandedViewHolder(viewHolder); + } + // Updates the hashmap with the most up-to-date state of the viewholder. + newVoicemailViewHolderArrayMap.put(viewHolder.getViewHolderId(), viewHolder); } /** - * Ensures a voicemail {@link NewVoicemailViewHolder} that was expanded and scrolled out of view, - * doesn't have it's corresponding recycled view also expanded. It also ensures than when the - * expanded voicemail is scrolled back into view, it still remains expanded. + * The {@link NewVoicemailAdapter} needs to keep track of {@link NewVoicemailViewHolder} that has + * been expanded. This is so that the adapter can ensure the correct {@link + * NewVoicemailMediaPlayerView} and {@link NewVoicemailViewHolder} states are maintained + * (playing/paused/reset) for the expanded viewholder, especially when views are recycled in + * {@link RecyclerView}. Since we can only have one expanded voicemail view holder, this method + * ensures that except for the currently expanded view holder, all the other view holders visible + * on the screen are collapsed. + * + * <p>The {@link NewVoicemailMediaPlayer} is also reset, if there is an existing playing + * voicemail. + * + * <p>This is the function that is responsible of keeping track of the expanded viewholder in the + * {@link NewVoicemailAdapter} + * + * <p>This is the first function called in the adapter when a viewholder has been expanded. + * + * <p>This is the function that is responsible of keeping track of the expanded viewholder in the + * {@link NewVoicemailAdapter} * - * @param viewHolder an {@link NewVoicemailViewHolder} that is either expanded or collapsed + * @param viewHolderRequestedToExpand is the view holder that is currently expanded. + * @param voicemailEntryOfViewHolder */ - private void expandOrCollapseViewHolder(NewVoicemailViewHolder viewHolder) { - if (viewHolder.getViewHolderId() == currentlyExpandedViewHolderId) { - viewHolder.expandViewHolder(); - } else { - viewHolder.collapseViewHolder(); + @Override + public void expandViewHolderFirstTimeAndCollapseAllOtherVisibleViewHolders( + NewVoicemailViewHolder viewHolderRequestedToExpand, + VoicemailEntry voicemailEntryOfViewHolder, + NewVoicemailViewHolderListener listener) { + + LogUtil.i( + "NewVoicemailAdapter.expandViewHolderFirstTimeAndCollapseAllOtherVisibleViewHolders", + "viewholder id:%d being request to expand, isExpanded:%b, size of our view holder " + + "dataset:%d, hashmap size:%d", + viewHolderRequestedToExpand.getViewHolderId(), + viewHolderRequestedToExpand.isViewHolderExpanded(), + newVoicemailViewHolderSet.size(), + newVoicemailViewHolderArrayMap.size()); + + currentlyExpandedViewHolderId = viewHolderRequestedToExpand.getViewHolderId(); + + for (NewVoicemailViewHolder viewHolder : newVoicemailViewHolderSet) { + if (viewHolder.getViewHolderId() != viewHolderRequestedToExpand.getViewHolderId()) { + viewHolder.collapseViewHolder(); + } + } + + // If the media player is playing and we expand something other than the currently playing one + // we should stop playing the media player + if (mediaPlayer.isPlaying() + && !Objects.equals( + mediaPlayer.getLastPlayedOrPlayingVoicemailUri(), + viewHolderRequestedToExpand.getViewHolderVoicemailUri())) { + LogUtil.i( + "NewVoicemailAdapter.expandViewHolderFirstTimeAndCollapseAllOtherVisibleViewHolders", + "Reset the media player since we expanded something other that the playing " + + "voicemail, MP was playing:%s, viewholderExpanded:%d, MP.isPlaying():%b", + String.valueOf(mediaPlayer.getLastPlayedOrPlayingVoicemailUri()), + viewHolderRequestedToExpand.getViewHolderId(), + mediaPlayer.isPlaying()); + mediaPlayer.reset(); + } + + // If the media player is paused and we expand something other than the currently paused one + // we should stop playing the media player + if (mediaPlayer.isPaused() + && !Objects.equals( + mediaPlayer.getLastPausedVoicemailUri(), + viewHolderRequestedToExpand.getViewHolderVoicemailUri())) { + LogUtil.i( + "NewVoicemailAdapter.expandViewHolderFirstTimeAndCollapseAllOtherVisibleViewHolders", + "There was an existing paused viewholder, the media player should reset since we " + + "expanded something other that the paused voicemail, MP.paused:%s", + String.valueOf(mediaPlayer.getLastPausedVoicemailUri())); + mediaPlayer.reset(); } + + Assert.checkArgument( + !viewHolderRequestedToExpand.isViewHolderExpanded(), + "cannot expand a voicemail that is not collapsed"); + + viewHolderRequestedToExpand.expandAndBindViewHolderAndMediaPlayerViewWithAdapterValues( + voicemailEntryOfViewHolder, fragmentManager, mediaPlayer, listener); + + // There should be nothing playing when we expand a viewholder for the first time + Assert.checkArgument(!mediaPlayer.isPlaying()); } + /** + * Ensures that when we collapse the expanded view, we don't expand it again when we are recycling + * the viewholders. If we collapse an existing playing voicemail viewholder, we should stop + * playing it. + * + * @param collapseViewHolder is the view holder that is currently collapsed. + */ @Override - public int getItemCount() { - return cursor.getCount(); + public void collapseExpandedViewHolder(NewVoicemailViewHolder collapseViewHolder) { + Assert.checkArgument(collapseViewHolder.getViewHolderId() == currentlyExpandedViewHolderId); + collapseViewHolder.collapseViewHolder(); + currentlyExpandedViewHolderId = -1; + + // If the view holder is currently playing, then we should stop playing it. + if (mediaPlayer.isPlaying()) { + Assert.checkArgument( + Objects.equals( + mediaPlayer.getLastPlayedOrPlayingVoicemailUri(), + collapseViewHolder.getViewHolderVoicemailUri()), + "the voicemail being played should have been of the recently collapsed view holder."); + mediaPlayer.reset(); + } + } + + @Override + public void pauseViewHolder(NewVoicemailViewHolder expandedViewHolder) { + Assert.isNotNull( + getCurrentlyExpandedViewHolder(), + "cannot have pressed pause if the viewholder wasn't expanded"); + Assert.checkArgument( + getCurrentlyExpandedViewHolder() + .getViewHolderVoicemailUri() + .equals(expandedViewHolder.getViewHolderVoicemailUri()), + "view holder whose pause button was pressed has to have been the expanded " + + "viewholder being tracked by the adapter."); + mediaPlayer.pauseMediaPlayer(expandedViewHolder.getViewHolderVoicemailUri()); + expandedViewHolder.setPausedStateOfMediaPlayerView( + expandedViewHolder.getViewHolderVoicemailUri(), mediaPlayer); + } + + @Override + public void resumePausedViewHolder(NewVoicemailViewHolder expandedViewHolder) { + Assert.isNotNull( + getCurrentlyExpandedViewHolder(), + "cannot have pressed pause if the viewholder wasn't expanded"); + Assert.checkArgument( + getCurrentlyExpandedViewHolder() + .getViewHolderVoicemailUri() + .equals(expandedViewHolder.getViewHolderVoicemailUri()), + "view holder whose play button was pressed has to have been the expanded " + + "viewholder being tracked by the adapter."); + Assert.isNotNull( + mediaPlayer.getLastPausedVoicemailUri(), "there should be be an pausedUri to resume"); + Assert.checkArgument( + mediaPlayer + .getLastPlayedOrPlayingVoicemailUri() + .equals(expandedViewHolder.getViewHolderVoicemailUri()), + "only the last playing uri can be resumed"); + Assert.checkArgument( + mediaPlayer + .getLastPreparedOrPreparingToPlayVoicemailUri() + .equals(expandedViewHolder.getViewHolderVoicemailUri()), + "only the last prepared uri can be resumed"); + Assert.checkArgument( + mediaPlayer + .getLastPreparedOrPreparingToPlayVoicemailUri() + .equals(mediaPlayer.getLastPlayedOrPlayingVoicemailUri()), + "the last prepared and playing voicemails have to be the same when resuming"); + + onPreparedListener.onPrepared(mediaPlayer.getMediaPlayer()); } /** - * We can only have one expanded voicemail view holder. This allows us to ensure that except for - * the currently expanded view holder, all the other view holders visible on the screen are - * collapsed. + * This function is called recursively to update the seekbar, duration, play/pause buttons of the + * expanded view holder if its playing. + * + * <p>Since this function is called at 30 frames/second, its possible (and eventually will happen) + * that between each update the playing voicemail state could have changed, in which case this + * method should stop calling itself. These conditions are: * - * @param expandedViewHolder is the view holder that is currently expanded. + * <ul> + * <li>The user scrolled the playing voicemail out of view. + * <li>Another view holder was expanded. + * <li>The playing voicemail was paused. + * <li>The media player returned {@link MediaPlayer#isPlaying()} to be true but had its {@link + * MediaPlayer#getCurrentPosition()} > {@link MediaPlayer#getDuration()}. + * <li>The {@link MediaPlayer} stopped playing. + * </ul> + * + * <p>Note: Since the update happens at 30 frames/second, it's also possible that the viewholder + * was recycled when scrolling the playing voicemail out of view. + * + * @param expandedViewHolderPossiblyPlaying the view holder that was expanded and could or could + * not be playing. This viewholder can be recycled. */ - @Override - public void onViewHolderExpanded(NewVoicemailViewHolder expandedViewHolder) { - currentlyExpandedViewHolderId = expandedViewHolder.getViewHolderId(); + private void recursivelyUpdateMediaPlayerViewOfExpandedViewHolder( + NewVoicemailViewHolder expandedViewHolderPossiblyPlaying) { + + // It's possible that by the time this is run, the expanded view holder has been + // scrolled out of view (and possibly recycled) + if (getCurrentlyExpandedViewHolder() == null) { + LogUtil.i( + "NewVoicemailAdapter.recursivelyUpdateMediaPlayerViewOfExpandedViewHolder", + "viewholder:%d media player view, no longer on screen, no need to update", + expandedViewHolderPossiblyPlaying.getViewHolderId()); + return; + } + + // Another viewholder was expanded, no need to update + if (!getCurrentlyExpandedViewHolder().equals(expandedViewHolderPossiblyPlaying)) { + LogUtil.i( + "NewVoicemailAdapter.recursivelyUpdateMediaPlayerViewOfExpandedViewHolder", + "currentlyExpandedViewHolderId:%d and the one we are attempting to update:%d " + + "aren't the same.", + currentlyExpandedViewHolderId, + expandedViewHolderPossiblyPlaying.getViewHolderId()); + return; + } + + Assert.checkArgument(expandedViewHolderPossiblyPlaying.isViewHolderExpanded()); + Assert.checkArgument( + expandedViewHolderPossiblyPlaying.getViewHolderId() + == getCurrentlyExpandedViewHolder().getViewHolderId()); + + // If the viewholder was paused, there is no need to update the media player view + if (mediaPlayer.isPaused()) { + Assert.checkArgument( + expandedViewHolderPossiblyPlaying + .getViewHolderVoicemailUri() + .equals(mediaPlayer.getLastPausedVoicemailUri()), + "only the expanded viewholder can be paused."); + + LogUtil.i( + "NewVoicemailAdapter.recursivelyUpdateMediaPlayerViewOfExpandedViewHolder", + "set the media player to a paused state"); + expandedViewHolderPossiblyPlaying.setPausedStateOfMediaPlayerView( + expandedViewHolderPossiblyPlaying.getViewHolderVoicemailUri(), mediaPlayer); + return; + } + + // In some weird corner cases a media player could return isPlaying() as true but would + // have getCurrentPosition > getDuration(). We consider that as the voicemail has finished + // playing. + if (mediaPlayer.isPlaying() && mediaPlayer.getCurrentPosition() < mediaPlayer.getDuration()) { + + Assert.checkArgument( + mediaPlayer + .getLastPlayedOrPlayingVoicemailUri() + .equals(getCurrentlyExpandedViewHolder().getViewHolderVoicemailUri())); + // TODO(uabdullah): Remove this, here for debugging during development. + LogUtil.i( + "NewVoicemailAdapter.recursivelyUpdateMediaPlayerViewOfExpandedViewHolder", + "recursely update the player, currentlyExpanded:%d", + expandedViewHolderPossiblyPlaying.getViewHolderId()); + + Assert.checkArgument( + expandedViewHolderPossiblyPlaying + .getViewHolderVoicemailUri() + .equals(getCurrentlyExpandedViewHolder().getViewHolderVoicemailUri())); + + expandedViewHolderPossiblyPlaying.updateMediaPlayerViewWithPlayingState( + expandedViewHolderPossiblyPlaying, mediaPlayer); + + ThreadUtil.postDelayedOnUiThread( + new Runnable() { + @Override + public void run() { + recursivelyUpdateMediaPlayerViewOfExpandedViewHolder( + expandedViewHolderPossiblyPlaying); + } + }, + 1000 / 30 /*30 FPS*/); + return; + } + + if (!mediaPlayer.isPlaying() + || (mediaPlayer.isPlaying() + && mediaPlayer.getCurrentPosition() > mediaPlayer.getDuration())) { + LogUtil.i( + "NewVoicemailAdapter.recursivelyUpdateMediaPlayerViewOfExpandedViewHolder", + "resetting the player, currentlyExpanded:%d, MPPlaying:%b", + getCurrentlyExpandedViewHolder().getViewHolderId(), + mediaPlayer.isPlaying()); + mediaPlayer.reset(); + Assert.checkArgument( + expandedViewHolderPossiblyPlaying + .getViewHolderVoicemailUri() + .equals(getCurrentlyExpandedViewHolder().getViewHolderVoicemailUri())); + expandedViewHolderPossiblyPlaying.setMediaPlayerViewToResetState( + expandedViewHolderPossiblyPlaying, mediaPlayer); + return; + } + + String error = + String.format( + "expandedViewHolderPossiblyPlaying:%d, expanded:%b, CurrentExpanded:%d, uri:%s, " + + "MPPlaying:%b, MPPaused:%b, MPPreparedUri:%s, MPPausedUri:%s", + expandedViewHolderPossiblyPlaying.getViewHolderId(), + expandedViewHolderPossiblyPlaying.isViewHolderExpanded(), + currentlyExpandedViewHolderId, + String.valueOf(expandedViewHolderPossiblyPlaying.getViewHolderVoicemailUri()), + mediaPlayer.isPlaying(), + mediaPlayer.isPaused(), + String.valueOf(mediaPlayer.getLastPreparedOrPreparingToPlayVoicemailUri()), + String.valueOf(mediaPlayer.getLastPreparedOrPreparingToPlayVoicemailUri())); + + throw Assert.createAssertionFailException( + "All cases should have been handled before. Error " + error); + } + + // When a voicemail has finished playing. + OnCompletionListener onCompletionListener = + new OnCompletionListener() { + + @Override + public void onCompletion(MediaPlayer mp) { + Assert.checkArgument( + mediaPlayer + .getLastPlayedOrPlayingVoicemailUri() + .equals(mediaPlayer.getLastPreparedOrPreparingToPlayVoicemailUri())); + Assert.checkArgument(!mediaPlayer.isPlaying()); + + LogUtil.i( + "NewVoicemailAdapter.onCompletionListener", + "completed playing voicemailUri: %s, expanded viewholder is %d, visibility :%b", + mediaPlayer.getLastPlayedOrPlayingVoicemailUri().toString(), + currentlyExpandedViewHolderId, + isCurrentlyExpandedViewHolderInViewHolderSet()); + + Assert.checkArgument( + currentlyExpandedViewHolderId != -1, + "a voicemail that was never expanded, should never be playing."); + mediaPlayer.reset(); + } + }; + + // When a voicemail has been prepared and can be played + private final OnPreparedListener onPreparedListener = + new OnPreparedListener() { + + /** + * When a user pressed the play button, this listener should be called immediately. The + * asserts ensures that is the case. This function starts playing the voicemail and updates + * the UI. + */ + @Override + public void onPrepared(MediaPlayer mp) { + LogUtil.i( + "NewVoicemailAdapter.onPrepared", + "MPPreparedUri: %s, currentlyExpandedViewHolderId:%d, and its visibility on " + + "the screen is:%b", + String.valueOf(mediaPlayer.getLastPreparedOrPreparingToPlayVoicemailUri()), + currentlyExpandedViewHolderId, + isCurrentlyExpandedViewHolderInViewHolderSet()); + + NewVoicemailViewHolder currentlyExpandedViewHolder = getCurrentlyExpandedViewHolder(); + Assert.checkArgument(currentlyExpandedViewHolder != null); + Assert.checkArgument( + currentlyExpandedViewHolder + .getViewHolderVoicemailUri() + .equals(mediaPlayer.getLastPreparedOrPreparingToPlayVoicemailUri()), + "should only have prepared the last expanded view holder."); + + mediaPlayer.start(mediaPlayer.getLastPreparedOrPreparingToPlayVoicemailUri()); + + recursivelyUpdateMediaPlayerViewOfExpandedViewHolder(currentlyExpandedViewHolder); + + Assert.checkArgument(mediaPlayer.isPlaying()); + LogUtil.i("NewVoicemailAdapter.onPrepared", "voicemail should be playing"); + } + }; + + // TODO(uabdullah): when playing the voicemail results in an error + // we must update the viewholder and mention there was an error playing the voicemail, and reset + // the media player and the media player view + private final OnErrorListener onErrorListener = + new OnErrorListener() { + @Override + public boolean onError(MediaPlayer mp, int what, int extra) { + Assert.checkArgument( + mediaPlayer.getMediaPlayer().equals(mp), + "there should always only be one instance of the media player"); + Assert.checkArgument( + mediaPlayer + .getLastPlayedOrPlayingVoicemailUri() + .equals(mediaPlayer.getLastPreparedOrPreparingToPlayVoicemailUri())); + LogUtil.i( + "NewVoicemailAdapter.onErrorListener", + "error playing voicemailUri: %s", + mediaPlayer.getLastPlayedOrPlayingVoicemailUri().toString()); + return false; + } + }; + + private boolean isCurrentlyExpandedViewHolderInViewHolderSet() { for (NewVoicemailViewHolder viewHolder : newVoicemailViewHolderSet) { - if (!viewHolder.equals(expandedViewHolder)) { - viewHolder.collapseViewHolder(); + if (viewHolder.getViewHolderId() == currentlyExpandedViewHolderId) { + return true; } } + return false; } /** - * Ensures that when we collapse the expanded view, we don't expand it again when we are recycling - * the viewholders. + * The expanded view holder may or may not be visible on the screen. Since the {@link + * NewVoicemailViewHolder} may be recycled, it's possible that the expanded view holder is + * recycled for a non-expanded view holder when the expanded view holder is scrolled out of view. * - * @param collapseViewHolder is the view holder that is currently collapsed. + * @return the expanded view holder if it is amongst the recycled views on the screen, otherwise + * null. */ - @Override - public void onViewHolderCollapsed(NewVoicemailViewHolder collapseViewHolder) { - if (collapseViewHolder.getViewHolderId() == currentlyExpandedViewHolderId) { - currentlyExpandedViewHolderId = -1; + @Nullable + private NewVoicemailViewHolder getCurrentlyExpandedViewHolder() { + if (newVoicemailViewHolderArrayMap.containsKey(currentlyExpandedViewHolderId)) { + Assert.checkArgument( + newVoicemailViewHolderArrayMap.get(currentlyExpandedViewHolderId).getViewHolderId() + == currentlyExpandedViewHolderId); + return newVoicemailViewHolderArrayMap.get(currentlyExpandedViewHolderId); + } else { + // returned when currentlyExpandedViewHolderId = -1 (viewholder was collapsed) + LogUtil.i( + "NewVoicemailAdapter.getCurrentlyExpandedViewHolder", + "no view holder found in newVoicemailViewHolderArrayMap size:%d for %d", + newVoicemailViewHolderArrayMap.size(), + currentlyExpandedViewHolderId); + return null; } } + + @Override + public int getItemCount() { + return cursor.getCount(); + } } diff --git a/java/com/android/dialer/voicemail/listui/NewVoicemailMediaPlayer.java b/java/com/android/dialer/voicemail/listui/NewVoicemailMediaPlayer.java new file mode 100644 index 000000000..2d59b241b --- /dev/null +++ b/java/com/android/dialer/voicemail/listui/NewVoicemailMediaPlayer.java @@ -0,0 +1,188 @@ +/* + * 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.voicemail.listui; + +import android.content.Context; +import android.media.MediaPlayer; +import android.media.MediaPlayer.OnCompletionListener; +import android.media.MediaPlayer.OnErrorListener; +import android.media.MediaPlayer.OnPreparedListener; +import android.net.Uri; +import android.support.annotation.NonNull; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import java.io.IOException; + +/** A wrapper around {@link MediaPlayer} */ +public class NewVoicemailMediaPlayer { + + private final MediaPlayer mediaPlayer; + private Uri voicemailLastPlayedOrPlayingUri; + private Uri voicemailUriLastPreparedOrPreparingToPlay; + + private OnErrorListener newVoicemailMediaPlayerOnErrorListener; + private OnPreparedListener newVoicemailMediaPlayerOnPreparedListener; + private OnCompletionListener newVoicemailMediaPlayerOnCompletionListener; + private Uri pausedUri; + + public NewVoicemailMediaPlayer(@NonNull MediaPlayer player) { + mediaPlayer = Assert.isNotNull(player); + } + + public void prepareMediaPlayerAndPlayVoicemailWhenReady(Context context, Uri uri) + throws IOException { + Assert.checkArgument(uri != null, "Media player cannot play a null uri"); + LogUtil.i( + "NewVoicemailMediaPlayer", + "trying to prepare playing voicemail uri: %s", + String.valueOf(uri)); + try { + reset(); + voicemailUriLastPreparedOrPreparingToPlay = uri; + verifyListenersNotNull(); + LogUtil.i("NewVoicemailMediaPlayer", "setData source"); + mediaPlayer.setDataSource(context, uri); + LogUtil.i("NewVoicemailMediaPlayer", "prepare async"); + mediaPlayer.prepareAsync(); + } catch (IllegalStateException e) { + LogUtil.i( + "NewVoicemailMediaPlayer", "caught an IllegalStateException state exception : \n" + e); + } catch (Exception e) { + LogUtil.i( + "NewVoicemailMediaPlayer", + "threw an Exception " + e + " for uri: " + uri + "for context : " + context); + } + } + + private void verifyListenersNotNull() { + Assert.isNotNull( + newVoicemailMediaPlayerOnErrorListener, + "newVoicemailMediaPlayerOnErrorListener must be set before preparing to " + + "play voicemails"); + Assert.isNotNull( + newVoicemailMediaPlayerOnCompletionListener, + "newVoicemailMediaPlayerOnCompletionListener must be set before preparing" + + " to play voicemails"); + Assert.isNotNull( + newVoicemailMediaPlayerOnPreparedListener, + "newVoicemailMediaPlayerOnPreparedListener must be set before preparing to" + + " play voicemails"); + } + + // Must be called from onPrepared + public void start(Uri startPlayingVoicemailUri) { + Assert.checkArgument( + startPlayingVoicemailUri.equals(voicemailUriLastPreparedOrPreparingToPlay), + "uri:%s was not prepared before calling start. Uri that is currently prepared: %s", + startPlayingVoicemailUri, + getLastPreparedOrPreparingToPlayVoicemailUri()); + + mediaPlayer.start(); + voicemailLastPlayedOrPlayingUri = startPlayingVoicemailUri; + pausedUri = null; + } + + public void reset() { + LogUtil.enterBlock("NewVoicemailMediaPlayer.reset"); + mediaPlayer.reset(); + voicemailLastPlayedOrPlayingUri = null; + voicemailUriLastPreparedOrPreparingToPlay = null; + pausedUri = null; + } + + public void pauseMediaPlayer(Uri voicemailUri) { + pausedUri = voicemailUri; + Assert.checkArgument( + voicemailUriLastPreparedOrPreparingToPlay.equals(voicemailLastPlayedOrPlayingUri), + "last prepared and last playing should be the same"); + Assert.checkArgument( + pausedUri.equals(voicemailLastPlayedOrPlayingUri), + "only the last played uri can be paused"); + mediaPlayer.pause(); + } + + public void seekTo(int progress) { + mediaPlayer.seekTo(progress); + } + + public void setOnErrorListener(OnErrorListener onErrorListener) { + mediaPlayer.setOnErrorListener(onErrorListener); + newVoicemailMediaPlayerOnErrorListener = onErrorListener; + } + + public void setOnPreparedListener(OnPreparedListener onPreparedListener) { + mediaPlayer.setOnPreparedListener(onPreparedListener); + newVoicemailMediaPlayerOnPreparedListener = onPreparedListener; + } + + public void setOnCompletionListener(OnCompletionListener onCompletionListener) { + mediaPlayer.setOnCompletionListener(onCompletionListener); + newVoicemailMediaPlayerOnCompletionListener = onCompletionListener; + } + + /** + * Note: In some cases it's possible mediaPlayer.isPlaying() can return true, but + * mediaPlayer.getCurrentPosition() can be greater than mediaPlayer.getDuration(), after which + * mediaPlayer.isPlaying() will be false. This is a weird corner case and adding the + * mediaPlayer.getCurrentPosition() < mediaPlayer.getDuration() check here messes with the + * mediaPlayer.start() (doesn't return mediaPlayer.isPlaying() to be true immediately). + * + * @return if the media plaer; + */ + public boolean isPlaying() { + return mediaPlayer.isPlaying(); + } + + public int getCurrentPosition() { + return mediaPlayer.getCurrentPosition(); + } + + public Uri getLastPlayedOrPlayingVoicemailUri() { + if (mediaPlayer.isPlaying()) { + Assert.isNotNull(voicemailLastPlayedOrPlayingUri); + } + + return voicemailLastPlayedOrPlayingUri == null ? Uri.EMPTY : voicemailLastPlayedOrPlayingUri; + } + + /** + * All the places that call this function, we expect the voicemail to have been prepared, but we + * could get rid of the assert check in the future if needed. + */ + public Uri getLastPreparedOrPreparingToPlayVoicemailUri() { + return Assert.isNotNull( + voicemailUriLastPreparedOrPreparingToPlay, + "we expect whoever called this to have prepared a voicemail before calling this function"); + } + + public Uri getLastPausedVoicemailUri() { + return pausedUri; + } + + public MediaPlayer getMediaPlayer() { + return mediaPlayer; + } + + public int getDuration() { + Assert.checkArgument(mediaPlayer != null); + return mediaPlayer.getDuration(); + } + + public boolean isPaused() { + return pausedUri != null; + } +} diff --git a/java/com/android/dialer/voicemail/listui/NewVoicemailMediaPlayerView.java b/java/com/android/dialer/voicemail/listui/NewVoicemailMediaPlayerView.java index d5db60846..77dd9cc4b 100644 --- a/java/com/android/dialer/voicemail/listui/NewVoicemailMediaPlayerView.java +++ b/java/com/android/dialer/voicemail/listui/NewVoicemailMediaPlayerView.java @@ -19,12 +19,9 @@ package com.android.dialer.voicemail.listui; import android.app.FragmentManager; import android.content.Context; import android.database.Cursor; -import android.media.MediaPlayer; -import android.media.MediaPlayer.OnCompletionListener; -import android.media.MediaPlayer.OnErrorListener; -import android.media.MediaPlayer.OnPreparedListener; import android.net.Uri; import android.provider.VoicemailContract; +import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.support.v4.util.Pair; import android.util.AttributeSet; @@ -32,13 +29,18 @@ import android.view.LayoutInflater; import android.view.View; import android.widget.ImageButton; import android.widget.LinearLayout; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; import android.widget.TextView; +import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.common.concurrent.DialerExecutor.SuccessListener; import com.android.dialer.common.concurrent.DialerExecutor.Worker; import com.android.dialer.common.concurrent.DialerExecutorComponent; +import com.android.dialer.voicemail.listui.NewVoicemailViewHolder.NewVoicemailViewHolderListener; import com.android.dialer.voicemail.model.VoicemailEntry; +import java.util.Locale; /** * The view of the media player that is visible when a {@link NewVoicemailViewHolder} is expanded. @@ -46,13 +48,18 @@ import com.android.dialer.voicemail.model.VoicemailEntry; public class NewVoicemailMediaPlayerView extends LinearLayout { private ImageButton playButton; + private ImageButton pauseButton; private ImageButton speakerButton; private ImageButton phoneButton; private ImageButton deleteButton; + private TextView currentSeekBarPosition; + private SeekBar seekBarView; private TextView totalDurationView; private Uri voicemailUri; private FragmentManager fragmentManager; - private MediaPlayer mediaPlayer; + private NewVoicemailViewHolder newVoicemailViewHolder; + private NewVoicemailMediaPlayer mediaPlayer; + private NewVoicemailViewHolderListener newVoicemailViewHolderListener; public NewVoicemailMediaPlayerView(Context context, AttributeSet attrs) { super(context, attrs); @@ -72,6 +79,9 @@ public class NewVoicemailMediaPlayerView extends LinearLayout { private void initializeMediaPlayerButtonsAndViews() { playButton = findViewById(R.id.playButton); + pauseButton = findViewById(R.id.pauseButton); + currentSeekBarPosition = findViewById(R.id.playback_position_text); + seekBarView = findViewById(R.id.playback_seek); speakerButton = findViewById(R.id.speakerButton); phoneButton = findViewById(R.id.phoneButton); deleteButton = findViewById(R.id.deleteButton); @@ -80,13 +90,202 @@ public class NewVoicemailMediaPlayerView extends LinearLayout { private void setupListenersForMediaPlayerButtons() { playButton.setOnClickListener(playButtonListener); + pauseButton.setOnClickListener(pauseButtonListener); + seekBarView.setOnSeekBarChangeListener(seekbarChangeListener); speakerButton.setOnClickListener(speakerButtonListener); phoneButton.setOnClickListener(phoneButtonListener); deleteButton.setOnClickListener(deleteButtonListener); } + public void reset() { + LogUtil.i("NewVoicemailMediaPlayer.reset", "the uri for this is " + voicemailUri); + voicemailUri = null; + } + + /** + * Can be called either when binding happens on the {@link NewVoicemailViewHolder} from {@link + * NewVoicemailAdapter} or when a user expands a {@link NewVoicemailViewHolder}. During the + * binding, since {@link NewVoicemailMediaPlayerView} is part of {@link NewVoicemailViewHolder}, + * we have to ensure that during the binding the values from the {@link NewVoicemailAdapter} are + * also propogated down to the {@link NewVoicemailMediaPlayerView} via {@link + * NewVoicemailViewHolder}. In the case of when the {@link NewVoicemailViewHolder} is expanded, + * the most recent value and states from the {@link NewVoicemailAdapter} are set for the expanded + * {@link NewVoicemailMediaPlayerView}. + * + * @param viewHolder + * @param voicemailEntryFromAdapter are the voicemail related values from the {@link + * AnnotatedCallLog} converted into {@link VoicemailEntry} format. + * @param fragmentManager + * @param mp the media player passed down from the adapter + * @param listener + */ + void bindValuesFromAdapterOfExpandedViewHolderMediaPlayerView( + NewVoicemailViewHolder viewHolder, + @NonNull VoicemailEntry voicemailEntryFromAdapter, + @NonNull FragmentManager fragmentManager, + NewVoicemailMediaPlayer mp, + NewVoicemailViewHolderListener listener) { + + Assert.isNotNull(voicemailEntryFromAdapter); + Uri uri = Uri.parse(voicemailEntryFromAdapter.voicemailUri()); + Assert.isNotNull(viewHolder); + Assert.isNotNull(uri); + Assert.isNotNull(listener); + Assert.isNotNull(totalDurationView); + Assert.checkArgument(uri.equals(viewHolder.getViewHolderVoicemailUri())); + + LogUtil.i( + "NewVoicemailMediaPlayerView.bindValuesFromAdapterOfExpandedViewHolderMediaPlayerView", + "Updating the viewholder:%d mediaPlayerView with uri value:%s", + viewHolder.getViewHolderId(), + uri.toString()); + + this.fragmentManager = fragmentManager; + + newVoicemailViewHolder = viewHolder; + newVoicemailViewHolderListener = listener; + mediaPlayer = mp; + voicemailUri = uri; + totalDurationView.setText( + VoicemailEntryText.getVoicemailDuration(getContext(), voicemailEntryFromAdapter)); + // Not sure if these are needed, but it'll ensure that onInflate() has atleast happened. + initializeMediaPlayerButtonsAndViews(); + setupListenersForMediaPlayerButtons(); + + // During the binding we only send a request to the adapter to tell us what the + // state of the media player should be and call that function. + // This could be the paused state, or the playing state of the resume state. + // Our job here is only to send the request upto the adapter and have it decide what we should + // do. + LogUtil.i( + "NewVoicemailMediaPlayerView.bindValuesFromAdapterOfExpandedViewHolderMediaPlayerView", + "Updating media player values for id:" + viewHolder.getViewHolderId()); + + // During the binding make sure that the first time we just set the mediaplayer view + // This does not take care of the constant update + if (mp.isPlaying() && mp.getLastPlayedOrPlayingVoicemailUri().equals(voicemailUri)) { + Assert.checkArgument( + mp.getLastPlayedOrPlayingVoicemailUri() + .equals(mp.getLastPreparedOrPreparingToPlayVoicemailUri())); + LogUtil.i( + "NewVoicemailMediaPlayerView.bindValuesFromAdapterOfExpandedViewHolderMediaPlayerView", + "show playing state"); + playButton.setVisibility(GONE); + pauseButton.setVisibility(VISIBLE); + currentSeekBarPosition.setText(formatAsMinutesAndSeconds(mp.getCurrentPosition())); + + if (seekBarView.getMax() != mp.getDuration()) { + seekBarView.setMax(mp.getDuration()); + } + seekBarView.setProgress(mp.getCurrentPosition()); + + } else if (mediaPlayer.isPaused() && mp.getLastPausedVoicemailUri().equals(voicemailUri)) { + LogUtil.i( + "NewVoicemailMediaPlayerView.bindValuesFromAdapterOfExpandedViewHolderMediaPlayerView", + "show paused state"); + Assert.checkArgument(viewHolder.getViewHolderVoicemailUri().equals(voicemailUri)); + playButton.setVisibility(VISIBLE); + pauseButton.setVisibility(GONE); + currentSeekBarPosition.setText(formatAsMinutesAndSeconds(mp.getCurrentPosition())); + if (seekBarView.getMax() != mp.getDuration()) { + seekBarView.setMax(mp.getDuration()); + } + seekBarView.setProgress(mp.getCurrentPosition()); + + } else { + LogUtil.i( + "NewVoicemailMediaPlayerView.bindValuesFromAdapterOfExpandedViewHolderMediaPlayerView", + "show reset state"); + playButton.setVisibility(VISIBLE); + pauseButton.setVisibility(GONE); + seekBarView.setProgress(0); + seekBarView.setMax(100); + currentSeekBarPosition.setText(formatAsMinutesAndSeconds(0)); + } + } + + private final OnSeekBarChangeListener seekbarChangeListener = + new OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBarfromProgress, int progress, boolean fromUser) { + // TODO(uabdullah): Only for debugging purposes, to be removed. + if (progress < 100) { + LogUtil.i( + "NewVoicemailMediaPlayer.seekbarChangeListener", + "onProgressChanged, progress:%d, seekbarMax: %d, fromUser:%b", + progress, + seekBarfromProgress.getMax(), + fromUser); + } + + if (fromUser) { + mediaPlayer.seekTo(progress); + currentSeekBarPosition.setText(formatAsMinutesAndSeconds(progress)); + } + } + + @Override + // TODO(uabdullah): Handle this case + public void onStartTrackingTouch(SeekBar seekBar) { + LogUtil.i("NewVoicemailMediaPlayer.onStartTrackingTouch", "does nothing for now"); + } + + @Override + // TODO(uabdullah): Handle this case + public void onStopTrackingTouch(SeekBar seekBar) { + LogUtil.i("NewVoicemailMediaPlayer.onStopTrackingTouch", "does nothing for now"); + } + }; + + private final View.OnClickListener pauseButtonListener = + new View.OnClickListener() { + @Override + public void onClick(View view) { + LogUtil.i( + "NewVoicemailMediaPlayer.pauseButtonListener", + "pauseMediaPlayerAndSetPausedStateOfViewHolder button for voicemailUri: %s", + voicemailUri.toString()); + + Assert.checkArgument(playButton.getVisibility() == GONE); + Assert.checkArgument(mediaPlayer != null); + Assert.checkArgument( + mediaPlayer.getLastPlayedOrPlayingVoicemailUri().equals((voicemailUri)), + "the voicemail being played is the only voicemail that should" + + " be paused. last played voicemail:%s, uri:%s", + mediaPlayer.getLastPlayedOrPlayingVoicemailUri().toString(), + voicemailUri.toString()); + Assert.checkArgument( + newVoicemailViewHolder.getViewHolderVoicemailUri().equals(voicemailUri), + "viewholder uri and mediaplayer view should be the same."); + newVoicemailViewHolderListener.pauseViewHolder(newVoicemailViewHolder); + } + }; + private final View.OnClickListener playButtonListener = - view -> playVoicemailWhenAvailableLocally(); + new View.OnClickListener() { + @Override + public void onClick(View view) { + LogUtil.i( + "NewVoicemailMediaPlayer.playButtonListener", + "play button for voicemailUri: %s", + voicemailUri.toString()); + if (mediaPlayer.getLastPausedVoicemailUri() != null + && mediaPlayer + .getLastPausedVoicemailUri() + .toString() + .contentEquals(voicemailUri.toString())) { + LogUtil.i( + "NewVoicemailMediaPlayer.playButtonListener", + "resume playing voicemailUri: %s", + voicemailUri.toString()); + + newVoicemailViewHolderListener.resumePausedViewHolder(newVoicemailViewHolder); + + } else { + playVoicemailWhenAvailableLocally(); + } + } + }; /** * Plays the voicemail when we are able to play the voicemail locally from the device. This @@ -113,7 +312,7 @@ public class NewVoicemailMediaPlayerView extends LinearLayout { Uri uri = contextUriPair.second; try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) { - if (cursor != null && cursor.moveToNext()) { + if (cursor != null && cursor.moveToFirst()) { return new Pair<>( cursor.getInt(cursor.getColumnIndex(VoicemailContract.Voicemails.HAS_CONTENT)) == 1, uri); @@ -137,17 +336,18 @@ public class NewVoicemailMediaPlayerView extends LinearLayout { if (voicemailAvailableLocally) { try { - mediaPlayer = new MediaPlayer(); - mediaPlayer.setOnPreparedListener(onPreparedListener); - mediaPlayer.setOnErrorListener(onErrorListener); - mediaPlayer.setOnCompletionListener(onCompletionListener); - - mediaPlayer.reset(); - mediaPlayer.setDataSource(getContext(), uri); - - mediaPlayer.prepareAsync(); + Assert.checkArgument(mediaPlayer != null, "media player should not have been null"); + mediaPlayer.prepareMediaPlayerAndPlayVoicemailWhenReady(getContext(), uri); } catch (Exception e) { - LogUtil.e("NewVoicemailMediaPlayer.prepareMediaPlayer", "IOException " + e); + LogUtil.e( + "NewVoicemailMediaPlayer.prepareMediaPlayer", + "Exception when mediaPlayer.prepareMediaPlayerAndPlayVoicemailWhenReady" + + "(getContext(), uri)\n" + + e + + "\n uri:" + + uri + + "context should not be null, its value is :" + + getContext()); } } else { // TODO(a bug): Add logic for downloading voicemail content from the server. @@ -189,57 +389,130 @@ public class NewVoicemailMediaPlayerView extends LinearLayout { } }; - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - OnCompletionListener onCompletionListener = - new OnCompletionListener() { + /** + * This is only called to update the media player view of the seekbar, and the duration and the + * play button. For constant updates the adapter should seek track. This is the state when a + * voicemail is playing. + */ + public void updateSeekBarDurationAndShowPlayButton(NewVoicemailMediaPlayer mp) { + if (!mp.isPlaying()) { + return; + } - @Override - public void onCompletion(MediaPlayer mp) { - LogUtil.i( - "NewVoicemailMediaPlayer.onCompletionListener", - "completed playing voicemailUri: %s", - voicemailUri.toString()); - } - }; + playButton.setVisibility(GONE); + pauseButton.setVisibility(VISIBLE); - private final OnPreparedListener onPreparedListener = - new OnPreparedListener() { + Assert.checkArgument( + mp.equals(mediaPlayer), "there should only be one instance of a media player"); + Assert.checkArgument( + mediaPlayer.getLastPreparedOrPreparingToPlayVoicemailUri().equals(voicemailUri)); + Assert.checkArgument(mediaPlayer.getLastPlayedOrPlayingVoicemailUri().equals(voicemailUri)); + Assert.isNotNull(mediaPlayer, "media player should have been set on bind"); + Assert.checkArgument(mediaPlayer.isPlaying()); + Assert.checkArgument(mediaPlayer.getCurrentPosition() >= 0); + Assert.checkArgument(mediaPlayer.getDuration() >= 0); + Assert.checkArgument(playButton.getVisibility() == GONE); + Assert.checkArgument(pauseButton.getVisibility() == VISIBLE); + Assert.checkArgument(seekBarView.getVisibility() == VISIBLE); + Assert.checkArgument(currentSeekBarPosition.getVisibility() == VISIBLE); - @Override - public void onPrepared(MediaPlayer mp) { - LogUtil.i( - "NewVoicemailMediaPlayer.onPreparedListener", - "about to play voicemailUri: %s", - voicemailUri.toString()); - mediaPlayer.start(); - } - }; + currentSeekBarPosition.setText(formatAsMinutesAndSeconds(mediaPlayer.getCurrentPosition())); + if (seekBarView.getMax() != mediaPlayer.getDuration()) { + seekBarView.setMax(mediaPlayer.getDuration()); + } + seekBarView.setProgress(mediaPlayer.getCurrentPosition()); + } - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - OnErrorListener onErrorListener = - new OnErrorListener() { - @Override - public boolean onError(MediaPlayer mp, int what, int extra) { - LogUtil.i( - "NewVoicemailMediaPlayer.onErrorListener", - "error playing voicemailUri: %s", - voicemailUri.toString()); - return false; - } - }; + /** + * What the default state of an expanded media player view should look like. + * + * @param currentlyExpandedViewHolderOnScreen + * @param mediaPlayer + */ + public void setToResetState( + NewVoicemailViewHolder currentlyExpandedViewHolderOnScreen, + NewVoicemailMediaPlayer mediaPlayer) { + LogUtil.i( + "NewVoicemailMediaPlayer.setToResetState", + "update the seekbar for viewholder id:%d, mediaplayer view uri:%s, play button " + + "visible:%b, pause button visible:%b", + currentlyExpandedViewHolderOnScreen.getViewHolderId(), + String.valueOf(voicemailUri), + playButton.getVisibility() == VISIBLE, + pauseButton.getVisibility() == VISIBLE); - void setFragmentManager(FragmentManager fragmentManager) { - this.fragmentManager = fragmentManager; + if (playButton.getVisibility() == GONE) { + playButton.setVisibility(VISIBLE); + pauseButton.setVisibility(GONE); + } + + Assert.checkArgument(playButton.getVisibility() == VISIBLE); + Assert.checkArgument(pauseButton.getVisibility() == GONE); + + Assert.checkArgument( + !mediaPlayer.isPlaying(), + "when resetting an expanded " + "state, there should be no voicemail playing"); + + Assert.checkArgument( + mediaPlayer.getLastPlayedOrPlayingVoicemailUri().equals(Uri.EMPTY), + "reset should have been called before updating its media player view"); + currentSeekBarPosition.setText(formatAsMinutesAndSeconds(0)); + seekBarView.setProgress(0); + seekBarView.setMax(100); } - void setVoicemailEntryValues(VoicemailEntry voicemailEntry) { - Assert.isNotNull(voicemailEntry); - Uri uri = Uri.parse(voicemailEntry.voicemailUri()); - Assert.isNotNull(uri); - Assert.isNotNull(totalDurationView); + public void setToPausedState(Uri toPausedState, NewVoicemailMediaPlayer mp) { + LogUtil.i( + "NewVoicemailMediaPlayer.setToPausedState", + "toPausedState uri:%s, play button visible:%b, pause button visible:%b", + toPausedState == null ? "null" : voicemailUri.toString(), + playButton.getVisibility() == VISIBLE, + pauseButton.getVisibility() == VISIBLE); - voicemailUri = uri; - totalDurationView.setText( - VoicemailEntryText.getVoicemailDuration(getContext(), voicemailEntry)); + playButton.setVisibility(VISIBLE); + pauseButton.setVisibility(GONE); + + currentSeekBarPosition.setText(formatAsMinutesAndSeconds(mediaPlayer.getCurrentPosition())); + if (seekBarView.getMax() != mediaPlayer.getDuration()) { + seekBarView.setMax(mediaPlayer.getDuration()); + } + seekBarView.setProgress(mediaPlayer.getCurrentPosition()); + + Assert.checkArgument(voicemailUri.equals(toPausedState)); + Assert.checkArgument(!mp.isPlaying()); + Assert.checkArgument( + mp.equals(mediaPlayer), "there should only be one instance of a media player"); + Assert.checkArgument( + this.mediaPlayer.getLastPreparedOrPreparingToPlayVoicemailUri().equals(voicemailUri)); + Assert.checkArgument( + this.mediaPlayer.getLastPlayedOrPlayingVoicemailUri().equals(voicemailUri)); + Assert.checkArgument(this.mediaPlayer.getLastPausedVoicemailUri().equals(voicemailUri)); + Assert.isNotNull(this.mediaPlayer, "media player should have been set on bind"); + Assert.checkArgument(this.mediaPlayer.getCurrentPosition() >= 0); + Assert.checkArgument(this.mediaPlayer.getDuration() >= 0); + Assert.checkArgument(playButton.getVisibility() == VISIBLE); + Assert.checkArgument(pauseButton.getVisibility() == GONE); + Assert.checkArgument(seekBarView.getVisibility() == VISIBLE); + Assert.checkArgument(currentSeekBarPosition.getVisibility() == VISIBLE); + } + + @NonNull + public Uri getVoicemailUri() { + return voicemailUri; + } + + private String formatAsMinutesAndSeconds(int millis) { + int seconds = millis / 1000; + int minutes = seconds / 60; + seconds -= minutes * 60; + if (minutes > 99) { + minutes = 99; + } + return String.format(Locale.US, "%02d:%02d", minutes, seconds); + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + void setFragmentManager(FragmentManager fragmentManager) { + this.fragmentManager = fragmentManager; } } diff --git a/java/com/android/dialer/voicemail/listui/NewVoicemailViewHolder.java b/java/com/android/dialer/voicemail/listui/NewVoicemailViewHolder.java index f08e6bfd7..02b05db03 100644 --- a/java/com/android/dialer/voicemail/listui/NewVoicemailViewHolder.java +++ b/java/com/android/dialer/voicemail/listui/NewVoicemailViewHolder.java @@ -15,17 +15,22 @@ */ package com.android.dialer.voicemail.listui; +import static android.view.View.GONE; +import static android.view.View.VISIBLE; + import android.app.FragmentManager; import android.content.Context; import android.database.Cursor; import android.net.Uri; -import android.support.annotation.VisibleForTesting; +import android.support.annotation.NonNull; import android.support.v7.widget.RecyclerView; import android.text.TextUtils; import android.view.View; import android.view.View.OnClickListener; import android.widget.QuickContactBadge; import android.widget.TextView; +import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog; +import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.contactphoto.ContactPhotoManager; import com.android.dialer.lettertile.LetterTileDrawable; @@ -44,6 +49,8 @@ final class NewVoicemailViewHolder extends RecyclerView.ViewHolder implements On private final Clock clock; private boolean isViewHolderExpanded; private int viewHolderId; + private VoicemailEntry voicemailEntryOfViewHolder; + @NonNull private Uri viewHolderVoicemailUri; private final NewVoicemailViewHolderListener voicemailViewHolderListener; NewVoicemailViewHolder( @@ -58,19 +65,45 @@ final class NewVoicemailViewHolder extends RecyclerView.ViewHolder implements On mediaPlayerView = view.findViewById(R.id.new_voicemail_media_player); this.clock = clock; voicemailViewHolderListener = newVoicemailViewHolderListener; + + viewHolderId = -1; + isViewHolderExpanded = false; + viewHolderVoicemailUri = null; } - void bind(Cursor cursor, FragmentManager fragmentManager) { - VoicemailEntry voicemailEntry = VoicemailCursorLoader.toVoicemailEntry(cursor); - viewHolderId = voicemailEntry.id(); - primaryTextView.setText(VoicemailEntryText.buildPrimaryVoicemailText(context, voicemailEntry)); + /** + * When the {@link RecyclerView} displays voicemail entries, it might recycle the views upon + * scrolling. In that case we need to ensure that the member variables of this {@link + * NewVoicemailViewHolder} and its views are correctly set, especially when this {@link + * NewVoicemailViewHolder} is recycled. + * + * @param cursor the voicemail data from {@link AnnotatedCallLog} generated by the {@link + * VoicemailCursorLoader} related + * @param fragmentManager FragmentManager retrieved from {@link + * NewVoicemailFragment#getActivity()} + * @param mediaPlayer + * @param position the position of the item within the adapter's data set. + * @param currentlyExpandedViewHolderId the value the adapter keeps track of which viewholder if + */ + void bindViewHolderValuesFromAdapter( + Cursor cursor, + FragmentManager fragmentManager, + NewVoicemailMediaPlayer mediaPlayer, + int position, + int currentlyExpandedViewHolderId) { + + voicemailEntryOfViewHolder = VoicemailCursorLoader.toVoicemailEntry(cursor); + viewHolderId = voicemailEntryOfViewHolder.id(); + viewHolderVoicemailUri = Uri.parse(voicemailEntryOfViewHolder.voicemailUri()); + primaryTextView.setText( + VoicemailEntryText.buildPrimaryVoicemailText(context, voicemailEntryOfViewHolder)); secondaryTextView.setText( - VoicemailEntryText.buildSecondaryVoicemailText(context, clock, voicemailEntry)); + VoicemailEntryText.buildSecondaryVoicemailText(context, clock, voicemailEntryOfViewHolder)); - String voicemailTranscription = voicemailEntry.transcription(); + String voicemailTranscription = voicemailEntryOfViewHolder.transcription(); if (TextUtils.isEmpty(voicemailTranscription)) { - transcriptionTextView.setVisibility(View.GONE); + transcriptionTextView.setVisibility(GONE); transcriptionTextView.setText(null); } else { transcriptionTextView.setVisibility(View.VISIBLE); @@ -78,9 +111,49 @@ final class NewVoicemailViewHolder extends RecyclerView.ViewHolder implements On } itemView.setOnClickListener(this); - setPhoto(voicemailEntry); - mediaPlayerView.setVoicemailEntryValues(voicemailEntry); - mediaPlayerView.setFragmentManager(fragmentManager); + setPhoto(voicemailEntryOfViewHolder); + + // Update the expanded/collapsed state of this view holder + // Only update the binding of the mediaPlayerView of the expanded view holder + if (viewHolderId == currentlyExpandedViewHolderId) { + LogUtil.i( + "NewVoicemailViewHolder.bindViewHolderValuesFromAdapter", + "viewHolderId:%d is expanded, update its mediaplayer view", + viewHolderId); + expandAndBindViewHolderAndMediaPlayerViewWithAdapterValues( + voicemailEntryOfViewHolder, fragmentManager, mediaPlayer, voicemailViewHolderListener); + LogUtil.i( + "NewVoicemailViewHolder.bindViewHolderValuesFromAdapter", + "After 2nd updating the MPPlayerView: viewHolderId:%d, uri:%s, MediaplayerView(after " + + "updated):%s, adapter position passed down:%d, getAdapterPos:%d", + viewHolderId, + String.valueOf(viewHolderVoicemailUri), + String.valueOf(mediaPlayerView.getVoicemailUri()), + position, + getAdapterPosition()); + Assert.checkArgument( + mediaPlayerView.getVisibility() == VISIBLE, + "a expanded viewholder should have its media player view visible"); + } else { + LogUtil.i( + "NewVoicemailViewHolder.bindViewHolderValuesFromAdapter", + "viewHolderId:%d is not the expanded one, collapse it and don't update the MpView", + viewHolderId); + collapseViewHolder(); + Assert.checkArgument( + mediaPlayerView.getVisibility() == GONE, + "a collapsed viewholder should not have its media player view visible"); + } + LogUtil.i( + "NewVoicemailViewHolder.bindViewHolderValuesFromAdapter", + "Final value after updating: viewHolderId:%d, uri:%s, MediaplayerView(not updated):%s," + + " adapter position passed down:%d, getAdapterPos:%d, MPPlayerVisibility:%b", + viewHolderId, + String.valueOf(viewHolderVoicemailUri), + String.valueOf(mediaPlayerView.getVoicemailUri()), + position, + getAdapterPosition(), + mediaPlayerView.getVisibility() == VISIBLE); } // TODO(uabdullah): Consider/Implement TYPE (e.g Spam, TYPE_VOICEMAIL) @@ -96,19 +169,148 @@ final class NewVoicemailViewHolder extends RecyclerView.ViewHolder implements On } void collapseViewHolder() { + LogUtil.i( + "NewVoicemailViewHolder.collapseViewHolder", + "viewHolderId:%d is being collapsed, its MPViewUri:%s, its Uri is :%s", + viewHolderId, + String.valueOf(mediaPlayerView.getVoicemailUri()), + String.valueOf(viewHolderVoicemailUri)); transcriptionTextView.setMaxLines(1); isViewHolderExpanded = false; - mediaPlayerView.setVisibility(View.GONE); + mediaPlayerView.setVisibility(GONE); + } + + // When we are recycling the views ensure that we reset the viewHolder, as if its brand new + public void reset() { + LogUtil.i( + "NewVoicemailViewHolder.reset()", + "Reset the viewholder, currently viewHolderId:%d, uri:%s, isViewHolderExpanded:%b, " + + "its MediaPlayerViewUri:%s", + viewHolderId, + String.valueOf(viewHolderVoicemailUri), + isViewHolderExpanded, + String.valueOf(mediaPlayerView.getVoicemailUri())); + + viewHolderId = -1; + isViewHolderExpanded = false; + viewHolderVoicemailUri = null; + + mediaPlayerView.reset(); + + LogUtil.i( + "NewVoicemailViewHolder.reset()", + "Reset the viewholder, after resetting viewHolderId:%d, uri:%s, isViewHolderExpanded:%b", + viewHolderId, + String.valueOf(viewHolderVoicemailUri), + isViewHolderExpanded); } - void expandViewHolder() { - LogUtil.i("NewVoicemailViewHolder.expandViewHolder", "voicemail id: %d", viewHolderId); + /** + * Is only called when a user either clicks a {@link NewVoicemailViewHolder} to expand it or if + * the user had already expanded, then scrolled the {@link NewVoicemailViewHolder} out of view and + * then scrolled it back into view, and during the binding (as the views are recyled in {@link + * RecyclerView}) we restore the expanded state of the {@link NewVoicemailViewHolder}. + * + * <p>This function also tracks if the state of this viewholder is expanded. + * + * @param voicemailEntry are the voicemail related values from the {@link AnnotatedCallLog} + * @param fragmentManager FragmentManager retrieved from {@link + * NewVoicemailFragment#getActivity()} + * @param mediaPlayer there should only be one instance of this passed down from the {@link + * NewVoicemailAdapter} + * @param voicemailViewHolderListener + */ + void expandAndBindViewHolderAndMediaPlayerViewWithAdapterValues( + VoicemailEntry voicemailEntry, + FragmentManager fragmentManager, + NewVoicemailMediaPlayer mediaPlayer, + NewVoicemailViewHolderListener voicemailViewHolderListener) { + + Assert.isNotNull(voicemailViewHolderListener); + Assert.checkArgument( + voicemailEntry.id() == viewHolderId, "ensure that the adapter binding has taken place"); + Assert.checkArgument( + Uri.parse(voicemailEntry.voicemailUri()).equals(viewHolderVoicemailUri), + "ensure that the adapter binding has taken place"); + LogUtil.i( + "NewVoicemailViewHolder.expandAndBindViewHolderAndMediaPlayerViewWithAdapterValues", + "voicemail id: %d, value of isViewHolderExpanded:%b, before setting it to be true, and" + + " value of ViewholderUri:%s, MPView:%s, before updating it", + viewHolderId, + isViewHolderExpanded, + String.valueOf(viewHolderVoicemailUri), + String.valueOf(mediaPlayerView.getVoicemailUri())); + transcriptionTextView.setMaxLines(999); isViewHolderExpanded = true; + // Once the media player is visible update its state mediaPlayerView.setVisibility(View.VISIBLE); + mediaPlayerView.bindValuesFromAdapterOfExpandedViewHolderMediaPlayerView( + this, voicemailEntry, fragmentManager, mediaPlayer, voicemailViewHolderListener); + LogUtil.i( + "NewVoicemailViewHolder.expandAndBindViewHolderAndMediaPlayerViewWithAdapterValues", + "voicemail id: %d, value of isViewHolderExpanded:%b, after setting it to be true, and" + + " value of ViewholderUri:%s, MPView:%s, after updating it", + viewHolderId, + isViewHolderExpanded, + String.valueOf(viewHolderVoicemailUri), + String.valueOf(mediaPlayerView.getVoicemailUri())); + } + + /** + * Called when we want to update the voicemail that is currently playing Updates the Seekbar, + * duration timer and the play/pause button visibility when the expanded voicemail is being + * played. + */ + public void updateMediaPlayerViewWithPlayingState( + NewVoicemailViewHolder newVoicemailViewHolder, NewVoicemailMediaPlayer mp) { + + LogUtil.i( + "NewVoicemailViewHolder.updateMediaPlayerViewWithPlayingState", + "viewholderUri:%s, mediaPlayerViewUri:%s, MPPosition:%d, MpDuration:%d, MpIsPlaying:%b", + newVoicemailViewHolder.getViewHolderVoicemailUri().toString(), + mediaPlayerView.getVoicemailUri().toString(), + mp.getCurrentPosition(), + mp.getDuration(), + mp.isPlaying()); + + Assert.checkArgument( + mp.isPlaying(), + "this method is only called when we are certain that the media player is playing"); + + LogUtil.i( + "NewVoicemailViewHolder.updateMediaPlayerViewWithPlayingState", + "viewholderUri:%s, mediaPlayerViewUri:%s", + newVoicemailViewHolder.getViewHolderVoicemailUri().toString(), + mediaPlayerView.getVoicemailUri().toString()); + Assert.checkArgument( + newVoicemailViewHolder + .getViewHolderVoicemailUri() + .equals(mediaPlayerView.getVoicemailUri()), + "the mediaplayer view must be that of the viewholder we are updating"); + Assert.checkArgument( + mp.getLastPlayedOrPlayingVoicemailUri() + .equals(mp.getLastPreparedOrPreparingToPlayVoicemailUri()), + "the media player view we are attempting to update should be of the " + + "currently prepared and playing voicemail"); + + mediaPlayerView.updateSeekBarDurationAndShowPlayButton(mp); + } + + public void setMediaPlayerViewToResetState( + NewVoicemailViewHolder currentlyExpandedViewHolderOnScreen, + NewVoicemailMediaPlayer mediaPlayer) { + Assert.isNotNull(currentlyExpandedViewHolderOnScreen); + mediaPlayerView.setToResetState(currentlyExpandedViewHolderOnScreen, mediaPlayer); + } + + public void setPausedStateOfMediaPlayerView(Uri uri, NewVoicemailMediaPlayer mediaPlayer) { + Assert.checkArgument(viewHolderVoicemailUri.equals(uri)); + Assert.checkArgument(mediaPlayerView.getVoicemailUri().equals(uri)); + Assert.checkArgument(mediaPlayerView.getVoicemailUri().equals(viewHolderVoicemailUri)); + mediaPlayerView.setToPausedState(uri, mediaPlayer); } - @VisibleForTesting(otherwise = VisibleForTesting.NONE) boolean isViewHolderExpanded() { return isViewHolderExpanded; } @@ -117,25 +319,37 @@ final class NewVoicemailViewHolder extends RecyclerView.ViewHolder implements On return viewHolderId; } + public Uri getViewHolderVoicemailUri() { + return viewHolderVoicemailUri; + } + interface NewVoicemailViewHolderListener { - void onViewHolderExpanded(NewVoicemailViewHolder expandedViewHolder); + void expandViewHolderFirstTimeAndCollapseAllOtherVisibleViewHolders( + NewVoicemailViewHolder expandedViewHolder, + VoicemailEntry voicemailEntryOfViewHolder, + NewVoicemailViewHolderListener listener); + + void collapseExpandedViewHolder(NewVoicemailViewHolder expandedViewHolder); - void onViewHolderCollapsed(NewVoicemailViewHolder expandedViewHolder); + void pauseViewHolder(NewVoicemailViewHolder expandedViewHolder); + + void resumePausedViewHolder(NewVoicemailViewHolder expandedViewHolder); } @Override public void onClick(View v) { LogUtil.i( "NewVoicemailViewHolder.onClick", - "voicemail id: %d, isViewHolderExpanded:%b", + "voicemail id: %d, isViewHolderCurrentlyExpanded:%b", viewHolderId, isViewHolderExpanded); if (isViewHolderExpanded) { - collapseViewHolder(); - voicemailViewHolderListener.onViewHolderCollapsed(this); + voicemailViewHolderListener.collapseExpandedViewHolder(this); } else { - expandViewHolder(); - voicemailViewHolderListener.onViewHolderExpanded(this); + voicemailViewHolderListener.expandViewHolderFirstTimeAndCollapseAllOtherVisibleViewHolders( + this, + Assert.isNotNull(voicemailEntryOfViewHolder), + Assert.isNotNull(voicemailViewHolderListener)); } } } diff --git a/java/com/android/dialer/voicemail/listui/res/layout/new_voicemail_media_player_layout.xml b/java/com/android/dialer/voicemail/listui/res/layout/new_voicemail_media_player_layout.xml index 07ce86a1d..32726a9e5 100644 --- a/java/com/android/dialer/voicemail/listui/res/layout/new_voicemail_media_player_layout.xml +++ b/java/com/android/dialer/voicemail/listui/res/layout/new_voicemail_media_player_layout.xml @@ -71,6 +71,12 @@ android:orientation="horizontal" android:weightSum="4"> + <ImageButton + android:id="@+id/pauseButton" + style="@style/voicemail_media_player_buttons" + android:layout_weight="1" + android:src="@drawable/quantum_ic_pause_vd_theme_24" + android:visibility="gone"/> <ImageButton android:id="@+id/playButton" |