| /* |
| * 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.contacts.voicemail; |
| |
| import static com.android.contacts.CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK; |
| import static com.android.contacts.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)); |
| } |
| } |
| } |
| } |