Loading src/com/android/dialer/calllog/CallLogAdapter.java +4 −2 Original line number Diff line number Diff line Loading @@ -117,8 +117,10 @@ public class CallLogAdapter extends GroupingListAdapter return; } if (mVoicemailPlaybackPresenter != null) { // Always reset the voicemail playback state on expand or collapse. mVoicemailPlaybackPresenter.reset(); } if (viewHolder.getAdapterPosition() == mCurrentlyExpandedPosition) { // Hide actions, if the clicked item is the expanded item. Loading src/com/android/dialer/calllog/CallLogFragment.java +17 −6 Original line number Diff line number Diff line Loading @@ -187,7 +187,10 @@ public class CallLogFragment extends Fragment resolver.registerContentObserver(Status.CONTENT_URI, true, mVoicemailStatusObserver); setHasOptionsMenu(true); mVoicemailPlaybackPresenter = new VoicemailPlaybackPresenter(activity, state); if (mCallTypeFilter == Calls.VOICEMAIL_TYPE) { mVoicemailPlaybackPresenter = VoicemailPlaybackPresenter .getInstance(activity, state); } } /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */ Loading Loading @@ -321,23 +324,28 @@ public class CallLogFragment extends Fragment @Override public void onPause() { mVoicemailPlaybackPresenter.onPause(getActivity().isFinishing()); if (mVoicemailPlaybackPresenter != null) { mVoicemailPlaybackPresenter.onPause(); } mAdapter.pauseCache(); super.onPause(); } @Override public void onStop() { super.onStop(); updateOnTransition(false /* onEntry */); super.onStop(); } @Override public void onDestroy() { mAdapter.pauseCache(); mAdapter.changeCursor(null); mVoicemailPlaybackPresenter.onDestroy(getActivity().isFinishing()); if (mVoicemailPlaybackPresenter != null) { mVoicemailPlaybackPresenter.onDestroy(); } getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver); getActivity().getContentResolver().unregisterContentObserver(mContactsObserver); Loading @@ -353,8 +361,11 @@ public class CallLogFragment extends Fragment outState.putLong(KEY_DATE_LIMIT, mDateLimit); mAdapter.onSaveInstanceState(outState); if (mVoicemailPlaybackPresenter != null) { mVoicemailPlaybackPresenter.onSaveInstanceState(outState); } } @Override public void fetchCalls() { Loading src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java +8 −7 Original line number Diff line number Diff line Loading @@ -50,8 +50,9 @@ import javax.annotation.concurrent.NotThreadSafe; import javax.annotation.concurrent.ThreadSafe; /** * Displays and plays a single voicemail. * <p> * Displays and plays a single voicemail. See {@link VoicemailPlaybackPresenter} for * details on the voicemail playback implementation. * * 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. */ Loading Loading @@ -178,12 +179,13 @@ public class VoicemailPlaybackLayout extends LinearLayout if (mPresenter == null) { return; } CallLogAsyncTaskUtil.deleteVoicemail(mContext, mPresenter.getVoicemailUri(), null); CallLogAsyncTaskUtil.deleteVoicemail(mContext, mVoicemailUri, null); } }; private Context mContext; private VoicemailPlaybackPresenter mPresenter; private Uri mVoicemailUri; private boolean mIsPlaying = false; Loading @@ -209,8 +211,9 @@ public class VoicemailPlaybackLayout extends LinearLayout } @Override public void setPresenter(VoicemailPlaybackPresenter presenter) { public void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri) { mPresenter = presenter; mVoicemailUri = voicemailUri; } @Override Loading Loading @@ -256,15 +259,13 @@ public class VoicemailPlaybackLayout extends LinearLayout } @Override public void onPlaybackError(Exception e) { public void onPlaybackError() { if (mPositionUpdater != null) { mPositionUpdater.stopUpdating(); } disableUiElements(); mPlaybackPosition.setText(getString(R.string.voicemail_playback_error)); Log.e(TAG, "Could not play voicemail", e); } Loading src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java +169 −115 Original line number Diff line number Diff line Loading @@ -56,10 +56,13 @@ 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 in the call log. It is closely coupled * to assumptions about the behaviors and lifecycle of the call log, in particular in the * {@link CallLogFragment} and {@link CallLogAdapter}. * <p> * This controls a single {@link com.android.dialer.voicemail.VoicemailPlaybackLayout}. A single * instance can be reused for different such layouts, using {@link #setVoicemailPlaybackView}. * instance can be reused for different such layouts, using {@link #setVoicemailPlaybackView}. This * is to facilitate reuse across different voicemail call log entries. * <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. Loading @@ -77,7 +80,7 @@ public class VoicemailPlaybackPresenter int getDesiredClipPosition(); void disableUiElements(); void enableUiElements(); void onPlaybackError(Exception e); void onPlaybackError(); void onPlaybackStarted(int duration, ScheduledExecutorService executorService); void onPlaybackStopped(); void onSpeakerphoneOn(boolean on); Loading @@ -85,7 +88,7 @@ public class VoicemailPlaybackPresenter void setFetchContentTimeout(); void setIsBuffering(); void setIsFetchingContent(); void setPresenter(VoicemailPlaybackPresenter presenter); void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri); } /** The enumeration of {@link AsyncTask} objects we use in this class. */ Loading Loading @@ -121,12 +124,14 @@ public class VoicemailPlaybackPresenter */ private final AtomicInteger mDuration = new AtomicInteger(0); private static VoicemailPlaybackPresenter sInstance; private Activity mActivity; private Context mContext; private PlaybackView mView; private static MediaPlayer mMediaPlayer; private Uri mVoicemailUri; private MediaPlayer mMediaPlayer; private int mPosition; private boolean mIsPlaying; // MediaPlayer crashes on some method calls if not prepared but does not have a method which Loading @@ -134,9 +139,10 @@ public class VoicemailPlaybackPresenter private boolean mIsPrepared; private boolean mShouldResumePlaybackAfterSeeking; private int mInitialOrientation; // Used to run async tasks that need to interact with the UI. private final AsyncTaskExecutor mAsyncTaskExecutor; private AsyncTaskExecutor mAsyncTaskExecutor; private static ScheduledExecutorService mScheduledExecutorService; /** * Used to handle the result of a successful or time-out fetch result. Loading @@ -148,12 +154,49 @@ public class VoicemailPlaybackPresenter private PowerManager.WakeLock mProximityWakeLock; private AudioManager mAudioManager; public VoicemailPlaybackPresenter(Activity activity, Bundle savedInstanceState) { /** * Obtain singleton instance of this class. Use a single instance to provide a consistent * listener to the AudioManager when requesting and abandoning audio focus. * * Otherwise, after rotation the previous listener will still be active but a new listener * will be provided to calls to the AudioManager, which is bad. For example, abandoning * audio focus with the new listeners results in an AUDIO_FOCUS_GAIN callback to the * previous listener, which is the opposite of the intended behavior. */ public static VoicemailPlaybackPresenter getInstance( Activity activity, Bundle savedInstanceState) { if (sInstance == null) { sInstance = new VoicemailPlaybackPresenter(activity); } sInstance.init(activity, savedInstanceState); return sInstance; } /** * Initialize variables which are activity-independent and state-independent. */ private VoicemailPlaybackPresenter(Activity activity) { Context context = activity.getApplicationContext(); mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor(); mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); if (powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { mProximityWakeLock = powerManager.newWakeLock( PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG); } } /** * Update variables which are activity-dependent or state-dependent. */ private void init(Activity activity, Bundle savedInstanceState) { mActivity = activity; mContext = activity; mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor(); mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); mInitialOrientation = mContext.getResources().getConfiguration().orientation; mActivity.setVolumeControlStream(VoicemailPlaybackPresenter.PLAYBACK_STREAM); if (savedInstanceState != null) { Loading @@ -163,82 +206,96 @@ public class VoicemailPlaybackPresenter mPosition = savedInstanceState.getInt(CLIP_POSITION_KEY, 0); mIsPlaying = savedInstanceState.getBoolean(IS_PLAYING_STATE_KEY, false); } 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); } // mMediaPlayer is static to enable seamless playback during rotation. If we do not create // a new MediaPlayer, we still need to update listeners to the current Presenter instance. if (mMediaPlayer == null) { mMediaPlayer = new MediaPlayer(); mIsPrepared = false; } mMediaPlayer.setOnPreparedListener(this); mMediaPlayer.setOnErrorListener(this); mMediaPlayer.setOnCompletionListener(this); /** * Must be invoked when the parent Activity is saving it state. */ public void onSaveInstanceState(Bundle outState) { if (mView != null) { outState.putParcelable(VOICEMAIL_URI_KEY, mVoicemailUri); outState.putBoolean(IS_PREPARED_KEY, mIsPrepared); outState.putInt(CLIP_POSITION_KEY, mView.getDesiredClipPosition()); outState.putBoolean(IS_PLAYING_STATE_KEY, mIsPlaying); } public void reset() { pausePlayback(); mView = null; mVoicemailUri = null; mIsPrepared = false; mIsPlaying = false; mPosition = 0; mDuration.set(0); } /** * Specify the view which this presenter controls and the voicemail for playback. * Specify the view which this presenter controls and the voicemail to prepare to play. */ public void setPlaybackView( PlaybackView view, Uri voicemailUri, boolean startPlayingImmediately) { mView = view; mView.setPresenter(this); mView.setPresenter(this, voicemailUri); if (mVoicemailUri != null && mVoicemailUri.equals(voicemailUri)) { // Handles rotation case where playback view is set for the same voicemail. if (mIsPrepared) { if (mMediaPlayer != null && voicemailUri.equals(mVoicemailUri)) { // Handles case where MediaPlayer was retained after an orientation change. onPrepared(mMediaPlayer); } else { checkForContent(); } mView.onSpeakerphoneOn(isSpeakerphoneOn()); } else { mVoicemailUri = voicemailUri; if (!voicemailUri.equals(mVoicemailUri)) { mPosition = 0; } mVoicemailUri = voicemailUri; mDuration.set(0); mIsPlaying = startPlayingImmediately; checkForContent(); // Default to earpiece. mView.onSpeakerphoneOn(false); checkForContent(); } } public void onPause(boolean isFinishing) { // Do not pause for orientation changes. if (mIsPrepared && mMediaPlayer.isPlaying() && isFinishing) { pausePlayback(); /** * Reset the presenter for playback. */ public void reset() { if (mMediaPlayer != null) { mMediaPlayer.release(); mMediaPlayer = null; } disableProximitySensor(false /* waitForFarState */); mView = null; mVoicemailUri = null; mIsPrepared = false; mIsPlaying = false; mPosition = 0; mDuration.set(0); } /** * Must be invoked when the parent activity is paused. */ public void onPause() { int orientation = mContext.getResources().getConfiguration().orientation; if (mInitialOrientation != orientation && mIsPrepared) { // If an orientation change triggers the pause, retain the MediaPlayer. Log.d(TAG, "onPause: Orientation changed."); return; } public void onDestroy(boolean isFinishing) { // Do not release for orientation changes. if (mIsPrepared && isFinishing) { // Release the media player, otherwise there may be failures. if (mMediaPlayer != null) { mMediaPlayer.release(); mIsPrepared = false; mMediaPlayer = null; } disableProximitySensor(false /* waitForFarState */); } /** * Must be invoked when the parent activity is destroyed. */ public void onDestroy() { // Clear references to avoid leaks from the singleton instance. mActivity = null; mContext = null; if (mScheduledExecutorService != null) { mScheduledExecutorService.shutdown(); mScheduledExecutorService = null; Loading @@ -248,17 +305,6 @@ public class VoicemailPlaybackPresenter mFetchResultHandler.destroy(); mFetchResultHandler = null; } disableProximitySensor(false /* waitForFarState */); } public void onSaveInstanceState(Bundle outState) { if (mView != null) { outState.putParcelable(VOICEMAIL_URI_KEY, mVoicemailUri); outState.putBoolean(IS_PREPARED_KEY, mIsPrepared); outState.putInt(CLIP_POSITION_KEY, mView.getDesiredClipPosition()); outState.putBoolean(IS_PLAYING_STATE_KEY, mIsPlaying); } } /** Loading @@ -268,7 +314,7 @@ public class VoicemailPlaybackPresenter * voicemail we've been asked to play has any content available. * <p> * 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 * the DB is set. If set, we proceed to {@link #prepareContent()} method. If not set, make * a request to fetch the content asynchronously via {@link #requestContent()}. */ private void checkForContent() { Loading @@ -282,7 +328,7 @@ public class VoicemailPlaybackPresenter @Override public void onPostExecute(Boolean hasContent) { if (hasContent) { prepareToPlayContent(); prepareContent(); } else { requestContent(); } Loading Loading @@ -314,7 +360,7 @@ public class VoicemailPlaybackPresenter * 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 #prepareToPlayContent()}. If the has_content field does not * proceed to {@link #prepareContent()}. 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. */ Loading @@ -323,8 +369,7 @@ public class VoicemailPlaybackPresenter mFetchResultHandler.destroy(); } mFetchResultHandler = new FetchResultHandler(new Handler()); mFetchResultHandler.registerContentObserver(mVoicemailUri); mFetchResultHandler = new FetchResultHandler(new Handler(), mVoicemailUri); // Send voicemail fetch request. Intent intent = new Intent(VoicemailContract.ACTION_FETCH_VOICEMAIL, mVoicemailUri); Loading @@ -336,25 +381,21 @@ public class VoicemailPlaybackPresenter private AtomicBoolean mIsWaitingForResult = new AtomicBoolean(true); private final Handler mFetchResultHandler; public FetchResultHandler(Handler handler) { public FetchResultHandler(Handler handler, Uri voicemailUri) { super(handler); mFetchResultHandler = handler; } public void registerContentObserver(Uri voicemailUri) { if (mIsWaitingForResult.get()) { mContext.getContentResolver().registerContentObserver( voicemailUri, false, this); mFetchResultHandler.postDelayed(this, FETCH_CONTENT_TIMEOUT_MS); } } /** * Stop waiting for content and notify UI if {@link FETCH_CONTENT_TIMEOUT_MS} has elapsed. */ @Override public void run() { if (mIsWaitingForResult.getAndSet(false)) { if (mIsWaitingForResult.getAndSet(false) && mContext != null) { mContext.getContentResolver().unregisterContentObserver(this); if (mView != null) { mView.setFetchContentTimeout(); Loading @@ -363,7 +404,7 @@ public class VoicemailPlaybackPresenter } public void destroy() { if (mIsWaitingForResult.getAndSet(false)) { if (mIsWaitingForResult.getAndSet(false) && mContext != null) { mContext.getContentResolver().unregisterContentObserver(this); mFetchResultHandler.removeCallbacks(this); } Loading @@ -380,12 +421,10 @@ public class VoicemailPlaybackPresenter @Override public void onPostExecute(Boolean hasContent) { if (hasContent) { if (mIsWaitingForResult.getAndSet(false)) { if (hasContent && mContext != null && mIsWaitingForResult.getAndSet(false)) { mContext.getContentResolver().unregisterContentObserver( FetchResultHandler.this); prepareToPlayContent(); } prepareContent(); } } }); Loading @@ -400,15 +439,27 @@ public class VoicemailPlaybackPresenter * media player. If preparation is successful, the media player will {@link #onPrepared()}, * and it will call {@link #onError()} otherwise. */ private void prepareToPlayContent() { private void prepareContent() { if (mView == null) { return; } mIsPrepared = false; Log.d(TAG, "prepareContent"); // Release the previous media player, otherwise there may be failures. if (mMediaPlayer != null) { mMediaPlayer.release(); mMediaPlayer = null; } mView.setIsBuffering(); mIsPrepared = false; try { mMediaPlayer = new MediaPlayer(); mMediaPlayer.setOnPreparedListener(this); mMediaPlayer.setOnErrorListener(this); mMediaPlayer.setOnCompletionListener(this); mMediaPlayer.reset(); mMediaPlayer.setDataSource(mContext, mVoicemailUri); mMediaPlayer.setAudioStreamType(PLAYBACK_STREAM); Loading @@ -426,12 +477,15 @@ public class VoicemailPlaybackPresenter if (mView == null) { return; } Log.d(TAG, "onPrepared"); mIsPrepared = true; mDuration.set(mMediaPlayer.getDuration()); mView.enableUiElements(); Log.d(TAG, "onPrepared: mPosition=" + mPosition); mView.setClipPosition(mPosition, mDuration.get()); mMediaPlayer.seekTo(mPosition); if (mIsPlaying) { resumePlayback(); Loading @@ -451,12 +505,15 @@ public class VoicemailPlaybackPresenter } private void handleError(Exception e) { Log.d(TAG, "handleError: Could not play voicemail " + e); if (mIsPrepared) { mMediaPlayer.release(); mMediaPlayer = null; mIsPrepared = false; } mView.onPlaybackError(e); mView.onPlaybackError(); mPosition = 0; mIsPlaying = false; Loading @@ -476,15 +533,12 @@ public class VoicemailPlaybackPresenter @Override public void onAudioFocusChange(int focusChange) { if (!mIsPrepared) { return; } boolean lostFocus = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || focusChange == AudioManager.AUDIOFOCUS_LOSS; if (mMediaPlayer.isPlaying() && lostFocus) { Log.d(TAG, "onAudioFocusChange: focusChange=" + focusChange); boolean lostFocus = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || focusChange == AudioManager.AUDIOFOCUS_LOSS; if (mIsPlaying && focusChange == AudioManager.AUDIOFOCUS_LOSS) { pausePlayback(); } else if (!mMediaPlayer.isPlaying() && focusChange == AudioManager.AUDIOFOCUS_GAIN) { } else if (!mIsPlaying && focusChange == AudioManager.AUDIOFOCUS_GAIN) { resumePlayback(); } } Loading @@ -506,25 +560,25 @@ public class VoicemailPlaybackPresenter mMediaPlayer.seekTo(mPosition); try { // Grab audio focus here // Grab audio focus. int result = mAudioManager.requestAudioFocus( VoicemailPlaybackPresenter.this, this, PLAYBACK_STREAM, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { throw new RejectedExecutionException("Could not capture audio focus."); } // Can throw RejectedExecutionException // Can throw RejectedExecutionException. mMediaPlayer.start(); } catch (RejectedExecutionException e) { handleError(e); } } enableProximitySensor(); Log.d(TAG, "Resumed playback at " + mPosition + "."); mView.onPlaybackStarted(mDuration.get(), getScheduledExecutorServiceInstance()); enableProximitySensor(); } /** Loading @@ -535,17 +589,17 @@ public class VoicemailPlaybackPresenter return; } mPosition = mMediaPlayer.getCurrentPosition(); mIsPlaying = false; if (mMediaPlayer.isPlaying()) { mMediaPlayer.pause(); } mAudioManager.abandonAudioFocus(this); mView.onPlaybackStopped(); mPosition = mMediaPlayer.getCurrentPosition(); Log.d(TAG, "Paused playback at " + mPosition + "."); // Always disable the proximity sensor on stop. mView.onPlaybackStopped(); mAudioManager.abandonAudioFocus(this); disableProximitySensor(true /* waitForFarState */); } Loading @@ -567,6 +621,9 @@ public class VoicemailPlaybackPresenter } private void enableProximitySensor() { // Disable until proximity sensor behavior in onPause is fixed: b/21932251. /* if (mProximityWakeLock == null || isSpeakerphoneOn() || !mIsPrepared || !mMediaPlayer.isPlaying()) { return; Loading @@ -578,6 +635,7 @@ public class VoicemailPlaybackPresenter } else { Log.i(TAG, "Proximity wake lock already acquired"); } */ } private void disableProximitySensor(boolean waitForFarState) { Loading Loading @@ -606,12 +664,8 @@ public class VoicemailPlaybackPresenter return mAudioManager.isSpeakerphoneOn(); } public Uri getVoicemailUri() { return mVoicemailUri; } public int getMediaPlayerPosition() { return mIsPrepared ? mMediaPlayer.getCurrentPosition() : 0; return mIsPrepared && mMediaPlayer != null ? mMediaPlayer.getCurrentPosition() : 0; } private static synchronized ScheduledExecutorService getScheduledExecutorServiceInstance() { Loading tests/src/com/android/dialer/voicemail/VoicemailPlaybackTest.java +1 −1 Original line number Diff line number Diff line Loading @@ -85,7 +85,7 @@ public class VoicemailPlaybackTest extends ActivityInstrumentationTestCase2<Call mLayout = new VoicemailPlaybackLayout(mActivity); mLayout.onFinishInflate(); mPresenter = new VoicemailPlaybackPresenter(mActivity, null); mPresenter = VoicemailPlaybackPresenter.getInstance(mActivity, null); } @Override Loading Loading
src/com/android/dialer/calllog/CallLogAdapter.java +4 −2 Original line number Diff line number Diff line Loading @@ -117,8 +117,10 @@ public class CallLogAdapter extends GroupingListAdapter return; } if (mVoicemailPlaybackPresenter != null) { // Always reset the voicemail playback state on expand or collapse. mVoicemailPlaybackPresenter.reset(); } if (viewHolder.getAdapterPosition() == mCurrentlyExpandedPosition) { // Hide actions, if the clicked item is the expanded item. Loading
src/com/android/dialer/calllog/CallLogFragment.java +17 −6 Original line number Diff line number Diff line Loading @@ -187,7 +187,10 @@ public class CallLogFragment extends Fragment resolver.registerContentObserver(Status.CONTENT_URI, true, mVoicemailStatusObserver); setHasOptionsMenu(true); mVoicemailPlaybackPresenter = new VoicemailPlaybackPresenter(activity, state); if (mCallTypeFilter == Calls.VOICEMAIL_TYPE) { mVoicemailPlaybackPresenter = VoicemailPlaybackPresenter .getInstance(activity, state); } } /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */ Loading Loading @@ -321,23 +324,28 @@ public class CallLogFragment extends Fragment @Override public void onPause() { mVoicemailPlaybackPresenter.onPause(getActivity().isFinishing()); if (mVoicemailPlaybackPresenter != null) { mVoicemailPlaybackPresenter.onPause(); } mAdapter.pauseCache(); super.onPause(); } @Override public void onStop() { super.onStop(); updateOnTransition(false /* onEntry */); super.onStop(); } @Override public void onDestroy() { mAdapter.pauseCache(); mAdapter.changeCursor(null); mVoicemailPlaybackPresenter.onDestroy(getActivity().isFinishing()); if (mVoicemailPlaybackPresenter != null) { mVoicemailPlaybackPresenter.onDestroy(); } getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver); getActivity().getContentResolver().unregisterContentObserver(mContactsObserver); Loading @@ -353,8 +361,11 @@ public class CallLogFragment extends Fragment outState.putLong(KEY_DATE_LIMIT, mDateLimit); mAdapter.onSaveInstanceState(outState); if (mVoicemailPlaybackPresenter != null) { mVoicemailPlaybackPresenter.onSaveInstanceState(outState); } } @Override public void fetchCalls() { Loading
src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java +8 −7 Original line number Diff line number Diff line Loading @@ -50,8 +50,9 @@ import javax.annotation.concurrent.NotThreadSafe; import javax.annotation.concurrent.ThreadSafe; /** * Displays and plays a single voicemail. * <p> * Displays and plays a single voicemail. See {@link VoicemailPlaybackPresenter} for * details on the voicemail playback implementation. * * 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. */ Loading Loading @@ -178,12 +179,13 @@ public class VoicemailPlaybackLayout extends LinearLayout if (mPresenter == null) { return; } CallLogAsyncTaskUtil.deleteVoicemail(mContext, mPresenter.getVoicemailUri(), null); CallLogAsyncTaskUtil.deleteVoicemail(mContext, mVoicemailUri, null); } }; private Context mContext; private VoicemailPlaybackPresenter mPresenter; private Uri mVoicemailUri; private boolean mIsPlaying = false; Loading @@ -209,8 +211,9 @@ public class VoicemailPlaybackLayout extends LinearLayout } @Override public void setPresenter(VoicemailPlaybackPresenter presenter) { public void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri) { mPresenter = presenter; mVoicemailUri = voicemailUri; } @Override Loading Loading @@ -256,15 +259,13 @@ public class VoicemailPlaybackLayout extends LinearLayout } @Override public void onPlaybackError(Exception e) { public void onPlaybackError() { if (mPositionUpdater != null) { mPositionUpdater.stopUpdating(); } disableUiElements(); mPlaybackPosition.setText(getString(R.string.voicemail_playback_error)); Log.e(TAG, "Could not play voicemail", e); } Loading
src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java +169 −115 Original line number Diff line number Diff line Loading @@ -56,10 +56,13 @@ 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 in the call log. It is closely coupled * to assumptions about the behaviors and lifecycle of the call log, in particular in the * {@link CallLogFragment} and {@link CallLogAdapter}. * <p> * This controls a single {@link com.android.dialer.voicemail.VoicemailPlaybackLayout}. A single * instance can be reused for different such layouts, using {@link #setVoicemailPlaybackView}. * instance can be reused for different such layouts, using {@link #setVoicemailPlaybackView}. This * is to facilitate reuse across different voicemail call log entries. * <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. Loading @@ -77,7 +80,7 @@ public class VoicemailPlaybackPresenter int getDesiredClipPosition(); void disableUiElements(); void enableUiElements(); void onPlaybackError(Exception e); void onPlaybackError(); void onPlaybackStarted(int duration, ScheduledExecutorService executorService); void onPlaybackStopped(); void onSpeakerphoneOn(boolean on); Loading @@ -85,7 +88,7 @@ public class VoicemailPlaybackPresenter void setFetchContentTimeout(); void setIsBuffering(); void setIsFetchingContent(); void setPresenter(VoicemailPlaybackPresenter presenter); void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri); } /** The enumeration of {@link AsyncTask} objects we use in this class. */ Loading Loading @@ -121,12 +124,14 @@ public class VoicemailPlaybackPresenter */ private final AtomicInteger mDuration = new AtomicInteger(0); private static VoicemailPlaybackPresenter sInstance; private Activity mActivity; private Context mContext; private PlaybackView mView; private static MediaPlayer mMediaPlayer; private Uri mVoicemailUri; private MediaPlayer mMediaPlayer; private int mPosition; private boolean mIsPlaying; // MediaPlayer crashes on some method calls if not prepared but does not have a method which Loading @@ -134,9 +139,10 @@ public class VoicemailPlaybackPresenter private boolean mIsPrepared; private boolean mShouldResumePlaybackAfterSeeking; private int mInitialOrientation; // Used to run async tasks that need to interact with the UI. private final AsyncTaskExecutor mAsyncTaskExecutor; private AsyncTaskExecutor mAsyncTaskExecutor; private static ScheduledExecutorService mScheduledExecutorService; /** * Used to handle the result of a successful or time-out fetch result. Loading @@ -148,12 +154,49 @@ public class VoicemailPlaybackPresenter private PowerManager.WakeLock mProximityWakeLock; private AudioManager mAudioManager; public VoicemailPlaybackPresenter(Activity activity, Bundle savedInstanceState) { /** * Obtain singleton instance of this class. Use a single instance to provide a consistent * listener to the AudioManager when requesting and abandoning audio focus. * * Otherwise, after rotation the previous listener will still be active but a new listener * will be provided to calls to the AudioManager, which is bad. For example, abandoning * audio focus with the new listeners results in an AUDIO_FOCUS_GAIN callback to the * previous listener, which is the opposite of the intended behavior. */ public static VoicemailPlaybackPresenter getInstance( Activity activity, Bundle savedInstanceState) { if (sInstance == null) { sInstance = new VoicemailPlaybackPresenter(activity); } sInstance.init(activity, savedInstanceState); return sInstance; } /** * Initialize variables which are activity-independent and state-independent. */ private VoicemailPlaybackPresenter(Activity activity) { Context context = activity.getApplicationContext(); mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor(); mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); if (powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { mProximityWakeLock = powerManager.newWakeLock( PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG); } } /** * Update variables which are activity-dependent or state-dependent. */ private void init(Activity activity, Bundle savedInstanceState) { mActivity = activity; mContext = activity; mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor(); mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); mInitialOrientation = mContext.getResources().getConfiguration().orientation; mActivity.setVolumeControlStream(VoicemailPlaybackPresenter.PLAYBACK_STREAM); if (savedInstanceState != null) { Loading @@ -163,82 +206,96 @@ public class VoicemailPlaybackPresenter mPosition = savedInstanceState.getInt(CLIP_POSITION_KEY, 0); mIsPlaying = savedInstanceState.getBoolean(IS_PLAYING_STATE_KEY, false); } 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); } // mMediaPlayer is static to enable seamless playback during rotation. If we do not create // a new MediaPlayer, we still need to update listeners to the current Presenter instance. if (mMediaPlayer == null) { mMediaPlayer = new MediaPlayer(); mIsPrepared = false; } mMediaPlayer.setOnPreparedListener(this); mMediaPlayer.setOnErrorListener(this); mMediaPlayer.setOnCompletionListener(this); /** * Must be invoked when the parent Activity is saving it state. */ public void onSaveInstanceState(Bundle outState) { if (mView != null) { outState.putParcelable(VOICEMAIL_URI_KEY, mVoicemailUri); outState.putBoolean(IS_PREPARED_KEY, mIsPrepared); outState.putInt(CLIP_POSITION_KEY, mView.getDesiredClipPosition()); outState.putBoolean(IS_PLAYING_STATE_KEY, mIsPlaying); } public void reset() { pausePlayback(); mView = null; mVoicemailUri = null; mIsPrepared = false; mIsPlaying = false; mPosition = 0; mDuration.set(0); } /** * Specify the view which this presenter controls and the voicemail for playback. * Specify the view which this presenter controls and the voicemail to prepare to play. */ public void setPlaybackView( PlaybackView view, Uri voicemailUri, boolean startPlayingImmediately) { mView = view; mView.setPresenter(this); mView.setPresenter(this, voicemailUri); if (mVoicemailUri != null && mVoicemailUri.equals(voicemailUri)) { // Handles rotation case where playback view is set for the same voicemail. if (mIsPrepared) { if (mMediaPlayer != null && voicemailUri.equals(mVoicemailUri)) { // Handles case where MediaPlayer was retained after an orientation change. onPrepared(mMediaPlayer); } else { checkForContent(); } mView.onSpeakerphoneOn(isSpeakerphoneOn()); } else { mVoicemailUri = voicemailUri; if (!voicemailUri.equals(mVoicemailUri)) { mPosition = 0; } mVoicemailUri = voicemailUri; mDuration.set(0); mIsPlaying = startPlayingImmediately; checkForContent(); // Default to earpiece. mView.onSpeakerphoneOn(false); checkForContent(); } } public void onPause(boolean isFinishing) { // Do not pause for orientation changes. if (mIsPrepared && mMediaPlayer.isPlaying() && isFinishing) { pausePlayback(); /** * Reset the presenter for playback. */ public void reset() { if (mMediaPlayer != null) { mMediaPlayer.release(); mMediaPlayer = null; } disableProximitySensor(false /* waitForFarState */); mView = null; mVoicemailUri = null; mIsPrepared = false; mIsPlaying = false; mPosition = 0; mDuration.set(0); } /** * Must be invoked when the parent activity is paused. */ public void onPause() { int orientation = mContext.getResources().getConfiguration().orientation; if (mInitialOrientation != orientation && mIsPrepared) { // If an orientation change triggers the pause, retain the MediaPlayer. Log.d(TAG, "onPause: Orientation changed."); return; } public void onDestroy(boolean isFinishing) { // Do not release for orientation changes. if (mIsPrepared && isFinishing) { // Release the media player, otherwise there may be failures. if (mMediaPlayer != null) { mMediaPlayer.release(); mIsPrepared = false; mMediaPlayer = null; } disableProximitySensor(false /* waitForFarState */); } /** * Must be invoked when the parent activity is destroyed. */ public void onDestroy() { // Clear references to avoid leaks from the singleton instance. mActivity = null; mContext = null; if (mScheduledExecutorService != null) { mScheduledExecutorService.shutdown(); mScheduledExecutorService = null; Loading @@ -248,17 +305,6 @@ public class VoicemailPlaybackPresenter mFetchResultHandler.destroy(); mFetchResultHandler = null; } disableProximitySensor(false /* waitForFarState */); } public void onSaveInstanceState(Bundle outState) { if (mView != null) { outState.putParcelable(VOICEMAIL_URI_KEY, mVoicemailUri); outState.putBoolean(IS_PREPARED_KEY, mIsPrepared); outState.putInt(CLIP_POSITION_KEY, mView.getDesiredClipPosition()); outState.putBoolean(IS_PLAYING_STATE_KEY, mIsPlaying); } } /** Loading @@ -268,7 +314,7 @@ public class VoicemailPlaybackPresenter * voicemail we've been asked to play has any content available. * <p> * 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 * the DB is set. If set, we proceed to {@link #prepareContent()} method. If not set, make * a request to fetch the content asynchronously via {@link #requestContent()}. */ private void checkForContent() { Loading @@ -282,7 +328,7 @@ public class VoicemailPlaybackPresenter @Override public void onPostExecute(Boolean hasContent) { if (hasContent) { prepareToPlayContent(); prepareContent(); } else { requestContent(); } Loading Loading @@ -314,7 +360,7 @@ public class VoicemailPlaybackPresenter * 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 #prepareToPlayContent()}. If the has_content field does not * proceed to {@link #prepareContent()}. 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. */ Loading @@ -323,8 +369,7 @@ public class VoicemailPlaybackPresenter mFetchResultHandler.destroy(); } mFetchResultHandler = new FetchResultHandler(new Handler()); mFetchResultHandler.registerContentObserver(mVoicemailUri); mFetchResultHandler = new FetchResultHandler(new Handler(), mVoicemailUri); // Send voicemail fetch request. Intent intent = new Intent(VoicemailContract.ACTION_FETCH_VOICEMAIL, mVoicemailUri); Loading @@ -336,25 +381,21 @@ public class VoicemailPlaybackPresenter private AtomicBoolean mIsWaitingForResult = new AtomicBoolean(true); private final Handler mFetchResultHandler; public FetchResultHandler(Handler handler) { public FetchResultHandler(Handler handler, Uri voicemailUri) { super(handler); mFetchResultHandler = handler; } public void registerContentObserver(Uri voicemailUri) { if (mIsWaitingForResult.get()) { mContext.getContentResolver().registerContentObserver( voicemailUri, false, this); mFetchResultHandler.postDelayed(this, FETCH_CONTENT_TIMEOUT_MS); } } /** * Stop waiting for content and notify UI if {@link FETCH_CONTENT_TIMEOUT_MS} has elapsed. */ @Override public void run() { if (mIsWaitingForResult.getAndSet(false)) { if (mIsWaitingForResult.getAndSet(false) && mContext != null) { mContext.getContentResolver().unregisterContentObserver(this); if (mView != null) { mView.setFetchContentTimeout(); Loading @@ -363,7 +404,7 @@ public class VoicemailPlaybackPresenter } public void destroy() { if (mIsWaitingForResult.getAndSet(false)) { if (mIsWaitingForResult.getAndSet(false) && mContext != null) { mContext.getContentResolver().unregisterContentObserver(this); mFetchResultHandler.removeCallbacks(this); } Loading @@ -380,12 +421,10 @@ public class VoicemailPlaybackPresenter @Override public void onPostExecute(Boolean hasContent) { if (hasContent) { if (mIsWaitingForResult.getAndSet(false)) { if (hasContent && mContext != null && mIsWaitingForResult.getAndSet(false)) { mContext.getContentResolver().unregisterContentObserver( FetchResultHandler.this); prepareToPlayContent(); } prepareContent(); } } }); Loading @@ -400,15 +439,27 @@ public class VoicemailPlaybackPresenter * media player. If preparation is successful, the media player will {@link #onPrepared()}, * and it will call {@link #onError()} otherwise. */ private void prepareToPlayContent() { private void prepareContent() { if (mView == null) { return; } mIsPrepared = false; Log.d(TAG, "prepareContent"); // Release the previous media player, otherwise there may be failures. if (mMediaPlayer != null) { mMediaPlayer.release(); mMediaPlayer = null; } mView.setIsBuffering(); mIsPrepared = false; try { mMediaPlayer = new MediaPlayer(); mMediaPlayer.setOnPreparedListener(this); mMediaPlayer.setOnErrorListener(this); mMediaPlayer.setOnCompletionListener(this); mMediaPlayer.reset(); mMediaPlayer.setDataSource(mContext, mVoicemailUri); mMediaPlayer.setAudioStreamType(PLAYBACK_STREAM); Loading @@ -426,12 +477,15 @@ public class VoicemailPlaybackPresenter if (mView == null) { return; } Log.d(TAG, "onPrepared"); mIsPrepared = true; mDuration.set(mMediaPlayer.getDuration()); mView.enableUiElements(); Log.d(TAG, "onPrepared: mPosition=" + mPosition); mView.setClipPosition(mPosition, mDuration.get()); mMediaPlayer.seekTo(mPosition); if (mIsPlaying) { resumePlayback(); Loading @@ -451,12 +505,15 @@ public class VoicemailPlaybackPresenter } private void handleError(Exception e) { Log.d(TAG, "handleError: Could not play voicemail " + e); if (mIsPrepared) { mMediaPlayer.release(); mMediaPlayer = null; mIsPrepared = false; } mView.onPlaybackError(e); mView.onPlaybackError(); mPosition = 0; mIsPlaying = false; Loading @@ -476,15 +533,12 @@ public class VoicemailPlaybackPresenter @Override public void onAudioFocusChange(int focusChange) { if (!mIsPrepared) { return; } boolean lostFocus = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || focusChange == AudioManager.AUDIOFOCUS_LOSS; if (mMediaPlayer.isPlaying() && lostFocus) { Log.d(TAG, "onAudioFocusChange: focusChange=" + focusChange); boolean lostFocus = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || focusChange == AudioManager.AUDIOFOCUS_LOSS; if (mIsPlaying && focusChange == AudioManager.AUDIOFOCUS_LOSS) { pausePlayback(); } else if (!mMediaPlayer.isPlaying() && focusChange == AudioManager.AUDIOFOCUS_GAIN) { } else if (!mIsPlaying && focusChange == AudioManager.AUDIOFOCUS_GAIN) { resumePlayback(); } } Loading @@ -506,25 +560,25 @@ public class VoicemailPlaybackPresenter mMediaPlayer.seekTo(mPosition); try { // Grab audio focus here // Grab audio focus. int result = mAudioManager.requestAudioFocus( VoicemailPlaybackPresenter.this, this, PLAYBACK_STREAM, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { throw new RejectedExecutionException("Could not capture audio focus."); } // Can throw RejectedExecutionException // Can throw RejectedExecutionException. mMediaPlayer.start(); } catch (RejectedExecutionException e) { handleError(e); } } enableProximitySensor(); Log.d(TAG, "Resumed playback at " + mPosition + "."); mView.onPlaybackStarted(mDuration.get(), getScheduledExecutorServiceInstance()); enableProximitySensor(); } /** Loading @@ -535,17 +589,17 @@ public class VoicemailPlaybackPresenter return; } mPosition = mMediaPlayer.getCurrentPosition(); mIsPlaying = false; if (mMediaPlayer.isPlaying()) { mMediaPlayer.pause(); } mAudioManager.abandonAudioFocus(this); mView.onPlaybackStopped(); mPosition = mMediaPlayer.getCurrentPosition(); Log.d(TAG, "Paused playback at " + mPosition + "."); // Always disable the proximity sensor on stop. mView.onPlaybackStopped(); mAudioManager.abandonAudioFocus(this); disableProximitySensor(true /* waitForFarState */); } Loading @@ -567,6 +621,9 @@ public class VoicemailPlaybackPresenter } private void enableProximitySensor() { // Disable until proximity sensor behavior in onPause is fixed: b/21932251. /* if (mProximityWakeLock == null || isSpeakerphoneOn() || !mIsPrepared || !mMediaPlayer.isPlaying()) { return; Loading @@ -578,6 +635,7 @@ public class VoicemailPlaybackPresenter } else { Log.i(TAG, "Proximity wake lock already acquired"); } */ } private void disableProximitySensor(boolean waitForFarState) { Loading Loading @@ -606,12 +664,8 @@ public class VoicemailPlaybackPresenter return mAudioManager.isSpeakerphoneOn(); } public Uri getVoicemailUri() { return mVoicemailUri; } public int getMediaPlayerPosition() { return mIsPrepared ? mMediaPlayer.getCurrentPosition() : 0; return mIsPrepared && mMediaPlayer != null ? mMediaPlayer.getCurrentPosition() : 0; } private static synchronized ScheduledExecutorService getScheduledExecutorServiceInstance() { Loading
tests/src/com/android/dialer/voicemail/VoicemailPlaybackTest.java +1 −1 Original line number Diff line number Diff line Loading @@ -85,7 +85,7 @@ public class VoicemailPlaybackTest extends ActivityInstrumentationTestCase2<Call mLayout = new VoicemailPlaybackLayout(mActivity); mLayout.onFinishInflate(); mPresenter = new VoicemailPlaybackPresenter(mActivity, null); mPresenter = VoicemailPlaybackPresenter.getInstance(mActivity, null); } @Override Loading