Home | History | Annotate | Download | only in voicemail
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.contacts.voicemail;
     18 
     19 import static com.android.contacts.CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK;
     20 import static com.android.contacts.CallDetailActivity.EXTRA_VOICEMAIL_URI;
     21 
     22 import com.android.common.io.MoreCloseables;
     23 import com.android.contacts.ProximitySensorAware;
     24 import com.android.contacts.R;
     25 import com.android.contacts.util.AsyncTaskExecutors;
     26 import com.android.ex.variablespeed.MediaPlayerProxy;
     27 import com.android.ex.variablespeed.VariableSpeed;
     28 import com.google.common.base.Preconditions;
     29 
     30 import android.app.Activity;
     31 import android.app.Fragment;
     32 import android.content.ContentResolver;
     33 import android.content.Context;
     34 import android.content.Intent;
     35 import android.database.ContentObserver;
     36 import android.database.Cursor;
     37 import android.media.AudioManager;
     38 import android.net.Uri;
     39 import android.os.Bundle;
     40 import android.os.PowerManager;
     41 import android.provider.VoicemailContract;
     42 import android.util.Log;
     43 import android.view.LayoutInflater;
     44 import android.view.View;
     45 import android.view.ViewGroup;
     46 import android.widget.ImageButton;
     47 import android.widget.SeekBar;
     48 import android.widget.TextView;
     49 
     50 import java.util.concurrent.ExecutorService;
     51 import java.util.concurrent.Executors;
     52 import java.util.concurrent.ScheduledExecutorService;
     53 import java.util.concurrent.TimeUnit;
     54 
     55 import javax.annotation.concurrent.GuardedBy;
     56 import javax.annotation.concurrent.NotThreadSafe;
     57 
     58 /**
     59  * Displays and plays back a single voicemail.
     60  * <p>
     61  * When the Activity containing this Fragment is created, voicemail playback
     62  * will begin immediately. The Activity is expected to be started via an intent
     63  * containing a suitable voicemail uri to playback.
     64  * <p>
     65  * This class is not thread-safe, it is thread-confined. All calls to all public
     66  * methods on this class are expected to come from the main ui thread.
     67  */
     68 @NotThreadSafe
     69 public class VoicemailPlaybackFragment extends Fragment {
     70     private static final String TAG = "VoicemailPlayback";
     71     private static final int NUMBER_OF_THREADS_IN_POOL = 2;
     72     private static final String[] HAS_CONTENT_PROJECTION = new String[] {
     73         VoicemailContract.Voicemails.HAS_CONTENT,
     74     };
     75 
     76     private VoicemailPlaybackPresenter mPresenter;
     77     private ScheduledExecutorService mScheduledExecutorService;
     78     private View mPlaybackLayout;
     79 
     80     @Override
     81     public View onCreateView(LayoutInflater inflater, ViewGroup container,
     82             Bundle savedInstanceState) {
     83         mPlaybackLayout = inflater.inflate(R.layout.playback_layout, null);
     84         return mPlaybackLayout;
     85     }
     86 
     87     @Override
     88     public void onActivityCreated(Bundle savedInstanceState) {
     89         super.onActivityCreated(savedInstanceState);
     90         mScheduledExecutorService = createScheduledExecutorService();
     91         Bundle arguments = getArguments();
     92         Preconditions.checkNotNull(arguments, "fragment must be started with arguments");
     93         Uri voicemailUri = arguments.getParcelable(EXTRA_VOICEMAIL_URI);
     94         Preconditions.checkNotNull(voicemailUri, "fragment must contain EXTRA_VOICEMAIL_URI");
     95         boolean startPlayback = arguments.getBoolean(EXTRA_VOICEMAIL_START_PLAYBACK, false);
     96         PowerManager powerManager =
     97                 (PowerManager) getActivity().getSystemService(Context.POWER_SERVICE);
     98         PowerManager.WakeLock wakeLock =
     99                 powerManager.newWakeLock(
    100                         PowerManager.SCREEN_DIM_WAKE_LOCK, getClass().getSimpleName());
    101         mPresenter = new VoicemailPlaybackPresenter(createPlaybackViewImpl(),
    102                 createMediaPlayer(mScheduledExecutorService), voicemailUri,
    103                 mScheduledExecutorService, startPlayback,
    104                 AsyncTaskExecutors.createAsyncTaskExecutor(), wakeLock);
    105         mPresenter.onCreate(savedInstanceState);
    106     }
    107 
    108     @Override
    109     public void onSaveInstanceState(Bundle outState) {
    110         mPresenter.onSaveInstanceState(outState);
    111         super.onSaveInstanceState(outState);
    112     }
    113 
    114     @Override
    115     public void onDestroy() {
    116         mPresenter.onDestroy();
    117         mScheduledExecutorService.shutdown();
    118         super.onDestroy();
    119     }
    120 
    121     @Override
    122     public void onPause() {
    123         mPresenter.onPause();
    124         super.onPause();
    125     }
    126 
    127     private PlaybackViewImpl createPlaybackViewImpl() {
    128         return new PlaybackViewImpl(new ActivityReference(), getActivity().getApplicationContext(),
    129                 mPlaybackLayout);
    130     }
    131 
    132     private MediaPlayerProxy createMediaPlayer(ExecutorService executorService) {
    133         return VariableSpeed.createVariableSpeed(executorService);
    134     }
    135 
    136     private ScheduledExecutorService createScheduledExecutorService() {
    137         return Executors.newScheduledThreadPool(NUMBER_OF_THREADS_IN_POOL);
    138     }
    139 
    140     /**
    141      * Formats a number of milliseconds as something that looks like {@code 00:05}.
    142      * <p>
    143      * We always use four digits, two for minutes two for seconds.  In the very unlikely event
    144      * that the voicemail duration exceeds 99 minutes, the display is capped at 99 minutes.
    145      */
    146     private static String formatAsMinutesAndSeconds(int millis) {
    147         int seconds = millis / 1000;
    148         int minutes = seconds / 60;
    149         seconds -= minutes * 60;
    150         if (minutes > 99) {
    151             minutes = 99;
    152         }
    153         return String.format("%02d:%02d", minutes, seconds);
    154     }
    155 
    156     /**
    157      * An object that can provide us with an Activity.
    158      * <p>
    159      * Fragments suffer the drawback that the Activity they belong to may sometimes be null. This
    160      * can happen if the Fragment is detached, for example. In that situation a call to
    161      * {@link Fragment#getString(int)} will throw and {@link IllegalStateException}. Also, calling
    162      * {@link Fragment#getActivity()} is dangerous - it may sometimes return null. And thus blindly
    163      * calling a method on the result of getActivity() is dangerous too.
    164      * <p>
    165      * To work around this, I have made the {@link PlaybackViewImpl} class static, so that it does
    166      * not have access to any Fragment methods directly. Instead it uses an application Context for
    167      * things like accessing strings, accessing system services. It only uses the Activity when it
    168      * absolutely needs it - and does so through this class. This makes it easy to see where we have
    169      * to check for null properly.
    170      */
    171     private final class ActivityReference {
    172         /** Gets this Fragment's Activity: <b>may be null</b>. */
    173         public final Activity get() {
    174             return getActivity();
    175         }
    176     }
    177 
    178     /**  Methods required by the PlaybackView for the VoicemailPlaybackPresenter. */
    179     private static final class PlaybackViewImpl implements VoicemailPlaybackPresenter.PlaybackView {
    180         private final ActivityReference mActivityReference;
    181         private final Context mApplicationContext;
    182         private final SeekBar mPlaybackSeek;
    183         private final ImageButton mStartStopButton;
    184         private final ImageButton mPlaybackSpeakerphone;
    185         private final ImageButton mRateDecreaseButton;
    186         private final ImageButton mRateIncreaseButton;
    187         private final TextViewWithMessagesController mTextController;
    188 
    189         public PlaybackViewImpl(ActivityReference activityReference, Context applicationContext,
    190                 View playbackLayout) {
    191             Preconditions.checkNotNull(activityReference);
    192             Preconditions.checkNotNull(applicationContext);
    193             Preconditions.checkNotNull(playbackLayout);
    194             mActivityReference = activityReference;
    195             mApplicationContext = applicationContext;
    196             mPlaybackSeek = (SeekBar) playbackLayout.findViewById(R.id.playback_seek);
    197             mStartStopButton = (ImageButton) playbackLayout.findViewById(
    198                     R.id.playback_start_stop);
    199             mPlaybackSpeakerphone = (ImageButton) playbackLayout.findViewById(
    200                     R.id.playback_speakerphone);
    201             mRateDecreaseButton = (ImageButton) playbackLayout.findViewById(
    202                     R.id.rate_decrease_button);
    203             mRateIncreaseButton = (ImageButton) playbackLayout.findViewById(
    204                     R.id.rate_increase_button);
    205             mTextController = new TextViewWithMessagesController(
    206                     (TextView) playbackLayout.findViewById(R.id.playback_position_text),
    207                     (TextView) playbackLayout.findViewById(R.id.playback_speed_text));
    208         }
    209 
    210         @Override
    211         public void finish() {
    212             Activity activity = mActivityReference.get();
    213             if (activity != null) {
    214                 activity.finish();
    215             }
    216         }
    217 
    218         @Override
    219         public void runOnUiThread(Runnable runnable) {
    220             Activity activity = mActivityReference.get();
    221             if (activity != null) {
    222                 activity.runOnUiThread(runnable);
    223             }
    224         }
    225 
    226         @Override
    227         public Context getDataSourceContext() {
    228             return mApplicationContext;
    229         }
    230 
    231         @Override
    232         public void setRateDecreaseButtonListener(View.OnClickListener listener) {
    233             mRateDecreaseButton.setOnClickListener(listener);
    234         }
    235 
    236         @Override
    237         public void setRateIncreaseButtonListener(View.OnClickListener listener) {
    238             mRateIncreaseButton.setOnClickListener(listener);
    239         }
    240 
    241         @Override
    242         public void setStartStopListener(View.OnClickListener listener) {
    243             mStartStopButton.setOnClickListener(listener);
    244         }
    245 
    246         @Override
    247         public void setSpeakerphoneListener(View.OnClickListener listener) {
    248             mPlaybackSpeakerphone.setOnClickListener(listener);
    249         }
    250 
    251         @Override
    252         public void setRateDisplay(float rate, int stringResourceId) {
    253             mTextController.setTemporaryText(
    254                     mApplicationContext.getString(stringResourceId), 1, TimeUnit.SECONDS);
    255         }
    256 
    257         @Override
    258         public void setPositionSeekListener(SeekBar.OnSeekBarChangeListener listener) {
    259             mPlaybackSeek.setOnSeekBarChangeListener(listener);
    260         }
    261 
    262         @Override
    263         public void playbackStarted() {
    264             mStartStopButton.setImageResource(R.drawable.ic_hold_pause_holo_dark);
    265         }
    266 
    267         @Override
    268         public void playbackStopped() {
    269             mStartStopButton.setImageResource(R.drawable.ic_play);
    270         }
    271 
    272         @Override
    273         public void enableProximitySensor() {
    274             // Only change the state if the activity is still around.
    275             Activity activity = mActivityReference.get();
    276             if (activity != null && activity instanceof ProximitySensorAware) {
    277                 ((ProximitySensorAware) activity).enableProximitySensor();
    278             }
    279         }
    280 
    281         @Override
    282         public void disableProximitySensor() {
    283             // Only change the state if the activity is still around.
    284             Activity activity = mActivityReference.get();
    285             if (activity != null && activity instanceof ProximitySensorAware) {
    286                 ((ProximitySensorAware) activity).disableProximitySensor(true);
    287             }
    288         }
    289 
    290         @Override
    291         public void registerContentObserver(Uri uri, ContentObserver observer) {
    292             mApplicationContext.getContentResolver().registerContentObserver(uri, false, observer);
    293         }
    294 
    295         @Override
    296         public void unregisterContentObserver(ContentObserver observer) {
    297             mApplicationContext.getContentResolver().unregisterContentObserver(observer);
    298         }
    299 
    300         @Override
    301         public void setClipPosition(int clipPositionInMillis, int clipLengthInMillis) {
    302             int seekBarPosition = Math.max(0, clipPositionInMillis);
    303             int seekBarMax = Math.max(seekBarPosition, clipLengthInMillis);
    304             if (mPlaybackSeek.getMax() != seekBarMax) {
    305                 mPlaybackSeek.setMax(seekBarMax);
    306             }
    307             mPlaybackSeek.setProgress(seekBarPosition);
    308             mTextController.setPermanentText(
    309                     formatAsMinutesAndSeconds(seekBarMax - seekBarPosition));
    310         }
    311 
    312         private String getString(int resId) {
    313             return mApplicationContext.getString(resId);
    314         }
    315 
    316         @Override
    317         public void setIsBuffering() {
    318             disableUiElements();
    319             mTextController.setPermanentText(getString(R.string.voicemail_buffering));
    320         }
    321 
    322         @Override
    323         public void setIsFetchingContent() {
    324             disableUiElements();
    325             mTextController.setPermanentText(getString(R.string.voicemail_fetching_content));
    326         }
    327 
    328         @Override
    329         public void setFetchContentTimeout() {
    330             disableUiElements();
    331             mTextController.setPermanentText(getString(R.string.voicemail_fetching_timout));
    332         }
    333 
    334         @Override
    335         public int getDesiredClipPosition() {
    336             return mPlaybackSeek.getProgress();
    337         }
    338 
    339         @Override
    340         public void disableUiElements() {
    341             mRateIncreaseButton.setEnabled(false);
    342             mRateDecreaseButton.setEnabled(false);
    343             mStartStopButton.setEnabled(false);
    344             mPlaybackSpeakerphone.setEnabled(false);
    345             mPlaybackSeek.setProgress(0);
    346             mPlaybackSeek.setEnabled(false);
    347         }
    348 
    349         @Override
    350         public void playbackError(Exception e) {
    351             disableUiElements();
    352             mTextController.setPermanentText(getString(R.string.voicemail_playback_error));
    353             Log.e(TAG, "Could not play voicemail", e);
    354         }
    355 
    356         @Override
    357         public void enableUiElements() {
    358             mRateIncreaseButton.setEnabled(true);
    359             mRateDecreaseButton.setEnabled(true);
    360             mStartStopButton.setEnabled(true);
    361             mPlaybackSpeakerphone.setEnabled(true);
    362             mPlaybackSeek.setEnabled(true);
    363         }
    364 
    365         @Override
    366         public void sendFetchVoicemailRequest(Uri voicemailUri) {
    367             Intent intent = new Intent(VoicemailContract.ACTION_FETCH_VOICEMAIL, voicemailUri);
    368             mApplicationContext.sendBroadcast(intent);
    369         }
    370 
    371         @Override
    372         public boolean queryHasContent(Uri voicemailUri) {
    373             ContentResolver contentResolver = mApplicationContext.getContentResolver();
    374             Cursor cursor = contentResolver.query(
    375                     voicemailUri, HAS_CONTENT_PROJECTION, null, null, null);
    376             try {
    377                 if (cursor != null && cursor.moveToNext()) {
    378                     return cursor.getInt(cursor.getColumnIndexOrThrow(
    379                             VoicemailContract.Voicemails.HAS_CONTENT)) == 1;
    380                 }
    381             } finally {
    382                 MoreCloseables.closeQuietly(cursor);
    383             }
    384             return false;
    385         }
    386 
    387         private AudioManager getAudioManager() {
    388             return (AudioManager) mApplicationContext.getSystemService(Context.AUDIO_SERVICE);
    389         }
    390 
    391         @Override
    392         public boolean isSpeakerPhoneOn() {
    393             return getAudioManager().isSpeakerphoneOn();
    394         }
    395 
    396         @Override
    397         public void setSpeakerPhoneOn(boolean on) {
    398             getAudioManager().setSpeakerphoneOn(on);
    399             if (on) {
    400                 mPlaybackSpeakerphone.setImageResource(R.drawable.ic_speakerphone_on);
    401             } else {
    402                 mPlaybackSpeakerphone.setImageResource(R.drawable.ic_speakerphone_off);
    403             }
    404         }
    405 
    406         @Override
    407         public void setVolumeControlStream(int streamType) {
    408             Activity activity = mActivityReference.get();
    409             if (activity != null) {
    410                 activity.setVolumeControlStream(streamType);
    411             }
    412         }
    413     }
    414 
    415     /**
    416      * Controls a TextView with dynamically changing text.
    417      * <p>
    418      * There are two methods here of interest,
    419      * {@link TextViewWithMessagesController#setPermanentText(String)} and
    420      * {@link TextViewWithMessagesController#setTemporaryText(String, long, TimeUnit)}.  The
    421      * former is used to set the text on the text view immediately, and is used in our case for
    422      * the countdown of duration remaining during voicemail playback.  The second is used to
    423      * temporarily replace this countdown with a message, in our case faster voicemail speed or
    424      * slower voicemail speed, before returning to the countdown display.
    425      * <p>
    426      * All the methods on this class must be called from the ui thread.
    427      */
    428     private static final class TextViewWithMessagesController {
    429         private static final float VISIBLE = 1;
    430         private static final float INVISIBLE = 0;
    431         private static final long SHORT_ANIMATION_MS = 200;
    432         private static final long LONG_ANIMATION_MS = 400;
    433         private final Object mLock = new Object();
    434         private final TextView mPermanentTextView;
    435         private final TextView mTemporaryTextView;
    436         @GuardedBy("mLock") private Runnable mRunnable;
    437 
    438         public TextViewWithMessagesController(TextView permanentTextView,
    439                 TextView temporaryTextView) {
    440             mPermanentTextView = permanentTextView;
    441             mTemporaryTextView = temporaryTextView;
    442         }
    443 
    444         public void setPermanentText(String text) {
    445             mPermanentTextView.setText(text);
    446         }
    447 
    448         public void setTemporaryText(String text, long duration, TimeUnit units) {
    449             synchronized (mLock) {
    450                 mTemporaryTextView.setText(text);
    451                 mTemporaryTextView.animate().alpha(VISIBLE).setDuration(SHORT_ANIMATION_MS);
    452                 mPermanentTextView.animate().alpha(INVISIBLE).setDuration(SHORT_ANIMATION_MS);
    453                 mRunnable = new Runnable() {
    454                     @Override
    455                     public void run() {
    456                         synchronized (mLock) {
    457                             // We check for (mRunnable == this) becuase if not true, then another
    458                             // setTemporaryText call has taken place in the meantime, and this
    459                             // one is now defunct and needs to take no action.
    460                             if (mRunnable == this) {
    461                                 mRunnable = null;
    462                                 mTemporaryTextView.animate()
    463                                         .alpha(INVISIBLE).setDuration(LONG_ANIMATION_MS);
    464                                 mPermanentTextView.animate()
    465                                         .alpha(VISIBLE).setDuration(LONG_ANIMATION_MS);
    466                             }
    467                         }
    468                     }
    469                 };
    470                 mTemporaryTextView.postDelayed(mRunnable, units.toMillis(duration));
    471             }
    472         }
    473     }
    474 }
    475