/* * 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 android.util.MathUtils.constrain; import android.content.Context; import android.database.ContentObserver; import android.media.AudioManager; import android.media.MediaPlayer; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.PowerManager; import android.view.View; import android.widget.SeekBar; import com.android.dialer.R; import com.android.dialer.util.AsyncTaskExecutor; import com.android.ex.variablespeed.MediaPlayerProxy; import com.android.ex.variablespeed.SingleThreadedMediaPlayerProxy; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; 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. *
* Specifically right now this class is used to control the * {@link com.android.dialer.voicemail.VoicemailPlaybackFragment}. *
* 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 { /** 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 setRateDisplay(float rate, int stringResourceId); void setRateIncreaseButtonListener(View.OnClickListener listener); void setRateDecreaseButtonListener(View.OnClickListener listener); void setIsFetchingContent(); void disableUiElements(); void enableUiElements(); void sendFetchVoicemailRequest(Uri voicemailUri); boolean queryHasContent(Uri voicemailUri); void setFetchContentTimeout(); void registerContentObserver(Uri uri, ContentObserver observer); void unregisterContentObserver(ContentObserver observer); void enableProximitySensor(); void disableProximitySensor(); void setVolumeControlStream(int streamType); } /** 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, } /** 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. */ 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 PAUSED_STATE_KEY = VoicemailPlaybackPresenter.class.getName() + ".PAUSED_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 preset variable-speed rates. Each is greater than the previous by 25%. */ private static final float[] PRESET_RATES = new float[] { 0.64f, 0.8f, 1.0f, 1.25f, 1.5625f }; /** The string resource ids corresponding to the names given to the above preset rates. */ private static final int[] PRESET_NAMES = new int[] { R.string.voicemail_speed_slowest, R.string.voicemail_speed_slower, R.string.voicemail_speed_normal, R.string.voicemail_speed_faster, R.string.voicemail_speed_fastest, }; /** * Pointer into the {@link VoicemailPlaybackPresenter#PRESET_RATES} array. *
* This doesn't need to be synchronized, it's used only by the {@link RateChangeListener} * which in turn is only executed on the ui thread. This can't be encapsulated inside the * rate change listener since multiple rate change listeners must share the same value. */ private int mRateIndex = 2; /** * The most recently calculated duration. *
* We cache this in a field since we don't want to keep requesting it from the player, as * this can easily lead to throwing {@link IllegalStateException} (any time the player is * released, it's illegal to ask for the duration). */ private final AtomicInteger mDuration = new AtomicInteger(0); private final PlaybackView mView; private final MediaPlayerProxy mPlayer; 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 final AsyncTaskExecutor mAsyncTaskExecutor; /** * Used to handle the result of a successful or time-out fetch result. *
* This variable is thread-contained, accessed only on the ui thread.
*/
private FetchResultHandler mFetchResultHandler;
private PowerManager.WakeLock mWakeLock;
private AsyncTask
* 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.
*
* 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()}.
*/
private void checkThatWeHaveContent() {
mView.setIsFetchingContent();
mAsyncTaskExecutor.submit(Tasks.CHECK_FOR_CONTENT, new AsyncTask
* This method must be called on the ui thread.
*
* This method will be called when we realise that we don't have content for this voicemail. It
* 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
* 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();
Preconditions.checkState(mFetchResultHandler == null, "mFetchResultHandler should be null");
mFetchResultHandler = new FetchResultHandler(handler);
mView.registerContentObserver(mVoicemailUri, mFetchResultHandler);
handler.postDelayed(mFetchResultHandler.getTimeoutRunnable(), FETCH_CONTENT_TIMEOUT_MS);
mView.sendFetchVoicemailRequest(mVoicemailUri);
}
@ThreadSafe
private class FetchResultHandler extends ContentObserver implements Runnable {
private AtomicBoolean mResultStillPending = new AtomicBoolean(true);
private final Handler mHandler;
public FetchResultHandler(Handler handler) {
super(handler);
mHandler = handler;
}
public Runnable getTimeoutRunnable() {
return this;
}
@Override
public void run() {
if (mResultStillPending.getAndSet(false)) {
mView.unregisterContentObserver(FetchResultHandler.this);
mView.setFetchContentTimeout();
}
}
public void destroy() {
if (mResultStillPending.getAndSet(false)) {
mView.unregisterContentObserver(FetchResultHandler.this);
mHandler.removeCallbacks(this);
}
}
@Override
public void onChange(boolean selfChange) {
mAsyncTaskExecutor.submit(Tasks.CHECK_CONTENT_AFTER_CHANGE,
new AsyncTask
* 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.
*/
private void postSuccessfullyFetchedContent() {
mView.setIsBuffering();
mAsyncTaskExecutor.submit(Tasks.PREPARE_MEDIA_PLAYER,
new AsyncTask
* This will be called once we have successfully prepared the media player, and will optionally
* playback immediately.
*/
private void postSuccessfulPrepareActions() {
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());
mView.setRateDecreaseButtonListener(createRateDecreaseListener());
mView.setRateIncreaseButtonListener(createRateIncreaseListener());
mView.setClipPosition(0, mPlayer.getDuration());
mView.playbackStopped();
// Always disable on stop.
mView.disableProximitySensor();
if (mStartPlayingImmediately) {
resetPrepareStartPlaying(0);
}
// TODO: Now I'm ignoring the bundle, when previously I was checking for contains against
// the PAUSED_STATE_KEY, and CLIP_POSITION_KEY.
}
public void onSaveInstanceState(Bundle outState) {
outState.putInt(CLIP_POSITION_KEY, mView.getDesiredClipPosition());
if (!mPlayer.isPlaying()) {
outState.putBoolean(PAUSED_STATE_KEY, true);
}
}
public void onDestroy() {
mPlayer.release();
if (mFetchResultHandler != null) {
mFetchResultHandler.destroy();
mFetchResultHandler = null;
}
mPositionUpdater.stopUpdating();
if (mWakeLock.isHeld()) {
mWakeLock.release();
}
}
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);
}
});
}
}
public View.OnClickListener createRateDecreaseListener() {
return new RateChangeListener(false);
}
public View.OnClickListener createRateIncreaseListener() {
return new RateChangeListener(true);
}
/**
* Listens to clicks on the rate increase and decrease buttons.
*
* This class is not thread-safe, but all interactions with it will happen on the ui thread.
*/
private class RateChangeListener implements View.OnClickListener {
private final boolean mIncrease;
public RateChangeListener(boolean increase) {
mIncrease = increase;
}
@Override
public void onClick(View v) {
// Adjust the current rate, then clamp it to the allowed values.
mRateIndex = constrain(mRateIndex + (mIncrease ? 1 : -1), 0, PRESET_RATES.length - 1);
// Whether or not we have actually changed the index, call changeRate().
// This will ensure that we show the "fastest" or "slowest" text on the ui to indicate
// to the user that it doesn't get any faster or slower.
changeRate(PRESET_RATES[mRateIndex], PRESET_NAMES[mRateIndex]);
}
}
private void resetPrepareStartPlaying(final int clipPositionInMillis) {
if (mPrepareTask != null) {
mPrepareTask.cancel(false);
}
mPrepareTask = mAsyncTaskExecutor.submit(Tasks.RESET_PREPARE_START_MEDIA_PLAYER,
new AsyncTask