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.app.Activity; 20 import android.content.ContentResolver; 21 import android.content.ContentUris; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.database.ContentObserver; 25 import android.database.Cursor; 26 import android.media.AudioManager; 27 import android.media.MediaPlayer; 28 import android.net.Uri; 29 import android.os.AsyncTask; 30 import android.os.Bundle; 31 import android.os.Handler; 32 import android.os.PowerManager; 33 import android.provider.CallLog; 34 import android.provider.VoicemailContract; 35 import android.provider.VoicemailContract.Voicemails; 36 import android.support.annotation.MainThread; 37 import android.support.annotation.Nullable; 38 import android.support.annotation.VisibleForTesting; 39 import android.support.v4.content.FileProvider; 40 import android.text.TextUtils; 41 import android.util.Pair; 42 import android.view.View; 43 import android.view.WindowManager.LayoutParams; 44 import android.webkit.MimeTypeMap; 45 import com.android.common.io.MoreCloseables; 46 import com.android.dialer.app.R; 47 import com.android.dialer.app.calllog.CallLogListItemViewHolder; 48 import com.android.dialer.common.Assert; 49 import com.android.dialer.common.LogUtil; 50 import com.android.dialer.common.concurrent.AsyncTaskExecutor; 51 import com.android.dialer.common.concurrent.AsyncTaskExecutors; 52 import com.android.dialer.common.concurrent.DialerExecutor; 53 import com.android.dialer.common.concurrent.DialerExecutorComponent; 54 import com.android.dialer.configprovider.ConfigProviderComponent; 55 import com.android.dialer.constants.Constants; 56 import com.android.dialer.logging.DialerImpression; 57 import com.android.dialer.logging.Logger; 58 import com.android.dialer.phonenumbercache.CallLogQuery; 59 import com.android.dialer.strictmode.StrictModeUtils; 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 public class VoicemailPlaybackPresenter 92 implements MediaPlayer.OnPreparedListener, 93 MediaPlayer.OnCompletionListener, 94 MediaPlayer.OnErrorListener { 95 96 public static final int PLAYBACK_REQUEST = 0; 97 private static final int NUMBER_OF_THREADS_IN_POOL = 2; 98 // Time to wait for content to be fetched before timing out. 99 private static final long FETCH_CONTENT_TIMEOUT_MS = 20000; 100 private static final String VOICEMAIL_URI_KEY = 101 VoicemailPlaybackPresenter.class.getName() + ".VOICEMAIL_URI"; 102 private static final String IS_PREPARED_KEY = 103 VoicemailPlaybackPresenter.class.getName() + ".IS_PREPARED"; 104 // If present in the saved instance bundle, we should not resume playback on create. 105 private static final String IS_PLAYING_STATE_KEY = 106 VoicemailPlaybackPresenter.class.getName() + ".IS_PLAYING_STATE_KEY"; 107 // If present in the saved instance bundle, indicates where to set the playback slider. 108 private static final String CLIP_POSITION_KEY = 109 VoicemailPlaybackPresenter.class.getName() + ".CLIP_POSITION_KEY"; 110 private static final String IS_SPEAKERPHONE_ON_KEY = 111 VoicemailPlaybackPresenter.class.getName() + ".IS_SPEAKER_PHONE_ON"; 112 private static final String VOICEMAIL_SHARE_FILE_NAME_DATE_FORMAT = "MM-dd-yy_hhmmaa"; 113 private static final String CONFIG_SHARE_VOICEMAIL_ALLOWED = "share_voicemail_allowed"; 114 115 private static VoicemailPlaybackPresenter instance; 116 private static ScheduledExecutorService scheduledExecutorService; 117 /** 118 * The most recently cached duration. We cache this since we don't want to keep requesting it from 119 * the player, as this can easily lead to throwing {@link IllegalStateException} (any time the 120 * player is released, it's illegal to ask for the duration). 121 */ 122 private final AtomicInteger duration = new AtomicInteger(0); 123 124 protected Context context; 125 private long rowId; 126 protected Uri voicemailUri; 127 protected MediaPlayer mediaPlayer; 128 // Used to run async tasks that need to interact with the UI. 129 protected AsyncTaskExecutor asyncTaskExecutor; 130 private Activity activity; 131 private PlaybackView view; 132 private int position; 133 private boolean isPlaying; 134 // MediaPlayer crashes on some method calls if not prepared but does not have a method which 135 // exposes its prepared state. Store this locally, so we can check and prevent crashes. 136 private boolean isPrepared; 137 private boolean isSpeakerphoneOn; 138 139 private boolean shouldResumePlaybackAfterSeeking; 140 /** 141 * Used to handle the result of a successful or time-out fetch result. 142 * 143 * <p>This variable is thread-contained, accessed only on the ui thread. 144 */ 145 private FetchResultHandler fetchResultHandler; 146 147 private PowerManager.WakeLock proximityWakeLock; 148 private VoicemailAudioManager voicemailAudioManager; 149 private OnVoicemailDeletedListener onVoicemailDeletedListener; 150 private View shareVoicemailButtonView; 151 152 private DialerExecutor<Pair<Context, Uri>> shareVoicemailExecutor; 153 154 /** Initialize variables which are activity-independent and state-independent. */ VoicemailPlaybackPresenter(Activity activity)155 protected VoicemailPlaybackPresenter(Activity activity) { 156 Context context = activity.getApplicationContext(); 157 asyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor(); 158 voicemailAudioManager = new VoicemailAudioManager(context, this); 159 PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); 160 if (powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { 161 proximityWakeLock = 162 powerManager.newWakeLock( 163 PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, "VoicemailPlaybackPresenter"); 164 } 165 } 166 167 /** 168 * Obtain singleton instance of this class. Use a single instance to provide a consistent listener 169 * to the AudioManager when requesting and abandoning audio focus. 170 * 171 * <p>Otherwise, after rotation the previous listener will still be active but a new listener will 172 * be provided to calls to the AudioManager, which is bad. For example, abandoning audio focus 173 * with the new listeners results in an AUDIO_FOCUS_GAIN callback to the previous listener, which 174 * is the opposite of the intended behavior. 175 */ 176 @MainThread getInstance( Activity activity, Bundle savedInstanceState)177 public static VoicemailPlaybackPresenter getInstance( 178 Activity activity, Bundle savedInstanceState) { 179 if (instance == null) { 180 instance = new VoicemailPlaybackPresenter(activity); 181 } 182 183 instance.init(activity, savedInstanceState); 184 return instance; 185 } 186 getScheduledExecutorServiceInstance()187 private static synchronized ScheduledExecutorService getScheduledExecutorServiceInstance() { 188 if (scheduledExecutorService == null) { 189 scheduledExecutorService = Executors.newScheduledThreadPool(NUMBER_OF_THREADS_IN_POOL); 190 } 191 return scheduledExecutorService; 192 } 193 194 /** Update variables which are activity-dependent or state-dependent. */ 195 @MainThread init(Activity activity, Bundle savedInstanceState)196 protected void init(Activity activity, Bundle savedInstanceState) { 197 Assert.isMainThread(); 198 this.activity = activity; 199 context = activity; 200 201 if (savedInstanceState != null) { 202 // Restores playback state when activity is recreated, such as after rotation. 203 voicemailUri = savedInstanceState.getParcelable(VOICEMAIL_URI_KEY); 204 isPrepared = savedInstanceState.getBoolean(IS_PREPARED_KEY); 205 position = savedInstanceState.getInt(CLIP_POSITION_KEY, 0); 206 isPlaying = savedInstanceState.getBoolean(IS_PLAYING_STATE_KEY, false); 207 isSpeakerphoneOn = savedInstanceState.getBoolean(IS_SPEAKERPHONE_ON_KEY, false); 208 AudioManager audioManager = activity.getSystemService(AudioManager.class); 209 audioManager.setSpeakerphoneOn(isSpeakerphoneOn); 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. */ onSaveInstanceState(Bundle outState)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. */ setPlaybackView( PlaybackView view, long rowId, Uri voicemailUri, final boolean startPlayingImmediately, View shareVoicemailButtonView)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. */ resetAll()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 */ pausePresenter(boolean reset)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. */ onResume()357 public void onResume() { 358 voicemailAudioManager.registerReceivers(); 359 } 360 361 /** Must be invoked when the parent activity is paused. */ onPause()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. */ onDestroy()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. */ checkForContent(final OnContentCheckedListener callback)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 queryHasContent(Uri voicemailUri)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 */ requestContent(int code)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 */ prepareContent()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 prepareMediaPlayer()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 onPrepared(MediaPlayer mp)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 onError(MediaPlayer mp, int what, int extra)574 public boolean onError(MediaPlayer mp, int what, int extra) { 575 handleError(new IllegalStateException("MediaPlayer error listener invoked: " + extra)); 576 return true; 577 } 578 handleError(Exception e)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 } 595 596 /** After done playing the voicemail clip, reset the clip position to the start. */ 597 @Override onCompletion(MediaPlayer mediaPlayer)598 public void onCompletion(MediaPlayer mediaPlayer) { 599 pausePlayback(); 600 601 // Reset the seekbar position to the beginning. 602 position = 0; 603 if (view != null) { 604 mediaPlayer.seekTo(0); 605 view.setClipPosition(0, duration.get()); 606 } 607 } 608 609 /** 610 * Only play voicemail when audio focus is granted. When it is lost (usually by another 611 * application requesting focus), pause playback. Audio focus gain/lost only triggers the focus is 612 * requested. Audio focus is requested when the user pressed play and abandoned when the user 613 * pressed pause or the audio has finished. Losing focus should not abandon focus as the voicemail 614 * should resume once the focus is returned. 615 * 616 * @param gainedFocus {@code true} if the audio focus was gained, {@code} false otherwise. 617 */ onAudioFocusChange(boolean gainedFocus)618 public void onAudioFocusChange(boolean gainedFocus) { 619 if (isPlaying == gainedFocus) { 620 // Nothing new here, just exit. 621 return; 622 } 623 624 if (gainedFocus) { 625 resumePlayback(); 626 } else { 627 pausePlayback(true); 628 } 629 } 630 631 /** 632 * Resumes voicemail playback at the clip position stored by the presenter. Null-op if already 633 * playing. 634 */ resumePlayback()635 public void resumePlayback() { 636 if (view == null) { 637 return; 638 } 639 640 if (!isPrepared) { 641 /* 642 * Check content before requesting content to avoid duplicated requests. It is possible 643 * that the UI doesn't know content has arrived if the fetch took too long causing a 644 * timeout, but succeeded. 645 */ 646 checkForContent( 647 hasContent -> { 648 if (!hasContent) { 649 // No local content, download from server. Queue playing if the request was 650 // issued, 651 isPlaying = requestContent(PLAYBACK_REQUEST); 652 } else { 653 showShareVoicemailButton(true); 654 // Queue playing once the media play loaded the content. 655 isPlaying = true; 656 prepareContent(); 657 } 658 }); 659 return; 660 } 661 662 isPlaying = true; 663 664 activity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON); 665 666 if (mediaPlayer != null && !mediaPlayer.isPlaying()) { 667 // Clamp the start position between 0 and the duration. 668 position = Math.max(0, Math.min(position, duration.get())); 669 670 mediaPlayer.seekTo(position); 671 672 try { 673 // Grab audio focus. 674 // Can throw RejectedExecutionException. 675 voicemailAudioManager.requestAudioFocus(); 676 mediaPlayer.start(); 677 setSpeakerphoneOn(isSpeakerphoneOn); 678 voicemailAudioManager.setSpeakerphoneOn(isSpeakerphoneOn); 679 } catch (RejectedExecutionException e) { 680 handleError(e); 681 } 682 } 683 684 LogUtil.d("VoicemailPlaybackPresenter.resumePlayback", "resumed playback at %d.", position); 685 view.onPlaybackStarted(duration.get(), getScheduledExecutorServiceInstance()); 686 } 687 688 /** Pauses voicemail playback at the current position. Null-op if already paused. */ pausePlayback()689 public void pausePlayback() { 690 pausePlayback(false); 691 } 692 pausePlayback(boolean keepFocus)693 private void pausePlayback(boolean keepFocus) { 694 if (!isPrepared) { 695 return; 696 } 697 698 isPlaying = false; 699 700 if (mediaPlayer != null && mediaPlayer.isPlaying()) { 701 mediaPlayer.pause(); 702 } 703 704 position = mediaPlayer == null ? 0 : mediaPlayer.getCurrentPosition(); 705 706 LogUtil.d("VoicemailPlaybackPresenter.pausePlayback", "paused playback at %d.", position); 707 708 if (view != null) { 709 view.onPlaybackStopped(); 710 } 711 712 if (!keepFocus) { 713 voicemailAudioManager.abandonAudioFocus(); 714 } 715 if (activity != null) { 716 activity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON); 717 } 718 disableProximitySensor(true /* waitForFarState */); 719 } 720 721 /** 722 * Pauses playback when the user starts seeking the position, and notes whether the voicemail is 723 * playing to know whether to resume playback once the user selects a new position. 724 */ pausePlaybackForSeeking()725 public void pausePlaybackForSeeking() { 726 if (mediaPlayer != null) { 727 shouldResumePlaybackAfterSeeking = mediaPlayer.isPlaying(); 728 } 729 pausePlayback(true); 730 } 731 resumePlaybackAfterSeeking(int desiredPosition)732 public void resumePlaybackAfterSeeking(int desiredPosition) { 733 position = desiredPosition; 734 if (shouldResumePlaybackAfterSeeking) { 735 shouldResumePlaybackAfterSeeking = false; 736 resumePlayback(); 737 } 738 } 739 740 /** 741 * Seek to position. This is called when user manually seek the playback. It could be either by 742 * touch or volume button while in talkback mode. 743 */ seek(int position)744 public void seek(int position) { 745 this.position = position; 746 mediaPlayer.seekTo(this.position); 747 } 748 enableProximitySensor()749 private void enableProximitySensor() { 750 if (proximityWakeLock == null 751 || isSpeakerphoneOn 752 || !isPrepared 753 || mediaPlayer == null 754 || !mediaPlayer.isPlaying()) { 755 return; 756 } 757 758 if (!proximityWakeLock.isHeld()) { 759 LogUtil.i( 760 "VoicemailPlaybackPresenter.enableProximitySensor", "acquiring proximity wake lock"); 761 proximityWakeLock.acquire(); 762 } else { 763 LogUtil.i( 764 "VoicemailPlaybackPresenter.enableProximitySensor", 765 "proximity wake lock already acquired"); 766 } 767 } 768 disableProximitySensor(boolean waitForFarState)769 private void disableProximitySensor(boolean waitForFarState) { 770 if (proximityWakeLock == null) { 771 return; 772 } 773 if (proximityWakeLock.isHeld()) { 774 LogUtil.i( 775 "VoicemailPlaybackPresenter.disableProximitySensor", "releasing proximity wake lock"); 776 int flags = waitForFarState ? PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY : 0; 777 proximityWakeLock.release(flags); 778 } else { 779 LogUtil.i( 780 "VoicemailPlaybackPresenter.disableProximitySensor", 781 "proximity wake lock already released"); 782 } 783 } 784 785 /** This is for use by UI interactions only. It simplifies UI logic. */ toggleSpeakerphone()786 public void toggleSpeakerphone() { 787 voicemailAudioManager.setSpeakerphoneOn(!isSpeakerphoneOn); 788 setSpeakerphoneOn(!isSpeakerphoneOn); 789 } 790 setOnVoicemailDeletedListener(OnVoicemailDeletedListener listener)791 public void setOnVoicemailDeletedListener(OnVoicemailDeletedListener listener) { 792 onVoicemailDeletedListener = listener; 793 } 794 getMediaPlayerPosition()795 public int getMediaPlayerPosition() { 796 return isPrepared && mediaPlayer != null ? mediaPlayer.getCurrentPosition() : 0; 797 } 798 onVoicemailDeleted(CallLogListItemViewHolder viewHolder)799 void onVoicemailDeleted(CallLogListItemViewHolder viewHolder) { 800 if (onVoicemailDeletedListener != null) { 801 onVoicemailDeletedListener.onVoicemailDeleted(viewHolder, voicemailUri); 802 } 803 } 804 onVoicemailDeleteUndo(int adapterPosition)805 void onVoicemailDeleteUndo(int adapterPosition) { 806 if (onVoicemailDeletedListener != null) { 807 onVoicemailDeletedListener.onVoicemailDeleteUndo(rowId, adapterPosition, voicemailUri); 808 } 809 } 810 onVoicemailDeletedInDatabase()811 void onVoicemailDeletedInDatabase() { 812 if (onVoicemailDeletedListener != null) { 813 onVoicemailDeletedListener.onVoicemailDeletedInDatabase(rowId, voicemailUri); 814 } 815 } 816 817 @VisibleForTesting isPlaying()818 public boolean isPlaying() { 819 return isPlaying; 820 } 821 822 @VisibleForTesting isSpeakerphoneOn()823 public boolean isSpeakerphoneOn() { 824 return isSpeakerphoneOn; 825 } 826 827 /** 828 * This method only handles app-level changes to the speakerphone. Audio layer changes should be 829 * handled separately. This is so that the VoicemailAudioManager can trigger changes to the 830 * presenter without the presenter triggering the audio manager and duplicating actions. 831 */ setSpeakerphoneOn(boolean on)832 public void setSpeakerphoneOn(boolean on) { 833 if (view == null) { 834 return; 835 } 836 837 view.onSpeakerphoneOn(on); 838 839 isSpeakerphoneOn = on; 840 841 // This should run even if speakerphone is not being toggled because we may be switching 842 // from earpiece to headphone and vise versa. Also upon initial setup the default audio 843 // source is the earpiece, so we want to trigger the proximity sensor. 844 if (isPlaying) { 845 if (on || voicemailAudioManager.isWiredHeadsetPluggedIn()) { 846 disableProximitySensor(false /* waitForFarState */); 847 } else { 848 enableProximitySensor(); 849 } 850 } 851 } 852 853 @VisibleForTesting clearInstance()854 public void clearInstance() { 855 instance = null; 856 } 857 showShareVoicemailButton(boolean show)858 private void showShareVoicemailButton(boolean show) { 859 if (context == null) { 860 return; 861 } 862 if (isShareVoicemailAllowed(context) && shareVoicemailButtonView != null) { 863 if (show) { 864 Logger.get(context).logImpression(DialerImpression.Type.VVM_SHARE_VISIBLE); 865 } 866 LogUtil.d("VoicemailPlaybackPresenter.showShareVoicemailButton", "show: %b", show); 867 shareVoicemailButtonView.setVisibility(show ? View.VISIBLE : View.GONE); 868 } 869 } 870 isShareVoicemailAllowed(Context context)871 private static boolean isShareVoicemailAllowed(Context context) { 872 return ConfigProviderComponent.get(context) 873 .getConfigProvider() 874 .getBoolean(CONFIG_SHARE_VOICEMAIL_ALLOWED, true); 875 } 876 877 private static class ShareVoicemailWorker 878 implements DialerExecutor.Worker<Pair<Context, Uri>, Pair<Uri, String>> { 879 880 @Nullable 881 @Override doInBackground(Pair<Context, Uri> input)882 public Pair<Uri, String> doInBackground(Pair<Context, Uri> input) { 883 Context context = input.first; 884 Uri voicemailUri = input.second; 885 ContentResolver contentResolver = context.getContentResolver(); 886 try (Cursor callLogInfo = getCallLogInfoCursor(contentResolver, voicemailUri); 887 Cursor contentInfo = getContentInfoCursor(contentResolver, voicemailUri)) { 888 889 if (hasContent(callLogInfo) && hasContent(contentInfo)) { 890 String cachedName = callLogInfo.getString(CallLogQuery.CACHED_NAME); 891 String number = contentInfo.getString(contentInfo.getColumnIndex(Voicemails.NUMBER)); 892 long date = contentInfo.getLong(contentInfo.getColumnIndex(Voicemails.DATE)); 893 String mimeType = contentInfo.getString(contentInfo.getColumnIndex(Voicemails.MIME_TYPE)); 894 String transcription = 895 contentInfo.getString(contentInfo.getColumnIndex(Voicemails.TRANSCRIPTION)); 896 897 // Copy voicemail content to a new file. 898 // Please see reference in third_party/java_src/android_app/dialer/java/com/android/ 899 // dialer/app/res/xml/file_paths.xml for correct cache directory name. 900 File parentDir = new File(context.getCacheDir(), "my_cache"); 901 if (!parentDir.exists()) { 902 parentDir.mkdirs(); 903 } 904 File temporaryVoicemailFile = 905 new File(parentDir, getFileName(cachedName, number, mimeType, date)); 906 907 try (InputStream inputStream = contentResolver.openInputStream(voicemailUri); 908 OutputStream outputStream = 909 contentResolver.openOutputStream(Uri.fromFile(temporaryVoicemailFile))) { 910 if (inputStream != null && outputStream != null) { 911 ByteStreams.copy(inputStream, outputStream); 912 return new Pair<>( 913 FileProvider.getUriForFile( 914 context, Constants.get().getFileProviderAuthority(), temporaryVoicemailFile), 915 transcription); 916 } 917 } catch (IOException e) { 918 LogUtil.e( 919 "VoicemailAsyncTaskUtil.shareVoicemail", 920 "failed to copy voicemail content to new file: ", 921 e); 922 } 923 return null; 924 } 925 } 926 return null; 927 } 928 } 929 930 /** 931 * Share voicemail to be opened by user selected apps. This method will collect information, copy 932 * voicemail to a temporary file in background and launch a chooser intent to share it. 933 */ shareVoicemail()934 public void shareVoicemail() { 935 shareVoicemailExecutor.executeParallel(new Pair<>(context, voicemailUri)); 936 } 937 getFileName(String cachedName, String number, String mimeType, long date)938 private static String getFileName(String cachedName, String number, String mimeType, long date) { 939 String callerName = TextUtils.isEmpty(cachedName) ? number : cachedName; 940 SimpleDateFormat simpleDateFormat = 941 new SimpleDateFormat(VOICEMAIL_SHARE_FILE_NAME_DATE_FORMAT, Locale.getDefault()); 942 943 String fileExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); 944 945 return callerName 946 + "_" 947 + simpleDateFormat.format(new Date(date)) 948 + (TextUtils.isEmpty(fileExtension) ? "" : "." + fileExtension); 949 } 950 getShareIntent( Context context, Uri voicemailFileUri, String transcription)951 private static Intent getShareIntent( 952 Context context, Uri voicemailFileUri, String transcription) { 953 Intent shareIntent = new Intent(); 954 if (TextUtils.isEmpty(transcription)) { 955 shareIntent.setAction(Intent.ACTION_SEND); 956 shareIntent.putExtra(Intent.EXTRA_STREAM, voicemailFileUri); 957 shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 958 shareIntent.setType(context.getContentResolver().getType(voicemailFileUri)); 959 } else { 960 shareIntent.setAction(Intent.ACTION_SEND); 961 shareIntent.putExtra(Intent.EXTRA_STREAM, voicemailFileUri); 962 shareIntent.putExtra(Intent.EXTRA_TEXT, transcription); 963 shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 964 shareIntent.setType(context.getContentResolver().getType(voicemailFileUri)); 965 } 966 967 return shareIntent; 968 } 969 hasContent(@ullable Cursor cursor)970 private static boolean hasContent(@Nullable Cursor cursor) { 971 return cursor != null && cursor.moveToFirst(); 972 } 973 974 @Nullable getCallLogInfoCursor(ContentResolver contentResolver, Uri voicemailUri)975 private static Cursor getCallLogInfoCursor(ContentResolver contentResolver, Uri voicemailUri) { 976 return contentResolver.query( 977 ContentUris.withAppendedId( 978 CallLog.Calls.CONTENT_URI_WITH_VOICEMAIL, ContentUris.parseId(voicemailUri)), 979 CallLogQuery.getProjection(), 980 null, 981 null, 982 null); 983 } 984 985 @Nullable getContentInfoCursor(ContentResolver contentResolver, Uri voicemailUri)986 private static Cursor getContentInfoCursor(ContentResolver contentResolver, Uri voicemailUri) { 987 return contentResolver.query( 988 voicemailUri, 989 new String[] { 990 Voicemails._ID, 991 Voicemails.NUMBER, 992 Voicemails.DATE, 993 Voicemails.MIME_TYPE, 994 Voicemails.TRANSCRIPTION, 995 }, 996 null, 997 null, 998 null); 999 } 1000 1001 /** The enumeration of {@link AsyncTask} objects we use in this class. */ 1002 public enum Tasks { 1003 CHECK_FOR_CONTENT, 1004 CHECK_CONTENT_AFTER_CHANGE, 1005 SHARE_VOICEMAIL, 1006 SEND_FETCH_REQUEST 1007 } 1008 1009 /** Contract describing the behaviour we need from the ui we are controlling. */ 1010 public interface PlaybackView { 1011 getDesiredClipPosition()1012 int getDesiredClipPosition(); 1013 disableUiElements()1014 void disableUiElements(); 1015 enableUiElements()1016 void enableUiElements(); 1017 onPlaybackError()1018 void onPlaybackError(); 1019 onPlaybackStarted(int duration, ScheduledExecutorService executorService)1020 void onPlaybackStarted(int duration, ScheduledExecutorService executorService); 1021 onPlaybackStopped()1022 void onPlaybackStopped(); 1023 onSpeakerphoneOn(boolean on)1024 void onSpeakerphoneOn(boolean on); 1025 setClipPosition(int clipPositionInMillis, int clipLengthInMillis)1026 void setClipPosition(int clipPositionInMillis, int clipLengthInMillis); 1027 setSuccess()1028 void setSuccess(); 1029 setFetchContentTimeout()1030 void setFetchContentTimeout(); 1031 setIsFetchingContent()1032 void setIsFetchingContent(); 1033 setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri)1034 void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri); 1035 resetSeekBar()1036 void resetSeekBar(); 1037 } 1038 1039 public interface OnVoicemailDeletedListener { 1040 onVoicemailDeleted(CallLogListItemViewHolder viewHolder, Uri uri)1041 void onVoicemailDeleted(CallLogListItemViewHolder viewHolder, Uri uri); 1042 onVoicemailDeleteUndo(long rowId, int adaptorPosition, Uri uri)1043 void onVoicemailDeleteUndo(long rowId, int adaptorPosition, Uri uri); 1044 onVoicemailDeletedInDatabase(long rowId, Uri uri)1045 void onVoicemailDeletedInDatabase(long rowId, Uri uri); 1046 } 1047 1048 protected interface OnContentCheckedListener { 1049 onContentChecked(boolean hasContent)1050 void onContentChecked(boolean hasContent); 1051 } 1052 1053 @ThreadSafe 1054 private class FetchResultHandler extends ContentObserver implements Runnable { 1055 1056 private final Handler fetchResultHandler; 1057 private final Uri voicemailUri; 1058 private AtomicBoolean isWaitingForResult = new AtomicBoolean(true); 1059 FetchResultHandler(Handler handler, Uri uri, int code)1060 public FetchResultHandler(Handler handler, Uri uri, int code) { 1061 super(handler); 1062 fetchResultHandler = handler; 1063 voicemailUri = uri; 1064 if (context != null) { 1065 if (PermissionsUtil.hasReadVoicemailPermissions(context)) { 1066 context.getContentResolver().registerContentObserver(voicemailUri, false, this); 1067 } 1068 fetchResultHandler.postDelayed(this, FETCH_CONTENT_TIMEOUT_MS); 1069 } 1070 } 1071 1072 /** Stop waiting for content and notify UI if {@link FETCH_CONTENT_TIMEOUT_MS} has elapsed. */ 1073 @Override run()1074 public void run() { 1075 if (isWaitingForResult.getAndSet(false) && context != null) { 1076 context.getContentResolver().unregisterContentObserver(this); 1077 if (view != null) { 1078 view.setFetchContentTimeout(); 1079 } 1080 } 1081 } 1082 destroy()1083 public void destroy() { 1084 if (isWaitingForResult.getAndSet(false) && context != null) { 1085 context.getContentResolver().unregisterContentObserver(this); 1086 fetchResultHandler.removeCallbacks(this); 1087 } 1088 } 1089 1090 @Override onChange(boolean selfChange)1091 public void onChange(boolean selfChange) { 1092 asyncTaskExecutor.submit( 1093 Tasks.CHECK_CONTENT_AFTER_CHANGE, 1094 new AsyncTask<Void, Void, Boolean>() { 1095 1096 @Override 1097 public Boolean doInBackground(Void... params) { 1098 return queryHasContent(voicemailUri); 1099 } 1100 1101 @Override 1102 public void onPostExecute(Boolean hasContent) { 1103 if (hasContent && context != null && isWaitingForResult.getAndSet(false)) { 1104 context.getContentResolver().unregisterContentObserver(FetchResultHandler.this); 1105 showShareVoicemailButton(true); 1106 prepareContent(); 1107 } 1108 } 1109 }); 1110 } 1111 } 1112 } 1113