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