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 com.google.common.annotations.VisibleForTesting; 20 21 import android.app.Activity; 22 import android.content.Context; 23 import android.content.ContentResolver; 24 import android.content.Intent; 25 import android.database.ContentObserver; 26 import android.database.Cursor; 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.support.v4.content.FileProvider; 35 import android.util.Log; 36 import android.view.WindowManager.LayoutParams; 37 38 import com.android.dialer.R; 39 import com.android.dialer.calllog.CallLogAsyncTaskUtil; 40 import com.android.dialer.util.AsyncTaskExecutor; 41 import com.android.dialer.util.AsyncTaskExecutors; 42 import com.android.common.io.MoreCloseables; 43 44 import java.io.File; 45 import java.io.IOException; 46 import java.util.ArrayList; 47 import java.util.List; 48 import java.util.concurrent.Executors; 49 import java.util.concurrent.RejectedExecutionException; 50 import java.util.concurrent.ScheduledExecutorService; 51 import java.util.concurrent.TimeUnit; 52 import java.util.concurrent.atomic.AtomicBoolean; 53 import java.util.concurrent.atomic.AtomicInteger; 54 55 import javax.annotation.concurrent.NotThreadSafe; 56 import javax.annotation.concurrent.ThreadSafe; 57 58 /** 59 * Contains the controlling logic for a voicemail playback in the call log. It is closely coupled 60 * to assumptions about the behaviors and lifecycle of the call log, in particular in the 61 * {@link CallLogFragment} and {@link CallLogAdapter}. 62 * <p> 63 * This controls a single {@link com.android.dialer.voicemail.VoicemailPlaybackLayout}. A single 64 * instance can be reused for different such layouts, using {@link #setPlaybackView}. This 65 * is to facilitate reuse across different voicemail call log entries. 66 * <p> 67 * This class is not thread safe. The thread policy for this class is thread-confinement, all calls 68 * into this class from outside must be done from the main UI thread. 69 */ 70 @NotThreadSafe 71 @VisibleForTesting 72 public class VoicemailPlaybackPresenter implements MediaPlayer.OnPreparedListener, 73 MediaPlayer.OnCompletionListener, MediaPlayer.OnErrorListener { 74 75 private static final String TAG = "VmPlaybackPresenter"; 76 77 /** Contract describing the behaviour we need from the ui we are controlling. */ 78 public interface PlaybackView { 79 int getDesiredClipPosition(); 80 void disableUiElements(); 81 void enableUiElements(); 82 void onPlaybackError(); 83 void onPlaybackStarted(int duration, ScheduledExecutorService executorService); 84 void onPlaybackStopped(); 85 void onSpeakerphoneOn(boolean on); 86 void setClipPosition(int clipPositionInMillis, int clipLengthInMillis); 87 void setSuccess(); 88 void setFetchContentTimeout(); 89 void setIsFetchingContent(); 90 void onVoicemailArchiveSucceded(Uri voicemailUri); 91 void onVoicemailArchiveFailed(Uri voicemailUri); 92 void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri); 93 void resetSeekBar(); 94 } 95 96 public interface OnVoicemailDeletedListener { 97 void onVoicemailDeleted(Uri uri); 98 void onVoicemailDeleteUndo(); 99 void onVoicemailDeletedInDatabase(); 100 } 101 102 /** The enumeration of {@link AsyncTask} objects we use in this class. */ 103 public enum Tasks { 104 CHECK_FOR_CONTENT, 105 CHECK_CONTENT_AFTER_CHANGE, 106 ARCHIVE_VOICEMAIL 107 } 108 109 protected interface OnContentCheckedListener { 110 void onContentChecked(boolean hasContent); 111 } 112 113 private static final String[] HAS_CONTENT_PROJECTION = new String[] { 114 VoicemailContract.Voicemails.HAS_CONTENT, 115 VoicemailContract.Voicemails.DURATION 116 }; 117 118 private static final int NUMBER_OF_THREADS_IN_POOL = 2; 119 // Time to wait for content to be fetched before timing out. 120 private static final long FETCH_CONTENT_TIMEOUT_MS = 20000; 121 122 private static final String VOICEMAIL_URI_KEY = 123 VoicemailPlaybackPresenter.class.getName() + ".VOICEMAIL_URI"; 124 private static final String IS_PREPARED_KEY = 125 VoicemailPlaybackPresenter.class.getName() + ".IS_PREPARED"; 126 // If present in the saved instance bundle, we should not resume playback on create. 127 private static final String IS_PLAYING_STATE_KEY = 128 VoicemailPlaybackPresenter.class.getName() + ".IS_PLAYING_STATE_KEY"; 129 // If present in the saved instance bundle, indicates where to set the playback slider. 130 private static final String CLIP_POSITION_KEY = 131 VoicemailPlaybackPresenter.class.getName() + ".CLIP_POSITION_KEY"; 132 private static final String IS_SPEAKERPHONE_ON_KEY = 133 VoicemailPlaybackPresenter.class.getName() + ".IS_SPEAKER_PHONE_ON"; 134 public static final int PLAYBACK_REQUEST = 0; 135 public static final int ARCHIVE_REQUEST = 1; 136 public static final int SHARE_REQUEST = 2; 137 138 /** 139 * The most recently cached duration. We cache this since we don't want to keep requesting it 140 * from the player, as this can easily lead to throwing {@link IllegalStateException} (any time 141 * the player is released, it's illegal to ask for the duration). 142 */ 143 private final AtomicInteger mDuration = new AtomicInteger(0); 144 145 private static VoicemailPlaybackPresenter sInstance; 146 147 private Activity mActivity; 148 protected Context mContext; 149 private PlaybackView mView; 150 protected Uri mVoicemailUri; 151 152 protected MediaPlayer mMediaPlayer; 153 private int mPosition; 154 private boolean mIsPlaying; 155 // MediaPlayer crashes on some method calls if not prepared but does not have a method which 156 // exposes its prepared state. Store this locally, so we can check and prevent crashes. 157 private boolean mIsPrepared; 158 private boolean mIsSpeakerphoneOn; 159 160 private boolean mShouldResumePlaybackAfterSeeking; 161 private int mInitialOrientation; 162 163 // Used to run async tasks that need to interact with the UI. 164 protected AsyncTaskExecutor mAsyncTaskExecutor; 165 private static ScheduledExecutorService mScheduledExecutorService; 166 /** 167 * Used to handle the result of a successful or time-out fetch result. 168 * <p> 169 * This variable is thread-contained, accessed only on the ui thread. 170 */ 171 private FetchResultHandler mFetchResultHandler; 172 private final List<FetchResultHandler> mArchiveResultHandlers = new ArrayList<>(); 173 private Handler mHandler = new Handler(); 174 private PowerManager.WakeLock mProximityWakeLock; 175 private VoicemailAudioManager mVoicemailAudioManager; 176 177 private OnVoicemailDeletedListener mOnVoicemailDeletedListener; 178 private final VoicemailAsyncTaskUtil mVoicemailAsyncTaskUtil; 179 180 /** 181 * Obtain singleton instance of this class. Use a single instance to provide a consistent 182 * listener to the AudioManager when requesting and abandoning audio focus. 183 * 184 * Otherwise, after rotation the previous listener will still be active but a new listener 185 * will be provided to calls to the AudioManager, which is bad. For example, abandoning 186 * audio focus with the new listeners results in an AUDIO_FOCUS_GAIN callback to the 187 * previous listener, which is the opposite of the intended behavior. 188 */ 189 public static VoicemailPlaybackPresenter getInstance( 190 Activity activity, Bundle savedInstanceState) { 191 if (sInstance == null) { 192 sInstance = new VoicemailPlaybackPresenter(activity); 193 } 194 195 sInstance.init(activity, savedInstanceState); 196 return sInstance; 197 } 198 199 /** 200 * Initialize variables which are activity-independent and state-independent. 201 */ 202 protected VoicemailPlaybackPresenter(Activity activity) { 203 Context context = activity.getApplicationContext(); 204 mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor(); 205 mVoicemailAudioManager = new VoicemailAudioManager(context, this); 206 mVoicemailAsyncTaskUtil = new VoicemailAsyncTaskUtil(context.getContentResolver()); 207 PowerManager powerManager = 208 (PowerManager) context.getSystemService(Context.POWER_SERVICE); 209 if (powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { 210 mProximityWakeLock = powerManager.newWakeLock( 211 PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG); 212 } 213 } 214 215 /** 216 * Update variables which are activity-dependent or state-dependent. 217 */ 218 protected void init(Activity activity, Bundle savedInstanceState) { 219 mActivity = activity; 220 mContext = activity; 221 222 mInitialOrientation = mContext.getResources().getConfiguration().orientation; 223 mActivity.setVolumeControlStream(VoicemailAudioManager.PLAYBACK_STREAM); 224 225 if (savedInstanceState != null) { 226 // Restores playback state when activity is recreated, such as after rotation. 227 mVoicemailUri = (Uri) savedInstanceState.getParcelable(VOICEMAIL_URI_KEY); 228 mIsPrepared = savedInstanceState.getBoolean(IS_PREPARED_KEY); 229 mPosition = savedInstanceState.getInt(CLIP_POSITION_KEY, 0); 230 mIsPlaying = savedInstanceState.getBoolean(IS_PLAYING_STATE_KEY, false); 231 mIsSpeakerphoneOn = savedInstanceState.getBoolean(IS_SPEAKERPHONE_ON_KEY, false); 232 } 233 234 if (mMediaPlayer == null) { 235 mIsPrepared = false; 236 mIsPlaying = false; 237 } 238 } 239 240 /** 241 * Must be invoked when the parent Activity is saving it state. 242 */ 243 public void onSaveInstanceState(Bundle outState) { 244 if (mView != null) { 245 outState.putParcelable(VOICEMAIL_URI_KEY, mVoicemailUri); 246 outState.putBoolean(IS_PREPARED_KEY, mIsPrepared); 247 outState.putInt(CLIP_POSITION_KEY, mView.getDesiredClipPosition()); 248 outState.putBoolean(IS_PLAYING_STATE_KEY, mIsPlaying); 249 outState.putBoolean(IS_SPEAKERPHONE_ON_KEY, mIsSpeakerphoneOn); 250 } 251 } 252 253 /** 254 * Specify the view which this presenter controls and the voicemail to prepare to play. 255 */ 256 public void setPlaybackView( 257 PlaybackView view, Uri voicemailUri, boolean startPlayingImmediately) { 258 mView = view; 259 mView.setPresenter(this, voicemailUri); 260 261 // Handles cases where the same entry is binded again when scrolling in list, or where 262 // the MediaPlayer was retained after an orientation change. 263 if (mMediaPlayer != null && mIsPrepared && voicemailUri.equals(mVoicemailUri)) { 264 // If the voicemail card was rebinded, we need to set the position to the appropriate 265 // point. Since we retain the media player, we can just set it to the position of the 266 // media player. 267 mPosition = mMediaPlayer.getCurrentPosition(); 268 onPrepared(mMediaPlayer); 269 } else { 270 if (!voicemailUri.equals(mVoicemailUri)) { 271 mVoicemailUri = voicemailUri; 272 mPosition = 0; 273 // Default to earpiece. 274 setSpeakerphoneOn(false); 275 mVoicemailAudioManager.setSpeakerphoneOn(false); 276 } else { 277 // Update the view to the current speakerphone state. 278 mView.onSpeakerphoneOn(mIsSpeakerphoneOn); 279 } 280 /* 281 * Check to see if the content field in the DB is set. If set, we proceed to 282 * prepareContent() method. We get the duration of the voicemail from the query and set 283 * it if the content is not available. 284 */ 285 checkForContent(new OnContentCheckedListener() { 286 @Override 287 public void onContentChecked(boolean hasContent) { 288 if (hasContent) { 289 prepareContent(); 290 } else if (mView != null) { 291 mView.resetSeekBar(); 292 mView.setClipPosition(0, mDuration.get()); 293 } 294 } 295 }); 296 297 if (startPlayingImmediately) { 298 // Since setPlaybackView can get called during the view binding process, we don't 299 // want to reset mIsPlaying to false if the user is currently playing the 300 // voicemail and the view is rebound. 301 mIsPlaying = startPlayingImmediately; 302 } 303 } 304 } 305 306 /** 307 * Reset the presenter for playback back to its original state. 308 */ 309 public void resetAll() { 310 pausePresenter(true); 311 312 mView = null; 313 mVoicemailUri = null; 314 } 315 316 /** 317 * When navigating away from voicemail playback, we need to release the media player, 318 * pause the UI and save the position. 319 * 320 * @param reset {@code true} if we want to reset the position of the playback, {@code false} if 321 * we want to retain the current position (in case we return to the voicemail). 322 */ 323 public void pausePresenter(boolean reset) { 324 if (mMediaPlayer != null) { 325 mMediaPlayer.release(); 326 mMediaPlayer = null; 327 } 328 329 disableProximitySensor(false /* waitForFarState */); 330 331 mIsPrepared = false; 332 mIsPlaying = false; 333 334 if (reset) { 335 // We want to reset the position whether or not the view is valid. 336 mPosition = 0; 337 } 338 339 if (mView != null) { 340 mView.onPlaybackStopped(); 341 if (reset) { 342 mView.setClipPosition(0, mDuration.get()); 343 } else { 344 mPosition = mView.getDesiredClipPosition(); 345 } 346 } 347 } 348 349 /** 350 * Must be invoked when the parent activity is resumed. 351 */ 352 public void onResume() { 353 mVoicemailAudioManager.registerReceivers(); 354 } 355 356 /** 357 * Must be invoked when the parent activity is paused. 358 */ 359 public void onPause() { 360 mVoicemailAudioManager.unregisterReceivers(); 361 362 if (mContext != null && mIsPrepared 363 && mInitialOrientation != mContext.getResources().getConfiguration().orientation) { 364 // If an orientation change triggers the pause, retain the MediaPlayer. 365 Log.d(TAG, "onPause: Orientation changed."); 366 return; 367 } 368 369 // Release the media player, otherwise there may be failures. 370 pausePresenter(false); 371 372 if (mActivity != null) { 373 mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON); 374 } 375 376 } 377 378 /** 379 * Must be invoked when the parent activity is destroyed. 380 */ 381 public void onDestroy() { 382 // Clear references to avoid leaks from the singleton instance. 383 mActivity = null; 384 mContext = null; 385 386 if (mScheduledExecutorService != null) { 387 mScheduledExecutorService.shutdown(); 388 mScheduledExecutorService = null; 389 } 390 391 if (!mArchiveResultHandlers.isEmpty()) { 392 for (FetchResultHandler fetchResultHandler : mArchiveResultHandlers) { 393 fetchResultHandler.destroy(); 394 } 395 mArchiveResultHandlers.clear(); 396 } 397 398 if (mFetchResultHandler != null) { 399 mFetchResultHandler.destroy(); 400 mFetchResultHandler = null; 401 } 402 } 403 404 /** 405 * Checks to see if we have content available for this voicemail. 406 */ 407 protected void checkForContent(final OnContentCheckedListener callback) { 408 mAsyncTaskExecutor.submit(Tasks.CHECK_FOR_CONTENT, new AsyncTask<Void, Void, Boolean>() { 409 @Override 410 public Boolean doInBackground(Void... params) { 411 return queryHasContent(mVoicemailUri); 412 } 413 414 @Override 415 public void onPostExecute(Boolean hasContent) { 416 callback.onContentChecked(hasContent); 417 } 418 }); 419 } 420 421 private boolean queryHasContent(Uri voicemailUri) { 422 if (voicemailUri == null || mContext == null) { 423 return false; 424 } 425 426 ContentResolver contentResolver = mContext.getContentResolver(); 427 Cursor cursor = contentResolver.query( 428 voicemailUri, null, null, null, null); 429 try { 430 if (cursor != null && cursor.moveToNext()) { 431 int duration = cursor.getInt(cursor.getColumnIndex( 432 VoicemailContract.Voicemails.DURATION)); 433 // Convert database duration (seconds) into mDuration (milliseconds) 434 mDuration.set(duration > 0 ? duration * 1000 : 0); 435 return cursor.getInt(cursor.getColumnIndex( 436 VoicemailContract.Voicemails.HAS_CONTENT)) == 1; 437 } 438 } finally { 439 MoreCloseables.closeQuietly(cursor); 440 } 441 return false; 442 } 443 444 /** 445 * Makes a broadcast request to ask that a voicemail source fetch this content. 446 * <p> 447 * This method <b>must be called on the ui thread</b>. 448 * <p> 449 * This method will be called when we realise that we don't have content for this voicemail. It 450 * will trigger a broadcast to request that the content be downloaded. It will add a listener to 451 * the content resolver so that it will be notified when the has_content field changes. It will 452 * also set a timer. If the has_content field changes to true within the allowed time, we will 453 * proceed to {@link #prepareContent()}. If the has_content field does not 454 * become true within the allowed time, we will update the ui to reflect the fact that content 455 * was not available. 456 * 457 * @return whether issued request to fetch content 458 */ 459 protected boolean requestContent(int code) { 460 if (mContext == null || mVoicemailUri == null) { 461 return false; 462 } 463 464 FetchResultHandler tempFetchResultHandler = 465 new FetchResultHandler(new Handler(), mVoicemailUri, code); 466 467 switch (code) { 468 case ARCHIVE_REQUEST: 469 mArchiveResultHandlers.add(tempFetchResultHandler); 470 break; 471 default: 472 if (mFetchResultHandler != null) { 473 mFetchResultHandler.destroy(); 474 } 475 mView.setIsFetchingContent(); 476 mFetchResultHandler = tempFetchResultHandler; 477 break; 478 } 479 480 // Send voicemail fetch request. 481 Intent intent = new Intent(VoicemailContract.ACTION_FETCH_VOICEMAIL, mVoicemailUri); 482 mContext.sendBroadcast(intent); 483 return true; 484 } 485 486 @ThreadSafe 487 private class FetchResultHandler extends ContentObserver implements Runnable { 488 private AtomicBoolean mIsWaitingForResult = new AtomicBoolean(true); 489 private final Handler mFetchResultHandler; 490 private final Uri mVoicemailUri; 491 private final int mRequestCode; 492 493 public FetchResultHandler(Handler handler, Uri uri, int code) { 494 super(handler); 495 mFetchResultHandler = handler; 496 mRequestCode = code; 497 mVoicemailUri = uri; 498 if (mContext != null) { 499 mContext.getContentResolver().registerContentObserver( 500 mVoicemailUri, false, this); 501 mFetchResultHandler.postDelayed(this, FETCH_CONTENT_TIMEOUT_MS); 502 } 503 } 504 505 /** 506 * Stop waiting for content and notify UI if {@link FETCH_CONTENT_TIMEOUT_MS} has elapsed. 507 */ 508 @Override 509 public void run() { 510 if (mIsWaitingForResult.getAndSet(false) && mContext != null) { 511 mContext.getContentResolver().unregisterContentObserver(this); 512 if (mView != null) { 513 mView.setFetchContentTimeout(); 514 } 515 } 516 } 517 518 public void destroy() { 519 if (mIsWaitingForResult.getAndSet(false) && mContext != null) { 520 mContext.getContentResolver().unregisterContentObserver(this); 521 mFetchResultHandler.removeCallbacks(this); 522 } 523 } 524 525 @Override 526 public void onChange(boolean selfChange) { 527 mAsyncTaskExecutor.submit(Tasks.CHECK_CONTENT_AFTER_CHANGE, 528 new AsyncTask<Void, Void, Boolean>() { 529 530 @Override 531 public Boolean doInBackground(Void... params) { 532 return queryHasContent(mVoicemailUri); 533 } 534 535 @Override 536 public void onPostExecute(Boolean hasContent) { 537 if (hasContent && mContext != null && mIsWaitingForResult.getAndSet(false)) { 538 mContext.getContentResolver().unregisterContentObserver( 539 FetchResultHandler.this); 540 prepareContent(); 541 if (mRequestCode == ARCHIVE_REQUEST) { 542 startArchiveVoicemailTask(mVoicemailUri, true /* archivedByUser */); 543 } else if (mRequestCode == SHARE_REQUEST) { 544 startArchiveVoicemailTask(mVoicemailUri, false /* archivedByUser */); 545 } 546 } 547 } 548 }); 549 } 550 } 551 552 /** 553 * Prepares the voicemail content for playback. 554 * <p> 555 * This method will be called once we know that our voicemail has content (according to the 556 * content provider). this method asynchronously tries to prepare the data source through the 557 * media player. If preparation is successful, the media player will {@link #onPrepared()}, 558 * and it will call {@link #onError()} otherwise. 559 */ 560 protected void prepareContent() { 561 if (mView == null) { 562 return; 563 } 564 Log.d(TAG, "prepareContent"); 565 566 // Release the previous media player, otherwise there may be failures. 567 if (mMediaPlayer != null) { 568 mMediaPlayer.release(); 569 mMediaPlayer = null; 570 } 571 572 mView.disableUiElements(); 573 mIsPrepared = false; 574 575 try { 576 mMediaPlayer = new MediaPlayer(); 577 mMediaPlayer.setOnPreparedListener(this); 578 mMediaPlayer.setOnErrorListener(this); 579 mMediaPlayer.setOnCompletionListener(this); 580 581 mMediaPlayer.reset(); 582 mMediaPlayer.setDataSource(mContext, mVoicemailUri); 583 mMediaPlayer.setAudioStreamType(VoicemailAudioManager.PLAYBACK_STREAM); 584 mMediaPlayer.prepareAsync(); 585 } catch (IOException e) { 586 handleError(e); 587 } 588 } 589 590 /** 591 * Once the media player is prepared, enables the UI and adopts the appropriate playback state. 592 */ 593 @Override 594 public void onPrepared(MediaPlayer mp) { 595 if (mView == null) { 596 return; 597 } 598 Log.d(TAG, "onPrepared"); 599 mIsPrepared = true; 600 601 // Update the duration in the database if it was not previously retrieved 602 CallLogAsyncTaskUtil.updateVoicemailDuration(mContext, mVoicemailUri, 603 TimeUnit.MILLISECONDS.toSeconds(mMediaPlayer.getDuration())); 604 605 mDuration.set(mMediaPlayer.getDuration()); 606 607 Log.d(TAG, "onPrepared: mPosition=" + mPosition); 608 mView.setClipPosition(mPosition, mDuration.get()); 609 mView.enableUiElements(); 610 mView.setSuccess(); 611 mMediaPlayer.seekTo(mPosition); 612 613 if (mIsPlaying) { 614 resumePlayback(); 615 } else { 616 pausePlayback(); 617 } 618 } 619 620 /** 621 * Invoked if preparing the media player fails, for example, if file is missing or the voicemail 622 * is an unknown file format that can't be played. 623 */ 624 @Override 625 public boolean onError(MediaPlayer mp, int what, int extra) { 626 handleError(new IllegalStateException("MediaPlayer error listener invoked: " + extra)); 627 return true; 628 } 629 630 protected void handleError(Exception e) { 631 Log.d(TAG, "handleError: Could not play voicemail " + e); 632 633 if (mIsPrepared) { 634 mMediaPlayer.release(); 635 mMediaPlayer = null; 636 mIsPrepared = false; 637 } 638 639 if (mView != null) { 640 mView.onPlaybackError(); 641 } 642 643 mPosition = 0; 644 mIsPlaying = false; 645 } 646 647 /** 648 * After done playing the voicemail clip, reset the clip position to the start. 649 */ 650 @Override 651 public void onCompletion(MediaPlayer mediaPlayer) { 652 pausePlayback(); 653 654 // Reset the seekbar position to the beginning. 655 mPosition = 0; 656 if (mView != null) { 657 mView.setClipPosition(0, mDuration.get()); 658 } 659 } 660 661 /** 662 * Only play voicemail when audio focus is granted. When it is lost (usually by another 663 * application requesting focus), pause playback. 664 * 665 * @param gainedFocus {@code true} if the audio focus was gained, {@code} false otherwise. 666 */ 667 public void onAudioFocusChange(boolean gainedFocus) { 668 if (mIsPlaying == gainedFocus) { 669 // Nothing new here, just exit. 670 return; 671 } 672 673 if (!mIsPlaying) { 674 resumePlayback(); 675 } else { 676 pausePlayback(); 677 } 678 } 679 680 /** 681 * Resumes voicemail playback at the clip position stored by the presenter. Null-op if already 682 * playing. 683 */ 684 public void resumePlayback() { 685 if (mView == null) { 686 return; 687 } 688 689 if (!mIsPrepared) { 690 /* 691 * Check content before requesting content to avoid duplicated requests. It is possible 692 * that the UI doesn't know content has arrived if the fetch took too long causing a 693 * timeout, but succeeded. 694 */ 695 checkForContent(new OnContentCheckedListener() { 696 @Override 697 public void onContentChecked(boolean hasContent) { 698 if (!hasContent) { 699 // No local content, download from server. Queue playing if the request was 700 // issued, 701 mIsPlaying = requestContent(PLAYBACK_REQUEST); 702 } else { 703 // Queue playing once the media play loaded the content. 704 mIsPlaying = true; 705 prepareContent(); 706 } 707 } 708 }); 709 return; 710 } 711 712 mIsPlaying = true; 713 714 if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) { 715 // Clamp the start position between 0 and the duration. 716 mPosition = Math.max(0, Math.min(mPosition, mDuration.get())); 717 718 mMediaPlayer.seekTo(mPosition); 719 720 try { 721 // Grab audio focus. 722 // Can throw RejectedExecutionException. 723 mVoicemailAudioManager.requestAudioFocus(); 724 mMediaPlayer.start(); 725 setSpeakerphoneOn(mIsSpeakerphoneOn); 726 } catch (RejectedExecutionException e) { 727 handleError(e); 728 } 729 } 730 731 Log.d(TAG, "Resumed playback at " + mPosition + "."); 732 mView.onPlaybackStarted(mDuration.get(), getScheduledExecutorServiceInstance()); 733 } 734 735 /** 736 * Pauses voicemail playback at the current position. Null-op if already paused. 737 */ 738 public void pausePlayback() { 739 if (!mIsPrepared) { 740 return; 741 } 742 743 mIsPlaying = false; 744 745 if (mMediaPlayer != null && mMediaPlayer.isPlaying()) { 746 mMediaPlayer.pause(); 747 } 748 749 mPosition = mMediaPlayer == null ? 0 : mMediaPlayer.getCurrentPosition(); 750 751 Log.d(TAG, "Paused playback at " + mPosition + "."); 752 753 if (mView != null) { 754 mView.onPlaybackStopped(); 755 } 756 757 mVoicemailAudioManager.abandonAudioFocus(); 758 759 if (mActivity != null) { 760 mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON); 761 } 762 disableProximitySensor(true /* waitForFarState */); 763 } 764 765 /** 766 * Pauses playback when the user starts seeking the position, and notes whether the voicemail is 767 * playing to know whether to resume playback once the user selects a new position. 768 */ 769 public void pausePlaybackForSeeking() { 770 if (mMediaPlayer != null) { 771 mShouldResumePlaybackAfterSeeking = mMediaPlayer.isPlaying(); 772 } 773 pausePlayback(); 774 } 775 776 public void resumePlaybackAfterSeeking(int desiredPosition) { 777 mPosition = desiredPosition; 778 if (mShouldResumePlaybackAfterSeeking) { 779 mShouldResumePlaybackAfterSeeking = false; 780 resumePlayback(); 781 } 782 } 783 784 /** 785 * Seek to position. This is called when user manually seek the playback. It could be either 786 * by touch or volume button while in talkback mode. 787 * @param position 788 */ 789 public void seek(int position) { 790 mPosition = position; 791 } 792 793 private void enableProximitySensor() { 794 if (mProximityWakeLock == null || mIsSpeakerphoneOn || !mIsPrepared 795 || mMediaPlayer == null || !mMediaPlayer.isPlaying()) { 796 return; 797 } 798 799 if (!mProximityWakeLock.isHeld()) { 800 Log.i(TAG, "Acquiring proximity wake lock"); 801 mProximityWakeLock.acquire(); 802 } else { 803 Log.i(TAG, "Proximity wake lock already acquired"); 804 } 805 } 806 807 private void disableProximitySensor(boolean waitForFarState) { 808 if (mProximityWakeLock == null) { 809 return; 810 } 811 if (mProximityWakeLock.isHeld()) { 812 Log.i(TAG, "Releasing proximity wake lock"); 813 int flags = waitForFarState ? PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY : 0; 814 mProximityWakeLock.release(flags); 815 } else { 816 Log.i(TAG, "Proximity wake lock already released"); 817 } 818 } 819 820 /** 821 * This is for use by UI interactions only. It simplifies UI logic. 822 */ 823 public void toggleSpeakerphone() { 824 mVoicemailAudioManager.setSpeakerphoneOn(!mIsSpeakerphoneOn); 825 setSpeakerphoneOn(!mIsSpeakerphoneOn); 826 } 827 828 /** 829 * This method only handles app-level changes to the speakerphone. Audio layer changes should 830 * be handled separately. This is so that the VoicemailAudioManager can trigger changes to 831 * the presenter without the presenter triggering the audio manager and duplicating actions. 832 */ 833 public void setSpeakerphoneOn(boolean on) { 834 if (mView == null) { 835 return; 836 } 837 838 mView.onSpeakerphoneOn(on); 839 840 mIsSpeakerphoneOn = on; 841 842 // This should run even if speakerphone is not being toggled because we may be switching 843 // from earpiece to headphone and vise versa. Also upon initial setup the default audio 844 // source is the earpiece, so we want to trigger the proximity sensor. 845 if (mIsPlaying) { 846 if (on || mVoicemailAudioManager.isWiredHeadsetPluggedIn()) { 847 disableProximitySensor(false /* waitForFarState */); 848 if (mIsPrepared && mMediaPlayer != null && mMediaPlayer.isPlaying()) { 849 mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON); 850 } 851 } else { 852 enableProximitySensor(); 853 if (mActivity != null) { 854 mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON); 855 } 856 } 857 } 858 } 859 860 public void setOnVoicemailDeletedListener(OnVoicemailDeletedListener listener) { 861 mOnVoicemailDeletedListener = listener; 862 } 863 864 public int getMediaPlayerPosition() { 865 return mIsPrepared && mMediaPlayer != null ? mMediaPlayer.getCurrentPosition() : 0; 866 } 867 868 public void notifyUiOfArchiveResult(Uri voicemailUri, boolean archived) { 869 if (mView == null) { 870 return; 871 } 872 if (archived) { 873 mView.onVoicemailArchiveSucceded(voicemailUri); 874 } else { 875 mView.onVoicemailArchiveFailed(voicemailUri); 876 } 877 } 878 879 /* package */ void onVoicemailDeleted() { 880 // Trampoline the event notification to the interested listener. 881 if (mOnVoicemailDeletedListener != null) { 882 mOnVoicemailDeletedListener.onVoicemailDeleted(mVoicemailUri); 883 } 884 } 885 886 /* package */ void onVoicemailDeleteUndo() { 887 // Trampoline the event notification to the interested listener. 888 if (mOnVoicemailDeletedListener != null) { 889 mOnVoicemailDeletedListener.onVoicemailDeleteUndo(); 890 } 891 } 892 893 /* package */ void onVoicemailDeletedInDatabase() { 894 // Trampoline the event notification to the interested listener. 895 if (mOnVoicemailDeletedListener != null) { 896 mOnVoicemailDeletedListener.onVoicemailDeletedInDatabase(); 897 } 898 } 899 900 private static synchronized ScheduledExecutorService getScheduledExecutorServiceInstance() { 901 if (mScheduledExecutorService == null) { 902 mScheduledExecutorService = Executors.newScheduledThreadPool(NUMBER_OF_THREADS_IN_POOL); 903 } 904 return mScheduledExecutorService; 905 } 906 907 /** 908 * If voicemail has already been downloaded, go straight to archiving. Otherwise, request 909 * the voicemail content first. 910 */ 911 public void archiveContent(final Uri voicemailUri, final boolean archivedByUser) { 912 checkForContent(new OnContentCheckedListener() { 913 @Override 914 public void onContentChecked(boolean hasContent) { 915 if (!hasContent) { 916 requestContent(archivedByUser ? ARCHIVE_REQUEST : SHARE_REQUEST); 917 } else { 918 startArchiveVoicemailTask(voicemailUri, archivedByUser); 919 } 920 } 921 }); 922 } 923 924 /** 925 * Asynchronous task used to archive a voicemail given its uri. 926 */ 927 protected void startArchiveVoicemailTask(final Uri voicemailUri, final boolean archivedByUser) { 928 mVoicemailAsyncTaskUtil.archiveVoicemailContent( 929 new VoicemailAsyncTaskUtil.OnArchiveVoicemailListener() { 930 @Override 931 public void onArchiveVoicemail(final Uri archivedVoicemailUri) { 932 if (archivedVoicemailUri == null) { 933 notifyUiOfArchiveResult(voicemailUri, false); 934 return; 935 } 936 937 if (archivedByUser) { 938 setArchivedVoicemailStatusAndUpdateUI(voicemailUri, 939 archivedVoicemailUri, true); 940 } else { 941 sendShareIntent(archivedVoicemailUri); 942 } 943 } 944 }, voicemailUri); 945 } 946 947 /** 948 * Sends the intent for sharing the voicemail file. 949 */ 950 protected void sendShareIntent(final Uri voicemailUri) { 951 mVoicemailAsyncTaskUtil.getVoicemailFilePath( 952 new VoicemailAsyncTaskUtil.OnGetArchivedVoicemailFilePathListener() { 953 @Override 954 public void onGetArchivedVoicemailFilePath(String filePath) { 955 mView.enableUiElements(); 956 if (filePath == null) { 957 mView.setFetchContentTimeout(); 958 return; 959 } 960 Uri voicemailFileUri = FileProvider.getUriForFile( 961 mContext, 962 mContext.getString(R.string.contacts_file_provider_authority), 963 new File(filePath)); 964 mContext.startActivity(Intent.createChooser( 965 getShareIntent(voicemailFileUri), 966 mContext.getResources().getText( 967 R.string.call_log_share_voicemail))); 968 } 969 }, voicemailUri); 970 } 971 972 /** Sets archived_by_user field to the given boolean and updates the URI. */ 973 private void setArchivedVoicemailStatusAndUpdateUI( 974 final Uri voicemailUri, 975 final Uri archivedVoicemailUri, 976 boolean status) { 977 mVoicemailAsyncTaskUtil.setVoicemailArchiveStatus( 978 new VoicemailAsyncTaskUtil.OnSetVoicemailArchiveStatusListener() { 979 @Override 980 public void onSetVoicemailArchiveStatus(boolean success) { 981 notifyUiOfArchiveResult(voicemailUri, success); 982 } 983 }, archivedVoicemailUri, status); 984 } 985 986 private Intent getShareIntent(Uri voicemailFileUri) { 987 Intent shareIntent = new Intent(); 988 shareIntent.setAction(Intent.ACTION_SEND); 989 shareIntent.putExtra(Intent.EXTRA_STREAM, voicemailFileUri); 990 shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 991 shareIntent.setType(mContext.getContentResolver() 992 .getType(voicemailFileUri)); 993 return shareIntent; 994 } 995 996 @VisibleForTesting 997 public boolean isPlaying() { 998 return mIsPlaying; 999 } 1000 1001 @VisibleForTesting 1002 public boolean isSpeakerphoneOn() { 1003 return mIsSpeakerphoneOn; 1004 } 1005 1006 @VisibleForTesting 1007 public void clearInstance() { 1008 sInstance = null; 1009 } 1010 } 1011