1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.dialer.voicemail; 18 19 import static com.android.dialer.CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK; 20 import static com.android.dialer.CallDetailActivity.EXTRA_VOICEMAIL_URI; 21 22 import android.app.Activity; 23 import android.app.Fragment; 24 import android.content.ContentResolver; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.database.ContentObserver; 28 import android.database.Cursor; 29 import android.media.AudioManager; 30 import android.net.Uri; 31 import android.os.Bundle; 32 import android.os.PowerManager; 33 import android.provider.VoicemailContract; 34 import android.util.Log; 35 import android.view.LayoutInflater; 36 import android.view.View; 37 import android.view.ViewGroup; 38 import android.widget.ImageButton; 39 import android.widget.SeekBar; 40 import android.widget.TextView; 41 42 import com.android.common.io.MoreCloseables; 43 import com.android.dialer.ProximitySensorAware; 44 import com.android.dialer.R; 45 import com.android.dialer.util.AsyncTaskExecutors; 46 import com.android.ex.variablespeed.MediaPlayerProxy; 47 import com.android.ex.variablespeed.VariableSpeed; 48 import com.google.common.base.Preconditions; 49 50 import java.util.concurrent.ExecutorService; 51 import java.util.concurrent.Executors; 52 import java.util.concurrent.ScheduledExecutorService; 53 import java.util.concurrent.TimeUnit; 54 55 import javax.annotation.concurrent.GuardedBy; 56 import javax.annotation.concurrent.NotThreadSafe; 57 58 /** 59 * Displays and plays back a single voicemail. 60 * <p> 61 * When the Activity containing this Fragment is created, voicemail playback 62 * will begin immediately. The Activity is expected to be started via an intent 63 * containing a suitable voicemail uri to playback. 64 * <p> 65 * This class is not thread-safe, it is thread-confined. All calls to all public 66 * methods on this class are expected to come from the main ui thread. 67 */ 68 @NotThreadSafe 69 public class VoicemailPlaybackFragment extends Fragment { 70 private static final String TAG = "VoicemailPlayback"; 71 private static final int NUMBER_OF_THREADS_IN_POOL = 2; 72 private static final String[] HAS_CONTENT_PROJECTION = new String[] { 73 VoicemailContract.Voicemails.HAS_CONTENT, 74 }; 75 76 private VoicemailPlaybackPresenter mPresenter; 77 private static int mMediaPlayerRefCount = 0; 78 private static MediaPlayerProxy mMediaPlayerInstance; 79 private static ScheduledExecutorService mScheduledExecutorService; 80 private View mPlaybackLayout; 81 82 @Override 83 public View onCreateView(LayoutInflater inflater, ViewGroup container, 84 Bundle savedInstanceState) { 85 mPlaybackLayout = inflater.inflate(R.layout.playback_layout, null); 86 return mPlaybackLayout; 87 } 88 89 @Override 90 public void onActivityCreated(Bundle savedInstanceState) { 91 super.onActivityCreated(savedInstanceState); 92 Bundle arguments = getArguments(); 93 Preconditions.checkNotNull(arguments, "fragment must be started with arguments"); 94 Uri voicemailUri = arguments.getParcelable(EXTRA_VOICEMAIL_URI); 95 Preconditions.checkNotNull(voicemailUri, "fragment must contain EXTRA_VOICEMAIL_URI"); 96 boolean startPlayback = arguments.getBoolean(EXTRA_VOICEMAIL_START_PLAYBACK, false); 97 PowerManager powerManager = 98 (PowerManager) getActivity().getSystemService(Context.POWER_SERVICE); 99 PowerManager.WakeLock wakeLock = 100 powerManager.newWakeLock( 101 PowerManager.SCREEN_DIM_WAKE_LOCK, getClass().getSimpleName()); 102 mPresenter = new VoicemailPlaybackPresenter(createPlaybackViewImpl(), 103 getMediaPlayerInstance(), voicemailUri, 104 getScheduledExecutorServiceInstance(), startPlayback, 105 AsyncTaskExecutors.createAsyncTaskExecutor(), wakeLock); 106 mPresenter.onCreate(savedInstanceState); 107 } 108 109 @Override 110 public void onSaveInstanceState(Bundle outState) { 111 mPresenter.onSaveInstanceState(outState); 112 super.onSaveInstanceState(outState); 113 } 114 115 @Override 116 public void onDestroy() { 117 shutdownMediaPlayer(); 118 mPresenter.onDestroy(); 119 super.onDestroy(); 120 } 121 122 @Override 123 public void onPause() { 124 mPresenter.onPause(); 125 super.onPause(); 126 } 127 128 private PlaybackViewImpl createPlaybackViewImpl() { 129 return new PlaybackViewImpl(new ActivityReference(), getActivity().getApplicationContext(), 130 mPlaybackLayout); 131 } 132 133 private static synchronized MediaPlayerProxy getMediaPlayerInstance() { 134 ++mMediaPlayerRefCount; 135 if (mMediaPlayerInstance == null) { 136 mMediaPlayerInstance = VariableSpeed.createVariableSpeed( 137 getScheduledExecutorServiceInstance()); 138 } 139 return mMediaPlayerInstance; 140 } 141 142 private static synchronized ScheduledExecutorService getScheduledExecutorServiceInstance() { 143 if (mScheduledExecutorService == null) { 144 mScheduledExecutorService = Executors.newScheduledThreadPool( 145 NUMBER_OF_THREADS_IN_POOL); 146 } 147 return mScheduledExecutorService; 148 } 149 150 private static synchronized void shutdownMediaPlayer() { 151 --mMediaPlayerRefCount; 152 if (mMediaPlayerRefCount > 0) { 153 return; 154 } 155 if (mScheduledExecutorService != null) { 156 mScheduledExecutorService.shutdown(); 157 mScheduledExecutorService = null; 158 } 159 if (mMediaPlayerInstance != null) { 160 mMediaPlayerInstance.release(); 161 mMediaPlayerInstance = null; 162 } 163 } 164 165 /** 166 * Formats a number of milliseconds as something that looks like {@code 00:05}. 167 * <p> 168 * We always use four digits, two for minutes two for seconds. In the very unlikely event 169 * that the voicemail duration exceeds 99 minutes, the display is capped at 99 minutes. 170 */ 171 private static String formatAsMinutesAndSeconds(int millis) { 172 int seconds = millis / 1000; 173 int minutes = seconds / 60; 174 seconds -= minutes * 60; 175 if (minutes > 99) { 176 minutes = 99; 177 } 178 return String.format("%02d:%02d", minutes, seconds); 179 } 180 181 /** 182 * An object that can provide us with an Activity. 183 * <p> 184 * Fragments suffer the drawback that the Activity they belong to may sometimes be null. This 185 * can happen if the Fragment is detached, for example. In that situation a call to 186 * {@link Fragment#getString(int)} will throw and {@link IllegalStateException}. Also, calling 187 * {@link Fragment#getActivity()} is dangerous - it may sometimes return null. And thus blindly 188 * calling a method on the result of getActivity() is dangerous too. 189 * <p> 190 * To work around this, I have made the {@link PlaybackViewImpl} class static, so that it does 191 * not have access to any Fragment methods directly. Instead it uses an application Context for 192 * things like accessing strings, accessing system services. It only uses the Activity when it 193 * absolutely needs it - and does so through this class. This makes it easy to see where we have 194 * to check for null properly. 195 */ 196 private final class ActivityReference { 197 /** Gets this Fragment's Activity: <b>may be null</b>. */ 198 public final Activity get() { 199 return getActivity(); 200 } 201 } 202 203 /** Methods required by the PlaybackView for the VoicemailPlaybackPresenter. */ 204 private static final class PlaybackViewImpl implements VoicemailPlaybackPresenter.PlaybackView { 205 private final ActivityReference mActivityReference; 206 private final Context mApplicationContext; 207 private final SeekBar mPlaybackSeek; 208 private final ImageButton mStartStopButton; 209 private final ImageButton mPlaybackSpeakerphone; 210 private final ImageButton mRateDecreaseButton; 211 private final ImageButton mRateIncreaseButton; 212 private final TextViewWithMessagesController mTextController; 213 214 public PlaybackViewImpl(ActivityReference activityReference, Context applicationContext, 215 View playbackLayout) { 216 Preconditions.checkNotNull(activityReference); 217 Preconditions.checkNotNull(applicationContext); 218 Preconditions.checkNotNull(playbackLayout); 219 mActivityReference = activityReference; 220 mApplicationContext = applicationContext; 221 mPlaybackSeek = (SeekBar) playbackLayout.findViewById(R.id.playback_seek); 222 mStartStopButton = (ImageButton) playbackLayout.findViewById( 223 R.id.playback_start_stop); 224 mPlaybackSpeakerphone = (ImageButton) playbackLayout.findViewById( 225 R.id.playback_speakerphone); 226 mRateDecreaseButton = (ImageButton) playbackLayout.findViewById( 227 R.id.rate_decrease_button); 228 mRateIncreaseButton = (ImageButton) playbackLayout.findViewById( 229 R.id.rate_increase_button); 230 mTextController = new TextViewWithMessagesController( 231 (TextView) playbackLayout.findViewById(R.id.playback_position_text), 232 (TextView) playbackLayout.findViewById(R.id.playback_speed_text)); 233 } 234 235 @Override 236 public void finish() { 237 Activity activity = mActivityReference.get(); 238 if (activity != null) { 239 activity.finish(); 240 } 241 } 242 243 @Override 244 public void runOnUiThread(Runnable runnable) { 245 Activity activity = mActivityReference.get(); 246 if (activity != null) { 247 activity.runOnUiThread(runnable); 248 } 249 } 250 251 @Override 252 public Context getDataSourceContext() { 253 return mApplicationContext; 254 } 255 256 @Override 257 public void setRateDecreaseButtonListener(View.OnClickListener listener) { 258 mRateDecreaseButton.setOnClickListener(listener); 259 } 260 261 @Override 262 public void setRateIncreaseButtonListener(View.OnClickListener listener) { 263 mRateIncreaseButton.setOnClickListener(listener); 264 } 265 266 @Override 267 public void setStartStopListener(View.OnClickListener listener) { 268 mStartStopButton.setOnClickListener(listener); 269 } 270 271 @Override 272 public void setSpeakerphoneListener(View.OnClickListener listener) { 273 mPlaybackSpeakerphone.setOnClickListener(listener); 274 } 275 276 @Override 277 public void setRateDisplay(float rate, int stringResourceId) { 278 mTextController.setTemporaryText( 279 mApplicationContext.getString(stringResourceId), 1, TimeUnit.SECONDS); 280 } 281 282 @Override 283 public void setPositionSeekListener(SeekBar.OnSeekBarChangeListener listener) { 284 mPlaybackSeek.setOnSeekBarChangeListener(listener); 285 } 286 287 @Override 288 public void playbackStarted() { 289 mStartStopButton.setImageResource(R.drawable.ic_hold_pause); 290 } 291 292 @Override 293 public void playbackStopped() { 294 mStartStopButton.setImageResource(R.drawable.ic_play); 295 } 296 297 @Override 298 public void enableProximitySensor() { 299 // Only change the state if the activity is still around. 300 Activity activity = mActivityReference.get(); 301 if (activity != null && activity instanceof ProximitySensorAware) { 302 ((ProximitySensorAware) activity).enableProximitySensor(); 303 } 304 } 305 306 @Override 307 public void disableProximitySensor() { 308 // Only change the state if the activity is still around. 309 Activity activity = mActivityReference.get(); 310 if (activity != null && activity instanceof ProximitySensorAware) { 311 ((ProximitySensorAware) activity).disableProximitySensor(true); 312 } 313 } 314 315 @Override 316 public void registerContentObserver(Uri uri, ContentObserver observer) { 317 mApplicationContext.getContentResolver().registerContentObserver(uri, false, observer); 318 } 319 320 @Override 321 public void unregisterContentObserver(ContentObserver observer) { 322 mApplicationContext.getContentResolver().unregisterContentObserver(observer); 323 } 324 325 @Override 326 public void setClipPosition(int clipPositionInMillis, int clipLengthInMillis) { 327 int seekBarPosition = Math.max(0, clipPositionInMillis); 328 int seekBarMax = Math.max(seekBarPosition, clipLengthInMillis); 329 if (mPlaybackSeek.getMax() != seekBarMax) { 330 mPlaybackSeek.setMax(seekBarMax); 331 } 332 mPlaybackSeek.setProgress(seekBarPosition); 333 mTextController.setPermanentText( 334 formatAsMinutesAndSeconds(seekBarMax - seekBarPosition)); 335 } 336 337 private String getString(int resId) { 338 return mApplicationContext.getString(resId); 339 } 340 341 @Override 342 public void setIsBuffering() { 343 disableUiElements(); 344 mTextController.setPermanentText(getString(R.string.voicemail_buffering)); 345 } 346 347 @Override 348 public void setIsFetchingContent() { 349 disableUiElements(); 350 mTextController.setPermanentText(getString(R.string.voicemail_fetching_content)); 351 } 352 353 @Override 354 public void setFetchContentTimeout() { 355 disableUiElements(); 356 mTextController.setPermanentText(getString(R.string.voicemail_fetching_timout)); 357 } 358 359 @Override 360 public int getDesiredClipPosition() { 361 return mPlaybackSeek.getProgress(); 362 } 363 364 @Override 365 public void disableUiElements() { 366 mRateIncreaseButton.setEnabled(false); 367 mRateDecreaseButton.setEnabled(false); 368 mStartStopButton.setEnabled(false); 369 mPlaybackSpeakerphone.setEnabled(false); 370 mPlaybackSeek.setProgress(0); 371 mPlaybackSeek.setEnabled(false); 372 } 373 374 @Override 375 public void playbackError(Exception e) { 376 disableUiElements(); 377 mTextController.setPermanentText(getString(R.string.voicemail_playback_error)); 378 Log.e(TAG, "Could not play voicemail", e); 379 } 380 381 @Override 382 public void enableUiElements() { 383 mRateIncreaseButton.setEnabled(true); 384 mRateDecreaseButton.setEnabled(true); 385 mStartStopButton.setEnabled(true); 386 mPlaybackSpeakerphone.setEnabled(true); 387 mPlaybackSeek.setEnabled(true); 388 } 389 390 @Override 391 public void sendFetchVoicemailRequest(Uri voicemailUri) { 392 Intent intent = new Intent(VoicemailContract.ACTION_FETCH_VOICEMAIL, voicemailUri); 393 mApplicationContext.sendBroadcast(intent); 394 } 395 396 @Override 397 public boolean queryHasContent(Uri voicemailUri) { 398 ContentResolver contentResolver = mApplicationContext.getContentResolver(); 399 Cursor cursor = contentResolver.query( 400 voicemailUri, HAS_CONTENT_PROJECTION, null, null, null); 401 try { 402 if (cursor != null && cursor.moveToNext()) { 403 return cursor.getInt(cursor.getColumnIndexOrThrow( 404 VoicemailContract.Voicemails.HAS_CONTENT)) == 1; 405 } 406 } finally { 407 MoreCloseables.closeQuietly(cursor); 408 } 409 return false; 410 } 411 412 private AudioManager getAudioManager() { 413 return (AudioManager) mApplicationContext.getSystemService(Context.AUDIO_SERVICE); 414 } 415 416 @Override 417 public boolean isSpeakerPhoneOn() { 418 return getAudioManager().isSpeakerphoneOn(); 419 } 420 421 @Override 422 public void setSpeakerPhoneOn(boolean on) { 423 getAudioManager().setSpeakerphoneOn(on); 424 if (on) { 425 mPlaybackSpeakerphone.setImageResource(R.drawable.ic_speakerphone_on); 426 } else { 427 mPlaybackSpeakerphone.setImageResource(R.drawable.ic_speakerphone_off); 428 } 429 } 430 431 @Override 432 public void setVolumeControlStream(int streamType) { 433 Activity activity = mActivityReference.get(); 434 if (activity != null) { 435 activity.setVolumeControlStream(streamType); 436 } 437 } 438 } 439 440 /** 441 * Controls a TextView with dynamically changing text. 442 * <p> 443 * There are two methods here of interest, 444 * {@link TextViewWithMessagesController#setPermanentText(String)} and 445 * {@link TextViewWithMessagesController#setTemporaryText(String, long, TimeUnit)}. The 446 * former is used to set the text on the text view immediately, and is used in our case for 447 * the countdown of duration remaining during voicemail playback. The second is used to 448 * temporarily replace this countdown with a message, in our case faster voicemail speed or 449 * slower voicemail speed, before returning to the countdown display. 450 * <p> 451 * All the methods on this class must be called from the ui thread. 452 */ 453 private static final class TextViewWithMessagesController { 454 private static final float VISIBLE = 1; 455 private static final float INVISIBLE = 0; 456 private static final long SHORT_ANIMATION_MS = 200; 457 private static final long LONG_ANIMATION_MS = 400; 458 private final Object mLock = new Object(); 459 private final TextView mPermanentTextView; 460 private final TextView mTemporaryTextView; 461 @GuardedBy("mLock") private Runnable mRunnable; 462 463 public TextViewWithMessagesController(TextView permanentTextView, 464 TextView temporaryTextView) { 465 mPermanentTextView = permanentTextView; 466 mTemporaryTextView = temporaryTextView; 467 } 468 469 public void setPermanentText(String text) { 470 mPermanentTextView.setText(text); 471 } 472 473 public void setTemporaryText(String text, long duration, TimeUnit units) { 474 synchronized (mLock) { 475 mTemporaryTextView.setText(text); 476 mTemporaryTextView.animate().alpha(VISIBLE).setDuration(SHORT_ANIMATION_MS); 477 mPermanentTextView.animate().alpha(INVISIBLE).setDuration(SHORT_ANIMATION_MS); 478 mRunnable = new Runnable() { 479 @Override 480 public void run() { 481 synchronized (mLock) { 482 // We check for (mRunnable == this) becuase if not true, then another 483 // setTemporaryText call has taken place in the meantime, and this 484 // one is now defunct and needs to take no action. 485 if (mRunnable == this) { 486 mRunnable = null; 487 mTemporaryTextView.animate() 488 .alpha(INVISIBLE).setDuration(LONG_ANIMATION_MS); 489 mPermanentTextView.animate() 490 .alpha(VISIBLE).setDuration(LONG_ANIMATION_MS); 491 } 492 } 493 } 494 }; 495 mTemporaryTextView.postDelayed(mRunnable, units.toMillis(duration)); 496 } 497 } 498 } 499 } 500