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 android.app.Activity; 20 import android.content.Context; 21 import android.content.ContentResolver; 22 import android.content.Intent; 23 import android.database.ContentObserver; 24 import android.database.Cursor; 25 import android.media.AudioManager; 26 import android.media.AudioManager.OnAudioFocusChangeListener; 27 import android.media.MediaPlayer; 28 import android.net.Uri; 29 import android.os.AsyncTask; 30 import android.os.Bundle; 31 import android.os.Handler; 32 import android.os.PowerManager; 33 import android.provider.VoicemailContract; 34 import android.util.Log; 35 import android.view.View; 36 import android.view.WindowManager.LayoutParams; 37 import android.widget.SeekBar; 38 39 import com.android.dialer.R; 40 import com.android.dialer.util.AsyncTaskExecutor; 41 import com.android.dialer.util.AsyncTaskExecutors; 42 43 import com.android.common.io.MoreCloseables; 44 import com.google.common.annotations.VisibleForTesting; 45 import com.google.common.base.Preconditions; 46 47 import java.io.IOException; 48 import java.util.concurrent.Executors; 49 import java.util.concurrent.ScheduledExecutorService; 50 import java.util.concurrent.RejectedExecutionException; 51 import java.util.concurrent.ScheduledExecutorService; 52 import java.util.concurrent.ScheduledFuture; 53 import java.util.concurrent.atomic.AtomicBoolean; 54 import java.util.concurrent.atomic.AtomicInteger; 55 56 import javax.annotation.concurrent.NotThreadSafe; 57 import javax.annotation.concurrent.ThreadSafe; 58 59 /** 60 * Contains the controlling logic for a voicemail playback in the call log. It is closely coupled 61 * to assumptions about the behaviors and lifecycle of the call log, in particular in the 62 * {@link CallLogFragment} and {@link CallLogAdapter}. 63 * <p> 64 * This controls a single {@link com.android.dialer.voicemail.VoicemailPlaybackLayout}. A single 65 * instance can be reused for different such layouts, using {@link #setVoicemailPlaybackView}. This 66 * is to facilitate reuse across different voicemail call log entries. 67 * <p> 68 * This class is not thread safe. The thread policy for this class is thread-confinement, all calls 69 * into this class from outside must be done from the main UI thread. 70 */ 71 @NotThreadSafe 72 @VisibleForTesting 73 public class VoicemailPlaybackPresenter 74 implements OnAudioFocusChangeListener, MediaPlayer.OnPreparedListener, 75 MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener { 76 77 private static final String TAG = VoicemailPlaybackPresenter.class.getSimpleName(); 78 79 /** Contract describing the behaviour we need from the ui we are controlling. */ 80 public interface PlaybackView { getDesiredClipPosition()81 int getDesiredClipPosition(); disableUiElements()82 void disableUiElements(); enableUiElements()83 void enableUiElements(); onPlaybackError()84 void onPlaybackError(); onPlaybackStarted(int duration, ScheduledExecutorService executorService)85 void onPlaybackStarted(int duration, ScheduledExecutorService executorService); onPlaybackStopped()86 void onPlaybackStopped(); onSpeakerphoneOn(boolean on)87 void onSpeakerphoneOn(boolean on); setClipPosition(int clipPositionInMillis, int clipLengthInMillis)88 void setClipPosition(int clipPositionInMillis, int clipLengthInMillis); setFetchContentTimeout()89 void setFetchContentTimeout(); setIsBuffering()90 void setIsBuffering(); setIsFetchingContent()91 void setIsFetchingContent(); setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri)92 void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri); 93 } 94 95 public interface OnVoicemailDeletedListener { onVoicemailDeleted(Uri uri)96 void onVoicemailDeleted(Uri uri); 97 } 98 99 /** The enumeration of {@link AsyncTask} objects we use in this class. */ 100 public enum Tasks { 101 CHECK_FOR_CONTENT, 102 CHECK_CONTENT_AFTER_CHANGE, 103 } 104 105 private static final String[] HAS_CONTENT_PROJECTION = new String[] { 106 VoicemailContract.Voicemails.HAS_CONTENT, 107 }; 108 109 public static final int PLAYBACK_STREAM = AudioManager.STREAM_VOICE_CALL; 110 private static final int NUMBER_OF_THREADS_IN_POOL = 2; 111 // Time to wait for content to be fetched before timing out. 112 private static final long FETCH_CONTENT_TIMEOUT_MS = 20000; 113 114 private static final String VOICEMAIL_URI_KEY = 115 VoicemailPlaybackPresenter.class.getName() + ".VOICEMAIL_URI"; 116 private static final String IS_PREPARED_KEY = 117 VoicemailPlaybackPresenter.class.getName() + ".IS_PREPARED"; 118 // If present in the saved instance bundle, we should not resume playback on create. 119 private static final String IS_PLAYING_STATE_KEY = 120 VoicemailPlaybackPresenter.class.getName() + ".IS_PLAYING_STATE_KEY"; 121 // If present in the saved instance bundle, indicates where to set the playback slider. 122 private static final String CLIP_POSITION_KEY = 123 VoicemailPlaybackPresenter.class.getName() + ".CLIP_POSITION_KEY"; 124 125 /** 126 * The most recently cached duration. We cache this since we don't want to keep requesting it 127 * from the player, as this can easily lead to throwing {@link IllegalStateException} (any time 128 * the player is released, it's illegal to ask for the duration). 129 */ 130 private final AtomicInteger mDuration = new AtomicInteger(0); 131 132 private static VoicemailPlaybackPresenter sInstance; 133 134 private Activity mActivity; 135 private Context mContext; 136 private PlaybackView mView; 137 private Uri mVoicemailUri; 138 139 private MediaPlayer mMediaPlayer; 140 private int mPosition; 141 private boolean mIsPlaying; 142 // MediaPlayer crashes on some method calls if not prepared but does not have a method which 143 // exposes its prepared state. Store this locally, so we can check and prevent crashes. 144 private boolean mIsPrepared; 145 146 private boolean mShouldResumePlaybackAfterSeeking; 147 private int mInitialOrientation; 148 149 // Used to run async tasks that need to interact with the UI. 150 private AsyncTaskExecutor mAsyncTaskExecutor; 151 private static ScheduledExecutorService mScheduledExecutorService; 152 /** 153 * Used to handle the result of a successful or time-out fetch result. 154 * <p> 155 * This variable is thread-contained, accessed only on the ui thread. 156 */ 157 private FetchResultHandler mFetchResultHandler; 158 private Handler mHandler = new Handler(); 159 private PowerManager.WakeLock mProximityWakeLock; 160 private AudioManager mAudioManager; 161 162 private OnVoicemailDeletedListener mOnVoicemailDeletedListener; 163 164 /** 165 * Obtain singleton instance of this class. Use a single instance to provide a consistent 166 * listener to the AudioManager when requesting and abandoning audio focus. 167 * 168 * Otherwise, after rotation the previous listener will still be active but a new listener 169 * will be provided to calls to the AudioManager, which is bad. For example, abandoning 170 * audio focus with the new listeners results in an AUDIO_FOCUS_GAIN callback to the 171 * previous listener, which is the opposite of the intended behavior. 172 */ getInstance( Activity activity, Bundle savedInstanceState)173 public static VoicemailPlaybackPresenter getInstance( 174 Activity activity, Bundle savedInstanceState) { 175 if (sInstance == null) { 176 sInstance = new VoicemailPlaybackPresenter(activity); 177 } 178 179 sInstance.init(activity, savedInstanceState); 180 return sInstance; 181 } 182 183 /** 184 * Initialize variables which are activity-independent and state-independent. 185 */ VoicemailPlaybackPresenter(Activity activity)186 private VoicemailPlaybackPresenter(Activity activity) { 187 Context context = activity.getApplicationContext(); 188 mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor(); 189 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 190 191 PowerManager powerManager = 192 (PowerManager) context.getSystemService(Context.POWER_SERVICE); 193 if (powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { 194 mProximityWakeLock = powerManager.newWakeLock( 195 PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG); 196 } 197 } 198 199 /** 200 * Update variables which are activity-dependent or state-dependent. 201 */ init(Activity activity, Bundle savedInstanceState)202 private void init(Activity activity, Bundle savedInstanceState) { 203 mActivity = activity; 204 mContext = activity; 205 206 mInitialOrientation = mContext.getResources().getConfiguration().orientation; 207 mActivity.setVolumeControlStream(VoicemailPlaybackPresenter.PLAYBACK_STREAM); 208 209 if (savedInstanceState != null) { 210 // Restores playback state when activity is recreated, such as after rotation. 211 mVoicemailUri = (Uri) savedInstanceState.getParcelable(VOICEMAIL_URI_KEY); 212 mIsPrepared = savedInstanceState.getBoolean(IS_PREPARED_KEY); 213 mPosition = savedInstanceState.getInt(CLIP_POSITION_KEY, 0); 214 mIsPlaying = savedInstanceState.getBoolean(IS_PLAYING_STATE_KEY, false); 215 } 216 217 if (mMediaPlayer == null) { 218 mIsPrepared = false; 219 mIsPlaying = false; 220 } 221 } 222 223 /** 224 * Must be invoked when the parent Activity is saving it state. 225 */ onSaveInstanceState(Bundle outState)226 public void onSaveInstanceState(Bundle outState) { 227 if (mView != null) { 228 outState.putParcelable(VOICEMAIL_URI_KEY, mVoicemailUri); 229 outState.putBoolean(IS_PREPARED_KEY, mIsPrepared); 230 outState.putInt(CLIP_POSITION_KEY, mView.getDesiredClipPosition()); 231 outState.putBoolean(IS_PLAYING_STATE_KEY, mIsPlaying); 232 } 233 } 234 235 /** 236 * Specify the view which this presenter controls and the voicemail to prepare to play. 237 */ setPlaybackView( PlaybackView view, Uri voicemailUri, boolean startPlayingImmediately)238 public void setPlaybackView( 239 PlaybackView view, Uri voicemailUri, boolean startPlayingImmediately) { 240 mView = view; 241 mView.setPresenter(this, voicemailUri); 242 243 if (mMediaPlayer != null && voicemailUri.equals(mVoicemailUri)) { 244 // Handles case where MediaPlayer was retained after an orientation change. 245 onPrepared(mMediaPlayer); 246 mView.onSpeakerphoneOn(isSpeakerphoneOn()); 247 } else { 248 if (!voicemailUri.equals(mVoicemailUri)) { 249 mPosition = 0; 250 } 251 252 mVoicemailUri = voicemailUri; 253 mDuration.set(0); 254 255 if (startPlayingImmediately) { 256 // Since setPlaybackView can get called during the view binding process, we don't 257 // want to reset mIsPlaying to false if the user is currently playing the 258 // voicemail and the view is rebound. 259 mIsPlaying = startPlayingImmediately; 260 checkForContent(); 261 } 262 263 // Default to earpiece. 264 mView.onSpeakerphoneOn(false); 265 } 266 } 267 268 /** 269 * Reset the presenter for playback back to its original state. 270 */ resetAll()271 public void resetAll() { 272 reset(); 273 274 mView = null; 275 mVoicemailUri = null; 276 } 277 278 /** 279 * Reset the presenter such that it is as if the voicemail has not been played. 280 */ reset()281 public void reset() { 282 if (mMediaPlayer != null) { 283 mMediaPlayer.release(); 284 mMediaPlayer = null; 285 } 286 287 disableProximitySensor(false /* waitForFarState */); 288 289 mIsPrepared = false; 290 mIsPlaying = false; 291 mPosition = 0; 292 mDuration.set(0); 293 294 if (mView != null) { 295 mView.onPlaybackStopped(); 296 mView.setClipPosition(0, mDuration.get()); 297 } 298 } 299 300 /** 301 * Must be invoked when the parent activity is paused. 302 */ onPause()303 public void onPause() { 304 if (mContext != null && mIsPrepared 305 && mInitialOrientation != mContext.getResources().getConfiguration().orientation) { 306 // If an orientation change triggers the pause, retain the MediaPlayer. 307 Log.d(TAG, "onPause: Orientation changed."); 308 return; 309 } 310 311 // Release the media player, otherwise there may be failures. 312 reset(); 313 314 if (mActivity != null) { 315 mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON); 316 } 317 } 318 319 /** 320 * Must be invoked when the parent activity is destroyed. 321 */ onDestroy()322 public void onDestroy() { 323 // Clear references to avoid leaks from the singleton instance. 324 mActivity = null; 325 mContext = null; 326 327 if (mScheduledExecutorService != null) { 328 mScheduledExecutorService.shutdown(); 329 mScheduledExecutorService = null; 330 } 331 332 if (mFetchResultHandler != null) { 333 mFetchResultHandler.destroy(); 334 mFetchResultHandler = null; 335 } 336 } 337 338 /** 339 * Checks to see if we have content available for this voicemail. 340 * <p> 341 * This method will be called once, after the fragment has been created, before we know if the 342 * voicemail we've been asked to play has any content available. 343 * <p> 344 * Notify the user that we are fetching the content, then check to see if the content field in 345 * the DB is set. If set, we proceed to {@link #prepareContent()} method. If not set, make 346 * a request to fetch the content asynchronously via {@link #requestContent()}. 347 */ checkForContent()348 private void checkForContent() { 349 mView.setIsFetchingContent(); 350 mAsyncTaskExecutor.submit(Tasks.CHECK_FOR_CONTENT, new AsyncTask<Void, Void, Boolean>() { 351 @Override 352 public Boolean doInBackground(Void... params) { 353 return queryHasContent(mVoicemailUri); 354 } 355 356 @Override 357 public void onPostExecute(Boolean hasContent) { 358 if (hasContent) { 359 prepareContent(); 360 } else { 361 requestContent(); 362 } 363 } 364 }); 365 } 366 queryHasContent(Uri voicemailUri)367 private boolean queryHasContent(Uri voicemailUri) { 368 if (voicemailUri == null || mContext == null) { 369 return false; 370 } 371 372 ContentResolver contentResolver = mContext.getContentResolver(); 373 Cursor cursor = contentResolver.query( 374 voicemailUri, HAS_CONTENT_PROJECTION, null, null, null); 375 try { 376 if (cursor != null && cursor.moveToNext()) { 377 return cursor.getInt(cursor.getColumnIndexOrThrow( 378 VoicemailContract.Voicemails.HAS_CONTENT)) == 1; 379 } 380 } finally { 381 MoreCloseables.closeQuietly(cursor); 382 } 383 return false; 384 } 385 386 /** 387 * Makes a broadcast request to ask that a voicemail source fetch this content. 388 * <p> 389 * This method <b>must be called on the ui thread</b>. 390 * <p> 391 * This method will be called when we realise that we don't have content for this voicemail. It 392 * will trigger a broadcast to request that the content be downloaded. It will add a listener to 393 * the content resolver so that it will be notified when the has_content field changes. It will 394 * also set a timer. If the has_content field changes to true within the allowed time, we will 395 * proceed to {@link #prepareContent()}. If the has_content field does not 396 * become true within the allowed time, we will update the ui to reflect the fact that content 397 * was not available. 398 */ requestContent()399 private void requestContent() { 400 if (mFetchResultHandler != null) { 401 mFetchResultHandler.destroy(); 402 } 403 404 mFetchResultHandler = new FetchResultHandler(new Handler(), mVoicemailUri); 405 406 // Send voicemail fetch request. 407 Intent intent = new Intent(VoicemailContract.ACTION_FETCH_VOICEMAIL, mVoicemailUri); 408 mContext.sendBroadcast(intent); 409 } 410 411 @ThreadSafe 412 private class FetchResultHandler extends ContentObserver implements Runnable { 413 private AtomicBoolean mIsWaitingForResult = new AtomicBoolean(true); 414 private final Handler mFetchResultHandler; 415 FetchResultHandler(Handler handler, Uri voicemailUri)416 public FetchResultHandler(Handler handler, Uri voicemailUri) { 417 super(handler); 418 mFetchResultHandler = handler; 419 420 if (mContext != null) { 421 mContext.getContentResolver().registerContentObserver( 422 voicemailUri, false, this); 423 mFetchResultHandler.postDelayed(this, FETCH_CONTENT_TIMEOUT_MS); 424 } 425 } 426 427 /** 428 * Stop waiting for content and notify UI if {@link FETCH_CONTENT_TIMEOUT_MS} has elapsed. 429 */ 430 @Override run()431 public void run() { 432 if (mIsWaitingForResult.getAndSet(false) && mContext != null) { 433 mContext.getContentResolver().unregisterContentObserver(this); 434 if (mView != null) { 435 mView.setFetchContentTimeout(); 436 } 437 } 438 } 439 destroy()440 public void destroy() { 441 if (mIsWaitingForResult.getAndSet(false) && mContext != null) { 442 mContext.getContentResolver().unregisterContentObserver(this); 443 mFetchResultHandler.removeCallbacks(this); 444 } 445 } 446 447 @Override onChange(boolean selfChange)448 public void onChange(boolean selfChange) { 449 mAsyncTaskExecutor.submit(Tasks.CHECK_CONTENT_AFTER_CHANGE, 450 new AsyncTask<Void, Void, Boolean>() { 451 @Override 452 public Boolean doInBackground(Void... params) { 453 return queryHasContent(mVoicemailUri); 454 } 455 456 @Override 457 public void onPostExecute(Boolean hasContent) { 458 if (hasContent && mContext != null && mIsWaitingForResult.getAndSet(false)) { 459 mContext.getContentResolver().unregisterContentObserver( 460 FetchResultHandler.this); 461 prepareContent(); 462 } 463 } 464 }); 465 } 466 } 467 468 /** 469 * Prepares the voicemail content for playback. 470 * <p> 471 * This method will be called once we know that our voicemail has content (according to the 472 * content provider). this method asynchronously tries to prepare the data source through the 473 * media player. If preparation is successful, the media player will {@link #onPrepared()}, 474 * and it will call {@link #onError()} otherwise. 475 */ prepareContent()476 private void prepareContent() { 477 if (mView == null) { 478 return; 479 } 480 Log.d(TAG, "prepareContent"); 481 482 // Release the previous media player, otherwise there may be failures. 483 if (mMediaPlayer != null) { 484 mMediaPlayer.release(); 485 mMediaPlayer = null; 486 } 487 488 mView.setIsBuffering(); 489 mIsPrepared = false; 490 491 try { 492 mMediaPlayer = new MediaPlayer(); 493 mMediaPlayer.setOnPreparedListener(this); 494 mMediaPlayer.setOnErrorListener(this); 495 mMediaPlayer.setOnCompletionListener(this); 496 497 mMediaPlayer.reset(); 498 mMediaPlayer.setDataSource(mContext, mVoicemailUri); 499 mMediaPlayer.setAudioStreamType(PLAYBACK_STREAM); 500 mMediaPlayer.prepareAsync(); 501 } catch (IOException e) { 502 handleError(e); 503 } 504 } 505 506 /** 507 * Once the media player is prepared, enables the UI and adopts the appropriate playback state. 508 */ 509 @Override onPrepared(MediaPlayer mp)510 public void onPrepared(MediaPlayer mp) { 511 if (mView == null) { 512 return; 513 } 514 Log.d(TAG, "onPrepared"); 515 mIsPrepared = true; 516 517 mDuration.set(mMediaPlayer.getDuration()); 518 mPosition = mMediaPlayer.getCurrentPosition(); 519 520 mView.enableUiElements(); 521 Log.d(TAG, "onPrepared: mPosition=" + mPosition); 522 mView.setClipPosition(mPosition, mDuration.get()); 523 mMediaPlayer.seekTo(mPosition); 524 525 if (mIsPlaying) { 526 resumePlayback(); 527 } else { 528 pausePlayback(); 529 } 530 } 531 532 /** 533 * Invoked if preparing the media player fails, for example, if file is missing or the voicemail 534 * is an unknown file format that can't be played. 535 */ 536 @Override onError(MediaPlayer mp, int what, int extra)537 public boolean onError(MediaPlayer mp, int what, int extra) { 538 handleError(new IllegalStateException("MediaPlayer error listener invoked: " + extra)); 539 return true; 540 } 541 handleError(Exception e)542 private void handleError(Exception e) { 543 Log.d(TAG, "handleError: Could not play voicemail " + e); 544 545 if (mIsPrepared) { 546 mMediaPlayer.release(); 547 mMediaPlayer = null; 548 mIsPrepared = false; 549 } 550 551 if (mView != null) { 552 mView.onPlaybackError(); 553 } 554 555 mPosition = 0; 556 mIsPlaying = false; 557 } 558 559 /** 560 * After done playing the voicemail clip, reset the clip position to the start. 561 */ 562 @Override onCompletion(MediaPlayer mediaPlayer)563 public void onCompletion(MediaPlayer mediaPlayer) { 564 pausePlayback(); 565 566 // Reset the seekbar position to the beginning. 567 mPosition = 0; 568 if (mView != null) { 569 mView.setClipPosition(0, mDuration.get()); 570 } 571 } 572 573 @Override onAudioFocusChange(int focusChange)574 public void onAudioFocusChange(int focusChange) { 575 Log.d(TAG, "onAudioFocusChange: focusChange=" + focusChange); 576 boolean lostFocus = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT 577 || focusChange == AudioManager.AUDIOFOCUS_LOSS; 578 if (mIsPlaying && focusChange == AudioManager.AUDIOFOCUS_LOSS) { 579 pausePlayback(); 580 } else if (!mIsPlaying && focusChange == AudioManager.AUDIOFOCUS_GAIN) { 581 resumePlayback(); 582 } 583 } 584 585 /** 586 * Resumes voicemail playback at the clip position stored by the presenter. Null-op if already 587 * playing. 588 */ resumePlayback()589 public void resumePlayback() { 590 if (mView == null || mContext == null) { 591 return; 592 } 593 594 if (!mIsPrepared) { 595 // If we haven't downloaded the voicemail yet, attempt to download it. 596 checkForContent(); 597 mIsPlaying = true; 598 599 return; 600 } 601 602 mIsPlaying = true; 603 604 if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) { 605 // Clamp the start position between 0 and the duration. 606 mPosition = Math.max(0, Math.min(mPosition, mDuration.get())); 607 mMediaPlayer.seekTo(mPosition); 608 609 try { 610 // Grab audio focus. 611 int result = mAudioManager.requestAudioFocus( 612 this, 613 PLAYBACK_STREAM, 614 AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); 615 if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { 616 throw new RejectedExecutionException("Could not capture audio focus."); 617 } 618 619 // Can throw RejectedExecutionException. 620 mMediaPlayer.start(); 621 } catch (RejectedExecutionException e) { 622 handleError(e); 623 } 624 } 625 626 Log.d(TAG, "Resumed playback at " + mPosition + "."); 627 mView.onPlaybackStarted(mDuration.get(), getScheduledExecutorServiceInstance()); 628 if (isSpeakerphoneOn()) { 629 mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON); 630 } else { 631 enableProximitySensor(); 632 } 633 } 634 635 /** 636 * Pauses voicemail playback at the current position. Null-op if already paused. 637 */ pausePlayback()638 public void pausePlayback() { 639 if (!mIsPrepared) { 640 return; 641 } 642 643 mIsPlaying = false; 644 645 if (mMediaPlayer != null && mMediaPlayer.isPlaying()) { 646 mMediaPlayer.pause(); 647 } 648 649 mPosition = mMediaPlayer == null ? 0 : mMediaPlayer.getCurrentPosition(); 650 651 Log.d(TAG, "Paused playback at " + mPosition + "."); 652 653 if (mView != null) { 654 mView.onPlaybackStopped(); 655 } 656 mAudioManager.abandonAudioFocus(this); 657 658 if (mActivity != null) { 659 mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON); 660 } 661 disableProximitySensor(true /* waitForFarState */); 662 } 663 664 /** 665 * Pauses playback when the user starts seeking the position, and notes whether the voicemail is 666 * playing to know whether to resume playback once the user selects a new position. 667 */ pausePlaybackForSeeking()668 public void pausePlaybackForSeeking() { 669 if (mMediaPlayer != null) { 670 mShouldResumePlaybackAfterSeeking = mMediaPlayer.isPlaying(); 671 } 672 pausePlayback(); 673 } 674 resumePlaybackAfterSeeking(int desiredPosition)675 public void resumePlaybackAfterSeeking(int desiredPosition) { 676 mPosition = desiredPosition; 677 if (mShouldResumePlaybackAfterSeeking) { 678 mShouldResumePlaybackAfterSeeking = false; 679 resumePlayback(); 680 } 681 } 682 enableProximitySensor()683 private void enableProximitySensor() { 684 if (mProximityWakeLock == null || isSpeakerphoneOn() || !mIsPrepared 685 || mMediaPlayer == null || !mMediaPlayer.isPlaying()) { 686 return; 687 } 688 689 if (!mProximityWakeLock.isHeld()) { 690 Log.i(TAG, "Acquiring proximity wake lock"); 691 mProximityWakeLock.acquire(); 692 } else { 693 Log.i(TAG, "Proximity wake lock already acquired"); 694 } 695 } 696 disableProximitySensor(boolean waitForFarState)697 private void disableProximitySensor(boolean waitForFarState) { 698 if (mProximityWakeLock == null) { 699 return; 700 } 701 if (mProximityWakeLock.isHeld()) { 702 Log.i(TAG, "Releasing proximity wake lock"); 703 int flags = waitForFarState ? PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY : 0; 704 mProximityWakeLock.release(flags); 705 } else { 706 Log.i(TAG, "Proximity wake lock already released"); 707 } 708 } 709 setSpeakerphoneOn(boolean on)710 public void setSpeakerphoneOn(boolean on) { 711 mAudioManager.setSpeakerphoneOn(on); 712 713 if (on) { 714 disableProximitySensor(false /* waitForFarState */); 715 if (mIsPrepared && mMediaPlayer != null && mMediaPlayer.isPlaying()) { 716 mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON); 717 } 718 } else { 719 enableProximitySensor(); 720 if (mActivity != null) { 721 mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON); 722 } 723 } 724 } 725 isSpeakerphoneOn()726 public boolean isSpeakerphoneOn() { 727 return mAudioManager.isSpeakerphoneOn(); 728 } 729 setOnVoicemailDeletedListener(OnVoicemailDeletedListener listener)730 public void setOnVoicemailDeletedListener(OnVoicemailDeletedListener listener) { 731 mOnVoicemailDeletedListener = listener; 732 } 733 getMediaPlayerPosition()734 public int getMediaPlayerPosition() { 735 return mIsPrepared && mMediaPlayer != null ? mMediaPlayer.getCurrentPosition() : 0; 736 } 737 onVoicemailDeleted()738 /* package */ void onVoicemailDeleted() { 739 // Trampoline the event notification to the interested listener 740 if (mOnVoicemailDeletedListener != null) { 741 mOnVoicemailDeletedListener.onVoicemailDeleted(mVoicemailUri); 742 } 743 } 744 getScheduledExecutorServiceInstance()745 private static synchronized ScheduledExecutorService getScheduledExecutorServiceInstance() { 746 if (mScheduledExecutorService == null) { 747 mScheduledExecutorService = Executors.newScheduledThreadPool(NUMBER_OF_THREADS_IN_POOL); 748 } 749 return mScheduledExecutorService; 750 } 751 752 @VisibleForTesting isPlaying()753 public boolean isPlaying() { 754 return mIsPlaying; 755 } 756 } 757