diff options
author | Andrew Lee <anwlee@google.com> | 2015-05-26 16:14:31 -0700 |
---|---|---|
committer | Andrew Lee <anwlee@google.com> | 2015-05-27 16:38:54 -0700 |
commit | 58eaabcc31e23fd4c071ad911b96da6eea4abc28 (patch) | |
tree | fcbfef6b072f8442432e39f1995ae18f2f34332e /src/com/android/dialer | |
parent | a56893c156b997d796cd8343f99acd5d4198a280 (diff) |
Refactor Voicemail Playback into standalone view.
+ Substitutes the existing playback widget in CallDetailActivity,
although the plan is to move this to the call log shortly.
+ Convert the widget from a fragment into a layout. This allows us
to more easily create multiple instances of the voicemail widget in
the same view, as we intend to do in the call log.
+ Shift UI-related logic from Presenter to the Layout.
+ Fix janky seeking, so that it now works correctly consistently
rather than sporadically, and doesn't need to buffer again.
- Remove the VariableSpeed player formerly used in the Presenter. We
don't use this functionality anymore, and this allows us to directly
used the framework MediaPlayer (instead of a custom legacy proxy).
Bug: 21170557
Bug: 20693172
Change-Id: Ia34f459df10e43763b32fdb0954f83e882664231
Diffstat (limited to 'src/com/android/dialer')
5 files changed, 675 insertions, 870 deletions
diff --git a/src/com/android/dialer/CallDetailActivity.java b/src/com/android/dialer/CallDetailActivity.java index 1c684a510..7fa3750d2 100644 --- a/src/com/android/dialer/CallDetailActivity.java +++ b/src/com/android/dialer/CallDetailActivity.java @@ -61,7 +61,8 @@ import com.android.dialer.calllog.PhoneNumberUtilsWrapper; import com.android.dialer.util.IntentUtil; import com.android.dialer.util.DialerUtils; import com.android.dialer.util.TelecomUtil; -import com.android.dialer.voicemail.VoicemailPlaybackFragment; +import com.android.dialer.voicemail.VoicemailPlaybackLayout; +import com.android.dialer.voicemail.VoicemailPlaybackPresenter; import java.util.List; @@ -217,7 +218,7 @@ public class CallDetailActivity extends Activity { /** Helper to load contact photos. */ private ContactPhotoManager mContactPhotoManager; - private LinearLayout mVoicemailHeader; + private VoicemailPlaybackPresenter mVoicemailPlaybackPresenter; private Uri mVoicemailUri; private BidiFormatter mBidiFormatter = BidiFormatter.getInstance(); @@ -255,6 +256,7 @@ public class CallDetailActivity extends Activity { getActionBar().setDisplayHomeAsUpEnabled(true); optionallyHandleVoicemail(); + if (getIntent().getBooleanExtra(EXTRA_FROM_NOTIFICATION, false)) { closeSystemDialogs(); } @@ -267,6 +269,38 @@ public class CallDetailActivity extends Activity { CallLogAsyncTaskUtil.getCallDetails(this, getCallLogEntryUris(), mCallLogAsyncTaskListener); } + @Override + public void onPause() { + if (mVoicemailPlaybackPresenter != null) { + mVoicemailPlaybackPresenter.onPause(); + } + super.onPause(); + } + + @Override + public void onDestroy() { + if (mVoicemailPlaybackPresenter != null) { + mVoicemailPlaybackPresenter.onDestroy(); + } + super.onDestroy(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (mVoicemailPlaybackPresenter != null) { + mVoicemailPlaybackPresenter.onSaveInstanceState(outState); + } + } + + @Override + public void onRestoreInstanceState(Bundle savedInstanceState) { + if (mVoicemailPlaybackPresenter != null) { + mVoicemailPlaybackPresenter.onRestoreInstanceState(savedInstanceState); + } + super.onRestoreInstanceState(savedInstanceState); + } + /** * Handle voicemail playback or hide voicemail ui. * <p> @@ -274,37 +308,15 @@ public class CallDetailActivity extends Activity { * playback. If it doesn't, then don't inflate the voicemail ui. */ private void optionallyHandleVoicemail() { - if (hasVoicemail()) { - LayoutInflater inflater = - (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); - mVoicemailHeader = - (LinearLayout) inflater.inflate(R.layout.call_details_voicemail_header, null); - View voicemailContainer = mVoicemailHeader.findViewById(R.id.voicemail_container); - ListView historyList = (ListView) findViewById(R.id.history); - historyList.addHeaderView(mVoicemailHeader); - // Has voicemail: add the voicemail fragment. Add suitable arguments to set the uri - // to play and optionally start the playback. - // Do a query to fetch the voicemail status messages. - VoicemailPlaybackFragment playbackFragment; - - playbackFragment = (VoicemailPlaybackFragment) getFragmentManager().findFragmentByTag( - VOICEMAIL_FRAGMENT_TAG); - - if (playbackFragment == null) { - playbackFragment = new VoicemailPlaybackFragment(); - Bundle fragmentArguments = new Bundle(); - fragmentArguments.putParcelable(EXTRA_VOICEMAIL_URI, mVoicemailUri); - if (getIntent().getBooleanExtra(EXTRA_VOICEMAIL_START_PLAYBACK, false)) { - fragmentArguments.putBoolean(EXTRA_VOICEMAIL_START_PLAYBACK, true); - } - playbackFragment.setArguments(fragmentArguments); - getFragmentManager().beginTransaction() - .add(R.id.voicemail_container, playbackFragment, VOICEMAIL_FRAGMENT_TAG) - .commitAllowingStateLoss(); - } + VoicemailPlaybackLayout voicemailPlaybackLayout = + (VoicemailPlaybackLayout) findViewById(R.id.voicemail_playback_layout); + + mVoicemailPlaybackPresenter = new VoicemailPlaybackPresenter(this); + mVoicemailPlaybackPresenter.setPlaybackView( + voicemailPlaybackLayout, mVoicemailUri, false /* startPlayingImmediately */); - voicemailContainer.setVisibility(View.VISIBLE); + voicemailPlaybackLayout.setVisibility(View.VISIBLE); CallLogAsyncTaskUtil.markVoicemailAsRead(this, mVoicemailUri); } } diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java index f98fc2169..36d9bb6ea 100644 --- a/src/com/android/dialer/calllog/CallLogFragment.java +++ b/src/com/android/dialer/calllog/CallLogFragment.java @@ -92,7 +92,6 @@ public class CallLogFragment extends Fragment /** Whether there is at least one voicemail source installed. */ private boolean mVoicemailSourcesAvailable = false; - private VoicemailStatusHelper mVoicemailStatusHelper; private View mEmptyListView; private KeyguardManager mKeyguardManager; @@ -277,7 +276,6 @@ public class CallLogFragment extends Fragment this); mRecyclerView.setAdapter(mAdapter); - mVoicemailStatusHelper = new VoicemailStatusHelperImpl(); fetchCalls(); return view; } diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java b/src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java deleted file mode 100644 index ed7055147..000000000 --- a/src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java +++ /dev/null @@ -1,378 +0,0 @@ -/* - * Copyright (C) 2011 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; - -import static com.android.dialer.CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK; -import static com.android.dialer.CallDetailActivity.EXTRA_VOICEMAIL_URI; - -import android.app.Activity; -import android.app.Fragment; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.database.ContentObserver; -import android.database.Cursor; -import android.media.AudioManager; -import android.net.Uri; -import android.os.Bundle; -import android.os.PowerManager; -import android.provider.VoicemailContract; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageButton; -import android.widget.SeekBar; -import android.widget.TextView; - -import com.android.common.io.MoreCloseables; -import com.android.contacts.commonbind.analytics.AnalyticsUtil; -import com.android.dialer.R; - -import com.google.common.base.Preconditions; - -import java.util.concurrent.TimeUnit; - -import javax.annotation.concurrent.GuardedBy; -import javax.annotation.concurrent.NotThreadSafe; - -/** - * Displays and plays back a single voicemail. - * <p> - * When the Activity containing this Fragment is created, voicemail playback - * will begin immediately. The Activity is expected to be started via an intent - * containing a suitable voicemail uri to playback. - * <p> - * This class is not thread-safe, it is thread-confined. All calls to all public - * methods on this class are expected to come from the main ui thread. - */ -@NotThreadSafe -public class VoicemailPlaybackFragment extends Fragment { - private static final String TAG = VoicemailPlaybackFragment.class.getSimpleName(); - private static final int NUMBER_OF_THREADS_IN_POOL = 2; - private static final String[] HAS_CONTENT_PROJECTION = new String[] { - VoicemailContract.Voicemails.HAS_CONTENT, - }; - - private VoicemailPlaybackPresenter mPresenter; - private View mPlaybackLayout; - - private PowerManager.WakeLock mProximityWakeLock; - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - mPlaybackLayout = inflater.inflate(R.layout.playback_layout, null); - return mPlaybackLayout; - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - Bundle arguments = getArguments(); - Preconditions.checkNotNull(arguments, "fragment must be started with arguments"); - Uri voicemailUri = arguments.getParcelable(EXTRA_VOICEMAIL_URI); - Preconditions.checkNotNull(voicemailUri, "fragment must contain EXTRA_VOICEMAIL_URI"); - boolean startPlayback = arguments.getBoolean(EXTRA_VOICEMAIL_START_PLAYBACK, false); - - PowerManager powerManager = - (PowerManager) getActivity().getSystemService(Context.POWER_SERVICE); - if (powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { - mProximityWakeLock = powerManager.newWakeLock( - PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG); - } else { - mProximityWakeLock = null; - } - - mPresenter = new VoicemailPlaybackPresenter( - createPlaybackViewImpl(), - voicemailUri, - startPlayback, - mProximityWakeLock); - mPresenter.onCreate(savedInstanceState); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - mPresenter.onSaveInstanceState(outState); - super.onSaveInstanceState(outState); - } - - @Override - public void onStart() { - super.onStart(); - AnalyticsUtil.sendScreenView(this); - } - - @Override - public void onViewStateRestored(Bundle savedInstanceState) { - mPresenter.onRestoreInstanceState(savedInstanceState); - super.onViewStateRestored(savedInstanceState); - } - - @Override - public void onPause() { - mPresenter.onPause(); - super.onPause(); - } - - @Override - public void onDestroy() { - mPresenter.onDestroy(); - super.onDestroy(); - } - - private PlaybackViewImpl createPlaybackViewImpl() { - return new PlaybackViewImpl(new ActivityReference(), getActivity().getApplicationContext(), - mPlaybackLayout); - } - - /** - * Formats a number of milliseconds as something that looks like {@code 00:05}. - * <p> - * We always use four digits, two for minutes two for seconds. In the very unlikely event - * that the voicemail duration exceeds 99 minutes, the display is capped at 99 minutes. - */ - private static String formatAsMinutesAndSeconds(int millis) { - int seconds = millis / 1000; - int minutes = seconds / 60; - seconds -= minutes * 60; - if (minutes > 99) { - minutes = 99; - } - return String.format("%02d:%02d", minutes, seconds); - } - - /** - * An object that can provide us with an Activity. - * <p> - * Fragments suffer the drawback that the Activity they belong to may sometimes be null. This - * can happen if the Fragment is detached, for example. In that situation a call to - * {@link Fragment#getString(int)} will throw and {@link IllegalStateException}. Also, calling - * {@link Fragment#getActivity()} is dangerous - it may sometimes return null. And thus blindly - * calling a method on the result of getActivity() is dangerous too. - * <p> - * To work around this, I have made the {@link PlaybackViewImpl} class static, so that it does - * not have access to any Fragment methods directly. Instead it uses an application Context for - * things like accessing strings, accessing system services. It only uses the Activity when it - * absolutely needs it - and does so through this class. This makes it easy to see where we have - * to check for null properly. - */ - private final class ActivityReference { - /** Gets this Fragment's Activity: <b>may be null</b>. */ - public final Activity get() { - return getActivity(); - } - } - - /** Methods required by the PlaybackView for the VoicemailPlaybackPresenter. */ - private final class PlaybackViewImpl implements VoicemailPlaybackPresenter.PlaybackView { - private final ActivityReference mActivityReference; - private final Context mApplicationContext; - private final SeekBar mPlaybackSeek; - private final ImageButton mStartStopButton; - private final ImageButton mPlaybackSpeakerphone; - private final TextView mPlaybackPosition; - - public PlaybackViewImpl(ActivityReference activityReference, Context applicationContext, - View playbackLayout) { - Preconditions.checkNotNull(activityReference); - Preconditions.checkNotNull(applicationContext); - Preconditions.checkNotNull(playbackLayout); - mActivityReference = activityReference; - mApplicationContext = applicationContext; - mPlaybackSeek = (SeekBar) playbackLayout.findViewById(R.id.playback_seek); - mStartStopButton = (ImageButton) playbackLayout.findViewById( - R.id.playback_start_stop); - mPlaybackSpeakerphone = (ImageButton) playbackLayout.findViewById( - R.id.playback_speakerphone); - mPlaybackPosition = - (TextView) playbackLayout.findViewById(R.id.playback_position_text); - } - - @Override - public void finish() { - Activity activity = mActivityReference.get(); - if (activity != null) { - activity.finish(); - } - } - - @Override - public void runOnUiThread(Runnable runnable) { - Activity activity = mActivityReference.get(); - if (activity != null) { - activity.runOnUiThread(runnable); - } - } - - @Override - public Context getDataSourceContext() { - return mApplicationContext; - } - - @Override - public void setStartStopListener(View.OnClickListener listener) { - mStartStopButton.setOnClickListener(listener); - } - - @Override - public void setSpeakerphoneListener(View.OnClickListener listener) { - mPlaybackSpeakerphone.setOnClickListener(listener); - } - - @Override - public void setPositionSeekListener(SeekBar.OnSeekBarChangeListener listener) { - mPlaybackSeek.setOnSeekBarChangeListener(listener); - } - - @Override - public void playbackStarted() { - mStartStopButton.setImageResource(R.drawable.ic_hold_pause); - } - - @Override - public void playbackStopped() { - mStartStopButton.setImageResource(R.drawable.ic_play); - } - - @Override - public void registerContentObserver(Uri uri, ContentObserver observer) { - mApplicationContext.getContentResolver().registerContentObserver(uri, false, observer); - } - - @Override - public void unregisterContentObserver(ContentObserver observer) { - mApplicationContext.getContentResolver().unregisterContentObserver(observer); - } - - @Override - public void setClipPosition(int clipPositionInMillis, int clipLengthInMillis) { - int seekBarPosition = Math.max(0, clipPositionInMillis); - int seekBarMax = Math.max(seekBarPosition, clipLengthInMillis); - if (mPlaybackSeek.getMax() != seekBarMax) { - mPlaybackSeek.setMax(seekBarMax); - } - mPlaybackSeek.setProgress(seekBarPosition); - mPlaybackPosition.setText(formatAsMinutesAndSeconds(seekBarMax - seekBarPosition)); - } - - private String getString(int resId) { - return mApplicationContext.getString(resId); - } - - @Override - public void setIsBuffering() { - disableUiElements(); - mPlaybackPosition.setText(getString(R.string.voicemail_buffering)); - } - - @Override - public void setIsFetchingContent() { - disableUiElements(); - mPlaybackPosition.setText(getString(R.string.voicemail_fetching_content)); - } - - @Override - public void setFetchContentTimeout() { - disableUiElements(); - mPlaybackPosition.setText(getString(R.string.voicemail_fetching_timout)); - } - - @Override - public int getDesiredClipPosition() { - return mPlaybackSeek.getProgress(); - } - - @Override - public void disableUiElements() { - mStartStopButton.setEnabled(false); - mPlaybackSpeakerphone.setEnabled(false); - mPlaybackSeek.setProgress(0); - mPlaybackSeek.setEnabled(false); - } - - @Override - public void playbackError(Exception e) { - disableUiElements(); - mPlaybackPosition.setText(getString(R.string.voicemail_playback_error)); - Log.e(TAG, "Could not play voicemail", e); - } - - @Override - public void enableUiElements() { - mStartStopButton.setEnabled(true); - mPlaybackSpeakerphone.setEnabled(true); - mPlaybackSeek.setEnabled(true); - } - - @Override - public void sendFetchVoicemailRequest(Uri voicemailUri) { - Intent intent = new Intent(VoicemailContract.ACTION_FETCH_VOICEMAIL, voicemailUri); - mApplicationContext.sendBroadcast(intent); - } - - @Override - public boolean queryHasContent(Uri voicemailUri) { - ContentResolver contentResolver = mApplicationContext.getContentResolver(); - Cursor cursor = contentResolver.query( - voicemailUri, HAS_CONTENT_PROJECTION, null, null, null); - try { - if (cursor != null && cursor.moveToNext()) { - return cursor.getInt(cursor.getColumnIndexOrThrow( - VoicemailContract.Voicemails.HAS_CONTENT)) == 1; - } - } finally { - MoreCloseables.closeQuietly(cursor); - } - return false; - } - - private AudioManager getAudioManager() { - return (AudioManager) mApplicationContext.getSystemService(Context.AUDIO_SERVICE); - } - - @Override - public boolean isSpeakerPhoneOn() { - return getAudioManager().isSpeakerphoneOn(); - } - - @Override - public void setSpeakerPhoneOn(boolean on) { - getAudioManager().setSpeakerphoneOn(on); - if (on) { - mPlaybackSpeakerphone.setImageResource(R.drawable.ic_speakerphone_on); - // Speaker is now on, tapping button will turn it off. - mPlaybackSpeakerphone.setContentDescription( - mApplicationContext.getString(R.string.voicemail_speaker_off)); - } else { - mPlaybackSpeakerphone.setImageResource(R.drawable.ic_speakerphone_off); - // Speaker is now off, tapping button will turn it on. - mPlaybackSpeakerphone.setContentDescription( - mApplicationContext.getString(R.string.voicemail_speaker_on)); - } - } - - @Override - public void setVolumeControlStream(int streamType) { - Activity activity = mActivityReference.get(); - if (activity != null) { - activity.setVolumeControlStream(streamType); - } - } - } -} diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java new file mode 100644 index 000000000..0e9ff3bdc --- /dev/null +++ b/src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2011 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; + +import android.app.Activity; +import android.app.Fragment; +import android.content.Context; +import android.media.MediaPlayer; +import android.net.Uri; +import android.os.Bundle; +import android.os.PowerManager; +import android.provider.VoicemailContract; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; +import android.widget.TextView; + +import com.android.common.io.MoreCloseables; +import com.android.dialer.R; + +import com.google.common.base.Preconditions; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledExecutorService; + +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.NotThreadSafe; +import javax.annotation.concurrent.ThreadSafe; + +/** + * Displays and plays a single voicemail. + * <p> + * This class is not thread-safe, it is thread-confined. All calls to all public + * methods on this class are expected to come from the main ui thread. + */ +@NotThreadSafe +public class VoicemailPlaybackLayout extends LinearLayout + implements VoicemailPlaybackPresenter.PlaybackView { + private static final String TAG = VoicemailPlaybackLayout.class.getSimpleName(); + + /** + * Controls the animation of the playback slider. + */ + @ThreadSafe + private final class PositionUpdater implements Runnable { + + /** Update rate for the slider, 30fps. */ + private static final int SLIDER_UPDATE_PERIOD_MILLIS = 1000 / 30; + + private final MediaPlayer mMediaPlayer; + private final int mDuration; + private final ScheduledExecutorService mExecutorService; + private final Object mLock = new Object(); + @GuardedBy("mLock") private ScheduledFuture<?> mScheduledFuture; + + public PositionUpdater( + MediaPlayer mediaPlayer, + int duration, + ScheduledExecutorService executorService) { + mMediaPlayer = mediaPlayer; + mDuration = duration; + mExecutorService = executorService; + } + + @Override + public void run() { + post(new Runnable() { + @Override + public void run() { + int currentPosition = 0; + synchronized (mLock) { + if (mScheduledFuture == null) { + // This task has been canceled. Just stop now. + return; + } + currentPosition = mMediaPlayer.getCurrentPosition(); + } + setClipPosition(currentPosition, mDuration); + } + }); + } + + public void startUpdating() { + synchronized (mLock) { + if (mScheduledFuture != null) { + mScheduledFuture.cancel(false); + mScheduledFuture = null; + } + mScheduledFuture = mExecutorService.scheduleAtFixedRate( + this, 0, SLIDER_UPDATE_PERIOD_MILLIS, TimeUnit.MILLISECONDS); + } + } + + public void stopUpdating() { + synchronized (mLock) { + if (mScheduledFuture != null) { + mScheduledFuture.cancel(false); + mScheduledFuture = null; + } + } + } + } + + /** + * Handle state changes when the user manipulates the seek bar. + */ + private final OnSeekBarChangeListener seekBarChangeListener = new OnSeekBarChangeListener() { + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + if (mPresenter != null) { + mPresenter.pausePlaybackForSeeking(); + } + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + if (mPresenter != null) { + mPresenter.resumePlaybackAfterSeeking(seekBar.getProgress()); + } + } + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + setClipPosition(seekBar.getProgress(), seekBar.getMax()); + } + }; + + /** + * Click listener to toggle speakerphone. + */ + private final View.OnClickListener speakerphoneListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + if (mPresenter != null) { + onSpeakerphoneOn(!mPresenter.isSpeakerphoneOn()); + } + } + }; + + /** + * Click listener to play or pause voicemail playback. + */ + private final View.OnClickListener startStopButtonListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + if (mPresenter == null) { + return; + } + if (mIsPlaying) { + mPresenter.pausePlayback(); + } else { + mPresenter.resumePlayback(); + } + } + }; + + private Context mContext; + private VoicemailPlaybackPresenter mPresenter; + + private boolean mIsPlaying = false; + + private SeekBar mPlaybackSeek; + private ImageButton mStartStopButton; + private ImageButton mPlaybackSpeakerphone; + private TextView mPlaybackPosition; + + private PositionUpdater mPositionUpdater; + + public VoicemailPlaybackLayout(Context context) { + this(context, null); + } + + public VoicemailPlaybackLayout(Context context, AttributeSet attrs) { + super(context, attrs); + + mContext = context; + LayoutInflater inflater = + (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.playback_layout, this); + } + + @Override + public void setPresenter(VoicemailPlaybackPresenter presenter) { + mPresenter = presenter; + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mPlaybackSeek = (SeekBar) findViewById(R.id.playback_seek); + mStartStopButton = (ImageButton) findViewById(R.id.playback_start_stop); + mPlaybackSpeakerphone = (ImageButton) findViewById(R.id.playback_speakerphone); + mPlaybackPosition = (TextView) findViewById(R.id.playback_position_text); + + mPlaybackSeek.setOnSeekBarChangeListener(seekBarChangeListener); + mStartStopButton.setOnClickListener(startStopButtonListener); + mPlaybackSpeakerphone.setOnClickListener(speakerphoneListener); + } + + @Override + public void onPlaybackStarted( + MediaPlayer mediaPlayer, + int duration, + ScheduledExecutorService executorService) { + mIsPlaying = true; + + mStartStopButton.setImageResource(R.drawable.ic_hold_pause); + + if (mPresenter != null) { + onSpeakerphoneOn(mPresenter.isSpeakerphoneOn()); + } + + mPositionUpdater = new PositionUpdater(mediaPlayer, duration, executorService); + mPositionUpdater.startUpdating(); + } + + @Override + public void onPlaybackStopped() { + mIsPlaying = false; + + mStartStopButton.setImageResource(R.drawable.ic_play); + + if (mPositionUpdater != null) { + mPositionUpdater.stopUpdating(); + mPositionUpdater = null; + } + } + + @Override + public void onPlaybackError(Exception e) { + if (mPositionUpdater != null) { + mPositionUpdater.stopUpdating(); + } + + disableUiElements(); + mPlaybackPosition.setText(getString(R.string.voicemail_playback_error)); + + Log.e(TAG, "Could not play voicemail", e); + } + + + public void onSpeakerphoneOn(boolean on) { + if (mPresenter != null) { + mPresenter.setSpeakerphoneOn(on); + } + + if (on) { + mPlaybackSpeakerphone.setImageResource(R.drawable.ic_speakerphone_on); + // Speaker is now on, tapping button will turn it off. + mPlaybackSpeakerphone.setContentDescription( + mContext.getString(R.string.voicemail_speaker_off)); + } else { + mPlaybackSpeakerphone.setImageResource(R.drawable.ic_speakerphone_off); + // Speaker is now off, tapping button will turn it on. + mPlaybackSpeakerphone.setContentDescription( + mContext.getString(R.string.voicemail_speaker_on)); + } + } + + @Override + public void setClipPosition(int clipPositionInMillis, int clipLengthInMillis) { + int seekBarPosition = Math.max(0, clipPositionInMillis); + int seekBarMax = Math.max(seekBarPosition, clipLengthInMillis); + if (mPlaybackSeek.getMax() != seekBarMax) { + mPlaybackSeek.setMax(seekBarMax); + } + mPlaybackSeek.setProgress(seekBarPosition); + mPlaybackPosition.setText(formatAsMinutesAndSeconds(seekBarMax - seekBarPosition)); + } + + @Override + public void setIsBuffering() { + disableUiElements(); + mPlaybackPosition.setText(getString(R.string.voicemail_buffering)); + } + + @Override + public void setIsFetchingContent() { + disableUiElements(); + mPlaybackPosition.setText(getString(R.string.voicemail_fetching_content)); + } + + @Override + public void setFetchContentTimeout() { + disableUiElements(); + mPlaybackPosition.setText(getString(R.string.voicemail_fetching_timout)); + } + + @Override + public int getDesiredClipPosition() { + return mPlaybackSeek.getProgress(); + } + + @Override + public void disableUiElements() { + mStartStopButton.setEnabled(false); + mPlaybackSpeakerphone.setEnabled(false); + mPlaybackSeek.setProgress(0); + mPlaybackSeek.setEnabled(false); + } + + @Override + public void enableUiElements() { + mStartStopButton.setEnabled(true); + mPlaybackSpeakerphone.setEnabled(true); + mPlaybackSeek.setEnabled(true); + } + + private String getString(int resId) { + return mContext.getString(resId); + } + + /** + * Formats a number of milliseconds as something that looks like {@code 00:05}. + * <p> + * We always use four digits, two for minutes two for seconds. In the very unlikely event + * that the voicemail duration exceeds 99 minutes, the display is capped at 99 minutes. + */ + private String formatAsMinutesAndSeconds(int millis) { + int seconds = millis / 1000; + int minutes = seconds / 60; + seconds -= minutes * 60; + if (minutes > 99) { + minutes = 99; + } + return String.format("%02d:%02d", minutes, seconds); + } +} diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java index 1f63f5d88..1ab87fd24 100644 --- a/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java +++ b/src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java @@ -16,8 +16,12 @@ package com.android.dialer.voicemail; +import android.app.Activity; import android.content.Context; +import android.content.ContentResolver; +import android.content.Intent; import android.database.ContentObserver; +import android.database.Cursor; import android.media.AudioManager; import android.media.AudioManager.OnAudioFocusChangeListener; import android.media.MediaPlayer; @@ -26,6 +30,7 @@ import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.PowerManager; +import android.provider.VoicemailContract; import android.util.Log; import android.view.View; import android.widget.SeekBar; @@ -33,10 +38,8 @@ import android.widget.SeekBar; import com.android.dialer.R; import com.android.dialer.util.AsyncTaskExecutor; import com.android.dialer.util.AsyncTaskExecutors; -import com.android.ex.variablespeed.MediaPlayerProxy; -import com.android.ex.variablespeed.SingleThreadedMediaPlayerProxy; -import com.android.ex.variablespeed.VariableSpeed; +import com.android.common.io.MoreCloseables; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; @@ -45,83 +48,66 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.NotThreadSafe; import javax.annotation.concurrent.ThreadSafe; /** - * Contains the controlling logic for a voicemail playback ui. + * Contains the controlling logic for a voicemail playback UI. * <p> - * Specifically right now this class is used to control the - * {@link com.android.dialer.voicemail.VoicemailPlaybackFragment}. + * This controls a single {@link com.android.dialer.voicemail.VoicemailPlaybackLayout}. A single + * instance can be reused for different such layouts, using {@link #setVoicemailPlaybackView}. * <p> - * This class is not thread safe. The thread policy for this class is - * thread-confinement, all calls into this class from outside must be done from - * the main ui thread. + * This class is not thread safe. The thread policy for this class is thread-confinement, all calls + * into this class from outside must be done from the main UI thread. */ @NotThreadSafe @VisibleForTesting -public class VoicemailPlaybackPresenter implements OnAudioFocusChangeListener { +public class VoicemailPlaybackPresenter + implements OnAudioFocusChangeListener, MediaPlayer.OnPreparedListener, + MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener { + private static final String TAG = VoicemailPlaybackPresenter.class.getSimpleName(); - /** The stream used to playback voicemail. */ - private static final int PLAYBACK_STREAM = AudioManager.STREAM_VOICE_CALL; /** Contract describing the behaviour we need from the ui we are controlling. */ public interface PlaybackView { - Context getDataSourceContext(); - void runOnUiThread(Runnable runnable); - void setStartStopListener(View.OnClickListener listener); - void setPositionSeekListener(SeekBar.OnSeekBarChangeListener listener); - void setSpeakerphoneListener(View.OnClickListener listener); - void setIsBuffering(); - void setClipPosition(int clipPositionInMillis, int clipLengthInMillis); int getDesiredClipPosition(); - void playbackStarted(); - void playbackStopped(); - void playbackError(Exception e); - boolean isSpeakerPhoneOn(); - void setSpeakerPhoneOn(boolean on); - void finish(); - void setIsFetchingContent(); void disableUiElements(); void enableUiElements(); - void sendFetchVoicemailRequest(Uri voicemailUri); - boolean queryHasContent(Uri voicemailUri); + void onPlaybackError(Exception e); + void onPlaybackStarted(MediaPlayer mediaPlayer, int duration, + ScheduledExecutorService executorService); + void onPlaybackStopped(); + void setClipPosition(int clipPositionInMillis, int clipLengthInMillis); void setFetchContentTimeout(); - void registerContentObserver(Uri uri, ContentObserver observer); - void unregisterContentObserver(ContentObserver observer); - void setVolumeControlStream(int streamType); + void setIsBuffering(); + void setIsFetchingContent(); + void setPresenter(VoicemailPlaybackPresenter presenter); } /** The enumeration of {@link AsyncTask} objects we use in this class. */ public enum Tasks { CHECK_FOR_CONTENT, CHECK_CONTENT_AFTER_CHANGE, - PREPARE_MEDIA_PLAYER, - RESET_PREPARE_START_MEDIA_PLAYER, } + private static final String[] HAS_CONTENT_PROJECTION = new String[] { + VoicemailContract.Voicemails.HAS_CONTENT, + }; + + private static final int PLAYBACK_STREAM = AudioManager.STREAM_VOICE_CALL; private static final int NUMBER_OF_THREADS_IN_POOL = 2; - /** Update rate for the slider, 30fps. */ - private static final int SLIDER_UPDATE_PERIOD_MILLIS = 1000 / 30; - /** Time our ui will wait for content to be fetched before reporting not available. */ + // Time to wait for content to be fetched before timing out. private static final long FETCH_CONTENT_TIMEOUT_MS = 20000; - /** - * If present in the saved instance bundle, we should not resume playback on - * create. - */ - private static final String IS_PLAYING_STATE_KEY = VoicemailPlaybackPresenter.class.getName() - + ".IS_PLAYING_STATE_KEY"; - /** - * If present in the saved instance bundle, indicates where to set the - * playback slider. - */ - private static final String CLIP_POSITION_KEY = VoicemailPlaybackPresenter.class.getName() - + ".CLIP_POSITION_KEY"; + + // If present in the saved instance bundle, we should not resume playback on create. + private static final String IS_PLAYING_STATE_KEY = + VoicemailPlaybackPresenter.class.getName() + ".IS_PLAYING_STATE_KEY"; + // If present in the saved instance bundle, indicates where to set the playback slider. + private static final String CLIP_POSITION_KEY = + VoicemailPlaybackPresenter.class.getName() + ".CLIP_POSITION_KEY"; /** * The most recently calculated duration. @@ -132,20 +118,18 @@ public class VoicemailPlaybackPresenter implements OnAudioFocusChangeListener { */ private final AtomicInteger mDuration = new AtomicInteger(0); - private MediaPlayerProxy mPlayer; - private static int mMediaPlayerRefCount = 0; - private static MediaPlayerProxy mMediaPlayerInstance; - - private final PlaybackView mView; - private final PositionUpdater mPositionUpdater; - /** Voicemail uri to play. */ - private final Uri mVoicemailUri; - /** Start playing in onCreate iff this is true. */ - private final boolean mStartPlayingImmediately; - /** Used to run async tasks that need to interact with the ui. */ + private Context mContext; + private MediaPlayer mMediaPlayer; + private PlaybackView mView; + + private Uri mVoicemailUri; + private int mPosition; + private boolean mIsPlaying; + private boolean mShouldResumePlaybackAfterSeeking; + + // Used to run async tasks that need to interact with the UI. private final AsyncTaskExecutor mAsyncTaskExecutor; private static ScheduledExecutorService mScheduledExecutorService; - /** * Used to handle the result of a successful or time-out fetch result. * <p> @@ -153,37 +137,78 @@ public class VoicemailPlaybackPresenter implements OnAudioFocusChangeListener { */ private FetchResultHandler mFetchResultHandler; private PowerManager.WakeLock mProximityWakeLock; - private AsyncTask<Void, ?, ?> mPrepareTask; - private int mPosition; - private boolean mPlaying; private AudioManager mAudioManager; - public VoicemailPlaybackPresenter( - PlaybackView view, - Uri voicemailUri, - boolean startPlayingImmediately, - PowerManager.WakeLock wakeLock) { + public VoicemailPlaybackPresenter(Activity activity) { + mContext = activity; + mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor(); + mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + + PowerManager powerManager = + (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + if (powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { + mProximityWakeLock = powerManager.newWakeLock( + PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG); + } + + if (mMediaPlayer == null) { + mMediaPlayer = new MediaPlayer(); + mMediaPlayer.setOnPreparedListener(this); + mMediaPlayer.setOnErrorListener(this); + mMediaPlayer.setOnCompletionListener(this); + } + + activity.setVolumeControlStream(PLAYBACK_STREAM); + } + + /** + * Specify the view which this presenter controls and the voicemail for playback. + */ + public void setPlaybackView( + PlaybackView view, Uri voicemailUri, boolean startPlayingImmediately) { mView = view; mVoicemailUri = voicemailUri; - mStartPlayingImmediately = startPlayingImmediately; - mPositionUpdater = new PositionUpdater( - getScheduledExecutorServiceInstance(), SLIDER_UPDATE_PERIOD_MILLIS); - mProximityWakeLock = wakeLock; + setPosition(0, startPlayingImmediately); - mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor(); - mPlayer = VariableSpeed.createVariableSpeed(getScheduledExecutorServiceInstance()); + mView.setPresenter(this); + + checkForContent(); + } - ++mMediaPlayerRefCount; - if (mMediaPlayerInstance == null) { - mMediaPlayerInstance = VariableSpeed.createVariableSpeed( - getScheduledExecutorServiceInstance()); + public void onPause() { + if (mMediaPlayer.isPlaying()) { + pausePlayback(mMediaPlayer.getCurrentPosition(), mIsPlaying); } - mPlayer = mMediaPlayerInstance; + + disableProximitySensor(false /* waitForFarState */); } - public void onCreate(Bundle bundle) { - mView.setVolumeControlStream(PLAYBACK_STREAM); - checkThatWeHaveContent(); + public void onDestroy() { + if (mScheduledExecutorService != null) { + mScheduledExecutorService.shutdown(); + mScheduledExecutorService = null; + } + + if (mFetchResultHandler != null) { + mFetchResultHandler.destroy(); + mFetchResultHandler = null; + } + + disableProximitySensor(false /* waitForFarState */); + } + + public void onSaveInstanceState(Bundle outState) { + outState.putInt(CLIP_POSITION_KEY, mView.getDesiredClipPosition()); + outState.putBoolean(IS_PLAYING_STATE_KEY, mIsPlaying); + } + + public void onRestoreInstanceState(Bundle inState) { + if (inState != null) { + int position = inState.getInt(CLIP_POSITION_KEY, 0); + boolean isPlaying = inState.getBoolean(IS_PLAYING_STATE_KEY, false); + // Playback will be automatically resumed, if appropriate, in onPrepared(). + setPosition(position, isPlaying); + } } /** @@ -192,30 +217,44 @@ public class VoicemailPlaybackPresenter implements OnAudioFocusChangeListener { * This method will be called once, after the fragment has been created, before we know if the * voicemail we've been asked to play has any content available. * <p> - * This method will notify the user through the ui that we are fetching the content, then check - * to see if the content field in the db is set. If set, we proceed to - * {@link #postSuccessfullyFetchedContent()} method. If not set, we will make a request to fetch - * the content asynchronously via {@link #makeRequestForContent()}. + * Notify the user that we are fetching the content, then check to see if the content field in + * the DB is set. If set, we proceed to {@link #prepareToPlayContent()} method. If not set, make + * a request to fetch the content asynchronously via {@link #requestContent()}. */ - private void checkThatWeHaveContent() { + private void checkForContent() { mView.setIsFetchingContent(); mAsyncTaskExecutor.submit(Tasks.CHECK_FOR_CONTENT, new AsyncTask<Void, Void, Boolean>() { @Override public Boolean doInBackground(Void... params) { - return mView.queryHasContent(mVoicemailUri); + return queryHasContent(mVoicemailUri); } @Override public void onPostExecute(Boolean hasContent) { if (hasContent) { - postSuccessfullyFetchedContent(); + prepareToPlayContent(); } else { - makeRequestForContent(); + requestContent(); } } }); } + private boolean queryHasContent(Uri voicemailUri) { + ContentResolver contentResolver = mContext.getContentResolver(); + Cursor cursor = contentResolver.query( + voicemailUri, HAS_CONTENT_PROJECTION, null, null, null); + try { + if (cursor != null && cursor.moveToNext()) { + return cursor.getInt(cursor.getColumnIndexOrThrow( + VoicemailContract.Voicemails.HAS_CONTENT)) == 1; + } + } finally { + MoreCloseables.closeQuietly(cursor); + } + return false; + } + /** * Makes a broadcast request to ask that a voicemail source fetch this content. * <p> @@ -225,17 +264,22 @@ public class VoicemailPlaybackPresenter implements OnAudioFocusChangeListener { * will trigger a broadcast to request that the content be downloaded. It will add a listener to * the content resolver so that it will be notified when the has_content field changes. It will * also set a timer. If the has_content field changes to true within the allowed time, we will - * proceed to {@link #postSuccessfullyFetchedContent()}. If the has_content field does not + * proceed to {@link #prepareToPlayContent()}. If the has_content field does not * become true within the allowed time, we will update the ui to reflect the fact that content * was not available. */ - private void makeRequestForContent() { - Handler handler = new Handler(); + private void requestContent() { Preconditions.checkState(mFetchResultHandler == null, "mFetchResultHandler should be null"); + + Handler handler = new Handler(); mFetchResultHandler = new FetchResultHandler(handler); - mView.registerContentObserver(mVoicemailUri, mFetchResultHandler); + mContext.getContentResolver().registerContentObserver( + mVoicemailUri, false, mFetchResultHandler); handler.postDelayed(mFetchResultHandler.getTimeoutRunnable(), FETCH_CONTENT_TIMEOUT_MS); - mView.sendFetchVoicemailRequest(mVoicemailUri); + + // Send voicemail fetch request. + Intent intent = new Intent(VoicemailContract.ACTION_FETCH_VOICEMAIL, mVoicemailUri); + mContext.sendBroadcast(intent); } @ThreadSafe @@ -255,14 +299,14 @@ public class VoicemailPlaybackPresenter implements OnAudioFocusChangeListener { @Override public void run() { if (mResultStillPending.getAndSet(false)) { - mView.unregisterContentObserver(FetchResultHandler.this); + mContext.getContentResolver().unregisterContentObserver(FetchResultHandler.this); mView.setFetchContentTimeout(); } } public void destroy() { if (mResultStillPending.getAndSet(false)) { - mView.unregisterContentObserver(FetchResultHandler.this); + mContext.getContentResolver().unregisterContentObserver(FetchResultHandler.this); mHandler.removeCallbacks(this); } } @@ -273,15 +317,16 @@ public class VoicemailPlaybackPresenter implements OnAudioFocusChangeListener { new AsyncTask<Void, Void, Boolean>() { @Override public Boolean doInBackground(Void... params) { - return mView.queryHasContent(mVoicemailUri); + return queryHasContent(mVoicemailUri); } @Override public void onPostExecute(Boolean hasContent) { if (hasContent) { if (mResultStillPending.getAndSet(false)) { - mView.unregisterContentObserver(FetchResultHandler.this); - postSuccessfullyFetchedContent(); + mContext.getContentResolver().unregisterContentObserver( + FetchResultHandler.this); + prepareToPlayContent(); } } } @@ -293,124 +338,160 @@ public class VoicemailPlaybackPresenter implements OnAudioFocusChangeListener { * Prepares the voicemail content for playback. * <p> * This method will be called once we know that our voicemail has content (according to the - * content provider). This method will try to prepare the data source through the media player. - * If preparing the media player works, we will call through to - * {@link #postSuccessfulPrepareActions()}. If preparing the media player fails (perhaps the - * file the content provider points to is actually missing, perhaps it is of an unknown file - * format that we can't play, who knows) then we will show an error on the ui. + * content provider). this method asynchronously tries to prepare the data source through the + * media player. If preparation is successful, the media player will {@link #onPrepared()}, + * and it will call {@link #onError()} otherwise. */ - private void postSuccessfullyFetchedContent() { + private void prepareToPlayContent() { mView.setIsBuffering(); - mAsyncTaskExecutor.submit(Tasks.PREPARE_MEDIA_PLAYER, - new AsyncTask<Void, Void, Exception>() { - @Override - public Exception doInBackground(Void... params) { - try { - mPlayer.reset(); - mPlayer.setDataSource(mView.getDataSourceContext(), mVoicemailUri); - mPlayer.setAudioStreamType(PLAYBACK_STREAM); - mPlayer.prepare(); - mDuration.set(mPlayer.getDuration()); - return null; - } catch (Exception e) { - return e; - } - } - @Override - public void onPostExecute(Exception exception) { - if (exception == null) { - postSuccessfulPrepareActions(); - } else { - mView.playbackError(exception); - } - } - }); + try { + mMediaPlayer.reset(); + mMediaPlayer.setDataSource(mContext, mVoicemailUri); + mMediaPlayer.setAudioStreamType(PLAYBACK_STREAM); + mMediaPlayer.prepareAsync(); + } catch (Exception e) { + handleError(e); + } } /** - * Enables the ui, and optionally starts playback immediately. - * <p> - * This will be called once we have successfully prepared the media player, and will optionally - * playback immediately. + * Once the media player is prepared, enables the UI and adopts the appropriate playback state. */ - private void postSuccessfulPrepareActions() { + @Override + public void onPrepared(MediaPlayer mp) { mView.enableUiElements(); - mView.setPositionSeekListener(new PlaybackPositionListener()); - mView.setStartStopListener(new StartStopButtonListener()); - mView.setSpeakerphoneListener(new SpeakerphoneListener()); - mPlayer.setOnErrorListener(new MediaPlayerErrorListener()); - mPlayer.setOnCompletionListener(new MediaPlayerCompletionListener()); - mView.setSpeakerPhoneOn(mView.isSpeakerPhoneOn()); - if (mPlaying) { - resetPrepareStartPlaying(mPosition); + + if (mIsPlaying) { + resumePlayback(); } else { - stopPlaybackAtPosition(mPosition, mDuration.get()); - if ((mPosition == 0) && (mStartPlayingImmediately)) { - resetPrepareStartPlaying(0); - } + pausePlayback(); } } - public void onSaveInstanceState(Bundle outState) { - outState.putInt(CLIP_POSITION_KEY, mView.getDesiredClipPosition()); - outState.putBoolean(IS_PLAYING_STATE_KEY, mPlaying); + /** + * Invoked if preparing the media player fails, for example, if file is missing or the voicemail + * is an unknown file format that can't be played. + */ + @Override + public boolean onError(MediaPlayer mp, int what, int extra) { + handleError(new IllegalStateException("MediaPlayer error listener invoked")); + return true; } - public void onRestoreInstanceState(Bundle inState) { - int position = 0; - boolean isPlaying = false; - if (inState != null) { - position = inState.getInt(CLIP_POSITION_KEY, 0); - isPlaying = inState.getBoolean(IS_PLAYING_STATE_KEY, false); + private void handleError(Exception e) { + mMediaPlayer.release(); + mView.onPlaybackError(e); + setPosition(0, false); + } + + /** + * After done playing the voicemail clip, reset the clip position to the start. + */ + @Override + public void onCompletion(MediaPlayer mediaPlayer) { + pausePlayback(0, false); + } + + @Override + public void onAudioFocusChange(int focusChange) { + boolean lostFocus = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || + focusChange == AudioManager.AUDIOFOCUS_LOSS; + if (mMediaPlayer.isPlaying() && lostFocus) { + pausePlayback(); + } else if (!mMediaPlayer.isPlaying() && focusChange == AudioManager.AUDIOFOCUS_GAIN) { + resumePlayback(); } - setPositionAndPlayingStatus(position, isPlaying) ; } - private void setPositionAndPlayingStatus(int position, boolean isPlaying) { - mPosition = position; - mPlaying = isPlaying; + /** + * Sets the position and playing state for when playback is resumed. + */ + private void setPosition(int position, boolean isPlaying) { + mPosition = position; + mIsPlaying = isPlaying; } - public void onDestroy() { - --mMediaPlayerRefCount; - if (mMediaPlayerRefCount == 0) { - if (mScheduledExecutorService != null) { - mScheduledExecutorService.shutdown(); - mScheduledExecutorService = null; - } - if (mPlayer != null) { - mPlayer.release(); - mPlayer = null; + /** + * Resumes voicemail playback at the clip position stored by the presenter. + */ + public void resumePlayback() { + final int duration = mMediaPlayer.getDuration(); + mDuration.set(duration); + + // Clamp the start position between 0 and the duration. + int startPosition = Math.max(0, Math.min(mPosition, duration)); + mMediaPlayer.seekTo(startPosition); + setPosition(startPosition, true); + + try { + // Grab audio focus here + int result = mAudioManager.requestAudioFocus( + VoicemailPlaybackPresenter.this, + PLAYBACK_STREAM, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); + + if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + throw new RejectedExecutionException("Could not capture audio focus."); } - } - if (mPrepareTask != null) { - mPrepareTask.cancel(false); - mPrepareTask = null; - } - if (mFetchResultHandler != null) { - mFetchResultHandler.destroy(); - mFetchResultHandler = null; + // Can throw RejectedExecutionException + mMediaPlayer.start(); + + mView.onPlaybackStarted(mMediaPlayer, duration, getScheduledExecutorServiceInstance()); + enableProximitySensor(); + } catch (RejectedExecutionException e) { + handleError(e); } - mPositionUpdater.stopUpdating(); - if (mProximityWakeLock.isHeld()) { - mProximityWakeLock.release(); + } + + public void pausePlayback() { + pausePlayback(mMediaPlayer.getCurrentPosition(), false); + } + + /** + * {@link isPlaying} may be set to {@code true} so voicemail playback can be resumed after a + * rotation. + */ + private void pausePlayback(int position, boolean isPlaying) { + setPosition(position, isPlaying); + + if (mMediaPlayer.isPlaying()) { + mMediaPlayer.pause(); } + + mAudioManager.abandonAudioFocus(this); + mView.onPlaybackStopped(); + + // Always disable the proximity sensor on stop. + disableProximitySensor(true /* waitForFarState */); + + int duration = mDuration.get(); + mView.setClipPosition(position, duration); } - private static synchronized ScheduledExecutorService getScheduledExecutorServiceInstance() { - if (mScheduledExecutorService == null) { - mScheduledExecutorService = Executors.newScheduledThreadPool( - NUMBER_OF_THREADS_IN_POOL); + /** + * Pauses playback when the user starts seeking the position, and notes whether the voicemail is + * playing to know whether to resume playback once the user selects a new position. + */ + public void pausePlaybackForSeeking() { + mShouldResumePlaybackAfterSeeking = mMediaPlayer.isPlaying(); + pausePlayback(); + } + + public void resumePlaybackAfterSeeking(int desiredPosition) { + setPosition(desiredPosition, mShouldResumePlaybackAfterSeeking); + if (mShouldResumePlaybackAfterSeeking) { + resumePlayback(); } - return mScheduledExecutorService; + mShouldResumePlaybackAfterSeeking = false; } private void enableProximitySensor() { - if (mProximityWakeLock == null) { + if (mProximityWakeLock == null || isSpeakerphoneOn() || !mMediaPlayer.isPlaying()) { return; } + if (!mProximityWakeLock.isHeld()) { Log.i(TAG, "Acquiring proximity wake lock"); mProximityWakeLock.acquire(); @@ -432,282 +513,24 @@ public class VoicemailPlaybackPresenter implements OnAudioFocusChangeListener { } } - private class MediaPlayerErrorListener implements MediaPlayer.OnErrorListener { - @Override - public boolean onError(MediaPlayer mp, int what, int extra) { - mView.runOnUiThread(new Runnable() { - @Override - public void run() { - handleError(new IllegalStateException("MediaPlayer error listener invoked")); - } - }); - return true; - } - } - - private class MediaPlayerCompletionListener implements MediaPlayer.OnCompletionListener { - @Override - public void onCompletion(final MediaPlayer mp) { - mView.runOnUiThread(new Runnable() { - @Override - public void run() { - handleCompletion(mp); - } - }); - } - } - - private class AsyncPrepareTask extends AsyncTask<Void, Void, Exception> { - private int mClipPositionInMillis; - - AsyncPrepareTask(int clipPositionInMillis) { - mClipPositionInMillis = clipPositionInMillis; - } - - @Override - public Exception doInBackground(Void... params) { - try { - mPlayer.reset(); - mPlayer.setDataSource(mView.getDataSourceContext(), mVoicemailUri); - mPlayer.setAudioStreamType(PLAYBACK_STREAM); - mPlayer.prepare(); - return null; - } catch (Exception e) { - return e; - } - } - - @Override - public void onPostExecute(Exception exception) { - mPrepareTask = null; - if (exception == null) { - final int duration = mPlayer.getDuration(); - mDuration.set(duration); - int startPosition = - constrain(mClipPositionInMillis, 0, duration); - mPlayer.seekTo(startPosition); - mView.setClipPosition(startPosition, duration); - try { - // Grab audio focus here - int result = getAudioManager().requestAudioFocus( - VoicemailPlaybackPresenter.this, - PLAYBACK_STREAM, - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); - - if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { - throw new RejectedExecutionException("Could not capture audio focus."); - } - // Can throw RejectedExecutionException - mPlayer.start(); - setPositionAndPlayingStatus(mPlayer.getCurrentPosition(), true); - mView.playbackStarted(); - if (!mProximityWakeLock.isHeld()) { - mProximityWakeLock.acquire(); - } - // Only enable if we are not currently using the speaker phone. - if (!mView.isSpeakerPhoneOn()) { - enableProximitySensor(); - } - // Can throw RejectedExecutionException - mPositionUpdater.startUpdating(startPosition, duration); - } catch (RejectedExecutionException e) { - handleError(e); - } - } else { - handleError(exception); - } - } - } - - private AudioManager getAudioManager() { - if (mAudioManager == null) { - mAudioManager = (AudioManager) - mView.getDataSourceContext().getSystemService(Context.AUDIO_SERVICE); - } - return mAudioManager; - } - - @Override - public void onAudioFocusChange(int focusChange) { - boolean lostFocus = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || - focusChange == AudioManager.AUDIOFOCUS_LOSS; - // Note: the below logic is the same as in {@code StartStopButtonListener}. - if (mPlayer.isPlaying() && lostFocus) { - setPositionAndPlayingStatus(mPlayer.getCurrentPosition(), false); - stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get()); - } else if (!mPlayer.isPlaying() && focusChange == AudioManager.AUDIOFOCUS_GAIN) { - setPositionAndPlayingStatus(mPosition, true); - postSuccessfullyFetchedContent(); - } - } - - - private void resetPrepareStartPlaying(final int clipPositionInMillis) { - if (mPrepareTask != null) { - mPrepareTask.cancel(false); - mPrepareTask = null; - } - mPrepareTask = mAsyncTaskExecutor.submit(Tasks.RESET_PREPARE_START_MEDIA_PLAYER, - new AsyncPrepareTask(clipPositionInMillis)); - } - - private void handleError(Exception e) { - mView.playbackError(e); - mPositionUpdater.stopUpdating(); - mPlayer.release(); - setPositionAndPlayingStatus(0, false); - } - - public void handleCompletion(MediaPlayer mediaPlayer) { - stopPlaybackAtPosition(0, mDuration.get()); - } - - private void stopPlaybackAtPosition(int clipPosition, int duration) { - getAudioManager().abandonAudioFocus(this); - mPositionUpdater.stopUpdating(); - mView.playbackStopped(); - if (mProximityWakeLock.isHeld()) { - mProximityWakeLock.release(); - } - // Always disable on stop. - disableProximitySensor(true /* waitForFarState */); - mView.setClipPosition(clipPosition, duration); - if (mPlayer.isPlaying()) { - mPlayer.pause(); - } - } - - private class PlaybackPositionListener implements SeekBar.OnSeekBarChangeListener { - private boolean mShouldResumePlaybackAfterSeeking; - - @Override - public void onStartTrackingTouch(SeekBar arg0) { - if (mPlayer.isPlaying()) { - mShouldResumePlaybackAfterSeeking = true; - stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get()); - } else { - mShouldResumePlaybackAfterSeeking = false; - } - } - - @Override - public void onStopTrackingTouch(SeekBar arg0) { - if (mPlayer.isPlaying()) { - setPositionAndPlayingStatus(mPlayer.getCurrentPosition(), false); - stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get()); - } else { - setPositionAndPlayingStatus(mView.getDesiredClipPosition(), - mShouldResumePlaybackAfterSeeking); - } - - if (mShouldResumePlaybackAfterSeeking) { - postSuccessfullyFetchedContent(); - } - } - - @Override - public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { - mView.setClipPosition(seekBar.getProgress(), seekBar.getMax()); - } - } - - private class SpeakerphoneListener implements View.OnClickListener { - @Override - public void onClick(View v) { - boolean previousState = mView.isSpeakerPhoneOn(); - mView.setSpeakerPhoneOn(!previousState); - if (mPlayer.isPlaying() && previousState) { - // If we are currently playing and we are disabling the speaker phone, enable the - // sensor. - enableProximitySensor(); - } else { - // If we are not currently playing, disable the sensor. - disableProximitySensor(true /* waitForFarState */); - } + public void setSpeakerphoneOn(boolean on) { + mAudioManager.setSpeakerphoneOn(on); + if (on) { + disableProximitySensor(false /* waitForFarState */); + } else { + enableProximitySensor(); } } - private class StartStopButtonListener implements View.OnClickListener { - @Override - public void onClick(View arg0) { - if (mPlayer.isPlaying()) { - setPositionAndPlayingStatus(mPlayer.getCurrentPosition(), false); - stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get()); - } else { - setPositionAndPlayingStatus(mPosition, true); - postSuccessfullyFetchedContent(); - } - } + public boolean isSpeakerphoneOn() { + return mAudioManager.isSpeakerphoneOn(); } - /** - * Controls the animation of the playback slider. - */ - @ThreadSafe - private final class PositionUpdater implements Runnable { - private final ScheduledExecutorService mExecutorService; - private final int mPeriodMillis; - private final Object mLock = new Object(); - @GuardedBy("mLock") private ScheduledFuture<?> mScheduledFuture; - private final Runnable mSetClipPostitionRunnable = new Runnable() { - @Override - public void run() { - int currentPosition = 0; - synchronized (mLock) { - if (mScheduledFuture == null) { - // This task has been canceled. Just stop now. - return; - } - currentPosition = mPlayer.getCurrentPosition(); - } - mView.setClipPosition(currentPosition, mDuration.get()); - } - }; - - public PositionUpdater(ScheduledExecutorService executorService, int periodMillis) { - mExecutorService = executorService; - mPeriodMillis = periodMillis; - } - - @Override - public void run() { - mView.runOnUiThread(mSetClipPostitionRunnable); - } - - public void startUpdating(int beginPosition, int endPosition) { - synchronized (mLock) { - if (mScheduledFuture != null) { - mScheduledFuture.cancel(false); - mScheduledFuture = null; - } - mScheduledFuture = mExecutorService.scheduleAtFixedRate(this, 0, mPeriodMillis, - TimeUnit.MILLISECONDS); - } - } - - public void stopUpdating() { - synchronized (mLock) { - if (mScheduledFuture != null) { - mScheduledFuture.cancel(false); - mScheduledFuture = null; - } - } - } - } - - public void onPause() { - if (mPlayer.isPlaying()) { - stopPlaybackAtPosition(mPlayer.getCurrentPosition(), mDuration.get()); - } - if (mPrepareTask != null) { - mPrepareTask.cancel(false); - mPrepareTask = null; + private static synchronized ScheduledExecutorService getScheduledExecutorServiceInstance() { + if (mScheduledExecutorService == null) { + mScheduledExecutorService = Executors.newScheduledThreadPool(NUMBER_OF_THREADS_IN_POOL); } - - disableProximitySensor(false /* waitForFarState */); + return mScheduledExecutorService; } - private static int constrain(int amount, int low, int high) { - return amount < low ? low : (amount > high ? high : amount); - } } |