summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Lee <anwlee@google.com>2015-05-26 16:14:31 -0700
committerAndrew Lee <anwlee@google.com>2015-05-27 16:38:54 -0700
commit58eaabcc31e23fd4c071ad911b96da6eea4abc28 (patch)
treefcbfef6b072f8442432e39f1995ae18f2f34332e
parenta56893c156b997d796cd8343f99acd5d4198a280 (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
-rw-r--r--Android.mk3
-rw-r--r--res/layout/call_detail.xml6
-rw-r--r--src/com/android/dialer/CallDetailActivity.java74
-rw-r--r--src/com/android/dialer/calllog/CallLogFragment.java2
-rw-r--r--src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java378
-rw-r--r--src/com/android/dialer/voicemail/VoicemailPlaybackLayout.java350
-rw-r--r--src/com/android/dialer/voicemail/VoicemailPlaybackPresenter.java741
-rw-r--r--tests/src/com/android/dialer/CallDetailActivityTest.java6
8 files changed, 682 insertions, 878 deletions
diff --git a/Android.mk b/Android.mk
index 0a93c32b0..1440fcc3f 100644
--- a/Android.mk
+++ b/Android.mk
@@ -32,7 +32,6 @@ LOCAL_AAPT_FLAGS := \
LOCAL_JAVA_LIBRARIES := telephony-common
LOCAL_STATIC_JAVA_LIBRARIES := \
android-common \
- android-ex-variablespeed \
android-support-v13 \
android-support-v4 \
android-support-v7-cardview \
@@ -42,8 +41,6 @@ LOCAL_STATIC_JAVA_LIBRARIES := \
guava \
libphonenumber
-LOCAL_REQUIRED_MODULES := libvariablespeed
-
LOCAL_PACKAGE_NAME := Dialer
LOCAL_CERTIFICATE := shared
LOCAL_PRIVILEGED_MODULE := true
diff --git a/res/layout/call_detail.xml b/res/layout/call_detail.xml
index c07785159..5d1607edf 100644
--- a/res/layout/call_detail.xml
+++ b/res/layout/call_detail.xml
@@ -87,6 +87,12 @@
</LinearLayout>
</LinearLayout>
+ <com.android.dialer.voicemail.VoicemailPlaybackLayout
+ android:id="@+id/voicemail_playback_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone" />
+
<!--
The list view is under everything.
It contains a first header element which is hidden under the controls UI.
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);
- }
}
diff --git a/tests/src/com/android/dialer/CallDetailActivityTest.java b/tests/src/com/android/dialer/CallDetailActivityTest.java
index aca8f2985..4dc9ebb81 100644
--- a/tests/src/com/android/dialer/CallDetailActivityTest.java
+++ b/tests/src/com/android/dialer/CallDetailActivityTest.java
@@ -18,7 +18,6 @@ package com.android.dialer;
import static com.android.dialer.calllog.CallLogAsyncTaskUtil.Tasks.GET_CALL_DETAILS;
import static com.android.dialer.voicemail.VoicemailPlaybackPresenter.Tasks.CHECK_FOR_CONTENT;
-import static com.android.dialer.voicemail.VoicemailPlaybackPresenter.Tasks.PREPARE_MEDIA_PLAYER;
import android.content.ContentResolver;
import android.content.ContentUris;
@@ -118,10 +117,8 @@ public class CallDetailActivityTest extends ActivityInstrumentationTestCase2<Cal
setActivityIntentForTestVoicemailEntry();
startActivityUnderTest();
mFakeAsyncTaskExecutor.runTask(CHECK_FOR_CONTENT);
- // There should be exactly one background task ready to prepare the media player.
- // Preparing the media player will have thrown an IOException since the file doesn't exist.
+ // The media player will have thrown an IOException since the file doesn't exist.
// This should have put a failed to play message on screen, buffering is gone.
- mFakeAsyncTaskExecutor.runTask(PREPARE_MEDIA_PLAYER);
assertHasOneTextViewContaining("Couldn't play voicemail");
assertZeroTextViewsContaining("Buffering");
}
@@ -192,7 +189,6 @@ public class CallDetailActivityTest extends ActivityInstrumentationTestCase2<Cal
setActivityIntentForRealFileVoicemailEntry();
startActivityUnderTest();
mFakeAsyncTaskExecutor.runTask(CHECK_FOR_CONTENT);
- mFakeAsyncTaskExecutor.runTask(PREPARE_MEDIA_PLAYER);
mTestUtils.clickButton(mActivityUnderTest, R.id.playback_speakerphone);
mTestUtils.clickButton(mActivityUnderTest, R.id.playback_start_stop);
Thread.sleep(2000);