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.LogUtil; 51 import com.android.dialer.common.concurrent.AsyncTaskExecutor; 52 import com.android.dialer.common.concurrent.AsyncTaskExecutors; 53 import com.android.dialer.common.concurrent.DialerExecutor; 54 import com.android.dialer.common.concurrent.DialerExecutors; 55 import com.android.dialer.configprovider.ConfigProviderBindings; 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.telecom.TelecomUtil; 61 import com.android.dialer.util.PermissionsUtil; 62 import com.google.common.io.ByteStreams; 63 import java.io.File; 64 import java.io.IOException; 65 import java.io.InputStream; 66 import java.io.OutputStream; 67 import java.text.SimpleDateFormat; 68 import java.util.Date; 69 import java.util.Locale; 70 import java.util.concurrent.Executors; 71 import java.util.concurrent.RejectedExecutionException; 72 import java.util.concurrent.ScheduledExecutorService; 73 import java.util.concurrent.atomic.AtomicBoolean; 74 import java.util.concurrent.atomic.AtomicInteger; 75 import javax.annotation.concurrent.NotThreadSafe; 76 import javax.annotation.concurrent.ThreadSafe; 77 78 /** 79 * Contains the controlling logic for a voicemail playback in the call log. It is closely coupled to 80 * assumptions about the behaviors and lifecycle of the call log, in particular in the {@link 81 * CallLogFragment} and {@link CallLogAdapter}. 82 * 83 * <p>This controls a single {@link com.android.dialer.app.voicemail.VoicemailPlaybackLayout}. A 84 * single instance can be reused for different such layouts, using {@link #setPlaybackView}. This is 85 * to facilitate reuse across different voicemail call log entries. 86 * 87 * <p>This class is not thread safe. The thread policy for this class is thread-confinement, all 88 * calls into this class from outside must be done from the main UI thread. 89 */ 90 @NotThreadSafe 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 || mContext == 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 if (mContext != null && TelecomUtil.isInCall(mContext)) { 520 handleError(new IllegalStateException("Cannot play voicemail when call is in progress")); 521 return; 522 } 523 524 try { 525 mMediaPlayer = new MediaPlayer(); 526 mMediaPlayer.setOnPreparedListener(this); 527 mMediaPlayer.setOnErrorListener(this); 528 mMediaPlayer.setOnCompletionListener(this); 529 530 mMediaPlayer.reset(); 531 mMediaPlayer.setDataSource(mContext, mVoicemailUri); 532 mMediaPlayer.setAudioStreamType(VoicemailAudioManager.PLAYBACK_STREAM); 533 mMediaPlayer.prepareAsync(); 534 } catch (IOException e) { 535 handleError(e); 536 } 537 } 538 539 /** 540 * Once the media player is prepared, enables the UI and adopts the appropriate playback state. 541 */ 542 @Override onPrepared(MediaPlayer mp)543 public void onPrepared(MediaPlayer mp) { 544 if (mView == null || mContext == null) { 545 return; 546 } 547 LogUtil.d("VoicemailPlaybackPresenter.onPrepared", null); 548 mIsPrepared = true; 549 550 mDuration.set(mMediaPlayer.getDuration()); 551 552 LogUtil.d("VoicemailPlaybackPresenter.onPrepared", "mPosition=" + mPosition); 553 mView.setClipPosition(mPosition, mDuration.get()); 554 mView.enableUiElements(); 555 mView.setSuccess(); 556 if (!mp.isPlaying()) { 557 mMediaPlayer.seekTo(mPosition); 558 } 559 560 if (mIsPlaying) { 561 resumePlayback(); 562 } else { 563 pausePlayback(); 564 } 565 } 566 567 /** 568 * Invoked if preparing the media player fails, for example, if file is missing or the voicemail 569 * is an unknown file format that can't be played. 570 */ 571 @Override onError(MediaPlayer mp, int what, int extra)572 public boolean onError(MediaPlayer mp, int what, int extra) { 573 handleError(new IllegalStateException("MediaPlayer error listener invoked: " + extra)); 574 return true; 575 } 576 handleError(Exception e)577 protected void handleError(Exception e) { 578 LogUtil.e("VoicemailPlaybackPresenter.handlerError", "could not play voicemail", e); 579 580 if (mIsPrepared) { 581 mMediaPlayer.release(); 582 mMediaPlayer = null; 583 mIsPrepared = false; 584 } 585 586 if (mView != null) { 587 mView.onPlaybackError(); 588 } 589 590 mPosition = 0; 591 mIsPlaying = false; 592 showShareVoicemailButton(false); 593 } 594 595 /** After done playing the voicemail clip, reset the clip position to the start. */ 596 @Override onCompletion(MediaPlayer mediaPlayer)597 public void onCompletion(MediaPlayer mediaPlayer) { 598 pausePlayback(); 599 600 // Reset the seekbar position to the beginning. 601 mPosition = 0; 602 if (mView != null) { 603 mediaPlayer.seekTo(0); 604 mView.setClipPosition(0, mDuration.get()); 605 } 606 } 607 608 /** 609 * Only play voicemail when audio focus is granted. When it is lost (usually by another 610 * application requesting focus), pause playback. Audio focus gain/lost only triggers the focus is 611 * requested. Audio focus is requested when the user pressed play and abandoned when the user 612 * pressed pause or the audio has finished. Losing focus should not abandon focus as the voicemail 613 * should resume once the focus is returned. 614 * 615 * @param gainedFocus {@code true} if the audio focus was gained, {@code} false otherwise. 616 */ onAudioFocusChange(boolean gainedFocus)617 public void onAudioFocusChange(boolean gainedFocus) { 618 if (mIsPlaying == gainedFocus) { 619 // Nothing new here, just exit. 620 return; 621 } 622 623 if (gainedFocus) { 624 resumePlayback(); 625 } else { 626 pausePlayback(true); 627 } 628 } 629 630 /** 631 * Resumes voicemail playback at the clip position stored by the presenter. Null-op if already 632 * playing. 633 */ resumePlayback()634 public void resumePlayback() { 635 if (mView == null) { 636 return; 637 } 638 639 if (!mIsPrepared) { 640 /* 641 * Check content before requesting content to avoid duplicated requests. It is possible 642 * that the UI doesn't know content has arrived if the fetch took too long causing a 643 * timeout, but succeeded. 644 */ 645 checkForContent( 646 hasContent -> { 647 if (!hasContent) { 648 // No local content, download from server. Queue playing if the request was 649 // issued, 650 mIsPlaying = requestContent(PLAYBACK_REQUEST); 651 } else { 652 showShareVoicemailButton(true); 653 // Queue playing once the media play loaded the content. 654 mIsPlaying = true; 655 prepareContent(); 656 } 657 }); 658 return; 659 } 660 661 mIsPlaying = true; 662 663 mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON); 664 665 if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) { 666 // Clamp the start position between 0 and the duration. 667 mPosition = Math.max(0, Math.min(mPosition, mDuration.get())); 668 669 mMediaPlayer.seekTo(mPosition); 670 671 try { 672 // Grab audio focus. 673 // Can throw RejectedExecutionException. 674 mVoicemailAudioManager.requestAudioFocus(); 675 mMediaPlayer.start(); 676 setSpeakerphoneOn(mIsSpeakerphoneOn); 677 mVoicemailAudioManager.setSpeakerphoneOn(mIsSpeakerphoneOn); 678 } catch (RejectedExecutionException e) { 679 handleError(e); 680 } 681 } 682 683 LogUtil.d("VoicemailPlaybackPresenter.resumePlayback", "resumed playback at %d.", mPosition); 684 mView.onPlaybackStarted(mDuration.get(), getScheduledExecutorServiceInstance()); 685 } 686 687 /** Pauses voicemail playback at the current position. Null-op if already paused. */ pausePlayback()688 public void pausePlayback() { 689 pausePlayback(false); 690 } 691 pausePlayback(boolean keepFocus)692 private void pausePlayback(boolean keepFocus) { 693 if (!mIsPrepared) { 694 return; 695 } 696 697 mIsPlaying = false; 698 699 if (mMediaPlayer != null && mMediaPlayer.isPlaying()) { 700 mMediaPlayer.pause(); 701 } 702 703 mPosition = mMediaPlayer == null ? 0 : mMediaPlayer.getCurrentPosition(); 704 705 LogUtil.d("VoicemailPlaybackPresenter.pausePlayback", "paused playback at %d.", mPosition); 706 707 if (mView != null) { 708 mView.onPlaybackStopped(); 709 } 710 711 if (!keepFocus) { 712 mVoicemailAudioManager.abandonAudioFocus(); 713 } 714 if (mActivity != null) { 715 mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON); 716 } 717 disableProximitySensor(true /* waitForFarState */); 718 } 719 720 /** 721 * Pauses playback when the user starts seeking the position, and notes whether the voicemail is 722 * playing to know whether to resume playback once the user selects a new position. 723 */ pausePlaybackForSeeking()724 public void pausePlaybackForSeeking() { 725 if (mMediaPlayer != null) { 726 mShouldResumePlaybackAfterSeeking = mMediaPlayer.isPlaying(); 727 } 728 pausePlayback(true); 729 } 730 resumePlaybackAfterSeeking(int desiredPosition)731 public void resumePlaybackAfterSeeking(int desiredPosition) { 732 mPosition = desiredPosition; 733 if (mShouldResumePlaybackAfterSeeking) { 734 mShouldResumePlaybackAfterSeeking = false; 735 resumePlayback(); 736 } 737 } 738 739 /** 740 * Seek to position. This is called when user manually seek the playback. It could be either by 741 * touch or volume button while in talkback mode. 742 */ seek(int position)743 public void seek(int position) { 744 mPosition = position; 745 mMediaPlayer.seekTo(mPosition); 746 } 747 enableProximitySensor()748 private void enableProximitySensor() { 749 if (mProximityWakeLock == null 750 || mIsSpeakerphoneOn 751 || !mIsPrepared 752 || mMediaPlayer == null 753 || !mMediaPlayer.isPlaying()) { 754 return; 755 } 756 757 if (!mProximityWakeLock.isHeld()) { 758 LogUtil.i( 759 "VoicemailPlaybackPresenter.enableProximitySensor", "acquiring proximity wake lock"); 760 mProximityWakeLock.acquire(); 761 } else { 762 LogUtil.i( 763 "VoicemailPlaybackPresenter.enableProximitySensor", 764 "proximity wake lock already acquired"); 765 } 766 } 767 disableProximitySensor(boolean waitForFarState)768 private void disableProximitySensor(boolean waitForFarState) { 769 if (mProximityWakeLock == null) { 770 return; 771 } 772 if (mProximityWakeLock.isHeld()) { 773 LogUtil.i( 774 "VoicemailPlaybackPresenter.disableProximitySensor", "releasing proximity wake lock"); 775 int flags = waitForFarState ? PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY : 0; 776 mProximityWakeLock.release(flags); 777 } else { 778 LogUtil.i( 779 "VoicemailPlaybackPresenter.disableProximitySensor", 780 "proximity wake lock already released"); 781 } 782 } 783 784 /** This is for use by UI interactions only. It simplifies UI logic. */ toggleSpeakerphone()785 public void toggleSpeakerphone() { 786 mVoicemailAudioManager.setSpeakerphoneOn(!mIsSpeakerphoneOn); 787 setSpeakerphoneOn(!mIsSpeakerphoneOn); 788 } 789 setOnVoicemailDeletedListener(OnVoicemailDeletedListener listener)790 public void setOnVoicemailDeletedListener(OnVoicemailDeletedListener listener) { 791 mOnVoicemailDeletedListener = listener; 792 } 793 getMediaPlayerPosition()794 public int getMediaPlayerPosition() { 795 return mIsPrepared && mMediaPlayer != null ? mMediaPlayer.getCurrentPosition() : 0; 796 } 797 onVoicemailDeleted(CallLogListItemViewHolder viewHolder)798 void onVoicemailDeleted(CallLogListItemViewHolder viewHolder) { 799 if (mOnVoicemailDeletedListener != null) { 800 mOnVoicemailDeletedListener.onVoicemailDeleted(viewHolder, mVoicemailUri); 801 } 802 } 803 onVoicemailDeleteUndo(int adapterPosition)804 void onVoicemailDeleteUndo(int adapterPosition) { 805 if (mOnVoicemailDeletedListener != null) { 806 mOnVoicemailDeletedListener.onVoicemailDeleteUndo(mRowId, adapterPosition, mVoicemailUri); 807 } 808 } 809 onVoicemailDeletedInDatabase()810 void onVoicemailDeletedInDatabase() { 811 if (mOnVoicemailDeletedListener != null) { 812 mOnVoicemailDeletedListener.onVoicemailDeletedInDatabase(mRowId, mVoicemailUri); 813 } 814 } 815 816 @VisibleForTesting isPlaying()817 public boolean isPlaying() { 818 return mIsPlaying; 819 } 820 821 @VisibleForTesting isSpeakerphoneOn()822 public boolean isSpeakerphoneOn() { 823 return mIsSpeakerphoneOn; 824 } 825 826 /** 827 * This method only handles app-level changes to the speakerphone. Audio layer changes should be 828 * handled separately. This is so that the VoicemailAudioManager can trigger changes to the 829 * presenter without the presenter triggering the audio manager and duplicating actions. 830 */ setSpeakerphoneOn(boolean on)831 public void setSpeakerphoneOn(boolean on) { 832 if (mView == null) { 833 return; 834 } 835 836 mView.onSpeakerphoneOn(on); 837 838 mIsSpeakerphoneOn = on; 839 840 // This should run even if speakerphone is not being toggled because we may be switching 841 // from earpiece to headphone and vise versa. Also upon initial setup the default audio 842 // source is the earpiece, so we want to trigger the proximity sensor. 843 if (mIsPlaying) { 844 if (on || mVoicemailAudioManager.isWiredHeadsetPluggedIn()) { 845 disableProximitySensor(false /* waitForFarState */); 846 } else { 847 enableProximitySensor(); 848 } 849 } 850 } 851 852 @VisibleForTesting clearInstance()853 public void clearInstance() { 854 sInstance = null; 855 } 856 showShareVoicemailButton(boolean show)857 private void showShareVoicemailButton(boolean show) { 858 if (mContext == null) { 859 return; 860 } 861 if (isShareVoicemailAllowed(mContext) && shareVoicemailButtonView != null) { 862 if (show) { 863 Logger.get(mContext).logImpression(DialerImpression.Type.VVM_SHARE_VISIBLE); 864 } 865 LogUtil.d("VoicemailPlaybackPresenter.showShareVoicemailButton", "show: %b", show); 866 shareVoicemailButtonView.setVisibility(show ? View.VISIBLE : View.GONE); 867 } 868 } 869 isShareVoicemailAllowed(Context context)870 private static boolean isShareVoicemailAllowed(Context context) { 871 return ConfigProviderBindings.get(context).getBoolean(CONFIG_SHARE_VOICEMAIL_ALLOWED, true); 872 } 873 874 private static class ShareVoicemailWorker 875 implements DialerExecutor.Worker<Pair<Context, Uri>, Pair<Uri, String>> { 876 877 @Nullable 878 @Override doInBackground(Pair<Context, Uri> input)879 public Pair<Uri, String> doInBackground(Pair<Context, Uri> input) { 880 Context context = input.first; 881 Uri voicemailUri = input.second; 882 ContentResolver contentResolver = context.getContentResolver(); 883 try (Cursor callLogInfo = getCallLogInfoCursor(contentResolver, voicemailUri); 884 Cursor contentInfo = getContentInfoCursor(contentResolver, voicemailUri)) { 885 886 if (hasContent(callLogInfo) && hasContent(contentInfo)) { 887 String cachedName = callLogInfo.getString(CallLogQuery.CACHED_NAME); 888 String number = contentInfo.getString(contentInfo.getColumnIndex(Voicemails.NUMBER)); 889 long date = contentInfo.getLong(contentInfo.getColumnIndex(Voicemails.DATE)); 890 String mimeType = contentInfo.getString(contentInfo.getColumnIndex(Voicemails.MIME_TYPE)); 891 String transcription = 892 contentInfo.getString(contentInfo.getColumnIndex(Voicemails.TRANSCRIPTION)); 893 894 // Copy voicemail content to a new file. 895 // Please see reference in third_party/java_src/android_app/dialer/java/com/android/ 896 // dialer/app/res/xml/file_paths.xml for correct cache directory name. 897 File parentDir = new File(context.getCacheDir(), "my_cache"); 898 if (!parentDir.exists()) { 899 parentDir.mkdirs(); 900 } 901 File temporaryVoicemailFile = 902 new File(parentDir, getFileName(cachedName, number, mimeType, date)); 903 904 try (InputStream inputStream = contentResolver.openInputStream(voicemailUri); 905 OutputStream outputStream = 906 contentResolver.openOutputStream(Uri.fromFile(temporaryVoicemailFile))) { 907 if (inputStream != null && outputStream != null) { 908 ByteStreams.copy(inputStream, outputStream); 909 return new Pair<>( 910 FileProvider.getUriForFile( 911 context, Constants.get().getFileProviderAuthority(), temporaryVoicemailFile), 912 transcription); 913 } 914 } catch (IOException e) { 915 LogUtil.e( 916 "VoicemailAsyncTaskUtil.shareVoicemail", 917 "failed to copy voicemail content to new file: ", 918 e); 919 } 920 return null; 921 } 922 } 923 return null; 924 } 925 } 926 927 /** 928 * Share voicemail to be opened by user selected apps. This method will collect information, copy 929 * voicemail to a temporary file in background and launch a chooser intent to share it. 930 */ shareVoicemail()931 public void shareVoicemail() { 932 shareVoicemailExecutor.executeParallel(new Pair<>(mContext, mVoicemailUri)); 933 } 934 getFileName(String cachedName, String number, String mimeType, long date)935 private static String getFileName(String cachedName, String number, String mimeType, long date) { 936 String callerName = TextUtils.isEmpty(cachedName) ? number : cachedName; 937 SimpleDateFormat simpleDateFormat = 938 new SimpleDateFormat(VOICEMAIL_SHARE_FILE_NAME_DATE_FORMAT, Locale.getDefault()); 939 940 String fileExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); 941 942 return callerName 943 + "_" 944 + simpleDateFormat.format(new Date(date)) 945 + (TextUtils.isEmpty(fileExtension) ? "" : "." + fileExtension); 946 } 947 getShareIntent( Context context, Uri voicemailFileUri, String transcription)948 private static Intent getShareIntent( 949 Context context, Uri voicemailFileUri, String transcription) { 950 Intent shareIntent = new Intent(); 951 if (TextUtils.isEmpty(transcription)) { 952 shareIntent.setAction(Intent.ACTION_SEND); 953 shareIntent.putExtra(Intent.EXTRA_STREAM, voicemailFileUri); 954 shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 955 shareIntent.setType(context.getContentResolver().getType(voicemailFileUri)); 956 } else { 957 shareIntent.setAction(Intent.ACTION_SEND); 958 shareIntent.putExtra(Intent.EXTRA_STREAM, voicemailFileUri); 959 shareIntent.putExtra(Intent.EXTRA_TEXT, transcription); 960 shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 961 shareIntent.setType("*/*"); 962 } 963 964 return shareIntent; 965 } 966 hasContent(@ullable Cursor cursor)967 private static boolean hasContent(@Nullable Cursor cursor) { 968 return cursor != null && cursor.moveToFirst(); 969 } 970 971 @Nullable getCallLogInfoCursor(ContentResolver contentResolver, Uri voicemailUri)972 private static Cursor getCallLogInfoCursor(ContentResolver contentResolver, Uri voicemailUri) { 973 return contentResolver.query( 974 ContentUris.withAppendedId( 975 CallLog.Calls.CONTENT_URI_WITH_VOICEMAIL, ContentUris.parseId(voicemailUri)), 976 CallLogQuery.getProjection(), 977 null, 978 null, 979 null); 980 } 981 982 @Nullable getContentInfoCursor(ContentResolver contentResolver, Uri voicemailUri)983 private static Cursor getContentInfoCursor(ContentResolver contentResolver, Uri voicemailUri) { 984 return contentResolver.query( 985 voicemailUri, 986 new String[] { 987 Voicemails._ID, 988 Voicemails.NUMBER, 989 Voicemails.DATE, 990 Voicemails.MIME_TYPE, 991 Voicemails.TRANSCRIPTION, 992 }, 993 null, 994 null, 995 null); 996 } 997 998 /** The enumeration of {@link AsyncTask} objects we use in this class. */ 999 public enum Tasks { 1000 CHECK_FOR_CONTENT, 1001 CHECK_CONTENT_AFTER_CHANGE, 1002 SHARE_VOICEMAIL, 1003 SEND_FETCH_REQUEST 1004 } 1005 1006 /** Contract describing the behaviour we need from the ui we are controlling. */ 1007 public interface PlaybackView { 1008 getDesiredClipPosition()1009 int getDesiredClipPosition(); 1010 disableUiElements()1011 void disableUiElements(); 1012 enableUiElements()1013 void enableUiElements(); 1014 onPlaybackError()1015 void onPlaybackError(); 1016 onPlaybackStarted(int duration, ScheduledExecutorService executorService)1017 void onPlaybackStarted(int duration, ScheduledExecutorService executorService); 1018 onPlaybackStopped()1019 void onPlaybackStopped(); 1020 onSpeakerphoneOn(boolean on)1021 void onSpeakerphoneOn(boolean on); 1022 setClipPosition(int clipPositionInMillis, int clipLengthInMillis)1023 void setClipPosition(int clipPositionInMillis, int clipLengthInMillis); 1024 setSuccess()1025 void setSuccess(); 1026 setFetchContentTimeout()1027 void setFetchContentTimeout(); 1028 setIsFetchingContent()1029 void setIsFetchingContent(); 1030 setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri)1031 void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri); 1032 resetSeekBar()1033 void resetSeekBar(); 1034 } 1035 1036 public interface OnVoicemailDeletedListener { 1037 onVoicemailDeleted(CallLogListItemViewHolder viewHolder, Uri uri)1038 void onVoicemailDeleted(CallLogListItemViewHolder viewHolder, Uri uri); 1039 onVoicemailDeleteUndo(long rowId, int adaptorPosition, Uri uri)1040 void onVoicemailDeleteUndo(long rowId, int adaptorPosition, Uri uri); 1041 onVoicemailDeletedInDatabase(long rowId, Uri uri)1042 void onVoicemailDeletedInDatabase(long rowId, Uri uri); 1043 } 1044 1045 protected interface OnContentCheckedListener { 1046 onContentChecked(boolean hasContent)1047 void onContentChecked(boolean hasContent); 1048 } 1049 1050 @ThreadSafe 1051 private class FetchResultHandler extends ContentObserver implements Runnable { 1052 1053 private final Handler mFetchResultHandler; 1054 private final Uri mVoicemailUri; 1055 private AtomicBoolean mIsWaitingForResult = new AtomicBoolean(true); 1056 FetchResultHandler(Handler handler, Uri uri, int code)1057 public FetchResultHandler(Handler handler, Uri uri, int code) { 1058 super(handler); 1059 mFetchResultHandler = handler; 1060 mVoicemailUri = uri; 1061 if (mContext != null) { 1062 if (PermissionsUtil.hasReadVoicemailPermissions(mContext)) { 1063 mContext.getContentResolver().registerContentObserver(mVoicemailUri, false, this); 1064 } 1065 mFetchResultHandler.postDelayed(this, FETCH_CONTENT_TIMEOUT_MS); 1066 } 1067 } 1068 1069 /** Stop waiting for content and notify UI if {@link FETCH_CONTENT_TIMEOUT_MS} has elapsed. */ 1070 @Override run()1071 public void run() { 1072 if (mIsWaitingForResult.getAndSet(false) && mContext != null) { 1073 mContext.getContentResolver().unregisterContentObserver(this); 1074 if (mView != null) { 1075 mView.setFetchContentTimeout(); 1076 } 1077 } 1078 } 1079 destroy()1080 public void destroy() { 1081 if (mIsWaitingForResult.getAndSet(false) && mContext != null) { 1082 mContext.getContentResolver().unregisterContentObserver(this); 1083 mFetchResultHandler.removeCallbacks(this); 1084 } 1085 } 1086 1087 @Override onChange(boolean selfChange)1088 public void onChange(boolean selfChange) { 1089 mAsyncTaskExecutor.submit( 1090 Tasks.CHECK_CONTENT_AFTER_CHANGE, 1091 new AsyncTask<Void, Void, Boolean>() { 1092 1093 @Override 1094 public Boolean doInBackground(Void... params) { 1095 return queryHasContent(mVoicemailUri); 1096 } 1097 1098 @Override 1099 public void onPostExecute(Boolean hasContent) { 1100 if (hasContent && mContext != null && mIsWaitingForResult.getAndSet(false)) { 1101 mContext.getContentResolver().unregisterContentObserver(FetchResultHandler.this); 1102 showShareVoicemailButton(true); 1103 prepareContent(); 1104 } 1105 } 1106 }); 1107 } 1108 } 1109 } 1110