summaryrefslogtreecommitdiff
path: root/src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java
diff options
context:
space:
mode:
authorChiao Cheng <chiaocheng@google.com>2012-08-17 16:59:12 -0700
committerChiao Cheng <chiaocheng@google.com>2012-08-21 13:31:19 -0700
commit94b10b530c0fc297e2974e57e094c500d3ee6003 (patch)
treeb74d663c2663b5db2f6da888081648ce054480f5 /src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java
parentdab5cd8890c0d0ca9001a13c2197114a4002338a (diff)
Initial move of dialer features from contacts app.
Bug: 6993891 Change-Id: I758ce359ca7e87a1d184303822979318be171921
Diffstat (limited to 'src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java')
-rw-r--r--src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java474
1 files changed, 474 insertions, 0 deletions
diff --git a/src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java b/src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java
new file mode 100644
index 000000000..473d40bc6
--- /dev/null
+++ b/src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java
@@ -0,0 +1,474 @@
+/*
+ * 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.ProximitySensorAware;
+import com.android.contacts.R;
+import com.android.contacts.util.AsyncTaskExecutors;
+import com.android.ex.variablespeed.MediaPlayerProxy;
+import com.android.ex.variablespeed.VariableSpeed;
+import com.google.common.base.Preconditions;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+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 = "VoicemailPlayback";
+ 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 ScheduledExecutorService mScheduledExecutorService;
+ private View mPlaybackLayout;
+
+ @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);
+ mScheduledExecutorService = createScheduledExecutorService();
+ 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);
+ PowerManager.WakeLock wakeLock =
+ powerManager.newWakeLock(
+ PowerManager.SCREEN_DIM_WAKE_LOCK, getClass().getSimpleName());
+ mPresenter = new VoicemailPlaybackPresenter(createPlaybackViewImpl(),
+ createMediaPlayer(mScheduledExecutorService), voicemailUri,
+ mScheduledExecutorService, startPlayback,
+ AsyncTaskExecutors.createAsyncTaskExecutor(), wakeLock);
+ mPresenter.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ mPresenter.onSaveInstanceState(outState);
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onDestroy() {
+ mPresenter.onDestroy();
+ mScheduledExecutorService.shutdown();
+ super.onDestroy();
+ }
+
+ @Override
+ public void onPause() {
+ mPresenter.onPause();
+ super.onPause();
+ }
+
+ private PlaybackViewImpl createPlaybackViewImpl() {
+ return new PlaybackViewImpl(new ActivityReference(), getActivity().getApplicationContext(),
+ mPlaybackLayout);
+ }
+
+ private MediaPlayerProxy createMediaPlayer(ExecutorService executorService) {
+ return VariableSpeed.createVariableSpeed(executorService);
+ }
+
+ private ScheduledExecutorService createScheduledExecutorService() {
+ return Executors.newScheduledThreadPool(NUMBER_OF_THREADS_IN_POOL);
+ }
+
+ /**
+ * 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 static 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 ImageButton mRateDecreaseButton;
+ private final ImageButton mRateIncreaseButton;
+ private final TextViewWithMessagesController mTextController;
+
+ 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);
+ mRateDecreaseButton = (ImageButton) playbackLayout.findViewById(
+ R.id.rate_decrease_button);
+ mRateIncreaseButton = (ImageButton) playbackLayout.findViewById(
+ R.id.rate_increase_button);
+ mTextController = new TextViewWithMessagesController(
+ (TextView) playbackLayout.findViewById(R.id.playback_position_text),
+ (TextView) playbackLayout.findViewById(R.id.playback_speed_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 setRateDecreaseButtonListener(View.OnClickListener listener) {
+ mRateDecreaseButton.setOnClickListener(listener);
+ }
+
+ @Override
+ public void setRateIncreaseButtonListener(View.OnClickListener listener) {
+ mRateIncreaseButton.setOnClickListener(listener);
+ }
+
+ @Override
+ public void setStartStopListener(View.OnClickListener listener) {
+ mStartStopButton.setOnClickListener(listener);
+ }
+
+ @Override
+ public void setSpeakerphoneListener(View.OnClickListener listener) {
+ mPlaybackSpeakerphone.setOnClickListener(listener);
+ }
+
+ @Override
+ public void setRateDisplay(float rate, int stringResourceId) {
+ mTextController.setTemporaryText(
+ mApplicationContext.getString(stringResourceId), 1, TimeUnit.SECONDS);
+ }
+
+ @Override
+ public void setPositionSeekListener(SeekBar.OnSeekBarChangeListener listener) {
+ mPlaybackSeek.setOnSeekBarChangeListener(listener);
+ }
+
+ @Override
+ public void playbackStarted() {
+ mStartStopButton.setImageResource(R.drawable.ic_hold_pause_holo_dark);
+ }
+
+ @Override
+ public void playbackStopped() {
+ mStartStopButton.setImageResource(R.drawable.ic_play);
+ }
+
+ @Override
+ public void enableProximitySensor() {
+ // Only change the state if the activity is still around.
+ Activity activity = mActivityReference.get();
+ if (activity != null && activity instanceof ProximitySensorAware) {
+ ((ProximitySensorAware) activity).enableProximitySensor();
+ }
+ }
+
+ @Override
+ public void disableProximitySensor() {
+ // Only change the state if the activity is still around.
+ Activity activity = mActivityReference.get();
+ if (activity != null && activity instanceof ProximitySensorAware) {
+ ((ProximitySensorAware) activity).disableProximitySensor(true);
+ }
+ }
+
+ @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);
+ mTextController.setPermanentText(
+ formatAsMinutesAndSeconds(seekBarMax - seekBarPosition));
+ }
+
+ private String getString(int resId) {
+ return mApplicationContext.getString(resId);
+ }
+
+ @Override
+ public void setIsBuffering() {
+ disableUiElements();
+ mTextController.setPermanentText(getString(R.string.voicemail_buffering));
+ }
+
+ @Override
+ public void setIsFetchingContent() {
+ disableUiElements();
+ mTextController.setPermanentText(getString(R.string.voicemail_fetching_content));
+ }
+
+ @Override
+ public void setFetchContentTimeout() {
+ disableUiElements();
+ mTextController.setPermanentText(getString(R.string.voicemail_fetching_timout));
+ }
+
+ @Override
+ public int getDesiredClipPosition() {
+ return mPlaybackSeek.getProgress();
+ }
+
+ @Override
+ public void disableUiElements() {
+ mRateIncreaseButton.setEnabled(false);
+ mRateDecreaseButton.setEnabled(false);
+ mStartStopButton.setEnabled(false);
+ mPlaybackSpeakerphone.setEnabled(false);
+ mPlaybackSeek.setProgress(0);
+ mPlaybackSeek.setEnabled(false);
+ }
+
+ @Override
+ public void playbackError(Exception e) {
+ disableUiElements();
+ mTextController.setPermanentText(getString(R.string.voicemail_playback_error));
+ Log.e(TAG, "Could not play voicemail", e);
+ }
+
+ @Override
+ public void enableUiElements() {
+ mRateIncreaseButton.setEnabled(true);
+ mRateDecreaseButton.setEnabled(true);
+ 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);
+ } else {
+ mPlaybackSpeakerphone.setImageResource(R.drawable.ic_speakerphone_off);
+ }
+ }
+
+ @Override
+ public void setVolumeControlStream(int streamType) {
+ Activity activity = mActivityReference.get();
+ if (activity != null) {
+ activity.setVolumeControlStream(streamType);
+ }
+ }
+ }
+
+ /**
+ * Controls a TextView with dynamically changing text.
+ * <p>
+ * There are two methods here of interest,
+ * {@link TextViewWithMessagesController#setPermanentText(String)} and
+ * {@link TextViewWithMessagesController#setTemporaryText(String, long, TimeUnit)}. The
+ * former is used to set the text on the text view immediately, and is used in our case for
+ * the countdown of duration remaining during voicemail playback. The second is used to
+ * temporarily replace this countdown with a message, in our case faster voicemail speed or
+ * slower voicemail speed, before returning to the countdown display.
+ * <p>
+ * All the methods on this class must be called from the ui thread.
+ */
+ private static final class TextViewWithMessagesController {
+ private static final float VISIBLE = 1;
+ private static final float INVISIBLE = 0;
+ private static final long SHORT_ANIMATION_MS = 200;
+ private static final long LONG_ANIMATION_MS = 400;
+ private final Object mLock = new Object();
+ private final TextView mPermanentTextView;
+ private final TextView mTemporaryTextView;
+ @GuardedBy("mLock") private Runnable mRunnable;
+
+ public TextViewWithMessagesController(TextView permanentTextView,
+ TextView temporaryTextView) {
+ mPermanentTextView = permanentTextView;
+ mTemporaryTextView = temporaryTextView;
+ }
+
+ public void setPermanentText(String text) {
+ mPermanentTextView.setText(text);
+ }
+
+ public void setTemporaryText(String text, long duration, TimeUnit units) {
+ synchronized (mLock) {
+ mTemporaryTextView.setText(text);
+ mTemporaryTextView.animate().alpha(VISIBLE).setDuration(SHORT_ANIMATION_MS);
+ mPermanentTextView.animate().alpha(INVISIBLE).setDuration(SHORT_ANIMATION_MS);
+ mRunnable = new Runnable() {
+ @Override
+ public void run() {
+ synchronized (mLock) {
+ // We check for (mRunnable == this) becuase if not true, then another
+ // setTemporaryText call has taken place in the meantime, and this
+ // one is now defunct and needs to take no action.
+ if (mRunnable == this) {
+ mRunnable = null;
+ mTemporaryTextView.animate()
+ .alpha(INVISIBLE).setDuration(LONG_ANIMATION_MS);
+ mPermanentTextView.animate()
+ .alpha(VISIBLE).setDuration(LONG_ANIMATION_MS);
+ }
+ }
+ }
+ };
+ mTemporaryTextView.postDelayed(mRunnable, units.toMillis(duration));
+ }
+ }
+ }
+}