diff options
author | Chiao Cheng <chiaocheng@google.com> | 2012-08-17 16:59:12 -0700 |
---|---|---|
committer | Chiao Cheng <chiaocheng@google.com> | 2012-08-21 13:31:19 -0700 |
commit | 94b10b530c0fc297e2974e57e094c500d3ee6003 (patch) | |
tree | b74d663c2663b5db2f6da888081648ce054480f5 /src/com/android/dialer/voicemail/VoicemailPlaybackFragment.java | |
parent | dab5cd8890c0d0ca9001a13c2197114a4002338a (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.java | 474 |
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)); + } + } + } +} |