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