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