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