diff options
author | uabdullah <uabdullah@google.com> | 2017-12-11 15:20:15 -0800 |
---|---|---|
committer | Copybara-Service <copybara-piper@google.com> | 2017-12-11 15:23:08 -0800 |
commit | 1fab0cca2259a7f8dd0b4349bc38f282eb4209a8 (patch) | |
tree | a677be3a11bd33ba9aa9903cc868ec607fc213f4 | |
parent | c875a5b7630376c767d86b708e4aab8382de8acc (diff) |
Play voicemails, update seekbar timer, allow seeking, and maintain state after recycling.
This CL adds the support for playing voicemails, changing the play button to pause button when playing the voicemail, updating the seekbar and duration timer when the voicemail is being played. It also adds the support to preserve the state of the media player such that when scrolling and recycling views, when an expanded and playing voicemail is recycled back into view, it's most recent state is shown i.e the duration and the seekbar are upto date.
Video: https://drive.google.com/open?id=1CKbLK5-1YDeXBZFiKvuTxoPuFJQ1rbj7
Test: Unit tests
PiperOrigin-RevId: 178681663
Change-Id: Ifdd1d945572926bdc7d652aa7a876d3156fc21ce
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" |